yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
session_coordinator.cc
Go to the documentation of this file.
2#include <absl/status/status.h>
3#include <absl/status/statusor.h>
4#include "util/i18n/tr.h"
5
6#include <algorithm>
7#include <cstdint>
8#include <cstdio>
9#include <cstring>
10#include <filesystem>
11#include <memory>
12#include <stdexcept>
13#include <string>
14#include <utility>
15
16#include "absl/strings/str_format.h"
25#include "app/gui/core/icons.h"
30#include "core/color.h"
31#include "editor/editor.h"
34#include "imgui/imgui.h"
35#include "util/log.h"
36#include "zelda3/game_data.h"
37
38namespace yaze {
39namespace editor {
40
41namespace {
42
43std::filesystem::path NormalizeBackingFilePath(const std::string& filepath) {
44 std::filesystem::path path(filepath);
45 if (path.empty()) {
46 return path;
47 }
48
49#ifndef __EMSCRIPTEN__
50 std::error_code ec;
51 auto absolute_path = std::filesystem::absolute(path, ec);
52 if (!ec) {
53 path = std::move(absolute_path);
54 }
55
56 ec.clear();
57 auto canonical_path = std::filesystem::weakly_canonical(path, ec);
58 if (!ec) {
59 path = std::move(canonical_path);
60 }
61#endif
62
63 return path.lexically_normal();
64}
65
66bool PathsReferToSameBackingFile(const std::string& lhs,
67 const std::string& rhs) {
68 if (lhs.empty() || rhs.empty()) {
69 return false;
70 }
71
72#ifndef __EMSCRIPTEN__
73 std::error_code ec;
74 if (std::filesystem::equivalent(lhs, rhs, ec) && !ec) {
75 return true;
76 }
77#endif
78
80}
81
82} // namespace
83
85 const std::string& rhs) {
86 return editor::PathsReferToSameBackingFile(lhs, rhs);
87}
88
90 ToastManager* toast_manager,
91 UserSettings* user_settings)
92 : window_manager_(window_manager),
93 toast_manager_(toast_manager),
94 user_settings_(user_settings) {}
95
97
99 size_t new_index,
100 RomSession* session,
101 bool transient) {
102 // Publish event to EventBus
103 if (event_bus_) {
105 SessionSwitchedEvent::Create(old_index, new_index, session, transient));
106 }
107}
108
110 RomSession* session) {
111 // Publish event to EventBus
112 if (event_bus_) {
114 }
115}
116
118 // Publish event to EventBus
119 if (event_bus_) {
121 }
122}
123
125 RomSession* session) {
126 // Publish event to EventBus
127 if (event_bus_ && session) {
129 RomLoadedEvent::Create(&session->rom, session->filepath, index));
130 }
131}
132
136 return;
137 }
138
139 // Create new empty session
140 sessions_.push_back(std::make_unique<RomSession>(
143
144 const size_t new_session_index = sessions_.size() - 1;
145
146 // Configure the new session
147 if (editor_manager_) {
148 auto& session = sessions_.back();
149 editor_manager_->ConfigureSession(session.get());
150 }
151
152 LOG_INFO("SessionCoordinator", "Created new session %zu (total: %zu)",
153 new_session_index, session_count_);
154
155 // Notify observers
156 NotifySessionCreated(new_session_index, sessions_.back().get());
157 ActivateCreatedSession(new_session_index);
158
159 ShowSessionOperationResult("Create Session", true);
160}
161
163 if (sessions_.empty())
164 return;
165
168 return;
169 }
170
171 // Create new empty session (cannot actually duplicate due to non-movable
172 // editors)
173 // TODO: Implement proper duplication when editors become movable
174 sessions_.push_back(std::make_unique<RomSession>(
177
178 const size_t new_session_index = sessions_.size() - 1;
179
180 // Configure the new session
181 if (editor_manager_) {
182 auto& session = sessions_.back();
183 editor_manager_->ConfigureSession(session.get());
184 }
185
186 LOG_INFO("SessionCoordinator", "Duplicated session %zu (total: %zu)",
187 new_session_index, session_count_);
188
189 // Notify observers
190 NotifySessionCreated(new_session_index, sessions_.back().get());
191 ActivateCreatedSession(new_session_index);
192
193 ShowSessionOperationResult("Duplicate Session", true);
194}
195
197 if (!IsValidSessionIndex(index)) {
198 return;
199 }
200
201 // There is no previous active index for the first session, so force the
202 // normal switch notification that binds global editor/session context.
203 if (sessions_.size() == 1) {
204 active_session_index_ = index;
205 if (window_manager_) {
207 }
208 NotifySessionSwitched(index, index, sessions_[index].get(),
209 /*transient=*/false);
210 return;
211 }
212
213 // Route activation through EditorManager when available. Besides publishing
214 // the normal switch lifecycle, this preserves the existing unsaved-work
215 // guard for the session being left behind.
216 if (editor_manager_) {
218 } else {
219 SwitchToSession(index);
220 }
221}
222
230
234
236 CloseSessionInternal(index, /*allow_closing_last=*/false);
237}
238
240 CloseSessionInternal(index, /*allow_closing_last=*/true);
241}
242
244 bool allow_closing_last) {
245 if (!IsValidSessionIndex(index))
246 return;
247
248 if (!allow_closing_last && session_count_ <= kMinSessions) {
249 // Don't allow closing the last session
250 if (toast_manager_) {
251 toast_manager_->Show("Cannot close the last session",
253 }
254 return;
255 }
256
257 const size_t session_id = GetSessionId(index);
258 const bool closing_active_session = index == active_session_index_;
259 if (closing_active_session) {
260 const auto transition = PrepareActiveSessionTransition();
261 if (!transition.ok()) {
263 "Close Session: " + std::string(transition.message()), false);
264 return;
265 }
266 }
267
268 // Editors retain non-owning references to their session's workspace panels.
269 // Detach those references before UnregisterSession destroys the panels.
270 sessions_[index]->editors.PrepareForSessionTeardown();
271
272 // Unregister cards for this stable session identity.
273 if (window_manager_) {
275 }
276
277 // Notify observers before removal
278 NotifySessionClosed(index);
279
280 // Remove session (safe now with unique_ptr!)
281 sessions_.erase(sessions_.begin() + index);
283
284 // Adjust active session index
285 if (active_session_index_ >= index && active_session_index_ > 0) {
287 }
288 if (window_manager_ && !sessions_.empty()) {
290 }
291
292 // Closing the active session can leave global editor, ROM, palette, and
293 // drawer context pointing at the destroyed RomSession. Reuse the normal
294 // session-switch lifecycle after the erase so every context observes the
295 // surviving session.
296 if (closing_active_session && !sessions_.empty()) {
299 /*transient=*/false);
300 } else if (sessions_.empty()) {
301 // The last session is gone: unbind every session-owned context (ROM,
302 // palettes, current editor, drawers) through the normal switch path with
303 // a null session.
305 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
306 }
307
308 LOG_INFO("SessionCoordinator", "Closed session %zu (total: %zu)", index,
310
311 ShowSessionOperationResult("Close Session", true);
312}
313
315 CloseSession(index);
316}
317
319 SwitchToSessionInternal(index, /*transient=*/false);
320}
321
322void SessionCoordinator::SwitchToSessionInternal(size_t index, bool transient) {
323 if (!IsValidSessionIndex(index))
324 return;
325
326 if (!transient && index != active_session_index_) {
327 const auto transition = PrepareActiveSessionTransition();
328 if (!transition.ok()) {
330 "Switch Session: " + std::string(transition.message()), false);
331 return;
332 }
333 }
334 ApplySessionActivation(index, transient);
335}
336
338 return before_session_transition_ && !sessions_.empty()
340 : absl::OkStatus();
341}
342
343void SessionCoordinator::ApplySessionActivation(size_t index, bool transient) {
344 size_t old_index = active_session_index_;
345 active_session_index_ = index;
346
347 if (window_manager_) {
349 }
350
351 // Only notify if actually switching to a different session
352 if (old_index != index) {
353 NotifySessionSwitched(old_index, index, sessions_[index].get(), transient);
354 }
355}
356
358 SwitchToSession(index);
359}
360
364
368
369size_t SessionCoordinator::GetSessionId(size_t index) const {
370 if (!IsValidSessionIndex(index)) {
371 return 0;
372 }
373 return sessions_[index]->session_id();
374}
375
378 return nullptr;
379 }
380 return sessions_[active_session_index_].get();
381}
382
386
388 auto* session = GetActiveRomSession();
389 return session ? &session->rom : nullptr;
390}
391
393 auto* session = GetActiveRomSession();
394 return session ? &session->game_data : nullptr;
395}
396
398 auto* session = GetActiveRomSession();
399 return session ? &session->editors : nullptr;
400}
401
402void* SessionCoordinator::GetSession(size_t index) const {
403 if (!IsValidSessionIndex(index)) {
404 return nullptr;
405 }
406 return sessions_[index].get();
407}
408
410 return session_count_ > 1;
411}
412
416
418 const std::string& filepath,
419 std::optional<size_t> excluded_session_id) const {
420 if (filepath.empty()) {
421 return absl::OkStatus();
422 }
423
424 for (const auto& session : sessions_) {
425 if (!session || !session->rom.is_loaded() ||
426 (excluded_session_id.has_value() &&
427 session->session_id() == *excluded_session_id)) {
428 continue;
429 }
430
431 const std::string rom_filepath = session->rom.filename();
432 if (PathsReferToSameBackingFile(filepath, session->filepath) ||
433 PathsReferToSameBackingFile(filepath, rom_filepath)) {
434 const std::string& owner_path =
435 session->filepath.empty() ? rom_filepath : session->filepath;
436 return absl::AlreadyExistsError(absl::StrFormat(
437 "ROM backing file '%s' is already open in session %zu ('%s')",
438 filepath, session->session_id(), owner_path));
439 }
440 }
441 return absl::OkStatus();
442}
443
445 const std::string& filepath) const {
446 return !CheckBackingFileAvailable(filepath).ok();
447}
448
450 if (sessions_.empty())
451 return;
452
454 return;
455
456 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
457 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
458 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
459
460 if (!ImGui::Begin("Session Switcher", &show_session_switcher_)) {
461 ImGui::End();
462 return;
463 }
464
465 ImGui::Text(tr("%s Active Sessions (%zu)"), ICON_MD_TAB, session_count_);
466 ImGui::Separator();
467
468 for (size_t i = 0; i < sessions_.size(); ++i) {
469 bool is_active = (i == active_session_index_);
470
471 ImGui::PushID(static_cast<int>(i));
472
473 // Session tab
474 if (ImGui::Selectable(GetSessionDisplayName(i).c_str(), is_active)) {
475 if (editor_manager_) {
477 } else {
479 }
480 }
481
482 // Right-click context menu
483 if (ImGui::BeginPopupContextItem("SessionContextMenu")) {
485 ImGui::EndPopup();
486 }
487
488 ImGui::PopID();
489 }
490
491 ImGui::Separator();
492
493 // Action buttons
494 if (ImGui::Button(absl::StrFormat("%s New Session", ICON_MD_ADD).c_str())) {
496 }
497
498 ImGui::SameLine();
499 if (ImGui::Button(
500 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
502 }
503
504 ImGui::SameLine();
505 if (HasMultipleSessions() &&
506 ImGui::Button(absl::StrFormat("%s Close", ICON_MD_CLOSE).c_str())) {
508 }
509
510 ImGui::End();
511}
512
514 if (sessions_.empty())
515 return;
516
518 return;
519
520 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
521 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
522 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
523
524 if (!ImGui::Begin("Session Manager", &show_session_manager_)) {
525 ImGui::End();
526 return;
527 }
528
529 // Session statistics
530 ImGui::Text(tr("%s Session Statistics"), ICON_MD_ANALYTICS);
531 ImGui::Separator();
532
533 ImGui::Text(tr("Total Sessions: %zu"), GetTotalSessionCount());
534 ImGui::Text(tr("Loaded Sessions: %zu"), GetLoadedSessionCount());
535 ImGui::Text(tr("Empty Sessions: %zu"), GetEmptySessionCount());
536
537 ImGui::Spacing();
538
539 // Session list
540 if (ImGui::BeginTable("SessionTable", 4,
541 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
542 ImGuiTableFlags_Resizable)) {
543 ImGui::TableSetupColumn("Session", ImGuiTableColumnFlags_WidthStretch,
544 0.3f);
545 ImGui::TableSetupColumn("ROM File", ImGuiTableColumnFlags_WidthStretch,
546 0.4f);
547 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
548 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
549 120.0f);
550 ImGui::TableHeadersRow();
551
552 for (size_t i = 0; i < sessions_.size(); ++i) {
553 const auto& session = sessions_[i];
554 bool is_active = (i == active_session_index_);
555
556 ImGui::PushID(static_cast<int>(i));
557
558 ImGui::TableNextRow();
559
560 // Session name
561 ImGui::TableNextColumn();
562 if (is_active) {
563 ImGui::TextColored(gui::GetSuccessColor(), "%s %s",
565 GetSessionDisplayName(i).c_str());
566 } else {
567 ImGui::Text("%s %s", ICON_MD_RADIO_BUTTON_UNCHECKED,
568 GetSessionDisplayName(i).c_str());
569 }
570
571 // ROM file
572 ImGui::TableNextColumn();
573 if (session->rom.is_loaded()) {
574 ImGui::Text("%s", session->filepath.c_str());
575 } else {
576 ImGui::TextDisabled(tr("(No ROM loaded)"));
577 }
578
579 // Status
580 ImGui::TableNextColumn();
581 if (IsSessionModified(i)) {
582 ImGui::TextColored(gui::GetWarningColor(), tr("Modified"));
583 } else if (session->rom.is_loaded()) {
584 ImGui::TextColored(gui::GetSuccessColor(), tr("Loaded"));
585 } else {
586 ImGui::TextColored(gui::GetWarningColor(), tr("Empty"));
587 }
588
589 // Actions
590 ImGui::TableNextColumn();
591 if (!is_active && ImGui::SmallButton(tr("Switch"))) {
592 if (editor_manager_) {
594 } else {
596 }
597 }
598
599 ImGui::SameLine();
600 if (HasMultipleSessions() && ImGui::SmallButton(tr("Close"))) {
601 if (editor_manager_) {
603 } else {
604 CloseSession(i);
605 }
606 }
607
608 ImGui::PopID();
609 }
610
611 ImGui::EndTable();
612 }
613
614 ImGui::End();
615}
616
619 return;
620
621 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
622 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
623 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
624
625 if (!ImGui::Begin("Rename Session", &show_session_rename_dialog_)) {
626 ImGui::End();
627 return;
628 }
629
630 ImGui::Text(tr("Rename session %zu:"), session_to_rename_);
631 ImGui::InputText(tr("Name"), session_rename_buffer_,
632 sizeof(session_rename_buffer_));
633
634 ImGui::Spacing();
635
636 if (ImGui::Button(tr("OK"))) {
639 session_rename_buffer_[0] = '\0';
640 }
641
642 ImGui::SameLine();
643 if (ImGui::Button(tr("Cancel"))) {
645 session_rename_buffer_[0] = '\0';
646 }
647
648 ImGui::End();
649}
650
652 if (sessions_.empty())
653 return;
654
655 if (gui::BeginThemedTabBar("SessionTabs")) {
656 for (size_t i = 0; i < sessions_.size(); ++i) {
657 bool is_active = (i == active_session_index_);
658 const auto& session = sessions_[i];
659
660 std::string tab_name = GetSessionDisplayName(i);
661 if (session->rom.is_loaded()) {
662 tab_name += " ";
663 tab_name += ICON_MD_CHECK_CIRCLE;
664 }
665 if (IsSessionModified(i)) {
666 tab_name += "*";
667 }
668
669 if (ImGui::BeginTabItem(tab_name.c_str())) {
670 if (!is_active) {
671 if (editor_manager_) {
673 } else {
675 }
676 }
677 ImGui::EndTabItem();
678 }
679
680 // Right-click context menu
681 if (ImGui::BeginPopupContextItem(
682 absl::StrFormat("SessionTabContext_%zu", i).c_str())) {
684 ImGui::EndPopup();
685 }
686 }
688 }
689}
690
692 if (!HasMultipleSessions())
693 return;
694
695 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
696 ImVec4 accent_color = ConvertColorToImVec4(theme.accent);
697
698 {
699 gui::StyleColorGuard accent_guard(ImGuiCol_Text, accent_color);
700 ImGui::Text(tr("%s Session %zu"), ICON_MD_TAB, active_session_index_);
701 }
702
703 if (ImGui::IsItemHovered()) {
704 ImGui::SetTooltip(tr("Active Session: %s\nClick to open session switcher"),
706 }
707
708 if (ImGui::IsItemClicked()) {
710 }
711}
712
713std::string SessionCoordinator::GetSessionDisplayName(size_t index) const {
714 if (!IsValidSessionIndex(index)) {
715 return "Invalid Session";
716 }
717
718 const auto& session = sessions_[index];
719
720 if (!session->custom_name.empty()) {
721 return session->custom_name;
722 }
723
724 if (session->rom.is_loaded()) {
725 return absl::StrFormat(
726 "Session %zu (%s)", index,
727 std::filesystem::path(session->filepath).stem().string());
728 }
729
730 return absl::StrFormat("Session %zu (Empty)", index);
731}
732
736
738 const std::string& new_name) {
739 if (!IsValidSessionIndex(index) || new_name.empty())
740 return;
741
742 sessions_[index]->custom_name = new_name;
743 LOG_INFO("SessionCoordinator", "Renamed session %zu to '%s'", index,
744 new_name.c_str());
745}
746
748 const std::string& editor_name, size_t session_index) const {
749 if (sessions_.size() <= 1) {
750 // Single session - use simple name
751 return editor_name;
752 }
753
754 if (session_index >= sessions_.size()) {
755 return editor_name;
756 }
757
758 // Multi-session - include session identifier
759 const auto& session = sessions_[session_index];
760 std::string session_name = session->custom_name.empty()
761 ? session->rom.title()
762 : session->custom_name;
763
764 // Truncate long session names
765 if (session_name.length() > 20) {
766 session_name = session_name.substr(0, 17) + "...";
767 }
768
769 return absl::StrFormat("%s - %s##session_%zu", editor_name, session_name,
770 session_index);
771}
772
774 SwitchToSession(index);
775}
776
780
781// Panel coordination across sessions
787
793
794void SessionCoordinator::ShowPanelsInCategory(const std::string& category) {
795 if (window_manager_) {
797 }
798}
799
800void SessionCoordinator::HidePanelsInCategory(const std::string& category) {
801 if (window_manager_) {
803 }
804}
805
807 return index < sessions_.size();
808}
809
811 if (sessions_.empty())
812 return;
813
814 size_t original_session_idx = active_session_index_;
815 Editor* original_editor =
817
818 for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) {
819 auto& session = sessions_[session_idx];
820 const bool rom_loaded = session->rom.is_loaded();
821 // Skip empty sessions except the active one so pre-ROM tooling (e.g.
822 // Graphics prototype research) can still tick.
823 if (!rom_loaded && session_idx != active_session_index_) {
824 continue;
825 }
826
827 // Switch context
828 SwitchToSessionInternal(session_idx, /*transient=*/true);
829
830 for (auto editor : session->editors.active_editors_) {
831 if (*editor->active()) {
832 if (!rom_loaded &&
834 continue;
835 }
836
837 if (rom_loaded && editor->type() == EditorType::kOverworld) {
838 auto& overworld_editor = static_cast<OverworldEditor&>(*editor);
839 if (overworld_editor.jump_to_tab() != -1) {
840 // Set the dungeon editor to the jump to tab
841 session->editors.GetDungeonEditor()->add_room(
842 overworld_editor.jump_to_tab());
843 overworld_editor.jump_to_tab_ = -1;
844 }
845 if (overworld_editor.jump_to_entrance_ != -1) {
846 session->editors.GetDungeonEditor()->OpenRegularEntrance(
847 overworld_editor.jump_to_entrance_);
848 overworld_editor.jump_to_entrance_ = -1;
849 }
850 }
851
852 // CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows
853 bool is_card_based_editor =
854 EditorManager::IsPanelBasedEditor(editor->type());
855
856 if (is_card_based_editor) {
857 // Panel-based editors create their own top-level windows
858 // No parent wrapper needed - this allows independent docking
859 if (editor_manager_) {
861 }
862
863 absl::Status status = editor->Update();
864
865 // Route editor errors to toast manager
866 if (!status.ok() && toast_manager_) {
867 std::string editor_name =
868 kEditorNames[static_cast<int>(editor->type())];
870 absl::StrFormat("%s Error: %s", editor_name, status.message()),
871 ToastType::kError, 8.0f);
872 }
873
874 } else {
875 // TRADITIONAL EDITORS: Wrap in Begin/End
876 std::string window_title = GenerateUniqueEditorTitle(
877 kEditorNames[static_cast<int>(editor->type())], session_idx);
878
879 // Set window to maximize on first open
880 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
881 ImGuiCond_FirstUseEver);
882 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
883 ImGuiCond_FirstUseEver);
884
885 if (ImGui::Begin(window_title.c_str(), editor->active(),
886 ImGuiWindowFlags_None)) { // Allow full docking
887 // Temporarily switch context for this editor's update
888 // (Already switched via SwitchToSession)
889 if (editor_manager_) {
891 }
892
893 absl::Status status = editor->Update();
894
895 // Route editor errors to toast manager
896 if (!status.ok() && toast_manager_) {
897 std::string editor_name =
898 kEditorNames[static_cast<int>(editor->type())];
899 toast_manager_->Show(absl::StrFormat("%s Error: %s", editor_name,
900 status.message()),
901 ToastType::kError, 8.0f);
902 }
903 }
904 ImGui::End();
905 }
906 }
907 }
908 }
909
910 // Restore original session context
911 SwitchToSessionInternal(original_session_idx, /*transient=*/true);
912 if (editor_manager_) {
913 editor_manager_->SetCurrentEditor(original_editor);
914 }
915}
916
917bool SessionCoordinator::IsSessionActive(size_t index) const {
918 return index == active_session_index_;
919}
920
921bool SessionCoordinator::IsSessionLoaded(size_t index) const {
922 return IsValidSessionIndex(index) && sessions_[index]->rom.is_loaded();
923}
924
928
930 size_t count = 0;
931 for (const auto& session : sessions_) {
932 if (session->rom.is_loaded()) {
933 count++;
934 }
935 }
936 return count;
937}
938
942
943absl::Status SessionCoordinator::LoadRomIntoSession(const std::string& filename,
944 size_t session_index) {
945 if (filename.empty()) {
946 return absl::InvalidArgumentError("Invalid parameters");
947 }
948
949 size_t target_index =
950 (session_index == SIZE_MAX) ? active_session_index_ : session_index;
951 if (!IsValidSessionIndex(target_index)) {
952 return absl::InvalidArgumentError("Invalid session index");
953 }
954
955 // TODO: Implement actual ROM loading
956 LOG_INFO("SessionCoordinator", "LoadRomIntoSession: %s -> session %zu",
957 filename.c_str(), target_index);
958
959 return absl::OkStatus();
960}
961
963 const std::string& filename) {
965 return absl::FailedPreconditionError("No active session");
966 }
967
968 // TODO: Implement actual ROM saving
969 LOG_INFO("SessionCoordinator", "SaveActiveSession: session %zu",
971
972 return absl::OkStatus();
973}
974
975absl::Status SessionCoordinator::SaveSessionAs(size_t session_index,
976 const std::string& filename) {
977 if (!IsValidSessionIndex(session_index) || filename.empty()) {
978 return absl::InvalidArgumentError("Invalid parameters");
979 }
980
981 // TODO: Implement actual ROM saving
982 LOG_INFO("SessionCoordinator", "SaveSessionAs: session %zu -> %s",
983 session_index, filename.c_str());
984
985 return absl::OkStatus();
986}
987
989 Rom&& rom, const std::string& filepath) {
990 auto path_status = CheckBackingFileAvailable(filepath);
991 if (!path_status.ok()) {
992 return path_status;
993 }
994 const auto transition = PrepareActiveSessionTransition();
995 if (!transition.ok()) {
996 return transition;
997 }
998
999 const size_t new_session_id = next_session_id_++;
1000 const size_t new_session_index = sessions_.size();
1001 sessions_.push_back(std::make_unique<RomSession>(
1002 std::move(rom), user_settings_, new_session_id, editor_registry_));
1003 auto& session = sessions_.back();
1004 session->filepath = filepath;
1005
1007 // Let observers attach project/runtime context before the session becomes
1008 // active. This prevents a switch from briefly exposing an unbound ROM.
1009 NotifySessionCreated(new_session_index, session.get());
1010
1011 if (sessions_.size() == 1) {
1012 active_session_index_ = new_session_index;
1013 if (window_manager_) {
1014 window_manager_->SetActiveSession(GetSessionId(new_session_index));
1015 }
1016 NotifySessionSwitched(new_session_index, new_session_index, session.get(),
1017 /*transient=*/false);
1018 } else {
1019 ApplySessionActivation(new_session_index, /*transient=*/false);
1020 }
1021
1022 NotifySessionRomLoaded(new_session_index, session.get());
1023
1024 return session.get();
1025}
1026
1027absl::Status SessionCoordinator::DiscardProvisionalSession(size_t session_id) {
1028 auto session_it =
1029 std::find_if(sessions_.begin(), sessions_.end(),
1030 [session_id](const std::unique_ptr<RomSession>& session) {
1031 return session && session->session_id() == session_id;
1032 });
1033 if (session_it == sessions_.end()) {
1034 return absl::NotFoundError(
1035 absl::StrFormat("Session %zu is no longer available", session_id));
1036 }
1037
1038 const size_t index =
1039 static_cast<size_t>(std::distance(sessions_.begin(), session_it));
1040 const bool closing_active_session = index == active_session_index_;
1041 if (closing_active_session) {
1042 const auto transition = PrepareActiveSessionTransition();
1043 if (!transition.ok()) {
1044 return transition;
1045 }
1046 }
1047 (*session_it)->editors.PrepareForSessionTeardown();
1048 if (window_manager_) {
1050 }
1051 NotifySessionClosed(index);
1052 sessions_.erase(session_it);
1054
1055 if (sessions_.empty()) {
1057 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
1058 } else {
1059 if (active_session_index_ >= index && active_session_index_ > 0) {
1061 }
1062 if (window_manager_) {
1064 }
1065 if (closing_active_session) {
1068 /*transient=*/false);
1069 }
1070 }
1071
1072 LOG_WARN("SessionCoordinator",
1073 "Discarded provisional session %zu after failed open", session_id);
1074 return absl::OkStatus();
1075}
1076
1078 // Mark empty sessions as closed (except keep at least one)
1079 size_t loaded_count = 0;
1080 for (const auto& session : sessions_) {
1081 if (session->rom.is_loaded()) {
1082 loaded_count++;
1083 }
1084 }
1085
1086 if (loaded_count > 0) {
1087 for (auto it = sessions_.begin(); it != sessions_.end();) {
1088 if (!(*it)->rom.is_loaded() && sessions_.size() > 1) {
1089 it = sessions_.erase(it);
1090 } else {
1091 ++it;
1092 }
1093 }
1094 }
1095
1097 LOG_INFO("SessionCoordinator", "Cleaned up closed sessions (remaining: %zu)",
1099}
1100
1102 if (sessions_.empty())
1103 return;
1104 const auto transition = PrepareActiveSessionTransition();
1105 if (!transition.ok()) {
1107 "Clear Sessions: " + std::string(transition.message()), false);
1108 return;
1109 }
1110
1111 // Editors retain non-owning references to session cards. Detach every editor
1112 // before unregistering any card so cross-panel callbacks cannot observe a
1113 // partially torn-down workspace.
1114 for (const auto& session : sessions_) {
1115 session->editors.PrepareForSessionTeardown();
1116 }
1117 if (window_manager_) {
1118 for (const auto& session : sessions_) {
1119 window_manager_->UnregisterSession(session->session_id());
1120 }
1121 }
1122
1123 sessions_.clear();
1126
1127 LOG_INFO("SessionCoordinator", "Cleared all sessions");
1128}
1129
1131 if (sessions_.empty())
1132 return;
1133
1134 size_t next_index = (active_session_index_ + 1) % sessions_.size();
1135 SwitchToSession(next_index);
1136}
1137
1139 if (sessions_.empty())
1140 return;
1141
1142 size_t prev_index = (active_session_index_ == 0) ? sessions_.size() - 1
1144 SwitchToSession(prev_index);
1145}
1146
1148 if (sessions_.empty())
1149 return;
1150 SwitchToSession(0);
1151}
1152
1154 if (sessions_.empty())
1155 return;
1156 SwitchToSession(sessions_.size() - 1);
1157}
1158
1160 if (!sessions_.empty() && active_session_index_ >= sessions_.size()) {
1161 active_session_index_ = sessions_.size() - 1;
1162 }
1163}
1164
1166 if (!IsValidSessionIndex(index)) {
1167 throw std::out_of_range(
1168 absl::StrFormat("Invalid session index: %zu", index));
1169 }
1170}
1171
1173 const std::string& base_name) const {
1174 if (sessions_.empty())
1175 return base_name;
1176
1177 std::string name = base_name;
1178 int counter = 1;
1179
1180 while (true) {
1181 bool found = false;
1182 for (const auto& session : sessions_) {
1183 if (session->custom_name == name) {
1184 found = true;
1185 break;
1186 }
1187 }
1188
1189 if (!found)
1190 break;
1191
1192 name = absl::StrFormat("%s %d", base_name, counter++);
1193 }
1194
1195 return name;
1196}
1197
1199 if (toast_manager_) {
1201 absl::StrFormat("Maximum %zu sessions allowed", kMaxSessions),
1203 }
1204}
1205
1207 const std::string& operation, bool success) {
1208 if (toast_manager_) {
1209 std::string message =
1210 absl::StrFormat("%s %s", operation, success ? "succeeded" : "failed");
1212 toast_manager_->Show(message, type);
1213 }
1214}
1215
1216void SessionCoordinator::DrawSessionTab(size_t index, bool is_active) {
1217 if (index >= sessions_.size())
1218 return;
1219
1220 const auto& session = sessions_[index];
1221
1222 ImVec4 color = GetSessionColor(index);
1223 gui::StyleColorGuard tab_color_guard(ImGuiCol_Text, color);
1224
1225 std::string tab_name = GetSessionDisplayName(index);
1226 if (session->rom.is_loaded()) {
1227 tab_name += " ";
1228 tab_name += ICON_MD_CHECK_CIRCLE;
1229 }
1230 if (IsSessionModified(index)) {
1231 tab_name += "*";
1232 }
1233
1234 if (ImGui::BeginTabItem(tab_name.c_str())) {
1235 if (!is_active) {
1236 if (editor_manager_) {
1238 } else {
1239 SwitchToSession(index);
1240 }
1241 }
1242 ImGui::EndTabItem();
1243 }
1244}
1245
1247 std::vector<gui::MenuItemSpec> items;
1248 items.emplace_back("Switch to Session", ICON_MD_TAB, [this, index]() {
1249 if (editor_manager_) {
1251 } else {
1252 SwitchToSession(index);
1253 }
1254 });
1255 items.back().separator_after = true;
1256
1257 items.emplace_back(
1258 "Rename...", ICON_MD_DRIVE_FILE_RENAME_OUTLINE, [this, index]() {
1259 session_to_rename_ = index;
1260 strncpy(session_rename_buffer_, GetSessionDisplayName(index).c_str(),
1261 sizeof(session_rename_buffer_) - 1);
1264 });
1265
1266 if (HasMultipleSessions()) {
1267 items.back().separator_after = true;
1268 // Closing drops the session's in-memory ROM. Confirm only when that
1269 // would discard unsaved changes.
1270 const bool modified = IsSessionModified(index);
1271 gui::MenuItemSpec close_item(
1272 modified ? "Close Session..." : "Close Session", ICON_MD_CLOSE,
1273 [this, index]() {
1274 if (editor_manager_) {
1276 } else {
1277 CloseSession(index);
1278 }
1279 });
1280 close_item.destructive = modified;
1281 close_item.requires_confirmation = modified;
1282 items.push_back(std::move(close_item));
1283 }
1284
1285 gui::RenderMenuItems(items);
1286}
1287
1289 if (index >= sessions_.size())
1290 return;
1291
1292 const auto& session = sessions_[index];
1293 ImVec4 color = GetSessionColor(index);
1294
1295 gui::StyleColorGuard badge_guard(ImGuiCol_Text, color);
1296
1297 if (session->rom.is_loaded()) {
1298 ImGui::Text("%s", ICON_MD_CHECK_CIRCLE);
1299 } else {
1300 ImGui::Text("%s", ICON_MD_RADIO_BUTTON_UNCHECKED);
1301 }
1302}
1303
1304ImVec4 SessionCoordinator::GetSessionColor(size_t index) const {
1305 // Generate consistent colors for sessions
1306 static const ImVec4 colors[] = {
1307 ImVec4(0.0f, 1.0f, 0.0f, 1.0f), // Green
1308 ImVec4(0.0f, 0.5f, 1.0f, 1.0f), // Blue
1309 ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // Orange
1310 ImVec4(1.0f, 0.0f, 1.0f, 1.0f), // Magenta
1311 ImVec4(1.0f, 1.0f, 0.0f, 1.0f), // Yellow
1312 ImVec4(0.0f, 1.0f, 1.0f, 1.0f), // Cyan
1313 ImVec4(1.0f, 0.0f, 0.0f, 1.0f), // Red
1314 ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Gray
1315 };
1316
1317 return colors[index % (sizeof(colors) / sizeof(colors[0]))];
1318}
1319
1320std::string SessionCoordinator::GetSessionIcon(size_t index) const {
1321 if (index >= sessions_.size())
1323
1324 const auto& session = sessions_[index];
1325
1326 if (session->rom.is_loaded()) {
1327 return ICON_MD_CHECK_CIRCLE;
1328 } else {
1330 }
1331}
1332
1333bool SessionCoordinator::IsSessionEmpty(size_t index) const {
1334 return IsValidSessionIndex(index) && !sessions_[index]->rom.is_loaded();
1335}
1336
1337bool SessionCoordinator::IsSessionClosed(size_t index) const {
1338 return !IsValidSessionIndex(index);
1339}
1340
1342 if (!IsValidSessionIndex(index)) {
1343 return false;
1344 }
1345
1346 const auto& session = sessions_[index];
1347 if (session->rom.is_loaded() && session->rom.dirty()) {
1348 return true;
1349 }
1350
1351 if (session->editors.HasPendingSpriteChanges() ||
1352 session->editors.HasPendingGraphicsChanges() ||
1353 session->HasPendingGfxGroupChanges()) {
1354 return true;
1355 }
1356
1357 if (session->editors.HasPendingScreenChanges()) {
1358 return true;
1359 }
1360
1361 if (gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
1362 return true;
1363 }
1364
1365 if (session->project_dirty ||
1366 session->editors.HasPendingProjectDraftChanges() ||
1367 (session->project_file_editor_state.initialized &&
1368 session->project_file_editor_state.modified)) {
1369 return true;
1370 }
1371
1372 if (auto* dungeon_editor = static_cast<DungeonEditorV2*>(
1373 session->editors.GetExistingEditor(EditorType::kDungeon))) {
1374 return dungeon_editor->HasPendingDungeonChanges();
1375 }
1376
1377 return false;
1378}
1379
1380} // namespace editor
1381} // namespace yaze
void Publish(const T &event)
Definition event_bus.h:35
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
bool is_loaded() const
Definition rom.h:199
DungeonEditorV2 - Simplified dungeon editor using component delegation.
static bool IsPanelBasedEditor(EditorType type)
static bool UpdateAllowedWithoutLoadedRom(EditorType type)
Contains a complete set of editors for a single ROM instance.
Interface for editor classes.
Definition editor.h:252
Rom * rom() const
Definition editor.h:368
virtual void RequestCloseSession(size_t index)=0
virtual void RequestSwitchToSession(size_t index)=0
virtual Editor * GetCurrentEditor() const =0
virtual void ConfigureSession(RomSession *session)=0
virtual void SetCurrentEditor(Editor *editor)=0
Main UI class for editing overworld maps in A Link to the Past.
void NotifySessionCreated(size_t index, RomSession *session)
void * GetSession(size_t index) const
std::string GenerateUniqueEditorTitle(const std::string &editor_name, size_t session_index) const
zelda3::GameData * GetCurrentGameData() const
size_t GetActiveSessionIndex() const
Compact zero-based UI position in sessions_.
void NotifySessionSwitched(size_t old_index, size_t new_index, RomSession *session, bool transient)
void CloseSessionInternal(size_t index, bool allow_closing_last)
void NotifySessionRomLoaded(size_t index, RomSession *session)
absl::StatusOr< RomSession * > CreateSessionFromRom(Rom &&rom, const std::string &filepath)
SessionCoordinator(WorkspaceWindowManager *window_manager, ToastManager *toast_manager, UserSettings *user_settings)
absl::Status SaveSessionAs(size_t session_index, const std::string &filename)
size_t GetSessionId(size_t index) const
Resolve a compact UI index to its stable workspace identity.
absl::Status SaveActiveSession(const std::string &filename="")
ImVec4 GetSessionColor(size_t index) const
absl::Status LoadRomIntoSession(const std::string &filename, size_t session_index=SIZE_MAX)
absl::Status CheckBackingFileAvailable(const std::string &filepath, std::optional< size_t > excluded_session_id=std::nullopt) const
std::string GetSessionDisplayName(size_t index) const
void RenameSession(size_t index, const std::string &new_name)
bool IsSessionModified(size_t index) const
absl::Status DiscardProvisionalSession(size_t session_id)
void ApplySessionActivation(size_t index, bool transient)
bool HasDuplicateSession(const std::string &filepath) const
void ShowPanelsInCategory(const std::string &category)
void HidePanelsInCategory(const std::string &category)
bool IsSessionClosed(size_t index) const
size_t GetActiveSessionId() const
Stable workspace identity that is never reused while this coordinator lives.
WorkspaceWindowManager * window_manager_
std::string GetSessionIcon(size_t index) const
std::string GetActiveSessionDisplayName() const
void ShowSessionOperationResult(const std::string &operation, bool success)
bool IsValidSessionIndex(size_t index) const
bool IsSessionEmpty(size_t index) const
void SwitchToSessionInternal(size_t index, bool transient)
void DrawSessionTab(size_t index, bool is_active)
std::function< absl::Status()> before_session_transition_
bool IsSessionActive(size_t index) const
std::vector< std::unique_ptr< RomSession > > sessions_
void ValidateSessionIndex(size_t index) const
bool IsSessionLoaded(size_t index) const
std::string GenerateUniqueSessionName(const std::string &base_name) const
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Manages user preferences and settings persistence.
Central registry for all editor cards with session awareness and dependency injection.
void HideAllWindowsInCategory(size_t session_id, const std::string &category)
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
static PaletteManager & Get()
Get the singleton instance.
RAII guard for ImGui style colors.
Definition style_guard.h:29
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_RADIO_BUTTON_CHECKED
Definition icons.h:1548
#define ICON_MD_TAB
Definition icons.h:1930
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_RADIO_BUTTON_UNCHECKED
Definition icons.h:1551
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_ANALYTICS
Definition icons.h:154
#define ICON_MD_DRIVE_FILE_RENAME_OUTLINE
Definition icons.h:630
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
std::filesystem::path NormalizeBackingFilePath(const std::string &filepath)
bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
constexpr std::array< const char *, 15 > kEditorNames
Definition editor.h:232
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
static RomLoadedEvent Create(Rom *r, const std::string &file, size_t session)
Definition core_events.h:32
Represents a single session, containing a ROM and its associated editors.
static SessionClosedEvent Create(size_t idx)
static SessionCreatedEvent Create(size_t idx, RomSession *sess)
static SessionSwitchedEvent Create(size_t old_idx, size_t new_idx, RomSession *sess, bool is_transient=false)
Definition core_events.h:95
Declarative menu item definition.
Definition canvas_menu.h:64