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 size_t new_index,
98 RomSession* session,
99 bool transient) {
100 // Publish event to EventBus
101 if (event_bus_) {
103 SessionSwitchedEvent::Create(old_index, new_index, session, transient));
104 }
105}
106
108 RomSession* session) {
109 // Publish event to EventBus
110 if (event_bus_) {
112 }
113}
114
116 // Publish event to EventBus
117 if (event_bus_) {
119 }
120}
121
123 RomSession* session) {
124 // Publish event to EventBus
125 if (event_bus_ && session) {
127 RomLoadedEvent::Create(&session->rom, session->filepath, index));
128 }
129}
130
134 return;
135 }
136
137 // Create new empty session
138 sessions_.push_back(std::make_unique<RomSession>(
141
142 const size_t new_session_index = sessions_.size() - 1;
143
144 // Configure the new session
145 if (editor_manager_) {
146 auto& session = sessions_.back();
147 editor_manager_->ConfigureSession(session.get());
148 }
149
150 LOG_INFO("SessionCoordinator", "Created new session %zu (total: %zu)",
151 new_session_index, session_count_);
152
153 // Notify observers
154 NotifySessionCreated(new_session_index, sessions_.back().get());
155 ActivateCreatedSession(new_session_index);
156
157 ShowSessionOperationResult("Create Session", true);
158}
159
161 if (sessions_.empty())
162 return;
163
166 return;
167 }
168
169 // Create new empty session (cannot actually duplicate due to non-movable
170 // editors)
171 // TODO: Implement proper duplication when editors become movable
172 sessions_.push_back(std::make_unique<RomSession>(
175
176 const size_t new_session_index = sessions_.size() - 1;
177
178 // Configure the new session
179 if (editor_manager_) {
180 auto& session = sessions_.back();
181 editor_manager_->ConfigureSession(session.get());
182 }
183
184 LOG_INFO("SessionCoordinator", "Duplicated session %zu (total: %zu)",
185 new_session_index, session_count_);
186
187 // Notify observers
188 NotifySessionCreated(new_session_index, sessions_.back().get());
189 ActivateCreatedSession(new_session_index);
190
191 ShowSessionOperationResult("Duplicate Session", true);
192}
193
195 if (!IsValidSessionIndex(index)) {
196 return;
197 }
198
199 // There is no previous active index for the first session, so force the
200 // normal switch notification that binds global editor/session context.
201 if (sessions_.size() == 1) {
202 active_session_index_ = index;
203 if (window_manager_) {
205 }
206 NotifySessionSwitched(index, index, sessions_[index].get(),
207 /*transient=*/false);
208 return;
209 }
210
211 // Route activation through EditorManager when available. Besides publishing
212 // the normal switch lifecycle, this preserves the existing unsaved-work
213 // guard for the session being left behind.
214 if (editor_manager_) {
216 } else {
217 SwitchToSession(index);
218 }
219}
220
228
232
234 CloseSessionInternal(index, /*allow_closing_last=*/false);
235}
236
238 CloseSessionInternal(index, /*allow_closing_last=*/true);
239}
240
242 bool allow_closing_last) {
243 if (!IsValidSessionIndex(index))
244 return;
245
246 if (!allow_closing_last && session_count_ <= kMinSessions) {
247 // Don't allow closing the last session
248 if (toast_manager_) {
249 toast_manager_->Show("Cannot close the last session",
251 }
252 return;
253 }
254
255 const size_t session_id = GetSessionId(index);
256 const bool closing_active_session = index == active_session_index_;
257
258 // Editors retain non-owning references to their session's workspace panels.
259 // Detach those references before UnregisterSession destroys the panels.
260 sessions_[index]->editors.PrepareForSessionTeardown();
261
262 // Unregister cards for this stable session identity.
263 if (window_manager_) {
265 }
266
267 // Notify observers before removal
268 NotifySessionClosed(index);
269
270 // Remove session (safe now with unique_ptr!)
271 sessions_.erase(sessions_.begin() + index);
273
274 // Adjust active session index
275 if (active_session_index_ >= index && active_session_index_ > 0) {
277 }
278 if (window_manager_ && !sessions_.empty()) {
280 }
281
282 // Closing the active session can leave global editor, ROM, palette, and
283 // drawer context pointing at the destroyed RomSession. Reuse the normal
284 // session-switch lifecycle after the erase so every context observes the
285 // surviving session.
286 if (closing_active_session && !sessions_.empty()) {
289 /*transient=*/false);
290 } else if (sessions_.empty()) {
291 // The last session is gone: unbind every session-owned context (ROM,
292 // palettes, current editor, drawers) through the normal switch path with
293 // a null session.
295 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
296 }
297
298 LOG_INFO("SessionCoordinator", "Closed session %zu (total: %zu)", index,
300
301 ShowSessionOperationResult("Close Session", true);
302}
303
305 CloseSession(index);
306}
307
309 SwitchToSessionInternal(index, /*transient=*/false);
310}
311
312void SessionCoordinator::SwitchToSessionInternal(size_t index, bool transient) {
313 if (!IsValidSessionIndex(index))
314 return;
315
316 size_t old_index = active_session_index_;
317 active_session_index_ = index;
318
319 if (window_manager_) {
321 }
322
323 // Only notify if actually switching to a different session
324 if (old_index != index) {
325 NotifySessionSwitched(old_index, index, sessions_[index].get(), transient);
326 }
327}
328
330 SwitchToSession(index);
331}
332
336
340
341size_t SessionCoordinator::GetSessionId(size_t index) const {
342 if (!IsValidSessionIndex(index)) {
343 return 0;
344 }
345 return sessions_[index]->session_id();
346}
347
350 return nullptr;
351 }
352 return sessions_[active_session_index_].get();
353}
354
358
360 auto* session = GetActiveRomSession();
361 return session ? &session->rom : nullptr;
362}
363
365 auto* session = GetActiveRomSession();
366 return session ? &session->game_data : nullptr;
367}
368
370 auto* session = GetActiveRomSession();
371 return session ? &session->editors : nullptr;
372}
373
374void* SessionCoordinator::GetSession(size_t index) const {
375 if (!IsValidSessionIndex(index)) {
376 return nullptr;
377 }
378 return sessions_[index].get();
379}
380
382 return session_count_ > 1;
383}
384
388
390 const std::string& filepath,
391 std::optional<size_t> excluded_session_id) const {
392 if (filepath.empty()) {
393 return absl::OkStatus();
394 }
395
396 for (const auto& session : sessions_) {
397 if (!session || !session->rom.is_loaded() ||
398 (excluded_session_id.has_value() &&
399 session->session_id() == *excluded_session_id)) {
400 continue;
401 }
402
403 const std::string rom_filepath = session->rom.filename();
404 if (PathsReferToSameBackingFile(filepath, session->filepath) ||
405 PathsReferToSameBackingFile(filepath, rom_filepath)) {
406 const std::string& owner_path =
407 session->filepath.empty() ? rom_filepath : session->filepath;
408 return absl::AlreadyExistsError(absl::StrFormat(
409 "ROM backing file '%s' is already open in session %zu ('%s')",
410 filepath, session->session_id(), owner_path));
411 }
412 }
413 return absl::OkStatus();
414}
415
417 const std::string& filepath) const {
418 return !CheckBackingFileAvailable(filepath).ok();
419}
420
422 if (sessions_.empty())
423 return;
424
426 return;
427
428 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
429 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
430 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
431
432 if (!ImGui::Begin("Session Switcher", &show_session_switcher_)) {
433 ImGui::End();
434 return;
435 }
436
437 ImGui::Text(tr("%s Active Sessions (%zu)"), ICON_MD_TAB, session_count_);
438 ImGui::Separator();
439
440 for (size_t i = 0; i < sessions_.size(); ++i) {
441 bool is_active = (i == active_session_index_);
442
443 ImGui::PushID(static_cast<int>(i));
444
445 // Session tab
446 if (ImGui::Selectable(GetSessionDisplayName(i).c_str(), is_active)) {
447 if (editor_manager_) {
449 } else {
451 }
452 }
453
454 // Right-click context menu
455 if (ImGui::BeginPopupContextItem("SessionContextMenu")) {
457 ImGui::EndPopup();
458 }
459
460 ImGui::PopID();
461 }
462
463 ImGui::Separator();
464
465 // Action buttons
466 if (ImGui::Button(absl::StrFormat("%s New Session", ICON_MD_ADD).c_str())) {
468 }
469
470 ImGui::SameLine();
471 if (ImGui::Button(
472 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
474 }
475
476 ImGui::SameLine();
477 if (HasMultipleSessions() &&
478 ImGui::Button(absl::StrFormat("%s Close", ICON_MD_CLOSE).c_str())) {
480 }
481
482 ImGui::End();
483}
484
486 if (sessions_.empty())
487 return;
488
490 return;
491
492 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
493 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
494 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
495
496 if (!ImGui::Begin("Session Manager", &show_session_manager_)) {
497 ImGui::End();
498 return;
499 }
500
501 // Session statistics
502 ImGui::Text(tr("%s Session Statistics"), ICON_MD_ANALYTICS);
503 ImGui::Separator();
504
505 ImGui::Text(tr("Total Sessions: %zu"), GetTotalSessionCount());
506 ImGui::Text(tr("Loaded Sessions: %zu"), GetLoadedSessionCount());
507 ImGui::Text(tr("Empty Sessions: %zu"), GetEmptySessionCount());
508
509 ImGui::Spacing();
510
511 // Session list
512 if (ImGui::BeginTable("SessionTable", 4,
513 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
514 ImGuiTableFlags_Resizable)) {
515 ImGui::TableSetupColumn("Session", ImGuiTableColumnFlags_WidthStretch,
516 0.3f);
517 ImGui::TableSetupColumn("ROM File", ImGuiTableColumnFlags_WidthStretch,
518 0.4f);
519 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
520 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
521 120.0f);
522 ImGui::TableHeadersRow();
523
524 for (size_t i = 0; i < sessions_.size(); ++i) {
525 const auto& session = sessions_[i];
526 bool is_active = (i == active_session_index_);
527
528 ImGui::PushID(static_cast<int>(i));
529
530 ImGui::TableNextRow();
531
532 // Session name
533 ImGui::TableNextColumn();
534 if (is_active) {
535 ImGui::TextColored(gui::GetSuccessColor(), "%s %s",
537 GetSessionDisplayName(i).c_str());
538 } else {
539 ImGui::Text("%s %s", ICON_MD_RADIO_BUTTON_UNCHECKED,
540 GetSessionDisplayName(i).c_str());
541 }
542
543 // ROM file
544 ImGui::TableNextColumn();
545 if (session->rom.is_loaded()) {
546 ImGui::Text("%s", session->filepath.c_str());
547 } else {
548 ImGui::TextDisabled(tr("(No ROM loaded)"));
549 }
550
551 // Status
552 ImGui::TableNextColumn();
553 if (IsSessionModified(i)) {
554 ImGui::TextColored(gui::GetWarningColor(), tr("Modified"));
555 } else if (session->rom.is_loaded()) {
556 ImGui::TextColored(gui::GetSuccessColor(), tr("Loaded"));
557 } else {
558 ImGui::TextColored(gui::GetWarningColor(), tr("Empty"));
559 }
560
561 // Actions
562 ImGui::TableNextColumn();
563 if (!is_active && ImGui::SmallButton(tr("Switch"))) {
564 if (editor_manager_) {
566 } else {
568 }
569 }
570
571 ImGui::SameLine();
572 if (HasMultipleSessions() && ImGui::SmallButton(tr("Close"))) {
573 if (editor_manager_) {
575 } else {
576 CloseSession(i);
577 }
578 }
579
580 ImGui::PopID();
581 }
582
583 ImGui::EndTable();
584 }
585
586 ImGui::End();
587}
588
591 return;
592
593 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
594 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
595 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
596
597 if (!ImGui::Begin("Rename Session", &show_session_rename_dialog_)) {
598 ImGui::End();
599 return;
600 }
601
602 ImGui::Text(tr("Rename session %zu:"), session_to_rename_);
603 ImGui::InputText(tr("Name"), session_rename_buffer_,
604 sizeof(session_rename_buffer_));
605
606 ImGui::Spacing();
607
608 if (ImGui::Button(tr("OK"))) {
611 session_rename_buffer_[0] = '\0';
612 }
613
614 ImGui::SameLine();
615 if (ImGui::Button(tr("Cancel"))) {
617 session_rename_buffer_[0] = '\0';
618 }
619
620 ImGui::End();
621}
622
624 if (sessions_.empty())
625 return;
626
627 if (gui::BeginThemedTabBar("SessionTabs")) {
628 for (size_t i = 0; i < sessions_.size(); ++i) {
629 bool is_active = (i == active_session_index_);
630 const auto& session = sessions_[i];
631
632 std::string tab_name = GetSessionDisplayName(i);
633 if (session->rom.is_loaded()) {
634 tab_name += " ";
635 tab_name += ICON_MD_CHECK_CIRCLE;
636 }
637 if (IsSessionModified(i)) {
638 tab_name += "*";
639 }
640
641 if (ImGui::BeginTabItem(tab_name.c_str())) {
642 if (!is_active) {
643 if (editor_manager_) {
645 } else {
647 }
648 }
649 ImGui::EndTabItem();
650 }
651
652 // Right-click context menu
653 if (ImGui::BeginPopupContextItem(
654 absl::StrFormat("SessionTabContext_%zu", i).c_str())) {
656 ImGui::EndPopup();
657 }
658 }
660 }
661}
662
664 if (!HasMultipleSessions())
665 return;
666
667 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
668 ImVec4 accent_color = ConvertColorToImVec4(theme.accent);
669
670 {
671 gui::StyleColorGuard accent_guard(ImGuiCol_Text, accent_color);
672 ImGui::Text(tr("%s Session %zu"), ICON_MD_TAB, active_session_index_);
673 }
674
675 if (ImGui::IsItemHovered()) {
676 ImGui::SetTooltip(tr("Active Session: %s\nClick to open session switcher"),
678 }
679
680 if (ImGui::IsItemClicked()) {
682 }
683}
684
685std::string SessionCoordinator::GetSessionDisplayName(size_t index) const {
686 if (!IsValidSessionIndex(index)) {
687 return "Invalid Session";
688 }
689
690 const auto& session = sessions_[index];
691
692 if (!session->custom_name.empty()) {
693 return session->custom_name;
694 }
695
696 if (session->rom.is_loaded()) {
697 return absl::StrFormat(
698 "Session %zu (%s)", index,
699 std::filesystem::path(session->filepath).stem().string());
700 }
701
702 return absl::StrFormat("Session %zu (Empty)", index);
703}
704
708
710 const std::string& new_name) {
711 if (!IsValidSessionIndex(index) || new_name.empty())
712 return;
713
714 sessions_[index]->custom_name = new_name;
715 LOG_INFO("SessionCoordinator", "Renamed session %zu to '%s'", index,
716 new_name.c_str());
717}
718
720 const std::string& editor_name, size_t session_index) const {
721 if (sessions_.size() <= 1) {
722 // Single session - use simple name
723 return editor_name;
724 }
725
726 if (session_index >= sessions_.size()) {
727 return editor_name;
728 }
729
730 // Multi-session - include session identifier
731 const auto& session = sessions_[session_index];
732 std::string session_name = session->custom_name.empty()
733 ? session->rom.title()
734 : session->custom_name;
735
736 // Truncate long session names
737 if (session_name.length() > 20) {
738 session_name = session_name.substr(0, 17) + "...";
739 }
740
741 return absl::StrFormat("%s - %s##session_%zu", editor_name, session_name,
742 session_index);
743}
744
746 SwitchToSession(index);
747}
748
752
753// Panel coordination across sessions
759
765
766void SessionCoordinator::ShowPanelsInCategory(const std::string& category) {
767 if (window_manager_) {
769 }
770}
771
772void SessionCoordinator::HidePanelsInCategory(const std::string& category) {
773 if (window_manager_) {
775 }
776}
777
779 return index < sessions_.size();
780}
781
783 if (sessions_.empty())
784 return;
785
786 size_t original_session_idx = active_session_index_;
787 Editor* original_editor =
789
790 for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) {
791 auto& session = sessions_[session_idx];
792 const bool rom_loaded = session->rom.is_loaded();
793 // Skip empty sessions except the active one so pre-ROM tooling (e.g.
794 // Graphics prototype research) can still tick.
795 if (!rom_loaded && session_idx != active_session_index_) {
796 continue;
797 }
798
799 // Switch context
800 SwitchToSessionInternal(session_idx, /*transient=*/true);
801
802 for (auto editor : session->editors.active_editors_) {
803 if (*editor->active()) {
804 if (!rom_loaded &&
806 continue;
807 }
808
809 if (rom_loaded && editor->type() == EditorType::kOverworld) {
810 auto& overworld_editor = static_cast<OverworldEditor&>(*editor);
811 if (overworld_editor.jump_to_tab() != -1) {
812 // Set the dungeon editor to the jump to tab
813 session->editors.GetDungeonEditor()->add_room(
814 overworld_editor.jump_to_tab());
815 overworld_editor.jump_to_tab_ = -1;
816 }
817 }
818
819 // CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows
820 bool is_card_based_editor =
821 EditorManager::IsPanelBasedEditor(editor->type());
822
823 if (is_card_based_editor) {
824 // Panel-based editors create their own top-level windows
825 // No parent wrapper needed - this allows independent docking
826 if (editor_manager_) {
828 }
829
830 absl::Status status = editor->Update();
831
832 // Route editor errors to toast manager
833 if (!status.ok() && toast_manager_) {
834 std::string editor_name =
835 kEditorNames[static_cast<int>(editor->type())];
837 absl::StrFormat("%s Error: %s", editor_name, status.message()),
838 ToastType::kError, 8.0f);
839 }
840
841 } else {
842 // TRADITIONAL EDITORS: Wrap in Begin/End
843 std::string window_title = GenerateUniqueEditorTitle(
844 kEditorNames[static_cast<int>(editor->type())], session_idx);
845
846 // Set window to maximize on first open
847 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
848 ImGuiCond_FirstUseEver);
849 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
850 ImGuiCond_FirstUseEver);
851
852 if (ImGui::Begin(window_title.c_str(), editor->active(),
853 ImGuiWindowFlags_None)) { // Allow full docking
854 // Temporarily switch context for this editor's update
855 // (Already switched via SwitchToSession)
856 if (editor_manager_) {
858 }
859
860 absl::Status status = editor->Update();
861
862 // Route editor errors to toast manager
863 if (!status.ok() && toast_manager_) {
864 std::string editor_name =
865 kEditorNames[static_cast<int>(editor->type())];
866 toast_manager_->Show(absl::StrFormat("%s Error: %s", editor_name,
867 status.message()),
868 ToastType::kError, 8.0f);
869 }
870 }
871 ImGui::End();
872 }
873 }
874 }
875 }
876
877 // Restore original session context
878 SwitchToSessionInternal(original_session_idx, /*transient=*/true);
879 if (editor_manager_) {
880 editor_manager_->SetCurrentEditor(original_editor);
881 }
882}
883
884bool SessionCoordinator::IsSessionActive(size_t index) const {
885 return index == active_session_index_;
886}
887
888bool SessionCoordinator::IsSessionLoaded(size_t index) const {
889 return IsValidSessionIndex(index) && sessions_[index]->rom.is_loaded();
890}
891
895
897 size_t count = 0;
898 for (const auto& session : sessions_) {
899 if (session->rom.is_loaded()) {
900 count++;
901 }
902 }
903 return count;
904}
905
909
910absl::Status SessionCoordinator::LoadRomIntoSession(const std::string& filename,
911 size_t session_index) {
912 if (filename.empty()) {
913 return absl::InvalidArgumentError("Invalid parameters");
914 }
915
916 size_t target_index =
917 (session_index == SIZE_MAX) ? active_session_index_ : session_index;
918 if (!IsValidSessionIndex(target_index)) {
919 return absl::InvalidArgumentError("Invalid session index");
920 }
921
922 // TODO: Implement actual ROM loading
923 LOG_INFO("SessionCoordinator", "LoadRomIntoSession: %s -> session %zu",
924 filename.c_str(), target_index);
925
926 return absl::OkStatus();
927}
928
930 const std::string& filename) {
932 return absl::FailedPreconditionError("No active session");
933 }
934
935 // TODO: Implement actual ROM saving
936 LOG_INFO("SessionCoordinator", "SaveActiveSession: session %zu",
938
939 return absl::OkStatus();
940}
941
942absl::Status SessionCoordinator::SaveSessionAs(size_t session_index,
943 const std::string& filename) {
944 if (!IsValidSessionIndex(session_index) || filename.empty()) {
945 return absl::InvalidArgumentError("Invalid parameters");
946 }
947
948 // TODO: Implement actual ROM saving
949 LOG_INFO("SessionCoordinator", "SaveSessionAs: session %zu -> %s",
950 session_index, filename.c_str());
951
952 return absl::OkStatus();
953}
954
956 Rom&& rom, const std::string& filepath) {
957 auto path_status = CheckBackingFileAvailable(filepath);
958 if (!path_status.ok()) {
959 return path_status;
960 }
961
962 const size_t new_session_id = next_session_id_++;
963 const size_t new_session_index = sessions_.size();
964 sessions_.push_back(std::make_unique<RomSession>(
965 std::move(rom), user_settings_, new_session_id, editor_registry_));
966 auto& session = sessions_.back();
967 session->filepath = filepath;
968
970 // Let observers attach project/runtime context before the session becomes
971 // active. This prevents a switch from briefly exposing an unbound ROM.
972 NotifySessionCreated(new_session_index, session.get());
973
974 if (sessions_.size() == 1) {
975 active_session_index_ = new_session_index;
976 if (window_manager_) {
977 window_manager_->SetActiveSession(GetSessionId(new_session_index));
978 }
979 NotifySessionSwitched(new_session_index, new_session_index, session.get(),
980 /*transient=*/false);
981 } else {
982 SwitchToSession(new_session_index);
983 }
984
985 NotifySessionRomLoaded(new_session_index, session.get());
986
987 return session.get();
988}
989
990absl::Status SessionCoordinator::DiscardProvisionalSession(size_t session_id) {
991 auto session_it =
992 std::find_if(sessions_.begin(), sessions_.end(),
993 [session_id](const std::unique_ptr<RomSession>& session) {
994 return session && session->session_id() == session_id;
995 });
996 if (session_it == sessions_.end()) {
997 return absl::NotFoundError(
998 absl::StrFormat("Session %zu is no longer available", session_id));
999 }
1000
1001 const size_t index =
1002 static_cast<size_t>(std::distance(sessions_.begin(), session_it));
1003 const bool closing_active_session = index == active_session_index_;
1004 (*session_it)->editors.PrepareForSessionTeardown();
1005 if (window_manager_) {
1007 }
1008 NotifySessionClosed(index);
1009 sessions_.erase(session_it);
1011
1012 if (sessions_.empty()) {
1014 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
1015 } else {
1016 if (active_session_index_ >= index && active_session_index_ > 0) {
1018 }
1019 if (window_manager_) {
1021 }
1022 if (closing_active_session) {
1025 /*transient=*/false);
1026 }
1027 }
1028
1029 LOG_WARN("SessionCoordinator",
1030 "Discarded provisional session %zu after failed open", session_id);
1031 return absl::OkStatus();
1032}
1033
1035 // Mark empty sessions as closed (except keep at least one)
1036 size_t loaded_count = 0;
1037 for (const auto& session : sessions_) {
1038 if (session->rom.is_loaded()) {
1039 loaded_count++;
1040 }
1041 }
1042
1043 if (loaded_count > 0) {
1044 for (auto it = sessions_.begin(); it != sessions_.end();) {
1045 if (!(*it)->rom.is_loaded() && sessions_.size() > 1) {
1046 it = sessions_.erase(it);
1047 } else {
1048 ++it;
1049 }
1050 }
1051 }
1052
1054 LOG_INFO("SessionCoordinator", "Cleaned up closed sessions (remaining: %zu)",
1056}
1057
1059 if (sessions_.empty())
1060 return;
1061
1062 // Editors retain non-owning references to session cards. Detach every editor
1063 // before unregistering any card so cross-panel callbacks cannot observe a
1064 // partially torn-down workspace.
1065 for (const auto& session : sessions_) {
1066 session->editors.PrepareForSessionTeardown();
1067 }
1068 if (window_manager_) {
1069 for (const auto& session : sessions_) {
1070 window_manager_->UnregisterSession(session->session_id());
1071 }
1072 }
1073
1074 sessions_.clear();
1077
1078 LOG_INFO("SessionCoordinator", "Cleared all sessions");
1079}
1080
1082 if (sessions_.empty())
1083 return;
1084
1085 size_t next_index = (active_session_index_ + 1) % sessions_.size();
1086 SwitchToSession(next_index);
1087}
1088
1090 if (sessions_.empty())
1091 return;
1092
1093 size_t prev_index = (active_session_index_ == 0) ? sessions_.size() - 1
1095 SwitchToSession(prev_index);
1096}
1097
1099 if (sessions_.empty())
1100 return;
1101 SwitchToSession(0);
1102}
1103
1105 if (sessions_.empty())
1106 return;
1107 SwitchToSession(sessions_.size() - 1);
1108}
1109
1111 if (!sessions_.empty() && active_session_index_ >= sessions_.size()) {
1112 active_session_index_ = sessions_.size() - 1;
1113 }
1114}
1115
1117 if (!IsValidSessionIndex(index)) {
1118 throw std::out_of_range(
1119 absl::StrFormat("Invalid session index: %zu", index));
1120 }
1121}
1122
1124 const std::string& base_name) const {
1125 if (sessions_.empty())
1126 return base_name;
1127
1128 std::string name = base_name;
1129 int counter = 1;
1130
1131 while (true) {
1132 bool found = false;
1133 for (const auto& session : sessions_) {
1134 if (session->custom_name == name) {
1135 found = true;
1136 break;
1137 }
1138 }
1139
1140 if (!found)
1141 break;
1142
1143 name = absl::StrFormat("%s %d", base_name, counter++);
1144 }
1145
1146 return name;
1147}
1148
1150 if (toast_manager_) {
1152 absl::StrFormat("Maximum %zu sessions allowed", kMaxSessions),
1154 }
1155}
1156
1158 const std::string& operation, bool success) {
1159 if (toast_manager_) {
1160 std::string message =
1161 absl::StrFormat("%s %s", operation, success ? "succeeded" : "failed");
1163 toast_manager_->Show(message, type);
1164 }
1165}
1166
1167void SessionCoordinator::DrawSessionTab(size_t index, bool is_active) {
1168 if (index >= sessions_.size())
1169 return;
1170
1171 const auto& session = sessions_[index];
1172
1173 ImVec4 color = GetSessionColor(index);
1174 gui::StyleColorGuard tab_color_guard(ImGuiCol_Text, color);
1175
1176 std::string tab_name = GetSessionDisplayName(index);
1177 if (session->rom.is_loaded()) {
1178 tab_name += " ";
1179 tab_name += ICON_MD_CHECK_CIRCLE;
1180 }
1181 if (IsSessionModified(index)) {
1182 tab_name += "*";
1183 }
1184
1185 if (ImGui::BeginTabItem(tab_name.c_str())) {
1186 if (!is_active) {
1187 if (editor_manager_) {
1189 } else {
1190 SwitchToSession(index);
1191 }
1192 }
1193 ImGui::EndTabItem();
1194 }
1195}
1196
1198 std::vector<gui::MenuItemSpec> items;
1199 items.emplace_back("Switch to Session", ICON_MD_TAB, [this, index]() {
1200 if (editor_manager_) {
1202 } else {
1203 SwitchToSession(index);
1204 }
1205 });
1206 items.back().separator_after = true;
1207
1208 items.emplace_back(
1209 "Rename...", ICON_MD_DRIVE_FILE_RENAME_OUTLINE, [this, index]() {
1210 session_to_rename_ = index;
1211 strncpy(session_rename_buffer_, GetSessionDisplayName(index).c_str(),
1212 sizeof(session_rename_buffer_) - 1);
1215 });
1216
1217 if (HasMultipleSessions()) {
1218 items.back().separator_after = true;
1219 // Closing drops the session's in-memory ROM. Confirm only when that
1220 // would discard unsaved changes.
1221 const bool modified = IsSessionModified(index);
1222 gui::MenuItemSpec close_item(
1223 modified ? "Close Session..." : "Close Session", ICON_MD_CLOSE,
1224 [this, index]() {
1225 if (editor_manager_) {
1227 } else {
1228 CloseSession(index);
1229 }
1230 });
1231 close_item.destructive = modified;
1232 close_item.requires_confirmation = modified;
1233 items.push_back(std::move(close_item));
1234 }
1235
1236 gui::RenderMenuItems(items);
1237}
1238
1240 if (index >= sessions_.size())
1241 return;
1242
1243 const auto& session = sessions_[index];
1244 ImVec4 color = GetSessionColor(index);
1245
1246 gui::StyleColorGuard badge_guard(ImGuiCol_Text, color);
1247
1248 if (session->rom.is_loaded()) {
1249 ImGui::Text("%s", ICON_MD_CHECK_CIRCLE);
1250 } else {
1251 ImGui::Text("%s", ICON_MD_RADIO_BUTTON_UNCHECKED);
1252 }
1253}
1254
1255ImVec4 SessionCoordinator::GetSessionColor(size_t index) const {
1256 // Generate consistent colors for sessions
1257 static const ImVec4 colors[] = {
1258 ImVec4(0.0f, 1.0f, 0.0f, 1.0f), // Green
1259 ImVec4(0.0f, 0.5f, 1.0f, 1.0f), // Blue
1260 ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // Orange
1261 ImVec4(1.0f, 0.0f, 1.0f, 1.0f), // Magenta
1262 ImVec4(1.0f, 1.0f, 0.0f, 1.0f), // Yellow
1263 ImVec4(0.0f, 1.0f, 1.0f, 1.0f), // Cyan
1264 ImVec4(1.0f, 0.0f, 0.0f, 1.0f), // Red
1265 ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Gray
1266 };
1267
1268 return colors[index % (sizeof(colors) / sizeof(colors[0]))];
1269}
1270
1271std::string SessionCoordinator::GetSessionIcon(size_t index) const {
1272 if (index >= sessions_.size())
1274
1275 const auto& session = sessions_[index];
1276
1277 if (session->rom.is_loaded()) {
1278 return ICON_MD_CHECK_CIRCLE;
1279 } else {
1281 }
1282}
1283
1284bool SessionCoordinator::IsSessionEmpty(size_t index) const {
1285 return IsValidSessionIndex(index) && !sessions_[index]->rom.is_loaded();
1286}
1287
1288bool SessionCoordinator::IsSessionClosed(size_t index) const {
1289 return !IsValidSessionIndex(index);
1290}
1291
1293 if (!IsValidSessionIndex(index)) {
1294 return false;
1295 }
1296
1297 const auto& session = sessions_[index];
1298 if (session->rom.is_loaded() && session->rom.dirty()) {
1299 return true;
1300 }
1301
1302 if (session->editors.HasPendingGraphicsChanges() ||
1303 session->HasPendingGfxGroupChanges()) {
1304 return true;
1305 }
1306
1307 if (session->editors.HasPendingScreenChanges()) {
1308 return true;
1309 }
1310
1311 if (gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
1312 return true;
1313 }
1314
1315 if (session->project_dirty ||
1316 session->editors.HasPendingProjectDraftChanges() ||
1317 (session->project_file_editor_state.initialized &&
1318 session->project_file_editor_state.modified)) {
1319 return true;
1320 }
1321
1322 if (auto* dungeon_editor = static_cast<DungeonEditorV2*>(
1323 session->editors.GetExistingEditor(EditorType::kDungeon))) {
1324 return dungeon_editor->HasPendingDungeonChanges();
1325 }
1326
1327 return false;
1328}
1329
1330} // namespace editor
1331} // 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:49
bool is_loaded() const
Definition rom.h:187
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:249
Rom * rom() const
Definition editor.h:356
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)
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)
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:27
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:229
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