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"
24#include "app/gui/core/icons.h"
29#include "core/color.h"
30#include "editor/editor.h"
33#include "imgui/imgui.h"
34#include "util/log.h"
35#include "zelda3/game_data.h"
36
37namespace yaze {
38namespace editor {
39
40namespace {
41
42std::filesystem::path NormalizeBackingFilePath(const std::string& filepath) {
43 std::filesystem::path path(filepath);
44 if (path.empty()) {
45 return path;
46 }
47
48#ifndef __EMSCRIPTEN__
49 std::error_code ec;
50 auto absolute_path = std::filesystem::absolute(path, ec);
51 if (!ec) {
52 path = std::move(absolute_path);
53 }
54
55 ec.clear();
56 auto canonical_path = std::filesystem::weakly_canonical(path, ec);
57 if (!ec) {
58 path = std::move(canonical_path);
59 }
60#endif
61
62 return path.lexically_normal();
63}
64
65bool PathsReferToSameBackingFile(const std::string& lhs,
66 const std::string& rhs) {
67 if (lhs.empty() || rhs.empty()) {
68 return false;
69 }
70
71#ifndef __EMSCRIPTEN__
72 std::error_code ec;
73 if (std::filesystem::equivalent(lhs, rhs, ec) && !ec) {
74 return true;
75 }
76#endif
77
79}
80
81} // namespace
82
84 const std::string& rhs) {
85 return editor::PathsReferToSameBackingFile(lhs, rhs);
86}
87
89 ToastManager* toast_manager,
90 UserSettings* user_settings)
91 : window_manager_(window_manager),
92 toast_manager_(toast_manager),
93 user_settings_(user_settings) {}
94
96 size_t new_index,
97 RomSession* session,
98 bool transient) {
99 // Publish event to EventBus
100 if (event_bus_) {
102 SessionSwitchedEvent::Create(old_index, new_index, session, transient));
103 }
104}
105
107 RomSession* session) {
108 // Publish event to EventBus
109 if (event_bus_) {
111 }
112}
113
115 // Publish event to EventBus
116 if (event_bus_) {
118 }
119}
120
122 RomSession* session) {
123 // Publish event to EventBus
124 if (event_bus_ && session) {
126 RomLoadedEvent::Create(&session->rom, session->filepath, index));
127 }
128}
129
133 return;
134 }
135
136 // Create new empty session
137 sessions_.push_back(std::make_unique<RomSession>(
140
141 const size_t new_session_index = sessions_.size() - 1;
142
143 // Configure the new session
144 if (editor_manager_) {
145 auto& session = sessions_.back();
146 editor_manager_->ConfigureSession(session.get());
147 }
148
149 LOG_INFO("SessionCoordinator", "Created new session %zu (total: %zu)",
150 new_session_index, session_count_);
151
152 // Notify observers
153 NotifySessionCreated(new_session_index, sessions_.back().get());
154 ActivateCreatedSession(new_session_index);
155
156 ShowSessionOperationResult("Create Session", true);
157}
158
160 if (sessions_.empty())
161 return;
162
165 return;
166 }
167
168 // Create new empty session (cannot actually duplicate due to non-movable
169 // editors)
170 // TODO: Implement proper duplication when editors become movable
171 sessions_.push_back(std::make_unique<RomSession>(
174
175 const size_t new_session_index = sessions_.size() - 1;
176
177 // Configure the new session
178 if (editor_manager_) {
179 auto& session = sessions_.back();
180 editor_manager_->ConfigureSession(session.get());
181 }
182
183 LOG_INFO("SessionCoordinator", "Duplicated session %zu (total: %zu)",
184 new_session_index, session_count_);
185
186 // Notify observers
187 NotifySessionCreated(new_session_index, sessions_.back().get());
188 ActivateCreatedSession(new_session_index);
189
190 ShowSessionOperationResult("Duplicate Session", true);
191}
192
194 if (!IsValidSessionIndex(index)) {
195 return;
196 }
197
198 // There is no previous active index for the first session, so force the
199 // normal switch notification that binds global editor/session context.
200 if (sessions_.size() == 1) {
201 active_session_index_ = index;
202 if (window_manager_) {
204 }
205 NotifySessionSwitched(index, index, sessions_[index].get(),
206 /*transient=*/false);
207 return;
208 }
209
210 // Route activation through EditorManager when available. Besides publishing
211 // the normal switch lifecycle, this preserves the existing unsaved-work
212 // guard for the session being left behind.
213 if (editor_manager_) {
215 } else {
216 SwitchToSession(index);
217 }
218}
219
227
231
233 if (!IsValidSessionIndex(index))
234 return;
235
237 // Don't allow closing the last session
238 if (toast_manager_) {
239 toast_manager_->Show("Cannot close the last session",
241 }
242 return;
243 }
244
245 const size_t session_id = GetSessionId(index);
246 const bool closing_active_session = index == active_session_index_;
247
248 // Editors retain non-owning references to their session's workspace panels.
249 // Detach those references before UnregisterSession destroys the panels.
250 sessions_[index]->editors.PrepareForSessionTeardown();
251
252 // Unregister cards for this stable session identity.
253 if (window_manager_) {
255 }
256
257 // Notify observers before removal
258 NotifySessionClosed(index);
259
260 // Remove session (safe now with unique_ptr!)
261 sessions_.erase(sessions_.begin() + index);
263
264 // Adjust active session index
265 if (active_session_index_ >= index && active_session_index_ > 0) {
267 }
268 if (window_manager_ && !sessions_.empty()) {
270 }
271
272 // Closing the active session can leave global editor, ROM, palette, and
273 // drawer context pointing at the destroyed RomSession. Reuse the normal
274 // session-switch lifecycle after the erase so every context observes the
275 // surviving session.
276 if (closing_active_session && !sessions_.empty()) {
279 /*transient=*/false);
280 }
281
282 LOG_INFO("SessionCoordinator", "Closed session %zu (total: %zu)", index,
284
285 ShowSessionOperationResult("Close Session", true);
286}
287
289 CloseSession(index);
290}
291
293 SwitchToSessionInternal(index, /*transient=*/false);
294}
295
296void SessionCoordinator::SwitchToSessionInternal(size_t index, bool transient) {
297 if (!IsValidSessionIndex(index))
298 return;
299
300 size_t old_index = active_session_index_;
301 active_session_index_ = index;
302
303 if (window_manager_) {
305 }
306
307 // Only notify if actually switching to a different session
308 if (old_index != index) {
309 NotifySessionSwitched(old_index, index, sessions_[index].get(), transient);
310 }
311}
312
314 SwitchToSession(index);
315}
316
320
324
325size_t SessionCoordinator::GetSessionId(size_t index) const {
326 if (!IsValidSessionIndex(index)) {
327 return 0;
328 }
329 return sessions_[index]->session_id();
330}
331
334 return nullptr;
335 }
336 return sessions_[active_session_index_].get();
337}
338
342
344 auto* session = GetActiveRomSession();
345 return session ? &session->rom : nullptr;
346}
347
349 auto* session = GetActiveRomSession();
350 return session ? &session->game_data : nullptr;
351}
352
354 auto* session = GetActiveRomSession();
355 return session ? &session->editors : nullptr;
356}
357
358void* SessionCoordinator::GetSession(size_t index) const {
359 if (!IsValidSessionIndex(index)) {
360 return nullptr;
361 }
362 return sessions_[index].get();
363}
364
366 return session_count_ > 1;
367}
368
372
374 const std::string& filepath,
375 std::optional<size_t> excluded_session_id) const {
376 if (filepath.empty()) {
377 return absl::OkStatus();
378 }
379
380 for (const auto& session : sessions_) {
381 if (!session || !session->rom.is_loaded() ||
382 (excluded_session_id.has_value() &&
383 session->session_id() == *excluded_session_id)) {
384 continue;
385 }
386
387 const std::string rom_filepath = session->rom.filename();
388 if (PathsReferToSameBackingFile(filepath, session->filepath) ||
389 PathsReferToSameBackingFile(filepath, rom_filepath)) {
390 const std::string& owner_path =
391 session->filepath.empty() ? rom_filepath : session->filepath;
392 return absl::AlreadyExistsError(absl::StrFormat(
393 "ROM backing file '%s' is already open in session %zu ('%s')",
394 filepath, session->session_id(), owner_path));
395 }
396 }
397 return absl::OkStatus();
398}
399
401 const std::string& filepath) const {
402 return !CheckBackingFileAvailable(filepath).ok();
403}
404
406 if (sessions_.empty())
407 return;
408
410 return;
411
412 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
413 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
414 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
415
416 if (!ImGui::Begin("Session Switcher", &show_session_switcher_)) {
417 ImGui::End();
418 return;
419 }
420
421 ImGui::Text(tr("%s Active Sessions (%zu)"), ICON_MD_TAB, session_count_);
422 ImGui::Separator();
423
424 for (size_t i = 0; i < sessions_.size(); ++i) {
425 bool is_active = (i == active_session_index_);
426
427 ImGui::PushID(static_cast<int>(i));
428
429 // Session tab
430 if (ImGui::Selectable(GetSessionDisplayName(i).c_str(), is_active)) {
431 if (editor_manager_) {
433 } else {
435 }
436 }
437
438 // Right-click context menu
439 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
440 ImGui::OpenPopup("SessionContextMenu");
441 }
442
443 if (ImGui::BeginPopup("SessionContextMenu")) {
445 ImGui::EndPopup();
446 }
447
448 ImGui::PopID();
449 }
450
451 ImGui::Separator();
452
453 // Action buttons
454 if (ImGui::Button(absl::StrFormat("%s New Session", ICON_MD_ADD).c_str())) {
456 }
457
458 ImGui::SameLine();
459 if (ImGui::Button(
460 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
462 }
463
464 ImGui::SameLine();
465 if (HasMultipleSessions() &&
466 ImGui::Button(absl::StrFormat("%s Close", ICON_MD_CLOSE).c_str())) {
468 }
469
470 ImGui::End();
471}
472
474 if (sessions_.empty())
475 return;
476
478 return;
479
480 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
481 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
482 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
483
484 if (!ImGui::Begin("Session Manager", &show_session_manager_)) {
485 ImGui::End();
486 return;
487 }
488
489 // Session statistics
490 ImGui::Text(tr("%s Session Statistics"), ICON_MD_ANALYTICS);
491 ImGui::Separator();
492
493 ImGui::Text(tr("Total Sessions: %zu"), GetTotalSessionCount());
494 ImGui::Text(tr("Loaded Sessions: %zu"), GetLoadedSessionCount());
495 ImGui::Text(tr("Empty Sessions: %zu"), GetEmptySessionCount());
496
497 ImGui::Spacing();
498
499 // Session list
500 if (ImGui::BeginTable("SessionTable", 4,
501 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
502 ImGuiTableFlags_Resizable)) {
503 ImGui::TableSetupColumn("Session", ImGuiTableColumnFlags_WidthStretch,
504 0.3f);
505 ImGui::TableSetupColumn("ROM File", ImGuiTableColumnFlags_WidthStretch,
506 0.4f);
507 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
508 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
509 120.0f);
510 ImGui::TableHeadersRow();
511
512 for (size_t i = 0; i < sessions_.size(); ++i) {
513 const auto& session = sessions_[i];
514 bool is_active = (i == active_session_index_);
515
516 ImGui::PushID(static_cast<int>(i));
517
518 ImGui::TableNextRow();
519
520 // Session name
521 ImGui::TableNextColumn();
522 if (is_active) {
523 ImGui::TextColored(gui::GetSuccessColor(), "%s %s",
525 GetSessionDisplayName(i).c_str());
526 } else {
527 ImGui::Text("%s %s", ICON_MD_RADIO_BUTTON_UNCHECKED,
528 GetSessionDisplayName(i).c_str());
529 }
530
531 // ROM file
532 ImGui::TableNextColumn();
533 if (session->rom.is_loaded()) {
534 ImGui::Text("%s", session->filepath.c_str());
535 } else {
536 ImGui::TextDisabled(tr("(No ROM loaded)"));
537 }
538
539 // Status
540 ImGui::TableNextColumn();
541 if (IsSessionModified(i)) {
542 ImGui::TextColored(gui::GetWarningColor(), tr("Modified"));
543 } else if (session->rom.is_loaded()) {
544 ImGui::TextColored(gui::GetSuccessColor(), tr("Loaded"));
545 } else {
546 ImGui::TextColored(gui::GetWarningColor(), tr("Empty"));
547 }
548
549 // Actions
550 ImGui::TableNextColumn();
551 if (!is_active && ImGui::SmallButton(tr("Switch"))) {
552 if (editor_manager_) {
554 } else {
556 }
557 }
558
559 ImGui::SameLine();
560 if (HasMultipleSessions() && ImGui::SmallButton(tr("Close"))) {
561 if (editor_manager_) {
563 } else {
564 CloseSession(i);
565 }
566 }
567
568 ImGui::PopID();
569 }
570
571 ImGui::EndTable();
572 }
573
574 ImGui::End();
575}
576
579 return;
580
581 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
582 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
583 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
584
585 if (!ImGui::Begin("Rename Session", &show_session_rename_dialog_)) {
586 ImGui::End();
587 return;
588 }
589
590 ImGui::Text(tr("Rename session %zu:"), session_to_rename_);
591 ImGui::InputText(tr("Name"), session_rename_buffer_,
592 sizeof(session_rename_buffer_));
593
594 ImGui::Spacing();
595
596 if (ImGui::Button(tr("OK"))) {
599 session_rename_buffer_[0] = '\0';
600 }
601
602 ImGui::SameLine();
603 if (ImGui::Button(tr("Cancel"))) {
605 session_rename_buffer_[0] = '\0';
606 }
607
608 ImGui::End();
609}
610
612 if (sessions_.empty())
613 return;
614
615 if (gui::BeginThemedTabBar("SessionTabs")) {
616 for (size_t i = 0; i < sessions_.size(); ++i) {
617 bool is_active = (i == active_session_index_);
618 const auto& session = sessions_[i];
619
620 std::string tab_name = GetSessionDisplayName(i);
621 if (session->rom.is_loaded()) {
622 tab_name += " ";
623 tab_name += ICON_MD_CHECK_CIRCLE;
624 }
625 if (IsSessionModified(i)) {
626 tab_name += "*";
627 }
628
629 if (ImGui::BeginTabItem(tab_name.c_str())) {
630 if (!is_active) {
631 if (editor_manager_) {
633 } else {
635 }
636 }
637 ImGui::EndTabItem();
638 }
639
640 // Right-click context menu
641 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
642 ImGui::OpenPopup(absl::StrFormat("SessionTabContext_%zu", i).c_str());
643 }
644
645 if (ImGui::BeginPopup(
646 absl::StrFormat("SessionTabContext_%zu", i).c_str())) {
648 ImGui::EndPopup();
649 }
650 }
652 }
653}
654
656 if (!HasMultipleSessions())
657 return;
658
659 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
660 ImVec4 accent_color = ConvertColorToImVec4(theme.accent);
661
662 {
663 gui::StyleColorGuard accent_guard(ImGuiCol_Text, accent_color);
664 ImGui::Text(tr("%s Session %zu"), ICON_MD_TAB, active_session_index_);
665 }
666
667 if (ImGui::IsItemHovered()) {
668 ImGui::SetTooltip(tr("Active Session: %s\nClick to open session switcher"),
670 }
671
672 if (ImGui::IsItemClicked()) {
674 }
675}
676
677std::string SessionCoordinator::GetSessionDisplayName(size_t index) const {
678 if (!IsValidSessionIndex(index)) {
679 return "Invalid Session";
680 }
681
682 const auto& session = sessions_[index];
683
684 if (!session->custom_name.empty()) {
685 return session->custom_name;
686 }
687
688 if (session->rom.is_loaded()) {
689 return absl::StrFormat(
690 "Session %zu (%s)", index,
691 std::filesystem::path(session->filepath).stem().string());
692 }
693
694 return absl::StrFormat("Session %zu (Empty)", index);
695}
696
700
702 const std::string& new_name) {
703 if (!IsValidSessionIndex(index) || new_name.empty())
704 return;
705
706 sessions_[index]->custom_name = new_name;
707 LOG_INFO("SessionCoordinator", "Renamed session %zu to '%s'", index,
708 new_name.c_str());
709}
710
712 const std::string& editor_name, size_t session_index) const {
713 if (sessions_.size() <= 1) {
714 // Single session - use simple name
715 return editor_name;
716 }
717
718 if (session_index >= sessions_.size()) {
719 return editor_name;
720 }
721
722 // Multi-session - include session identifier
723 const auto& session = sessions_[session_index];
724 std::string session_name = session->custom_name.empty()
725 ? session->rom.title()
726 : session->custom_name;
727
728 // Truncate long session names
729 if (session_name.length() > 20) {
730 session_name = session_name.substr(0, 17) + "...";
731 }
732
733 return absl::StrFormat("%s - %s##session_%zu", editor_name, session_name,
734 session_index);
735}
736
738 SwitchToSession(index);
739}
740
744
745// Panel coordination across sessions
751
757
758void SessionCoordinator::ShowPanelsInCategory(const std::string& category) {
759 if (window_manager_) {
761 }
762}
763
764void SessionCoordinator::HidePanelsInCategory(const std::string& category) {
765 if (window_manager_) {
767 }
768}
769
771 return index < sessions_.size();
772}
773
775 if (sessions_.empty())
776 return;
777
778 size_t original_session_idx = active_session_index_;
779 Editor* original_editor =
781
782 for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) {
783 auto& session = sessions_[session_idx];
784 const bool rom_loaded = session->rom.is_loaded();
785 // Skip empty sessions except the active one so pre-ROM tooling (e.g.
786 // Graphics prototype research) can still tick.
787 if (!rom_loaded && session_idx != active_session_index_) {
788 continue;
789 }
790
791 // Switch context
792 SwitchToSessionInternal(session_idx, /*transient=*/true);
793
794 for (auto editor : session->editors.active_editors_) {
795 if (*editor->active()) {
796 if (!rom_loaded &&
798 continue;
799 }
800
801 if (rom_loaded && editor->type() == EditorType::kOverworld) {
802 auto& overworld_editor = static_cast<OverworldEditor&>(*editor);
803 if (overworld_editor.jump_to_tab() != -1) {
804 // Set the dungeon editor to the jump to tab
805 session->editors.GetDungeonEditor()->add_room(
806 overworld_editor.jump_to_tab());
807 overworld_editor.jump_to_tab_ = -1;
808 }
809 }
810
811 // CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows
812 bool is_card_based_editor =
813 EditorManager::IsPanelBasedEditor(editor->type());
814
815 if (is_card_based_editor) {
816 // Panel-based editors create their own top-level windows
817 // No parent wrapper needed - this allows independent docking
818 if (editor_manager_) {
820 }
821
822 absl::Status status = editor->Update();
823
824 // Route editor errors to toast manager
825 if (!status.ok() && toast_manager_) {
826 std::string editor_name =
827 kEditorNames[static_cast<int>(editor->type())];
829 absl::StrFormat("%s Error: %s", editor_name, status.message()),
830 ToastType::kError, 8.0f);
831 }
832
833 } else {
834 // TRADITIONAL EDITORS: Wrap in Begin/End
835 std::string window_title = GenerateUniqueEditorTitle(
836 kEditorNames[static_cast<int>(editor->type())], session_idx);
837
838 // Set window to maximize on first open
839 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
840 ImGuiCond_FirstUseEver);
841 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
842 ImGuiCond_FirstUseEver);
843
844 if (ImGui::Begin(window_title.c_str(), editor->active(),
845 ImGuiWindowFlags_None)) { // Allow full docking
846 // Temporarily switch context for this editor's update
847 // (Already switched via SwitchToSession)
848 if (editor_manager_) {
850 }
851
852 absl::Status status = editor->Update();
853
854 // Route editor errors to toast manager
855 if (!status.ok() && toast_manager_) {
856 std::string editor_name =
857 kEditorNames[static_cast<int>(editor->type())];
858 toast_manager_->Show(absl::StrFormat("%s Error: %s", editor_name,
859 status.message()),
860 ToastType::kError, 8.0f);
861 }
862 }
863 ImGui::End();
864 }
865 }
866 }
867 }
868
869 // Restore original session context
870 SwitchToSessionInternal(original_session_idx, /*transient=*/true);
871 if (editor_manager_) {
872 editor_manager_->SetCurrentEditor(original_editor);
873 }
874}
875
876bool SessionCoordinator::IsSessionActive(size_t index) const {
877 return index == active_session_index_;
878}
879
880bool SessionCoordinator::IsSessionLoaded(size_t index) const {
881 return IsValidSessionIndex(index) && sessions_[index]->rom.is_loaded();
882}
883
887
889 size_t count = 0;
890 for (const auto& session : sessions_) {
891 if (session->rom.is_loaded()) {
892 count++;
893 }
894 }
895 return count;
896}
897
901
902absl::Status SessionCoordinator::LoadRomIntoSession(const std::string& filename,
903 size_t session_index) {
904 if (filename.empty()) {
905 return absl::InvalidArgumentError("Invalid parameters");
906 }
907
908 size_t target_index =
909 (session_index == SIZE_MAX) ? active_session_index_ : session_index;
910 if (!IsValidSessionIndex(target_index)) {
911 return absl::InvalidArgumentError("Invalid session index");
912 }
913
914 // TODO: Implement actual ROM loading
915 LOG_INFO("SessionCoordinator", "LoadRomIntoSession: %s -> session %zu",
916 filename.c_str(), target_index);
917
918 return absl::OkStatus();
919}
920
922 const std::string& filename) {
924 return absl::FailedPreconditionError("No active session");
925 }
926
927 // TODO: Implement actual ROM saving
928 LOG_INFO("SessionCoordinator", "SaveActiveSession: session %zu",
930
931 return absl::OkStatus();
932}
933
934absl::Status SessionCoordinator::SaveSessionAs(size_t session_index,
935 const std::string& filename) {
936 if (!IsValidSessionIndex(session_index) || filename.empty()) {
937 return absl::InvalidArgumentError("Invalid parameters");
938 }
939
940 // TODO: Implement actual ROM saving
941 LOG_INFO("SessionCoordinator", "SaveSessionAs: session %zu -> %s",
942 session_index, filename.c_str());
943
944 return absl::OkStatus();
945}
946
948 Rom&& rom, const std::string& filepath) {
949 auto path_status = CheckBackingFileAvailable(filepath);
950 if (!path_status.ok()) {
951 return path_status;
952 }
953
954 const size_t new_session_id = next_session_id_++;
955 const size_t new_session_index = sessions_.size();
956 sessions_.push_back(std::make_unique<RomSession>(
957 std::move(rom), user_settings_, new_session_id, editor_registry_));
958 auto& session = sessions_.back();
959 session->filepath = filepath;
960
962 // Let observers attach project/runtime context before the session becomes
963 // active. This prevents a switch from briefly exposing an unbound ROM.
964 NotifySessionCreated(new_session_index, session.get());
965
966 if (sessions_.size() == 1) {
967 active_session_index_ = new_session_index;
968 if (window_manager_) {
969 window_manager_->SetActiveSession(GetSessionId(new_session_index));
970 }
971 NotifySessionSwitched(new_session_index, new_session_index, session.get(),
972 /*transient=*/false);
973 } else {
974 SwitchToSession(new_session_index);
975 }
976
977 NotifySessionRomLoaded(new_session_index, session.get());
978
979 return session.get();
980}
981
982absl::Status SessionCoordinator::DiscardProvisionalSession(size_t session_id) {
983 auto session_it =
984 std::find_if(sessions_.begin(), sessions_.end(),
985 [session_id](const std::unique_ptr<RomSession>& session) {
986 return session && session->session_id() == session_id;
987 });
988 if (session_it == sessions_.end()) {
989 return absl::NotFoundError(
990 absl::StrFormat("Session %zu is no longer available", session_id));
991 }
992
993 const size_t index =
994 static_cast<size_t>(std::distance(sessions_.begin(), session_it));
995 const bool closing_active_session = index == active_session_index_;
996 (*session_it)->editors.PrepareForSessionTeardown();
997 if (window_manager_) {
999 }
1000 NotifySessionClosed(index);
1001 sessions_.erase(session_it);
1003
1004 if (sessions_.empty()) {
1006 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
1007 } else {
1008 if (active_session_index_ >= index && active_session_index_ > 0) {
1010 }
1011 if (window_manager_) {
1013 }
1014 if (closing_active_session) {
1017 /*transient=*/false);
1018 }
1019 }
1020
1021 LOG_WARN("SessionCoordinator",
1022 "Discarded provisional session %zu after failed open", session_id);
1023 return absl::OkStatus();
1024}
1025
1027 // Mark empty sessions as closed (except keep at least one)
1028 size_t loaded_count = 0;
1029 for (const auto& session : sessions_) {
1030 if (session->rom.is_loaded()) {
1031 loaded_count++;
1032 }
1033 }
1034
1035 if (loaded_count > 0) {
1036 for (auto it = sessions_.begin(); it != sessions_.end();) {
1037 if (!(*it)->rom.is_loaded() && sessions_.size() > 1) {
1038 it = sessions_.erase(it);
1039 } else {
1040 ++it;
1041 }
1042 }
1043 }
1044
1046 LOG_INFO("SessionCoordinator", "Cleaned up closed sessions (remaining: %zu)",
1048}
1049
1051 if (sessions_.empty())
1052 return;
1053
1054 // Editors retain non-owning references to session cards. Detach every editor
1055 // before unregistering any card so cross-panel callbacks cannot observe a
1056 // partially torn-down workspace.
1057 for (const auto& session : sessions_) {
1058 session->editors.PrepareForSessionTeardown();
1059 }
1060 if (window_manager_) {
1061 for (const auto& session : sessions_) {
1062 window_manager_->UnregisterSession(session->session_id());
1063 }
1064 }
1065
1066 sessions_.clear();
1069
1070 LOG_INFO("SessionCoordinator", "Cleared all sessions");
1071}
1072
1074 if (sessions_.empty())
1075 return;
1076
1077 size_t next_index = (active_session_index_ + 1) % sessions_.size();
1078 SwitchToSession(next_index);
1079}
1080
1082 if (sessions_.empty())
1083 return;
1084
1085 size_t prev_index = (active_session_index_ == 0) ? sessions_.size() - 1
1087 SwitchToSession(prev_index);
1088}
1089
1091 if (sessions_.empty())
1092 return;
1093 SwitchToSession(0);
1094}
1095
1097 if (sessions_.empty())
1098 return;
1099 SwitchToSession(sessions_.size() - 1);
1100}
1101
1103 if (!sessions_.empty() && active_session_index_ >= sessions_.size()) {
1104 active_session_index_ = sessions_.size() - 1;
1105 }
1106}
1107
1109 if (!IsValidSessionIndex(index)) {
1110 throw std::out_of_range(
1111 absl::StrFormat("Invalid session index: %zu", index));
1112 }
1113}
1114
1116 const std::string& base_name) const {
1117 if (sessions_.empty())
1118 return base_name;
1119
1120 std::string name = base_name;
1121 int counter = 1;
1122
1123 while (true) {
1124 bool found = false;
1125 for (const auto& session : sessions_) {
1126 if (session->custom_name == name) {
1127 found = true;
1128 break;
1129 }
1130 }
1131
1132 if (!found)
1133 break;
1134
1135 name = absl::StrFormat("%s %d", base_name, counter++);
1136 }
1137
1138 return name;
1139}
1140
1142 if (toast_manager_) {
1144 absl::StrFormat("Maximum %zu sessions allowed", kMaxSessions),
1146 }
1147}
1148
1150 const std::string& operation, bool success) {
1151 if (toast_manager_) {
1152 std::string message =
1153 absl::StrFormat("%s %s", operation, success ? "succeeded" : "failed");
1155 toast_manager_->Show(message, type);
1156 }
1157}
1158
1159void SessionCoordinator::DrawSessionTab(size_t index, bool is_active) {
1160 if (index >= sessions_.size())
1161 return;
1162
1163 const auto& session = sessions_[index];
1164
1165 ImVec4 color = GetSessionColor(index);
1166 gui::StyleColorGuard tab_color_guard(ImGuiCol_Text, color);
1167
1168 std::string tab_name = GetSessionDisplayName(index);
1169 if (session->rom.is_loaded()) {
1170 tab_name += " ";
1171 tab_name += ICON_MD_CHECK_CIRCLE;
1172 }
1173 if (IsSessionModified(index)) {
1174 tab_name += "*";
1175 }
1176
1177 if (ImGui::BeginTabItem(tab_name.c_str())) {
1178 if (!is_active) {
1179 if (editor_manager_) {
1181 } else {
1182 SwitchToSession(index);
1183 }
1184 }
1185 ImGui::EndTabItem();
1186 }
1187}
1188
1190 if (ImGui::MenuItem(
1191 absl::StrFormat("%s Switch to Session", ICON_MD_TAB).c_str())) {
1192 if (editor_manager_) {
1194 } else {
1195 SwitchToSession(index);
1196 }
1197 }
1198
1199 if (ImGui::MenuItem(absl::StrFormat("%s Rename", ICON_MD_EDIT).c_str())) {
1200 session_to_rename_ = index;
1201 strncpy(session_rename_buffer_, GetSessionDisplayName(index).c_str(),
1202 sizeof(session_rename_buffer_) - 1);
1205 }
1206
1207 if (ImGui::MenuItem(
1208 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
1209 // TODO: Implement session duplication
1210 }
1211
1212 ImGui::Separator();
1213
1214 if (HasMultipleSessions() &&
1215 ImGui::MenuItem(
1216 absl::StrFormat("%s Close Session", ICON_MD_CLOSE).c_str())) {
1217 if (editor_manager_) {
1219 } else {
1220 CloseSession(index);
1221 }
1222 }
1223}
1224
1226 if (index >= sessions_.size())
1227 return;
1228
1229 const auto& session = sessions_[index];
1230 ImVec4 color = GetSessionColor(index);
1231
1232 gui::StyleColorGuard badge_guard(ImGuiCol_Text, color);
1233
1234 if (session->rom.is_loaded()) {
1235 ImGui::Text("%s", ICON_MD_CHECK_CIRCLE);
1236 } else {
1237 ImGui::Text("%s", ICON_MD_RADIO_BUTTON_UNCHECKED);
1238 }
1239}
1240
1241ImVec4 SessionCoordinator::GetSessionColor(size_t index) const {
1242 // Generate consistent colors for sessions
1243 static const ImVec4 colors[] = {
1244 ImVec4(0.0f, 1.0f, 0.0f, 1.0f), // Green
1245 ImVec4(0.0f, 0.5f, 1.0f, 1.0f), // Blue
1246 ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // Orange
1247 ImVec4(1.0f, 0.0f, 1.0f, 1.0f), // Magenta
1248 ImVec4(1.0f, 1.0f, 0.0f, 1.0f), // Yellow
1249 ImVec4(0.0f, 1.0f, 1.0f, 1.0f), // Cyan
1250 ImVec4(1.0f, 0.0f, 0.0f, 1.0f), // Red
1251 ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Gray
1252 };
1253
1254 return colors[index % (sizeof(colors) / sizeof(colors[0]))];
1255}
1256
1257std::string SessionCoordinator::GetSessionIcon(size_t index) const {
1258 if (index >= sessions_.size())
1260
1261 const auto& session = sessions_[index];
1262
1263 if (session->rom.is_loaded()) {
1264 return ICON_MD_CHECK_CIRCLE;
1265 } else {
1267 }
1268}
1269
1270bool SessionCoordinator::IsSessionEmpty(size_t index) const {
1271 return IsValidSessionIndex(index) && !sessions_[index]->rom.is_loaded();
1272}
1273
1274bool SessionCoordinator::IsSessionClosed(size_t index) const {
1275 return !IsValidSessionIndex(index);
1276}
1277
1279 if (!IsValidSessionIndex(index)) {
1280 return false;
1281 }
1282
1283 const auto& session = sessions_[index];
1284 if (session->rom.is_loaded() && session->rom.dirty()) {
1285 return true;
1286 }
1287
1288 if (session->editors.HasPendingGraphicsChanges()) {
1289 return true;
1290 }
1291
1292 if (session->editors.HasPendingScreenChanges()) {
1293 return true;
1294 }
1295
1296 if (gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
1297 return true;
1298 }
1299
1300 if (session->project_dirty ||
1301 session->editors.HasPendingProjectDraftChanges() ||
1302 (session->project_file_editor_state.initialized &&
1303 session->project_file_editor_state.modified)) {
1304 return true;
1305 }
1306
1307 if (auto* dungeon_editor = static_cast<DungeonEditorV2*>(
1308 session->editors.GetExistingEditor(EditorType::kDungeon))) {
1309 return dungeon_editor->HasPendingDungeonChanges();
1310 }
1311
1312 return false;
1313}
1314
1315} // namespace editor
1316} // 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:28
bool is_loaded() const
Definition rom.h:155
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:245
Rom * rom() const
Definition editor.h:324
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 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)
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_EDIT
Definition icons.h:645
#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 LOG_WARN(category, format,...)
Definition log.h:108
#define LOG_INFO(category, format,...)
Definition log.h:106
std::filesystem::path NormalizeBackingFilePath(const std::string &filepath)
bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
constexpr std::array< const char *, 14 > kEditorNames
Definition editor.h:226
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
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
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