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 // Unregister cards for this stable session identity.
249 if (window_manager_) {
251 }
252
253 // Notify observers before removal
254 NotifySessionClosed(index);
255
256 // Remove session (safe now with unique_ptr!)
257 sessions_.erase(sessions_.begin() + index);
259
260 // Adjust active session index
261 if (active_session_index_ >= index && active_session_index_ > 0) {
263 }
264 if (window_manager_ && !sessions_.empty()) {
266 }
267
268 // Closing the active session can leave global editor, ROM, palette, and
269 // drawer context pointing at the destroyed RomSession. Reuse the normal
270 // session-switch lifecycle after the erase so every context observes the
271 // surviving session.
272 if (closing_active_session && !sessions_.empty()) {
275 /*transient=*/false);
276 }
277
278 LOG_INFO("SessionCoordinator", "Closed session %zu (total: %zu)", index,
280
281 ShowSessionOperationResult("Close Session", true);
282}
283
285 CloseSession(index);
286}
287
289 SwitchToSessionInternal(index, /*transient=*/false);
290}
291
292void SessionCoordinator::SwitchToSessionInternal(size_t index, bool transient) {
293 if (!IsValidSessionIndex(index))
294 return;
295
296 size_t old_index = active_session_index_;
297 active_session_index_ = index;
298
299 if (window_manager_) {
301 }
302
303 // Only notify if actually switching to a different session
304 if (old_index != index) {
305 NotifySessionSwitched(old_index, index, sessions_[index].get(), transient);
306 }
307}
308
310 SwitchToSession(index);
311}
312
316
320
321size_t SessionCoordinator::GetSessionId(size_t index) const {
322 if (!IsValidSessionIndex(index)) {
323 return 0;
324 }
325 return sessions_[index]->session_id();
326}
327
330 return nullptr;
331 }
332 return sessions_[active_session_index_].get();
333}
334
338
340 auto* session = GetActiveRomSession();
341 return session ? &session->rom : nullptr;
342}
343
345 auto* session = GetActiveRomSession();
346 return session ? &session->game_data : nullptr;
347}
348
350 auto* session = GetActiveRomSession();
351 return session ? &session->editors : nullptr;
352}
353
354void* SessionCoordinator::GetSession(size_t index) const {
355 if (!IsValidSessionIndex(index)) {
356 return nullptr;
357 }
358 return sessions_[index].get();
359}
360
362 return session_count_ > 1;
363}
364
368
370 const std::string& filepath,
371 std::optional<size_t> excluded_session_id) const {
372 if (filepath.empty()) {
373 return absl::OkStatus();
374 }
375
376 for (const auto& session : sessions_) {
377 if (!session || !session->rom.is_loaded() ||
378 (excluded_session_id.has_value() &&
379 session->session_id() == *excluded_session_id)) {
380 continue;
381 }
382
383 const std::string rom_filepath = session->rom.filename();
384 if (PathsReferToSameBackingFile(filepath, session->filepath) ||
385 PathsReferToSameBackingFile(filepath, rom_filepath)) {
386 const std::string& owner_path =
387 session->filepath.empty() ? rom_filepath : session->filepath;
388 return absl::AlreadyExistsError(absl::StrFormat(
389 "ROM backing file '%s' is already open in session %zu ('%s')",
390 filepath, session->session_id(), owner_path));
391 }
392 }
393 return absl::OkStatus();
394}
395
397 const std::string& filepath) const {
398 return !CheckBackingFileAvailable(filepath).ok();
399}
400
402 if (sessions_.empty())
403 return;
404
406 return;
407
408 ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
409 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
410 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
411
412 if (!ImGui::Begin("Session Switcher", &show_session_switcher_)) {
413 ImGui::End();
414 return;
415 }
416
417 ImGui::Text(tr("%s Active Sessions (%zu)"), ICON_MD_TAB, session_count_);
418 ImGui::Separator();
419
420 for (size_t i = 0; i < sessions_.size(); ++i) {
421 bool is_active = (i == active_session_index_);
422
423 ImGui::PushID(static_cast<int>(i));
424
425 // Session tab
426 if (ImGui::Selectable(GetSessionDisplayName(i).c_str(), is_active)) {
427 if (editor_manager_) {
429 } else {
431 }
432 }
433
434 // Right-click context menu
435 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
436 ImGui::OpenPopup("SessionContextMenu");
437 }
438
439 if (ImGui::BeginPopup("SessionContextMenu")) {
441 ImGui::EndPopup();
442 }
443
444 ImGui::PopID();
445 }
446
447 ImGui::Separator();
448
449 // Action buttons
450 if (ImGui::Button(absl::StrFormat("%s New Session", ICON_MD_ADD).c_str())) {
452 }
453
454 ImGui::SameLine();
455 if (ImGui::Button(
456 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
458 }
459
460 ImGui::SameLine();
461 if (HasMultipleSessions() &&
462 ImGui::Button(absl::StrFormat("%s Close", ICON_MD_CLOSE).c_str())) {
464 }
465
466 ImGui::End();
467}
468
470 if (sessions_.empty())
471 return;
472
474 return;
475
476 ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
477 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
478 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
479
480 if (!ImGui::Begin("Session Manager", &show_session_manager_)) {
481 ImGui::End();
482 return;
483 }
484
485 // Session statistics
486 ImGui::Text(tr("%s Session Statistics"), ICON_MD_ANALYTICS);
487 ImGui::Separator();
488
489 ImGui::Text(tr("Total Sessions: %zu"), GetTotalSessionCount());
490 ImGui::Text(tr("Loaded Sessions: %zu"), GetLoadedSessionCount());
491 ImGui::Text(tr("Empty Sessions: %zu"), GetEmptySessionCount());
492
493 ImGui::Spacing();
494
495 // Session list
496 if (ImGui::BeginTable("SessionTable", 4,
497 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
498 ImGuiTableFlags_Resizable)) {
499 ImGui::TableSetupColumn("Session", ImGuiTableColumnFlags_WidthStretch,
500 0.3f);
501 ImGui::TableSetupColumn("ROM File", ImGuiTableColumnFlags_WidthStretch,
502 0.4f);
503 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.2f);
504 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
505 120.0f);
506 ImGui::TableHeadersRow();
507
508 for (size_t i = 0; i < sessions_.size(); ++i) {
509 const auto& session = sessions_[i];
510 bool is_active = (i == active_session_index_);
511
512 ImGui::PushID(static_cast<int>(i));
513
514 ImGui::TableNextRow();
515
516 // Session name
517 ImGui::TableNextColumn();
518 if (is_active) {
519 ImGui::TextColored(gui::GetSuccessColor(), "%s %s",
521 GetSessionDisplayName(i).c_str());
522 } else {
523 ImGui::Text("%s %s", ICON_MD_RADIO_BUTTON_UNCHECKED,
524 GetSessionDisplayName(i).c_str());
525 }
526
527 // ROM file
528 ImGui::TableNextColumn();
529 if (session->rom.is_loaded()) {
530 ImGui::Text("%s", session->filepath.c_str());
531 } else {
532 ImGui::TextDisabled(tr("(No ROM loaded)"));
533 }
534
535 // Status
536 ImGui::TableNextColumn();
537 if (IsSessionModified(i)) {
538 ImGui::TextColored(gui::GetWarningColor(), tr("Modified"));
539 } else if (session->rom.is_loaded()) {
540 ImGui::TextColored(gui::GetSuccessColor(), tr("Loaded"));
541 } else {
542 ImGui::TextColored(gui::GetWarningColor(), tr("Empty"));
543 }
544
545 // Actions
546 ImGui::TableNextColumn();
547 if (!is_active && ImGui::SmallButton(tr("Switch"))) {
548 if (editor_manager_) {
550 } else {
552 }
553 }
554
555 ImGui::SameLine();
556 if (HasMultipleSessions() && ImGui::SmallButton(tr("Close"))) {
557 if (editor_manager_) {
559 } else {
560 CloseSession(i);
561 }
562 }
563
564 ImGui::PopID();
565 }
566
567 ImGui::EndTable();
568 }
569
570 ImGui::End();
571}
572
575 return;
576
577 ImGui::SetNextWindowSize(ImVec2(300, 150), ImGuiCond_Always);
578 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
579 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
580
581 if (!ImGui::Begin("Rename Session", &show_session_rename_dialog_)) {
582 ImGui::End();
583 return;
584 }
585
586 ImGui::Text(tr("Rename session %zu:"), session_to_rename_);
587 ImGui::InputText(tr("Name"), session_rename_buffer_,
588 sizeof(session_rename_buffer_));
589
590 ImGui::Spacing();
591
592 if (ImGui::Button(tr("OK"))) {
595 session_rename_buffer_[0] = '\0';
596 }
597
598 ImGui::SameLine();
599 if (ImGui::Button(tr("Cancel"))) {
601 session_rename_buffer_[0] = '\0';
602 }
603
604 ImGui::End();
605}
606
608 if (sessions_.empty())
609 return;
610
611 if (gui::BeginThemedTabBar("SessionTabs")) {
612 for (size_t i = 0; i < sessions_.size(); ++i) {
613 bool is_active = (i == active_session_index_);
614 const auto& session = sessions_[i];
615
616 std::string tab_name = GetSessionDisplayName(i);
617 if (session->rom.is_loaded()) {
618 tab_name += " ";
619 tab_name += ICON_MD_CHECK_CIRCLE;
620 }
621 if (IsSessionModified(i)) {
622 tab_name += "*";
623 }
624
625 if (ImGui::BeginTabItem(tab_name.c_str())) {
626 if (!is_active) {
627 if (editor_manager_) {
629 } else {
631 }
632 }
633 ImGui::EndTabItem();
634 }
635
636 // Right-click context menu
637 if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
638 ImGui::OpenPopup(absl::StrFormat("SessionTabContext_%zu", i).c_str());
639 }
640
641 if (ImGui::BeginPopup(
642 absl::StrFormat("SessionTabContext_%zu", i).c_str())) {
644 ImGui::EndPopup();
645 }
646 }
648 }
649}
650
652 if (!HasMultipleSessions())
653 return;
654
655 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
656 ImVec4 accent_color = ConvertColorToImVec4(theme.accent);
657
658 {
659 gui::StyleColorGuard accent_guard(ImGuiCol_Text, accent_color);
660 ImGui::Text(tr("%s Session %zu"), ICON_MD_TAB, active_session_index_);
661 }
662
663 if (ImGui::IsItemHovered()) {
664 ImGui::SetTooltip(tr("Active Session: %s\nClick to open session switcher"),
666 }
667
668 if (ImGui::IsItemClicked()) {
670 }
671}
672
673std::string SessionCoordinator::GetSessionDisplayName(size_t index) const {
674 if (!IsValidSessionIndex(index)) {
675 return "Invalid Session";
676 }
677
678 const auto& session = sessions_[index];
679
680 if (!session->custom_name.empty()) {
681 return session->custom_name;
682 }
683
684 if (session->rom.is_loaded()) {
685 return absl::StrFormat(
686 "Session %zu (%s)", index,
687 std::filesystem::path(session->filepath).stem().string());
688 }
689
690 return absl::StrFormat("Session %zu (Empty)", index);
691}
692
696
698 const std::string& new_name) {
699 if (!IsValidSessionIndex(index) || new_name.empty())
700 return;
701
702 sessions_[index]->custom_name = new_name;
703 LOG_INFO("SessionCoordinator", "Renamed session %zu to '%s'", index,
704 new_name.c_str());
705}
706
708 const std::string& editor_name, size_t session_index) const {
709 if (sessions_.size() <= 1) {
710 // Single session - use simple name
711 return editor_name;
712 }
713
714 if (session_index >= sessions_.size()) {
715 return editor_name;
716 }
717
718 // Multi-session - include session identifier
719 const auto& session = sessions_[session_index];
720 std::string session_name = session->custom_name.empty()
721 ? session->rom.title()
722 : session->custom_name;
723
724 // Truncate long session names
725 if (session_name.length() > 20) {
726 session_name = session_name.substr(0, 17) + "...";
727 }
728
729 return absl::StrFormat("%s - %s##session_%zu", editor_name, session_name,
730 session_index);
731}
732
734 SwitchToSession(index);
735}
736
740
741// Panel coordination across sessions
747
753
754void SessionCoordinator::ShowPanelsInCategory(const std::string& category) {
755 if (window_manager_) {
757 }
758}
759
760void SessionCoordinator::HidePanelsInCategory(const std::string& category) {
761 if (window_manager_) {
763 }
764}
765
767 return index < sessions_.size();
768}
769
771 if (sessions_.empty())
772 return;
773
774 size_t original_session_idx = active_session_index_;
775 Editor* original_editor =
777
778 for (size_t session_idx = 0; session_idx < sessions_.size(); ++session_idx) {
779 auto& session = sessions_[session_idx];
780 const bool rom_loaded = session->rom.is_loaded();
781 // Skip empty sessions except the active one so pre-ROM tooling (e.g.
782 // Graphics prototype research) can still tick.
783 if (!rom_loaded && session_idx != active_session_index_) {
784 continue;
785 }
786
787 // Switch context
788 SwitchToSessionInternal(session_idx, /*transient=*/true);
789
790 for (auto editor : session->editors.active_editors_) {
791 if (*editor->active()) {
792 if (!rom_loaded &&
794 continue;
795 }
796
797 if (rom_loaded && editor->type() == EditorType::kOverworld) {
798 auto& overworld_editor = static_cast<OverworldEditor&>(*editor);
799 if (overworld_editor.jump_to_tab() != -1) {
800 // Set the dungeon editor to the jump to tab
801 session->editors.GetDungeonEditor()->add_room(
802 overworld_editor.jump_to_tab());
803 overworld_editor.jump_to_tab_ = -1;
804 }
805 }
806
807 // CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows
808 bool is_card_based_editor =
809 EditorManager::IsPanelBasedEditor(editor->type());
810
811 if (is_card_based_editor) {
812 // Panel-based editors create their own top-level windows
813 // No parent wrapper needed - this allows independent docking
814 if (editor_manager_) {
816 }
817
818 absl::Status status = editor->Update();
819
820 // Route editor errors to toast manager
821 if (!status.ok() && toast_manager_) {
822 std::string editor_name =
823 kEditorNames[static_cast<int>(editor->type())];
825 absl::StrFormat("%s Error: %s", editor_name, status.message()),
826 ToastType::kError, 8.0f);
827 }
828
829 } else {
830 // TRADITIONAL EDITORS: Wrap in Begin/End
831 std::string window_title = GenerateUniqueEditorTitle(
832 kEditorNames[static_cast<int>(editor->type())], session_idx);
833
834 // Set window to maximize on first open
835 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize,
836 ImGuiCond_FirstUseEver);
837 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos,
838 ImGuiCond_FirstUseEver);
839
840 if (ImGui::Begin(window_title.c_str(), editor->active(),
841 ImGuiWindowFlags_None)) { // Allow full docking
842 // Temporarily switch context for this editor's update
843 // (Already switched via SwitchToSession)
844 if (editor_manager_) {
846 }
847
848 absl::Status status = editor->Update();
849
850 // Route editor errors to toast manager
851 if (!status.ok() && toast_manager_) {
852 std::string editor_name =
853 kEditorNames[static_cast<int>(editor->type())];
854 toast_manager_->Show(absl::StrFormat("%s Error: %s", editor_name,
855 status.message()),
856 ToastType::kError, 8.0f);
857 }
858 }
859 ImGui::End();
860 }
861 }
862 }
863 }
864
865 // Restore original session context
866 SwitchToSessionInternal(original_session_idx, /*transient=*/true);
867 if (editor_manager_) {
868 editor_manager_->SetCurrentEditor(original_editor);
869 }
870}
871
872bool SessionCoordinator::IsSessionActive(size_t index) const {
873 return index == active_session_index_;
874}
875
876bool SessionCoordinator::IsSessionLoaded(size_t index) const {
877 return IsValidSessionIndex(index) && sessions_[index]->rom.is_loaded();
878}
879
883
885 size_t count = 0;
886 for (const auto& session : sessions_) {
887 if (session->rom.is_loaded()) {
888 count++;
889 }
890 }
891 return count;
892}
893
897
898absl::Status SessionCoordinator::LoadRomIntoSession(const std::string& filename,
899 size_t session_index) {
900 if (filename.empty()) {
901 return absl::InvalidArgumentError("Invalid parameters");
902 }
903
904 size_t target_index =
905 (session_index == SIZE_MAX) ? active_session_index_ : session_index;
906 if (!IsValidSessionIndex(target_index)) {
907 return absl::InvalidArgumentError("Invalid session index");
908 }
909
910 // TODO: Implement actual ROM loading
911 LOG_INFO("SessionCoordinator", "LoadRomIntoSession: %s -> session %zu",
912 filename.c_str(), target_index);
913
914 return absl::OkStatus();
915}
916
918 const std::string& filename) {
920 return absl::FailedPreconditionError("No active session");
921 }
922
923 // TODO: Implement actual ROM saving
924 LOG_INFO("SessionCoordinator", "SaveActiveSession: session %zu",
926
927 return absl::OkStatus();
928}
929
930absl::Status SessionCoordinator::SaveSessionAs(size_t session_index,
931 const std::string& filename) {
932 if (!IsValidSessionIndex(session_index) || filename.empty()) {
933 return absl::InvalidArgumentError("Invalid parameters");
934 }
935
936 // TODO: Implement actual ROM saving
937 LOG_INFO("SessionCoordinator", "SaveSessionAs: session %zu -> %s",
938 session_index, filename.c_str());
939
940 return absl::OkStatus();
941}
942
944 Rom&& rom, const std::string& filepath) {
945 auto path_status = CheckBackingFileAvailable(filepath);
946 if (!path_status.ok()) {
947 return path_status;
948 }
949
950 const size_t new_session_id = next_session_id_++;
951 const size_t new_session_index = sessions_.size();
952 sessions_.push_back(std::make_unique<RomSession>(
953 std::move(rom), user_settings_, new_session_id, editor_registry_));
954 auto& session = sessions_.back();
955 session->filepath = filepath;
956
958 // Let observers attach project/runtime context before the session becomes
959 // active. This prevents a switch from briefly exposing an unbound ROM.
960 NotifySessionCreated(new_session_index, session.get());
961
962 if (sessions_.size() == 1) {
963 active_session_index_ = new_session_index;
964 if (window_manager_) {
965 window_manager_->SetActiveSession(GetSessionId(new_session_index));
966 }
967 NotifySessionSwitched(new_session_index, new_session_index, session.get(),
968 /*transient=*/false);
969 } else {
970 SwitchToSession(new_session_index);
971 }
972
973 NotifySessionRomLoaded(new_session_index, session.get());
974
975 return session.get();
976}
977
978absl::Status SessionCoordinator::DiscardProvisionalSession(size_t session_id) {
979 auto session_it =
980 std::find_if(sessions_.begin(), sessions_.end(),
981 [session_id](const std::unique_ptr<RomSession>& session) {
982 return session && session->session_id() == session_id;
983 });
984 if (session_it == sessions_.end()) {
985 return absl::NotFoundError(
986 absl::StrFormat("Session %zu is no longer available", session_id));
987 }
988
989 const size_t index =
990 static_cast<size_t>(std::distance(sessions_.begin(), session_it));
991 const bool closing_active_session = index == active_session_index_;
992 if (window_manager_) {
994 }
995 NotifySessionClosed(index);
996 sessions_.erase(session_it);
998
999 if (sessions_.empty()) {
1001 NotifySessionSwitched(index, 0, nullptr, /*transient=*/false);
1002 } else {
1003 if (active_session_index_ >= index && active_session_index_ > 0) {
1005 }
1006 if (window_manager_) {
1008 }
1009 if (closing_active_session) {
1012 /*transient=*/false);
1013 }
1014 }
1015
1016 LOG_WARN("SessionCoordinator",
1017 "Discarded provisional session %zu after failed open", session_id);
1018 return absl::OkStatus();
1019}
1020
1022 // Mark empty sessions as closed (except keep at least one)
1023 size_t loaded_count = 0;
1024 for (const auto& session : sessions_) {
1025 if (session->rom.is_loaded()) {
1026 loaded_count++;
1027 }
1028 }
1029
1030 if (loaded_count > 0) {
1031 for (auto it = sessions_.begin(); it != sessions_.end();) {
1032 if (!(*it)->rom.is_loaded() && sessions_.size() > 1) {
1033 it = sessions_.erase(it);
1034 } else {
1035 ++it;
1036 }
1037 }
1038 }
1039
1041 LOG_INFO("SessionCoordinator", "Cleaned up closed sessions (remaining: %zu)",
1043}
1044
1046 if (sessions_.empty())
1047 return;
1048
1049 // Unregister all session cards
1050 if (window_manager_) {
1051 for (const auto& session : sessions_) {
1052 window_manager_->UnregisterSession(session->session_id());
1053 }
1054 }
1055
1056 sessions_.clear();
1059
1060 LOG_INFO("SessionCoordinator", "Cleared all sessions");
1061}
1062
1064 if (sessions_.empty())
1065 return;
1066
1067 size_t next_index = (active_session_index_ + 1) % sessions_.size();
1068 SwitchToSession(next_index);
1069}
1070
1072 if (sessions_.empty())
1073 return;
1074
1075 size_t prev_index = (active_session_index_ == 0) ? sessions_.size() - 1
1077 SwitchToSession(prev_index);
1078}
1079
1081 if (sessions_.empty())
1082 return;
1083 SwitchToSession(0);
1084}
1085
1087 if (sessions_.empty())
1088 return;
1089 SwitchToSession(sessions_.size() - 1);
1090}
1091
1093 if (!sessions_.empty() && active_session_index_ >= sessions_.size()) {
1094 active_session_index_ = sessions_.size() - 1;
1095 }
1096}
1097
1099 if (!IsValidSessionIndex(index)) {
1100 throw std::out_of_range(
1101 absl::StrFormat("Invalid session index: %zu", index));
1102 }
1103}
1104
1106 const std::string& base_name) const {
1107 if (sessions_.empty())
1108 return base_name;
1109
1110 std::string name = base_name;
1111 int counter = 1;
1112
1113 while (true) {
1114 bool found = false;
1115 for (const auto& session : sessions_) {
1116 if (session->custom_name == name) {
1117 found = true;
1118 break;
1119 }
1120 }
1121
1122 if (!found)
1123 break;
1124
1125 name = absl::StrFormat("%s %d", base_name, counter++);
1126 }
1127
1128 return name;
1129}
1130
1132 if (toast_manager_) {
1134 absl::StrFormat("Maximum %zu sessions allowed", kMaxSessions),
1136 }
1137}
1138
1140 const std::string& operation, bool success) {
1141 if (toast_manager_) {
1142 std::string message =
1143 absl::StrFormat("%s %s", operation, success ? "succeeded" : "failed");
1145 toast_manager_->Show(message, type);
1146 }
1147}
1148
1149void SessionCoordinator::DrawSessionTab(size_t index, bool is_active) {
1150 if (index >= sessions_.size())
1151 return;
1152
1153 const auto& session = sessions_[index];
1154
1155 ImVec4 color = GetSessionColor(index);
1156 gui::StyleColorGuard tab_color_guard(ImGuiCol_Text, color);
1157
1158 std::string tab_name = GetSessionDisplayName(index);
1159 if (session->rom.is_loaded()) {
1160 tab_name += " ";
1161 tab_name += ICON_MD_CHECK_CIRCLE;
1162 }
1163 if (IsSessionModified(index)) {
1164 tab_name += "*";
1165 }
1166
1167 if (ImGui::BeginTabItem(tab_name.c_str())) {
1168 if (!is_active) {
1169 if (editor_manager_) {
1171 } else {
1172 SwitchToSession(index);
1173 }
1174 }
1175 ImGui::EndTabItem();
1176 }
1177}
1178
1180 if (ImGui::MenuItem(
1181 absl::StrFormat("%s Switch to Session", ICON_MD_TAB).c_str())) {
1182 if (editor_manager_) {
1184 } else {
1185 SwitchToSession(index);
1186 }
1187 }
1188
1189 if (ImGui::MenuItem(absl::StrFormat("%s Rename", ICON_MD_EDIT).c_str())) {
1190 session_to_rename_ = index;
1191 strncpy(session_rename_buffer_, GetSessionDisplayName(index).c_str(),
1192 sizeof(session_rename_buffer_) - 1);
1195 }
1196
1197 if (ImGui::MenuItem(
1198 absl::StrFormat("%s Duplicate", ICON_MD_CONTENT_COPY).c_str())) {
1199 // TODO: Implement session duplication
1200 }
1201
1202 ImGui::Separator();
1203
1204 if (HasMultipleSessions() &&
1205 ImGui::MenuItem(
1206 absl::StrFormat("%s Close Session", ICON_MD_CLOSE).c_str())) {
1207 if (editor_manager_) {
1209 } else {
1210 CloseSession(index);
1211 }
1212 }
1213}
1214
1216 if (index >= sessions_.size())
1217 return;
1218
1219 const auto& session = sessions_[index];
1220 ImVec4 color = GetSessionColor(index);
1221
1222 gui::StyleColorGuard badge_guard(ImGuiCol_Text, color);
1223
1224 if (session->rom.is_loaded()) {
1225 ImGui::Text("%s", ICON_MD_CHECK_CIRCLE);
1226 } else {
1227 ImGui::Text("%s", ICON_MD_RADIO_BUTTON_UNCHECKED);
1228 }
1229}
1230
1231ImVec4 SessionCoordinator::GetSessionColor(size_t index) const {
1232 // Generate consistent colors for sessions
1233 static const ImVec4 colors[] = {
1234 ImVec4(0.0f, 1.0f, 0.0f, 1.0f), // Green
1235 ImVec4(0.0f, 0.5f, 1.0f, 1.0f), // Blue
1236 ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // Orange
1237 ImVec4(1.0f, 0.0f, 1.0f, 1.0f), // Magenta
1238 ImVec4(1.0f, 1.0f, 0.0f, 1.0f), // Yellow
1239 ImVec4(0.0f, 1.0f, 1.0f, 1.0f), // Cyan
1240 ImVec4(1.0f, 0.0f, 0.0f, 1.0f), // Red
1241 ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Gray
1242 };
1243
1244 return colors[index % (sizeof(colors) / sizeof(colors[0]))];
1245}
1246
1247std::string SessionCoordinator::GetSessionIcon(size_t index) const {
1248 if (index >= sessions_.size())
1250
1251 const auto& session = sessions_[index];
1252
1253 if (session->rom.is_loaded()) {
1254 return ICON_MD_CHECK_CIRCLE;
1255 } else {
1257 }
1258}
1259
1260bool SessionCoordinator::IsSessionEmpty(size_t index) const {
1261 return IsValidSessionIndex(index) && !sessions_[index]->rom.is_loaded();
1262}
1263
1264bool SessionCoordinator::IsSessionClosed(size_t index) const {
1265 return !IsValidSessionIndex(index);
1266}
1267
1269 if (!IsValidSessionIndex(index)) {
1270 return false;
1271 }
1272
1273 const auto& session = sessions_[index];
1274 if (session->rom.is_loaded() && session->rom.dirty()) {
1275 return true;
1276 }
1277
1278 if (gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
1279 return true;
1280 }
1281
1282 if (auto* dungeon_editor =
1283 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
1284 return dungeon_editor->HasPendingRoomChanges();
1285 }
1286
1287 return false;
1288}
1289
1290} // namespace editor
1291} // 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:144
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:319
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:107
#define LOG_INFO(category, format,...)
Definition log.h:105
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