yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
editor_manager.cc
Go to the documentation of this file.
1// Related header
2#include "editor_manager.h"
3
4// C system headers
5#include <cctype>
6#include <cstdint>
7#include <cstdio>
8#include <cstring>
9#include <ctime>
10
11// C++ standard library headers
12#include <algorithm>
13#include <array>
14#include <chrono>
15#include <cstdlib>
16#include <exception>
17#include <filesystem>
18#include <fstream>
19#include <functional>
20#include <initializer_list>
21#include <memory>
22#include <optional>
23#include <sstream>
24#include <string>
25#include <unordered_map>
26#include <unordered_set>
27#include <utility>
28#include <vector>
29
30#ifdef __APPLE__
31#include <TargetConditionals.h>
32#endif
33
34// Third-party library headers
35#define IMGUI_DEFINE_MATH_OPERATORS
36#include "absl/status/status.h"
37#include "absl/status/statusor.h"
38#include "absl/strings/ascii.h"
39#include "absl/strings/match.h"
40#include "absl/strings/str_format.h"
41#include "absl/strings/str_join.h"
42#include "absl/strings/str_split.h"
43#include "absl/strings/string_view.h"
44#include "imgui/imgui.h"
45
46// Project headers
47#include "app/application.h"
49#include "app/editor/editor.h"
75#include "app/emu/emulator.h"
81#include "app/gui/core/icons.h"
86#include "app/platform/timing.h"
88#include "core/features.h"
90#include "core/project.h"
91#include "core/rom_settings.h"
100#include "rom/rom.h"
101#include "rom/rom_diff.h"
102#include "rom/transaction.h"
103#include "startup_flags.h"
104#include "util/file_util.h"
106#include "util/log.h"
107#include "util/macro.h"
108#include "util/rom_hash.h"
109#include "yaze_config.h"
115#include "zelda3/game_data.h"
122#include "zelda3/sprite/sprite.h"
123
124// Conditional platform headers
125#ifdef __EMSCRIPTEN__
129#endif
130
131// Conditional test headers
132#ifdef YAZE_ENABLE_TESTING
138#endif
139#ifdef YAZE_ENABLE_GTEST
141#endif
142#ifdef YAZE_WITH_GRPC
144#endif
145
146// Conditional agent UI headers
147#ifdef YAZE_BUILD_AGENT_UI
149#endif
150
151namespace yaze::editor {
152
153namespace {
154std::string LastNonEmptyLine(const std::string& text) {
155 std::vector<std::string> lines = absl::StrSplit(text, '\n');
156 for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
157 std::string line = std::string(*it);
158 absl::StripAsciiWhitespace(&line);
159 if (!line.empty()) {
160 return line;
161 }
162 }
163 return "";
164}
165
167 ProjectManagementPanel* project_panel,
168 const ProjectWorkflowStatus& status) {
170 if (status_bar != nullptr) {
171 status_bar->SetBuildStatus(status);
172 }
173 if (project_panel != nullptr) {
174 project_panel->SetBuildStatus(status);
175 }
176}
177
179 ProjectManagementPanel* project_panel,
180 const ProjectWorkflowStatus& status) {
182 if (status_bar != nullptr) {
183 status_bar->SetRunStatus(status);
184 }
185 if (project_panel != nullptr) {
186 project_panel->SetRunStatus(status);
187 }
188}
189
190void AppendWorkflowHistoryEntry(const std::string& kind,
191 const ProjectWorkflowStatus& status,
192 const std::string& output_log) {
194 {.kind = kind,
195 .status = status,
196 .output_log = output_log,
197 .timestamp = std::chrono::system_clock::now()});
198}
199
201 std::unordered_map<std::string, float>* preferences,
202 const std::unordered_map<std::string, float>& drawer_widths) {
203 if (preferences == nullptr) {
204 return;
205 }
206 constexpr std::array<const char*, 9> kDrawerWidthKeys = {
207 "right_sidebar.shared", "agent_chat", "proposals", "settings", "help",
208 "notifications", "properties", "project", "tool_output",
209 };
210 for (const char* key : kDrawerWidthKeys) {
211 preferences->erase(key);
212 }
213 for (const auto& [key, width] : drawer_widths) {
214 preferences->insert_or_assign(key, width);
215 }
216}
217
218std::string StripSessionPrefix(absl::string_view panel_id) {
219 if (panel_id.size() > 2 && panel_id[0] == 's' &&
220 absl::ascii_isdigit(panel_id[1])) {
221 const size_t dot = panel_id.find('.');
222 if (dot != absl::string_view::npos) {
223 return std::string(panel_id.substr(dot + 1));
224 }
225 }
226 return std::string(panel_id);
227}
228
230 namespace fs = std::filesystem;
231
232 std::vector<fs::path> roots;
233 if (const char* home = std::getenv("HOME");
234 home != nullptr && std::strlen(home) > 0) {
235 roots.push_back(fs::path(home) / "src" / "hobby" / "oracle-of-secrets");
236 }
237
238 std::vector<fs::path> candidates;
239 std::unordered_set<std::string> seen;
240 auto add_candidate = [&](const fs::path& path) {
241 std::error_code ec;
242 if (!fs::exists(path, ec) || ec) {
243 return;
244 }
245 std::string ext = path.extension().string();
246 absl::AsciiStrToLower(&ext);
247 if (ext != ".yaze" && ext != ".yazeproj") {
248 return;
249 }
250 const std::string normalized = fs::weakly_canonical(path, ec).string();
251 const std::string key = normalized.empty() ? path.string() : normalized;
252 if (key.empty() || seen.count(key) > 0) {
253 return;
254 }
255 seen.insert(key);
256 candidates.push_back(path);
257 };
258
259 for (const auto& root : roots) {
260 std::error_code ec;
261 if (!fs::exists(root, ec) || ec) {
262 continue;
263 }
264
265 // Priority candidates first.
266 add_candidate(root / "Oracle-of-Secrets.yaze");
267 add_candidate(root / "Oracle of Secrets.yaze");
268 add_candidate(root / "Oracle-of-Secrets.yazeproj");
269 add_candidate(root / "Oracle of Secrets.yazeproj");
270 add_candidate(root / "Roms" / "Oracle of Secrets.yaze");
271
272 // Also detect additional local project files nearby.
273 fs::recursive_directory_iterator it(
274 root, fs::directory_options::skip_permission_denied, ec);
275 fs::recursive_directory_iterator end;
276 if (ec) {
277 continue;
278 }
279 for (; it != end; it.increment(ec)) {
280 if (ec) {
281 ec.clear();
282 continue;
283 }
284 if (it.depth() > 2) {
285 it.disable_recursion_pending();
286 continue;
287 }
288 add_candidate(it->path());
289 }
290 }
291
292 if (candidates.empty()) {
293 return;
294 }
295
297 // Add in reverse so the first candidate remains most recent.
298 for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
299 manager.AddFile(it->string());
300 }
301 manager.Save();
302}
303
304} // namespace
305
306std::optional<EditorType> ParseEditorTypeFromString(absl::string_view name) {
307 std::string normalized = std::string(name);
308 absl::StripAsciiWhitespace(&normalized);
309 const std::string lower = absl::AsciiStrToLower(normalized);
310 for (int i = 0; i < static_cast<int>(kEditorTypeCount); ++i) {
311 const auto type = static_cast<EditorType>(i);
312 const std::string name_candidate =
313 absl::AsciiStrToLower(EditorRegistry::GetEditorName(type));
314 const std::string category_candidate =
315 absl::AsciiStrToLower(EditorRegistry::GetEditorCategory(type));
316 if (name_candidate == lower || category_candidate == lower) {
317 return type;
318 }
319 }
320 return std::nullopt;
321}
322
323// Static registry of editors that use the card-based layout system
324// These editors register their cards with WorkspaceWindowManager and manage their
325// own windows They do NOT need the traditional ImGui::Begin/End wrapper - they
326// create cards internally
330
334
335void EditorManager::ApplyLayoutPreset(const std::string& preset_name) {
337}
338
339bool EditorManager::ApplyLayoutProfile(const std::string& profile_id) {
340 if (!layout_manager_) {
341 return false;
342 }
343
344 const size_t session_id = GetCurrentSessionId();
345 const EditorType current_type =
347
348 LayoutProfile applied_profile;
349 if (!layout_manager_->ApplyBuiltInProfile(profile_id, session_id,
350 current_type, &applied_profile)) {
351 return false;
352 }
353
354 if (applied_profile.open_agent_chat && right_drawer_manager_) {
355 right_drawer_manager_->OpenDrawer(
357 const float default_width = RightDrawerManager::GetDefaultDrawerWidth(
359 right_drawer_manager_->SetDrawerWidth(
361 std::max(default_width, 480.0f));
362 }
363
365 absl::StrFormat("Layout Profile: %s", applied_profile.label),
367 return true;
368}
369
371 if (!layout_manager_) {
372 return;
373 }
374 layout_manager_->CaptureTemporarySessionLayout(GetCurrentSessionId());
375 toast_manager_.Show("Captured temporary layout snapshot", ToastType::kInfo);
376}
377
378void EditorManager::RestoreTemporaryLayoutSnapshot(bool clear_after_restore) {
379 if (!layout_manager_) {
380 return;
381 }
382
383 if (layout_manager_->RestoreTemporarySessionLayout(GetCurrentSessionId(),
384 clear_after_restore)) {
385 toast_manager_.Show("Restored temporary layout snapshot",
387 } else {
388 toast_manager_.Show("No temporary layout snapshot available",
390 }
391}
392
394 if (!layout_manager_) {
395 return;
396 }
397 layout_manager_->ClearTemporarySessionLayout();
398 toast_manager_.Show("Cleared temporary layout snapshot", ToastType::kInfo);
399}
400
401bool EditorManager::SaveLayoutSnapshotAs(const std::string& name) {
402 if (!layout_manager_)
403 return false;
404 if (name.empty()) {
405 toast_manager_.Show("Snapshot name cannot be empty", ToastType::kWarning);
406 return false;
407 }
408 const bool ok =
409 layout_manager_->SaveNamedSnapshot(name, GetCurrentSessionId());
410 if (ok) {
411 toast_manager_.Show(absl::StrFormat("Saved snapshot '%s'", name),
413 } else {
414 toast_manager_.Show(absl::StrFormat("Failed to save snapshot '%s'", name),
416 }
417 return ok;
418}
419
420bool EditorManager::RestoreLayoutSnapshot(const std::string& name,
421 bool remove_after_restore) {
422 if (!layout_manager_)
423 return false;
424 const bool ok = layout_manager_->RestoreNamedSnapshot(
425 name, GetCurrentSessionId(), remove_after_restore);
426 if (ok) {
427 toast_manager_.Show(absl::StrFormat("Restored snapshot '%s'", name),
429 } else {
430 toast_manager_.Show(absl::StrFormat("Snapshot '%s' not available", name),
432 }
433 return ok;
434}
435
436bool EditorManager::DeleteLayoutSnapshot(const std::string& name) {
437 if (!layout_manager_)
438 return false;
439 const bool ok = layout_manager_->DeleteNamedSnapshot(name);
440 if (ok) {
441 toast_manager_.Show(absl::StrFormat("Deleted snapshot '%s'", name),
443 }
444 return ok;
445}
446
447std::vector<std::string> EditorManager::ListLayoutSnapshots() const {
448 if (!layout_manager_)
449 return {};
450 return layout_manager_->ListNamedSnapshots(GetCurrentSessionId());
451}
452
454 if (!layout_manager_) {
455 return;
456 }
457
459 layout_manager_->SetProjectLayoutKey(
461 } else {
462 layout_manager_->UseGlobalLayouts();
463 }
464}
465
470
476
478 if (!current_editor_) {
479 toast_manager_.Show("No active editor to reset", ToastType::kWarning);
480 return;
481 }
482 // Choosing defaults ends automatic reapplication of a named layout. Keep
483 // the named layout itself; the recovery snapshot also retains live docking.
485 settings_dirty_ = true;
489}
490
491#ifdef YAZE_BUILD_AGENT_UI
492void EditorManager::ShowAIAgent() {
497 "AI Agent is experimental. It remains available; enable experimental "
498 "features in Settings to acknowledge this warning.",
500 }
501 // Apply saved agent settings from the current project when opening the Agent
502 // UI to respect the user's preferred provider/model.
503 // TODO: Implement LoadAgentSettingsFromProject in AgentChat or AgentEditor
507 for (const auto& window_id :
508 LayoutPresets::GetDefaultWindows(EditorType::kAgent)) {
509 window_manager_.OpenWindow(window_id);
510 }
511}
512
513void EditorManager::ShowChatHistory() {
515}
516#endif
517
519 : project_manager_(&toast_manager_), rom_file_manager_(&toast_manager_) {
520 std::stringstream ss;
521 ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "."
522 << YAZE_VERSION_PATCH;
523 ss >> version_;
524
525 // Initialize Core Context
526 editor_context_ = std::make_unique<GlobalEditorContext>(event_bus_);
529
533
534 // STEP 5: ShortcutConfigurator created later in Initialize() method
535 // It depends on all above coordinators being available
536}
537
540
541 // STEP 1: Initialize PopupManager FIRST
542 popup_manager_ = std::make_unique<PopupManager>(this);
543 popup_manager_->Initialize(); // Registers all popups with PopupID constants
544
545 // STEP 2: Initialize SessionCoordinator (independent of popups)
546 session_coordinator_ = std::make_unique<SessionCoordinator>(
548 session_coordinator_->SetEditorRegistry(&editor_registry_);
549
550 // STEP 3: Initialize MenuOrchestrator (depends on popup_manager_,
551 // session_coordinator_)
552 menu_orchestrator_ = std::make_unique<MenuOrchestrator>(
555
556 // Wire up the window manager for the View menu window listing
557 menu_orchestrator_->SetWindowManager(&window_manager_);
558 menu_orchestrator_->SetStatusBar(&status_bar_);
559 menu_orchestrator_->SetUserSettings(&user_settings_);
560 menu_orchestrator_->SetShortcutManager(&shortcut_manager_);
561
562 session_coordinator_->SetEditorManager(this);
563 session_coordinator_->SetEventBus(&event_bus_); // Enable event publishing
565 &event_bus_); // Global event bus access
566
567 // Expose UserSettings to cross-editor panels that don't get constructor
568 // injection (e.g. Layout Designer, which reads named_layouts). The
569 // LayoutManager setter happens below once the unique_ptr is constructed.
571
572 // STEP 3.5: Initialize RomLifecycleManager (depends on popup_manager_,
573 // session_coordinator_, rom_file_manager_, toast_manager_)
575 .rom_file_manager = &rom_file_manager_,
576 .session_coordinator = session_coordinator_.get(),
579 .project = &current_project_,
580 });
581
582 // STEP 4: Initialize UICoordinator (depends on popup_manager_,
583 // session_coordinator_, window_manager_)
584 ui_coordinator_ = std::make_unique<UICoordinator>(
588
589 // STEP 4.5: Initialize LayoutManager (DockBuilder layouts for editors)
590 layout_manager_ = std::make_unique<LayoutManager>();
591 layout_manager_->SetWindowManager(&window_manager_);
592 layout_manager_->UseGlobalLayouts();
593 // Expose to cross-editor panels that drive the live dockspace (Layout
594 // Designer). Must land after the unique_ptr is constructed.
598 [this](const std::string& preset_name) {
599 ApplyLayoutPreset(preset_name);
600 });
602 [this](const std::string& preset_name) {
603 ApplyLayoutPreset(preset_name);
604 });
605
606 // STEP 4.6: Initialize RightDrawerManager (right-side sliding drawers)
607 right_drawer_manager_ = std::make_unique<RightDrawerManager>();
608 right_drawer_manager_->SetToastManager(&toast_manager_);
609 right_drawer_manager_->SetProposalDrawer(&proposal_drawer_);
611 right_drawer_manager_->SetShortcutManager(&shortcut_manager_);
612 right_drawer_manager_->SetSettingsPanelProvider([this]() -> SettingsPanel* {
613 // The drawers button and the palette toggle open the Settings drawer
614 // directly; on the Welcome screen there is no session yet. Create an
615 // empty one, as SwitchToEditor does for ROM-less editors (BUG-020).
616 if (session_coordinator_ && GetCurrentEditorSet() == nullptr) {
617 session_coordinator_->CreateNewSession();
618 }
619 auto* editor_set = GetCurrentEditorSet();
620 return editor_set ? editor_set->GetSettingsPanel() : nullptr;
621 });
623 [this](const std::string& prompt) {
624#if defined(YAZE_BUILD_AGENT_UI)
625 auto* agent_editor = agent_ui_.GetAgentEditor();
626 if (!agent_editor) {
627 return;
628 }
629 auto* chat = agent_editor->GetAgentChat();
630 if (!chat) {
631 return;
632 }
633 chat->set_active(true);
634 chat->SendMessage(prompt);
635#else
636 (void)prompt;
637#endif
638 },
639 [this]() {
640#if defined(YAZE_BUILD_AGENT_UI)
642 right_drawer_manager_->OpenDrawer(
644 }
645#endif
646 });
649 right_drawer_manager_->ToggleDrawer(
651 } else {
653 }
654 });
655
656 // Initialize ProjectManagementPanel for project/version management
657 project_management_panel_ = std::make_unique<ProjectManagementPanel>();
658 project_management_panel_->SetToastManager(&toast_manager_);
660 [this](const std::string& filepath, const std::string& contents) {
661 return PrepareRawProjectFileSave(filepath, contents);
662 });
664 [this](const std::string& filepath, const std::string& contents) {
665 return CommitRawProjectFileSave(filepath, contents);
666 });
668 std::make_unique<workflow::ProjectWorkflowOutputPanel>());
669 project_management_panel_->SetSwapRomCallback([this]() {
670 // Prompt user to select a new ROM for the project
673 if (!rom_path.empty()) {
674 auto status = SwapProjectRom(rom_path);
675 if (status.ok()) {
676 toast_manager_.Show("Project ROM updated and reloaded.",
678 } else {
679 toast_manager_.Show(absl::StrFormat("Failed to update project ROM: %s",
680 status.message()),
682 }
683 }
684 });
685 project_management_panel_->SetReloadRomCallback([this]() {
688 auto status = ReloadProjectRom();
689 if (!status.ok()) {
691 absl::StrFormat("Failed to reload ROM: %s", status.message()),
693 }
694 }
695 });
696 project_management_panel_->SetSaveProjectCallback([this]() {
697 auto status = SaveProject();
698 if (status.ok()) {
699 toast_manager_.Show("Project saved", ToastType::kSuccess);
700 } else {
702 absl::StrFormat("Failed to save project: %s", status.message()),
704 }
705 return status;
706 });
707 project_management_panel_->SetBrowseFolderCallback(
708 [this](const std::string& type) {
710 if (!folder_path.empty()) {
711 if (type == "code") {
712 current_project_.code_folder = folder_path;
713 // Update assembly editor path
714 if (auto* editor_set = GetCurrentEditorSet()) {
715// iOS: avoid blocking folder enumeration on the UI thread.
716#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
717 editor_set->OpenAssemblyFolder(folder_path);
718#endif
719 window_manager_.SetFileBrowserPath("Assembly", folder_path);
720 }
721 } else if (type == "assets") {
722 current_project_.assets_folder = folder_path;
723 }
727 }
728 toast_manager_.Show(absl::StrFormat("%s folder set: %s", type.c_str(),
729 folder_path.c_str()),
731 }
732 });
733 project_management_panel_->SetBuildProjectCallback(
734 [this]() { QueueBuildCurrentProject(); });
735 project_management_panel_->SetCancelBuildCallback(
736 [this]() { CancelQueuedProjectBuild(); });
737 project_management_panel_->SetRunProjectCallback(
738 [this]() { (void)RunCurrentProject(); });
740 [this]() { QueueBuildCurrentProject(); });
742 [this]() { (void)RunCurrentProject(); });
744 window_manager_.OpenWindow("workflow.output");
745 window_manager_.MarkWindowRecentlyUsed("workflow.output");
746 });
748 [this]() { CancelQueuedProjectBuild(); });
749 right_drawer_manager_->SetProjectManagementPanel(
751
752 // STEP 4.6.1: Initialize LayoutCoordinator (facade for layout operations)
754 layout_deps.layout_manager = layout_manager_.get();
755 layout_deps.window_manager = &window_manager_;
756 layout_deps.ui_coordinator = ui_coordinator_.get();
757 layout_deps.toast_manager = &toast_manager_;
758 layout_deps.status_bar = &status_bar_;
760 layout_coordinator_.Initialize(layout_deps);
761
762 // STEP 4.6.2: Initialize EditorActivator (editor switching and jump navigation)
763 EditorActivator::Dependencies activator_deps;
764 activator_deps.window_manager = &window_manager_;
765 activator_deps.layout_manager = layout_manager_.get();
766 activator_deps.ui_coordinator = ui_coordinator_.get();
767 activator_deps.right_drawer_manager = right_drawer_manager_.get();
768 activator_deps.toast_manager = &toast_manager_;
769 activator_deps.event_bus = &event_bus_;
770 activator_deps.ensure_editor_assets_loaded = [this](EditorType type) {
771 return EnsureEditorAssetsLoaded(type);
772 };
773 activator_deps.get_current_editor_set = [this]() {
774 return GetCurrentEditorSet();
775 };
776 activator_deps.get_current_session_id = [this]() {
777 return GetCurrentSessionId();
778 };
779 activator_deps.queue_deferred_action = [this](std::function<void()> action) {
780 QueueDeferredAction(std::move(action));
781 };
782 editor_activator_.Initialize(activator_deps);
783
784 // STEP 4.7: Initialize ActivityBar
785 activity_bar_ = std::make_unique<ActivityBar>(
787 [this]() -> bool {
788 if (auto* editor_set = GetCurrentEditorSet()) {
789 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
791 return dungeon_editor->IsWorkbenchWorkflowEnabled();
792 }
793 }
794 return false;
795 },
796 [this](bool enabled) {
797 if (auto* editor_set = GetCurrentEditorSet()) {
798 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
800 dungeon_editor->QueueWorkbenchWorkflowMode(enabled);
801 }
802 }
803 },
804 [this](const std::string& category) {
805 return GetEditorContextSnapshot(category);
806 });
807
808 // Wire per-user sidebar prefs so right-click / drag mutate persisted state.
809 activity_bar_->SetUserSettings(&user_settings_);
810
811 // Populate the MoreActions registry with the default rail entries. External
812 // callers can Register/Unregister further entries at runtime.
813 auto& registry = activity_bar_->actions_registry();
814 registry.Register({"command_palette",
815 "Command Palette",
818 {}});
819 registry.Register({"keyboard_shortcuts",
820 "Keyboard Shortcuts",
823 {}});
824 registry.Register({"open_rom_project",
825 "Open ROM / Project",
827 [this]() { window_manager_.TriggerOpenRom(); },
828 {}});
829 registry.Register({"settings",
830 "Settings",
832 [this]() { window_manager_.TriggerShowSettings(); },
833 {}});
834
835 // WindowHost is the declarative window registration surface used by
836 // editor/runtime systems.
837 window_host_ = std::make_unique<WindowHost>(&window_manager_);
838
839 // Wire up EventBus to WorkspaceWindowManager for action event publishing
841}
842
844 // Auto-register panels from ContentRegistry (Unified Panel System)
845 auto registry_panels = ContentRegistry::Panels::CreateAll();
846 for (auto& panel : registry_panels) {
848 }
849
850 // STEP 4.8: Initialize DashboardPanel
851 dashboard_panel_ = std::make_unique<DashboardPanel>(this);
852
853 if (window_host_) {
854 WindowDefinition dashboard_definition;
855 dashboard_definition.id = "dashboard.main";
856 dashboard_definition.display_name = "Dashboard";
857 dashboard_definition.icon = ICON_MD_DASHBOARD;
858 dashboard_definition.category = "Dashboard";
859 dashboard_definition.window_title = " Dashboard";
860 dashboard_definition.shortcut_hint = "Ctrl+E";
861 dashboard_definition.priority = 0;
862 dashboard_definition.visibility_flag = dashboard_panel_->visibility_flag();
863 dashboard_definition.presentation =
865 window_host_->RegisterWindow(dashboard_definition);
866 } else {
868 {.card_id = "dashboard.main",
869 .display_name = "Dashboard",
870 .window_title = " Dashboard",
871 .icon = ICON_MD_DASHBOARD,
872 .category = "Dashboard",
874 .shortcut_hint = "Ctrl+E",
875 .visibility_flag = dashboard_panel_->visibility_flag(),
876 .priority = 0});
877 }
878}
879
881 // Subscribe to session lifecycle events via EventBus
882 // (replaces SessionObserver pattern)
884 [this](const SessionSwitchedEvent& e) {
885 HandleSessionSwitched(e.new_index, e.session, e.transient);
886 });
887
889 [this](const SessionCreatedEvent& e) {
890 HandleSessionCreated(e.index, e.session);
891 });
892
894 [this](const SessionClosedEvent& e) { HandleSessionClosed(e.index); });
895
897 HandleSessionRomLoaded(e.session_id, e.rom);
898 });
899
900 // Subscribe to FrameGuiBeginEvent for ImGui-safe deferred action processing
901 // This replaces scattered manual processing calls with event-driven execution
903 ui_sync_frame_id_.fetch_add(1, std::memory_order_relaxed);
904
905 // Process LayoutCoordinator's deferred actions
907
908 // Process EditorManager's deferred actions
909 if (!deferred_actions_.empty()) {
910 std::vector<std::function<void()>> actions_to_execute;
911 actions_to_execute.swap(deferred_actions_);
912 const int processed_count = static_cast<int>(actions_to_execute.size());
913 for (auto& action : actions_to_execute) {
914 action();
915 }
916 if (processed_count > 0) {
917 const int remaining = pending_editor_deferred_actions_.fetch_sub(
918 processed_count, std::memory_order_relaxed) -
919 processed_count;
920 if (remaining < 0) {
921 pending_editor_deferred_actions_.store(0, std::memory_order_relaxed);
922 }
923 }
924 }
925 });
926
927 // Subscribe to UIActionRequestEvent for activity bar actions
928 // This replaces direct callbacks from WorkspaceWindowManager
930 [this](const UIActionRequestEvent& e) {
931 HandleUIActionRequest(e.action);
932 });
933
935 [this](const PanelVisibilityChangedEvent& e) {
936 if (e.category.empty() ||
938 return;
939 }
940 if (IsTransientPanelVisibilityId(e.base_panel_id)) {
941 return;
942 }
943 if (e.visible && layout_manager_) {
944 layout_manager_->DockPresetPositionOnPanelOpen(e.session_id,
945 e.base_panel_id);
946 }
947 auto& prefs = user_settings_.prefs();
948 prefs.panel_visibility_state[e.category][e.base_panel_id] = e.visible;
949 settings_dirty_ = true;
951 });
952}
953
955 // ContentRegistry::Context holds non-owning pointers into our members
956 // (event_bus_, editor_context_, user_settings_, layout_manager_, callbacks).
957 // Clear them before our members destruct so widgets that read the singleton
958 // (e.g. Canvas::set_global_scale via Context::event_bus()) don't see
959 // dangling pointers in tests that instantiate multiple EditorManagers.
960 // Clear() resets fallback state but intentionally keeps global_context for
961 // session-close semantics, so null it explicitly here.
964
965 // GetResourceLabels() is a process-wide singleton. RefreshResourceLabelProvider
966 // hands it a pointer to current_project_.hack_manifest, which dangles after
967 // we destruct. Clear before our members go out of scope.
970
971 // ThemeManager is a singleton that outlives us. Clear the theme-changed
972 // callback so it stops calling back into a destroyed EditorManager via the
973 // captured `this` pointer. Matters for unit tests that construct and destroy
974 // multiple EditorManager instances in the same process.
977
978 // EventBus subscriptions are automatically cleaned up when event_bus_ is
979 // destroyed (owned by this class). No manual unsubscription needed.
980}
981
982// ============================================================================
983// Session Event Handlers (EventBus subscribers)
984// ============================================================================
985
987 auto& provider = zelda3::GetResourceLabels();
990
991 auto merge_labels =
994 for (const auto& [type, labels] : src) {
995 auto& out = (*dst)[type];
996 for (const auto& [key, value] : labels) {
997 out[key] = value;
998 }
999 }
1000 };
1001
1002 merged_labels.clear();
1003 std::vector<std::string> source_parts;
1004
1005 // 1) ROM-local labels as baseline for active session.
1006 if (auto* rom = GetCurrentRom();
1007 rom && rom->resource_label() && !rom->resource_label()->labels_.empty()) {
1008 merge_labels(&merged_labels, rom->resource_label()->labels_);
1009 source_parts.push_back("rom");
1010 }
1011
1012 // 2) Project registry labels from the active hack manifest.
1016 merge_labels(
1017 &merged_labels,
1019 source_parts.push_back("registry");
1020 }
1021
1022 // 3) Explicit project labels override all other sources.
1025 merge_labels(&merged_labels, current_project_.resource_labels);
1026 source_parts.push_back("project");
1027 }
1028
1029 auto* label_source = merged_labels.empty() ? &empty_labels : &merged_labels;
1030 std::string source =
1031 source_parts.empty() ? "empty" : absl::StrJoin(source_parts, "+");
1032
1033 provider.SetProjectLabels(label_source);
1034 provider.SetHackManifest(current_project_.project_opened() &&
1037 : nullptr);
1038
1039 const bool prefer_hmagic =
1043 provider.SetPreferHMagicNames(prefer_hmagic);
1044
1045 LOG_DEBUG("EditorManager",
1046 "ResourceLabelProvider refreshed (source=%s, project_open=%s)",
1047 source.c_str(),
1048 current_project_.project_opened() ? "true" : "false");
1049}
1050
1053 return;
1054 }
1055
1056 const auto index =
1058 if (!index.has_value()) {
1060 return;
1061 }
1062
1063 auto* session =
1064 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1065 if (!session) {
1067 return;
1068 }
1069
1071 if (session->project_context.has_value()) {
1072 session->project_context->feature_flags = session->feature_flags;
1073 }
1075 current_project_.feature_flags = session->feature_flags;
1076 }
1077}
1078
1080 if (!active_project_context_session_id_.has_value() ||
1082 return;
1083 }
1084
1087 const auto index =
1089 if (!index.has_value()) {
1091 return;
1092 }
1093
1094 auto* session =
1095 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1096 if (!session) {
1098 return;
1099 }
1100
1101 // The singleton can temporarily belong to another session while all editor
1102 // windows are ticked. Only use that value when it still belongs to this
1103 // project owner; otherwise its session-owned copy is authoritative.
1106 }
1107 // These preferences are exposed through global runtime settings, but they
1108 // are project workspace state. Capture them before leaving the session so a
1109 // later SaveProject cannot copy another session's values into this project.
1118 current_project_.feature_flags = session->feature_flags;
1120}
1121
1123 if (!active_project_context_session_id_.has_value() ||
1125 return;
1126 }
1127
1128 const auto index =
1130 if (!index.has_value()) {
1131 return;
1132 }
1133 auto* session =
1134 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1135 if (!session) {
1136 return;
1137 }
1138
1140 session->project_dirty = project_management_panel_->IsProjectDirty();
1141 }
1142 session->project_file_editor_state = project_file_editor_.CaptureState();
1143}
1144
1146 if (!session_coordinator_) {
1147 return absl::OkStatus();
1148 }
1149 auto* session = session_coordinator_->GetActiveRomSession();
1150 if (session == nullptr || !session->editors.HasPendingProjectDraftChanges()) {
1151 return absl::OkStatus();
1152 }
1153 if (!IsCurrentProjectContextOwnedBySession(session->session_id())) {
1154 return absl::FailedPreconditionError(
1155 "Project editor drafts require their project context to be active");
1156 }
1157 return session->editors.PrepareProjectSave();
1158}
1159
1169
1171 std::optional<size_t> previous_session_id) {
1174 if (previous_session_id.has_value() && session_coordinator_) {
1175 const auto index = ResolveSessionIndexById(*previous_session_id);
1176 if (index.has_value()) {
1177 auto* previous_session =
1178 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1179 if (previous_session) {
1180 if (session_coordinator_->GetActiveSessionIndex() != *index) {
1181 session_coordinator_->SwitchToSession(*index);
1182 } else {
1183 RestoreProjectContextForSession(previous_session);
1184 }
1185 return;
1186 }
1187 }
1188 }
1189
1193 version_manager_ = nullptr;
1196}
1197
1199 size_t previous_session_count) {
1200 if (!session_coordinator_ ||
1201 session_coordinator_->GetTotalSessionCount() <= previous_session_count) {
1202 return absl::OkStatus();
1203 }
1204 auto* provisional_session = session_coordinator_->GetActiveRomSession();
1205 if (!provisional_session) {
1206 return absl::InternalError(
1207 "A provisional session was created but is not active");
1208 }
1209 return session_coordinator_->DiscardProvisionalSession(
1210 provisional_session->session_id());
1211}
1212
1221
1223 const std::string& filepath, const std::string& contents) {
1225 const bool targets_current = !current_project_.filepath.empty() &&
1227 filepath, current_project_.filepath);
1228 if (!targets_current) {
1229 // Save As to a separate descriptor is the deterministic escape hatch when
1230 // both the structured panel and raw document have edits.
1231 return absl::OkStatus();
1232 }
1233 if (IsCurrentProjectDirty()) {
1234 return absl::FailedPreconditionError(
1235 "Project settings also have unsaved changes; use raw Save As to "
1236 "preserve this draft, then save the project settings");
1237 }
1238
1239 auto* session = session_coordinator_
1240 ? session_coordinator_->GetActiveRomSession()
1241 : nullptr;
1242 if (!session || !session->rom.is_loaded()) {
1243 return absl::FailedPreconditionError(
1244 "No active ROM session for this project file");
1245 }
1246
1247 project::YazeProject parsed_project;
1248 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1249 if (!parsed_project.project_opened()) {
1250 return absl::InvalidArgumentError(
1251 "Raw project file must contain a non-empty project name");
1252 }
1253 if (parsed_project.rom_filename.empty() ||
1255 parsed_project.rom_filename, session->filepath) ||
1257 parsed_project.rom_filename, session->rom.filename()))) {
1258 return absl::FailedPreconditionError(
1259 "Raw project file cannot change the ROM backing file of a loaded "
1260 "session; use Project Management > Swap ROM");
1261 }
1262 return session_coordinator_->CheckBackingFileAvailable(
1263 parsed_project.rom_filename, session->session_id());
1264}
1265
1267 const std::string& filepath, const std::string& contents) {
1268 if (current_project_.filepath.empty() ||
1270 filepath, current_project_.filepath)) {
1271 return absl::OkStatus();
1272 }
1273
1274 auto* session = session_coordinator_
1275 ? session_coordinator_->GetActiveRomSession()
1276 : nullptr;
1277 if (!session) {
1278 return absl::FailedPreconditionError("No active ROM session");
1279 }
1280
1281 project::YazeProject parsed_project;
1282 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1283 current_project_ = std::move(parsed_project);
1285 session->project_dirty = false;
1286 active_project_context_session_id_ = session->session_id();
1287 runtime_feature_flags_session_id_ = session->session_id();
1288 version_manager_ = session->version_manager.get();
1291 project_management_panel_->SetProject(&current_project_, false);
1292 }
1293 return absl::OkStatus();
1294}
1295
1296void EditorManager::RebaseCleanProjectFileDraft(const std::string& filepath) {
1297 if (!session_coordinator_ || filepath.empty()) {
1298 return;
1299 }
1300 auto* session = session_coordinator_->GetActiveRomSession();
1301 if (!session || !session->project_file_editor_state.initialized ||
1302 session->project_file_editor_state.modified) {
1303 return;
1304 }
1305
1306 const bool was_active = project_file_editor_.is_active();
1307 auto status = project_file_editor_.LoadFile(filepath);
1308 if (!status.ok()) {
1309 LOG_WARN("EditorManager", "Failed to rebase raw project editor: %s",
1310 status.message());
1311 return;
1312 }
1314 project_file_editor_.set_active(was_active);
1315 session->project_file_editor_state = project_file_editor_.CaptureState();
1316}
1317
1318absl::StatusOr<const project::YazeProject*>
1320 if (!session_coordinator_) {
1321 return absl::FailedPreconditionError("No session coordinator");
1322 }
1323 auto* session = session_coordinator_->GetActiveRomSession();
1324 if (!session || !session->rom.is_loaded()) {
1325 return absl::FailedPreconditionError("No active ROM session");
1326 }
1327
1329 if (!active_project_context_session_id_.has_value() ||
1330 *active_project_context_session_id_ != session->session_id()) {
1331 return absl::FailedPreconditionError(
1332 "Active ROM session project context is not restored");
1333 }
1335 if (!session->project_context.has_value()) {
1336 return absl::FailedPreconditionError(
1337 "Active ROM session has no project/save context");
1338 }
1339 return &*session->project_context;
1340}
1341
1343 using Action = UIActionRequestEvent::Action;
1344 switch (action) {
1345 case Action::kShowEmulator:
1346 if (ui_coordinator_) {
1347 ui_coordinator_->SetEmulatorVisible(true);
1348 }
1349 break;
1350
1351 case Action::kShowSettings:
1352 // Toggle Settings panel in sidebar
1354 right_drawer_manager_->ToggleDrawer(
1356 } else {
1358 }
1359 break;
1360
1361 case Action::kShowPanelBrowser:
1362 if (ui_coordinator_) {
1363 ui_coordinator_->ShowWindowBrowser();
1364 }
1365 break;
1366
1367 case Action::kShowSearch:
1368 if (ui_coordinator_) {
1369 ui_coordinator_->ShowGlobalSearch();
1370 }
1371 break;
1372
1373 case Action::kShowShortcuts:
1374 if (ui_coordinator_) {
1375 ui_coordinator_->ShowShortcutsBrowser();
1376 }
1377 break;
1378
1379 case Action::kShowCommandPalette:
1380 if (ui_coordinator_) {
1381 ui_coordinator_->ShowCommandPalette();
1382 }
1383 break;
1384
1385 case Action::kShowHelp:
1387 // Toggle Help panel in sidebar
1388 right_drawer_manager_->ToggleDrawer(
1390 } else if (popup_manager_) {
1391 // Fallback to "About" dialog if sidebar not available
1393 }
1394 break;
1395
1396 case Action::kShowAgentChatSidebar:
1398 right_drawer_manager_->OpenDrawer(
1400 } else {
1402 }
1403 break;
1404
1405 case Action::kShowAgentProposalsSidebar:
1407 right_drawer_manager_->OpenDrawer(
1409 }
1410 break;
1411
1412 case Action::kOpenRom: {
1413 auto status = LoadRom();
1414 if (!status.ok()) {
1416 std::string("Open failed: ") + std::string(status.message()),
1418 }
1419 } break;
1420
1421 case Action::kSaveRom:
1422 if (GetCurrentRom() && GetCurrentRom()->is_loaded()) {
1423 auto status = SaveRom();
1424 if (!absl::IsCancelled(status) && !status.ok()) {
1426 std::string("Save failed: ") + std::string(status.message()),
1428 }
1429 }
1430 break;
1431
1432 case Action::kUndo:
1433 if (auto* current_editor = GetCurrentEditor()) {
1434 auto status = current_editor->Undo();
1435 if (!status.ok()) {
1437 std::string("Undo failed: ") + std::string(status.message()),
1439 }
1440 }
1441 break;
1442
1443 case Action::kRedo:
1444 if (auto* current_editor = GetCurrentEditor()) {
1445 auto status = current_editor->Redo();
1446 if (!status.ok()) {
1448 std::string("Redo failed: ") + std::string(status.message()),
1450 }
1451 }
1452 break;
1453
1454 case Action::kResetLayout:
1456 break;
1457 }
1458}
1459
1461 auto& test_manager = test::TestManager::Get();
1462
1463#ifdef YAZE_ENABLE_TESTING
1464 // Register comprehensive test suites
1465 test_manager.RegisterTestSuite(
1466 std::make_unique<test::CoreSystemsTestSuite>());
1467 test_manager.RegisterTestSuite(std::make_unique<test::IntegratedTestSuite>());
1468 test_manager.RegisterTestSuite(
1469 std::make_unique<test::PerformanceTestSuite>());
1470 test_manager.RegisterTestSuite(std::make_unique<test::UITestSuite>());
1471 test_manager.RegisterTestSuite(
1472 std::make_unique<test::RomDependentTestSuite>());
1473
1474 // Register new E2E and ZSCustomOverworld test suites
1475 test_manager.RegisterTestSuite(std::make_unique<test::E2ETestSuite>());
1476 test_manager.RegisterTestSuite(
1477 std::make_unique<test::ZSCustomOverworldTestSuite>());
1478#endif
1479
1480 // Register Google Test suite if available
1481#ifdef YAZE_ENABLE_GTEST
1482 test_manager.RegisterTestSuite(std::make_unique<test::UnitTestSuite>());
1483#endif
1484
1485 // Register z3ed AI Agent test suites (requires gRPC)
1486#ifdef YAZE_WITH_GRPC
1488#endif
1489
1490 // Update resource monitoring to track Arena state
1491 test_manager.UpdateResourceStats();
1492}
1493
1495 const std::string& filename) {
1496 renderer_ = renderer;
1498 SeedOracleProjectInRecents();
1499
1500 // Inject the window manager into emulator and workspace_manager
1503
1504 // Point to a blank editor set when no ROM is loaded
1505 // current_editor_set_ = &blank_editor_set_;
1506
1507 // Note: PopupManager is now initialized in constructor before
1508 // MenuOrchestrator This ensures all menu callbacks can safely call
1509 // popup_manager_.Show()
1510
1514
1515 // Apply sidebar state from settings AFTER registering callbacks
1516 // This triggers the callbacks but they should be safe now
1518 /*notify=*/false);
1521 /*notify=*/false);
1523 user_settings_.prefs().sidebar_panel_width, /*notify=*/false);
1526 /*notify=*/false);
1527 {
1528 const bool prefer_dashboard_only =
1530 startup_editor_hint_.empty() && startup_panel_hints_.empty();
1531 if (!prefer_dashboard_only) {
1532 const std::string category = GetPreferredStartupCategory(
1534 if (!category.empty()) {
1535 window_manager_.SetActiveCategory(category, /*notify=*/false);
1537 auto it = user_settings_.prefs().panel_visibility_state.find(category);
1538 if (it != user_settings_.prefs().panel_visibility_state.end()) {
1540 window_manager_.GetActiveSessionId(), it->second);
1541 }
1542 }
1543 }
1544 }
1545
1549 }
1550
1551 // Initialize testing system only when tests are enabled
1552#ifdef YAZE_ENABLE_TESTING
1554#endif
1555
1556 // TestManager will be updated when ROMs are loaded via SetCurrentRom calls
1557
1559
1560 // Open a startup ROM last. The post-load surface reads Settings (Default
1561 // Editor), which InitializeServices() loads; opening earlier used the
1562 // built-in defaults and always showed the editor picker.
1563 if (!filename.empty()) {
1564 // The post-load surface must see --startup_dashboard/--welcome (saved by
1565 // SetStartupLoadHints) and Settings > Test mode now; Application applies
1566 // them only after this returns.
1570 }
1573 }
1574 }
1577 }
1578}
1579
1581 // Register emulator panels early (emulator Initialize might not be called).
1582 const std::vector<WindowDefinition> panel_definitions = {
1583 {.id = "emulator.cpu_debugger",
1584 .display_name = "CPU Debugger",
1585 .icon = ICON_MD_BUG_REPORT,
1586 .category = "Emulator",
1587 .priority = 10,
1589 {.id = "emulator.ppu_viewer",
1590 .display_name = "PPU Viewer",
1592 .category = "Emulator",
1593 .priority = 20,
1594 .presentation = WindowPresentationPolicy::CoreWorkspace()},
1595 {.id = "emulator.memory_viewer",
1596 .display_name = "Memory Viewer",
1597 .icon = ICON_MD_MEMORY,
1598 .category = "Emulator",
1599 .priority = 30,
1601 {.id = "emulator.breakpoints",
1602 .display_name = "Breakpoints",
1603 .icon = ICON_MD_STOP,
1604 .category = "Emulator",
1605 .priority = 40,
1607 {.id = "emulator.performance",
1608 .display_name = "Performance",
1609 .icon = ICON_MD_SPEED,
1610 .category = "Emulator",
1611 .priority = 50,
1613 {.id = "emulator.ai_agent",
1614 .display_name = "AI Agent",
1615 .icon = ICON_MD_SMART_TOY,
1616 .category = "Emulator",
1617 .priority = 60,
1619 {.id = "emulator.save_states",
1620 .display_name = "Save States",
1621 .icon = ICON_MD_SAVE,
1622 .category = "Emulator",
1623 .priority = 70,
1625 "Command palette (placeholder: Coming Soon)")},
1626 {.id = "emulator.keyboard_config",
1627 .display_name = "Keyboard Config",
1628 .icon = ICON_MD_KEYBOARD,
1629 .category = "Emulator",
1630 .priority = 80,
1632 {.id = "emulator.virtual_controller",
1633 .display_name = "Virtual Controller",
1634 .icon = ICON_MD_SPORTS_ESPORTS,
1635 .category = "Emulator",
1636 .priority = 85,
1638 {.id = "emulator.apu_debugger",
1639 .display_name = "APU Debugger",
1640 .icon = ICON_MD_AUDIOTRACK,
1641 .category = "Emulator",
1642 .priority = 90,
1644 {.id = "emulator.audio_mixer",
1645 .display_name = "Audio Mixer",
1646 .icon = ICON_MD_AUDIO_FILE,
1647 .category = "Emulator",
1648 .priority = 100,
1650 {.id = "memory.hex_editor",
1651 .display_name = "Hex Editor",
1652 .icon = ICON_MD_MEMORY,
1653 .category = "Memory",
1654 .window_title = ICON_MD_MEMORY " Hex Editor",
1655 .priority = 10,
1657 .legacy_ids = {"Memory Editor"}},
1658 };
1659
1660 if (window_host_) {
1661 window_host_->RegisterPanels(panel_definitions);
1662 return;
1663 }
1664
1665 for (const auto& definition : panel_definitions) {
1666 WindowDescriptor descriptor;
1667 descriptor.card_id = definition.id;
1668 descriptor.display_name = definition.display_name;
1669 descriptor.window_title = definition.window_title;
1670 descriptor.icon = definition.icon;
1671 descriptor.category = definition.category;
1672 descriptor.shortcut_hint = definition.shortcut_hint;
1673 descriptor.priority = definition.priority;
1674 descriptor.scope = definition.scope;
1675 descriptor.window_lifecycle = definition.window_lifecycle;
1676 descriptor.context_scope = definition.context_scope;
1677 descriptor.presentation = definition.presentation;
1678 descriptor.visibility_flag = definition.visibility_flag;
1679 descriptor.on_show = definition.on_show;
1680 descriptor.on_hide = definition.on_hide;
1681
1682 for (const auto& legacy_id : definition.legacy_ids) {
1683 window_manager_.RegisterPanelAlias(legacy_id, definition.id);
1684 }
1685
1686 window_manager_.RegisterWindow(descriptor);
1687 if (definition.visible_by_default) {
1688 window_manager_.OpenWindow(definition.id);
1689 }
1690 }
1691}
1692
1694 // Initialize project file editor
1696
1697 // Initialize agent UI (no-op when agent UI is disabled)
1701
1702 // Note: Unified gRPC Server is started from Application::Initialize()
1703 // after gRPC infrastructure is properly set up
1704
1705 // Load critical user settings first
1707 if (!status_.ok()) {
1708 LOG_WARN("EditorManager", "Failed to load user settings: %s",
1709 status_.ToString().c_str());
1710 }
1711
1716 if (popup_manager_) {
1718 }
1719 });
1720
1721 // Wire theme persistence. Any successful theme application (selector click,
1722 // command-palette switch, programmatic call) stamps the name into prefs and
1723 // rides the existing debounced-save path. Decouples ThemeManager (singleton
1724 // in app/gui) from UserSettings (editor-layer) — ThemeManager holds only a
1725 // std::function.
1727 [this](const std::string& theme_name) {
1728 user_settings_.prefs().last_theme_name = theme_name;
1729 settings_dirty_ = true;
1731 });
1733 [this](const std::string& locale) {
1734 user_settings_.prefs().language_locale = locale;
1735 settings_dirty_ = true;
1737 });
1738
1739 auto& prefs = user_settings_.prefs();
1740 i18n::LanguageManager::Get().SetLanguage(prefs.language_locale);
1741
1742 // Apply the persisted font selection (defaults to index 0 = Karla).
1743 // Fonts were loaded by the window backend before EditorManager init, so
1744 // ImGui::GetIO().Fonts->Fonts is fully populated here.
1745 ::yaze::SetActiveFontIndex(prefs.font_family_index);
1746
1747 prefs.switch_motion_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
1749 prefs.reduced_motion,
1750 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1751
1753
1755 // Rebuilding dock placement does not invalidate user-sized drawers.
1756 // Migrations that intentionally discard widths clear them in UserSettings.
1757 right_drawer_manager_->RestoreDrawerWidths(
1759 auto normalized_widths = user_settings_.prefs().right_panel_widths;
1760 MergeRightDrawerWidthPreferences(
1761 &normalized_widths, right_drawer_manager_->SerializeDrawerWidths());
1762 if (user_settings_.prefs().right_panel_widths != normalized_widths) {
1763 user_settings_.prefs().right_panel_widths = std::move(normalized_widths);
1764 settings_dirty_ = true;
1766 }
1767 right_drawer_manager_->SetDrawerWidthChangedCallback(
1768 [this](RightDrawerManager::DrawerType, float) {
1769 if (!right_drawer_manager_) {
1770 return;
1771 }
1772 MergeRightDrawerWidthPreferences(
1774 right_drawer_manager_->SerializeDrawerWidths());
1775 settings_dirty_ = true;
1777 });
1778 }
1780 // Apply sprite naming preference globally.
1783
1785
1786 // Apply font scale after loading (only if ImGui context exists)
1787 if (ImGui::GetCurrentContext() != nullptr) {
1788 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
1789 } else {
1790 LOG_WARN("EditorManager",
1791 "ImGui context not available; skipping FontGlobalScale update");
1792 }
1793
1794 // Restore the user's last theme. Empty (first run) or "Custom Accent"
1795 // (transient generated themes without a matching discovered preset) both
1796 // skip restoration and keep the built-in default (Classic YAZE / YAZE Tre).
1797 const auto& last_theme = user_settings_.prefs().last_theme_name;
1798 if (!last_theme.empty() && last_theme != "Custom Accent") {
1799 gui::ThemeManager::Get().ApplyTheme(last_theme);
1800 }
1801
1802 // Initialize WASM control and session APIs for browser/agent integration
1803#ifdef __EMSCRIPTEN__
1806 LOG_INFO("EditorManager", "WASM Control and Session APIs initialized");
1807#endif
1808}
1809
1813
1815 ui_coordinator_->SetWelcomeScreenVisible(false);
1816 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
1817 }
1818}
1819
1822 pending_editor_deferred_actions_.load(std::memory_order_relaxed) > 0;
1823}
1824
1830
1832 if (ui_coordinator_) {
1833 ui_coordinator_->SetWelcomeScreenVisible(false);
1834 }
1835
1836 // 1. Never cover an editor the user already has open. Re-enter the same
1837 // editor type so it binds to the newly loaded session.
1838 std::optional<EditorType> target;
1839 if (HasOpenEditor()) {
1840 target = current_editor_->type();
1841 }
1842
1843 // 2. Settings > Editor Behavior > Default Editor.
1844 if (!target.has_value()) {
1846 case 1:
1847 target = EditorType::kOverworld;
1848 break;
1849 case 2:
1850 target = EditorType::kDungeon;
1851 break;
1852 case 3:
1853 target = EditorType::kGraphics;
1854 break;
1855 default:
1856 break;
1857 }
1858 }
1859
1860 // 3. The editor picker, unless it is suppressed (--startup_dashboard=hide
1861 // or Settings > Test mode). Suppressed with no other choice: open the
1862 // most recently used editor, else Dungeon.
1863 if (!target.has_value()) {
1864 if (!ui_coordinator_ || ui_coordinator_->ShouldShowDashboard()) {
1865 if (ui_coordinator_) {
1866 ui_coordinator_->SetEditorSelectionVisible(true);
1867 }
1868 // Suppress panel drawing until the user picks an editor.
1871 return;
1872 }
1873 target =
1874 (dashboard_panel_ ? dashboard_panel_->MostRecentEditor() : std::nullopt)
1875 .value_or(EditorType::kDungeon);
1876 }
1877
1878 if (ui_coordinator_) {
1879 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
1880 }
1881 SwitchToEditor(*target, /*force_visible=*/true);
1882}
1883
1885 ShortcutDependencies shortcut_deps;
1886 shortcut_deps.editor_manager = this;
1887 shortcut_deps.editor_registry = &editor_registry_;
1888 shortcut_deps.menu_orchestrator = menu_orchestrator_.get();
1889 shortcut_deps.rom_file_manager = &rom_file_manager_;
1890 shortcut_deps.project_manager = &project_manager_;
1891 shortcut_deps.session_coordinator = session_coordinator_.get();
1892 shortcut_deps.ui_coordinator = ui_coordinator_.get();
1893 shortcut_deps.workspace_manager = &workspace_manager_;
1894 shortcut_deps.popup_manager = popup_manager_.get();
1895 shortcut_deps.toast_manager = &toast_manager_;
1896 shortcut_deps.window_manager = &window_manager_;
1897 shortcut_deps.user_settings = &user_settings_;
1898
1902}
1903
1905 const std::string& editor_name, const std::string& panels_str) {
1906 const bool has_editor = !editor_name.empty();
1907 const bool has_panels = !panels_str.empty();
1908
1909 if (!has_editor && !has_panels) {
1910 return;
1911 }
1912
1913 LOG_INFO("EditorManager",
1914 "Processing startup flags: editor='%s', panels='%s'",
1915 editor_name.c_str(), panels_str.c_str());
1916
1917 std::optional<EditorType> editor_type_to_open =
1918 has_editor ? ParseEditorTypeFromString(editor_name) : std::nullopt;
1919 if (has_editor && !editor_type_to_open.has_value()) {
1920 LOG_WARN("EditorManager", "Unknown editor specified via flag: %s",
1921 editor_name.c_str());
1922 } else if (editor_type_to_open.has_value()) {
1923 // Use EditorActivator to ensure layouts and default panels are initialized
1924 SwitchToEditor(*editor_type_to_open, true, /*from_dialog=*/true);
1925 }
1926
1927 // Open windows via WorkspaceWindowManager - works for any editor type
1928 if (!has_panels) {
1929 return;
1930 }
1931
1932 const size_t session_id = GetCurrentSessionId();
1933 std::string last_known_category = window_manager_.GetActiveCategory();
1934 bool applied_category_from_panel = false;
1935
1936 for (absl::string_view token :
1937 absl::StrSplit(panels_str, ',', absl::SkipWhitespace())) {
1938 if (token.empty()) {
1939 continue;
1940 }
1941 std::string panel_name = std::string(absl::StripAsciiWhitespace(token));
1942 LOG_DEBUG("EditorManager", "Attempting to open panel: '%s'",
1943 panel_name.c_str());
1944
1945 const std::string lower_name = absl::AsciiStrToLower(panel_name);
1946 if (lower_name == "welcome" || lower_name == "welcome_screen") {
1947 if (ui_coordinator_) {
1948 ui_coordinator_->SetWelcomeScreenBehavior(StartupVisibility::kShow);
1949 }
1950 continue;
1951 }
1952 if (lower_name == "dashboard" || lower_name == "dashboard.main" ||
1953 lower_name == "editor_selection") {
1954 if (dashboard_panel_) {
1955 dashboard_panel_->Show();
1956 }
1957 if (ui_coordinator_) {
1958 ui_coordinator_->SetDashboardBehavior(StartupVisibility::kShow);
1959 }
1962 /*notify=*/false);
1963 continue;
1964 }
1965
1966 // Special case: "Room <id>" opens a dungeon room
1967 if (absl::StartsWith(panel_name, "Room ")) {
1968 if (auto* editor_set = GetCurrentEditorSet()) {
1969 try {
1970 int room_id = std::stoi(panel_name.substr(5));
1972 JumpToRoomRequestEvent::Create(room_id, session_id));
1973 } catch (const std::exception& e) {
1974 LOG_WARN("EditorManager", "Invalid room ID format: %s",
1975 panel_name.c_str());
1976 }
1977 }
1978 continue;
1979 }
1980
1981 std::optional<std::string> resolved_panel;
1982 if (window_manager_.GetWindowDescriptor(session_id, panel_name)) {
1983 resolved_panel = panel_name;
1984 } else {
1985 for (const auto& [prefixed_id, descriptor] :
1987 const std::string base_id = StripSessionPrefix(prefixed_id);
1988 const std::string card_lower = absl::AsciiStrToLower(base_id);
1989 const std::string display_lower =
1990 absl::AsciiStrToLower(descriptor.display_name);
1991
1992 if (card_lower == lower_name || display_lower == lower_name) {
1993 resolved_panel = base_id;
1994 break;
1995 }
1996 }
1997 }
1998
1999 if (!resolved_panel.has_value()) {
2000 LOG_WARN("EditorManager",
2001 "Unknown panel '%s' from --open_panels (known count: %zu)",
2002 panel_name.c_str(),
2004 continue;
2005 }
2006
2007 if (window_manager_.OpenWindow(session_id, *resolved_panel)) {
2008 const auto* descriptor =
2009 window_manager_.GetWindowDescriptor(session_id, *resolved_panel);
2010 if (descriptor != nullptr) {
2011 const EditorType type =
2012 EditorRegistry::GetEditorTypeFromCategory(descriptor->category);
2013 const auto ensure_status = EnsureEditorAssetsLoaded(type);
2014 if (!ensure_status.ok()) {
2015 LOG_WARN("EditorManager", "Failed to load assets for panel '%s': %s",
2016 resolved_panel->c_str(), ensure_status.message().data());
2017 }
2018 }
2019 if (descriptor && !applied_category_from_panel &&
2020 descriptor->category != WorkspaceWindowManager::kDashboardCategory) {
2021 window_manager_.SetActiveCategory(descriptor->category);
2022 applied_category_from_panel = true;
2023 } else if (!applied_category_from_panel && descriptor &&
2024 descriptor->category.empty() && !last_known_category.empty()) {
2025 window_manager_.SetActiveCategory(last_known_category);
2026 }
2027 } else {
2028 LOG_WARN("EditorManager", "Failed to show panel '%s'",
2029 resolved_panel->c_str());
2030 }
2031 }
2032}
2033
2040
2048
2050 constexpr int kTargetRevision =
2052 const int previous_revision =
2054 if (!user_settings_.ApplyPanelLayoutDefaultsRevision(kTargetRevision)) {
2055 return;
2056 }
2057
2058 const bool includes_workspace_reset_revision =
2059 previous_revision <
2061 kTargetRevision >=
2064 pending_layout_defaults_reset_ || includes_workspace_reset_revision;
2065 settings_dirty_ = true;
2067
2068 LOG_INFO("EditorManager",
2069 "Applied panel layout defaults migration %d -> %d (%s)",
2070 previous_revision, kTargetRevision,
2071 includes_workspace_reset_revision ? "workspace reset"
2072 : "targeted preferences");
2073}
2074
2076 const std::string& saved_category,
2077 const std::vector<std::string>& available_categories) const {
2078 const std::string preferred =
2079 PreferStartupCategory(saved_category, available_categories);
2080 if (preferred.empty()) {
2081 return preferred;
2082 }
2083
2084 return preferred;
2085}
2086
2090
2092 if (ui_coordinator_) {
2093 ui_coordinator_->SetWelcomeScreenBehavior(welcome_mode_override_);
2094 ui_coordinator_->SetDashboardBehavior(dashboard_mode_override_);
2095 }
2096
2098 const bool sidebar_visible =
2100 window_manager_.SetSidebarVisible(sidebar_visible, /*notify=*/false);
2101 if (ui_coordinator_) {
2102 ui_coordinator_->SetPanelSidebarVisible(sidebar_visible);
2103 }
2104 }
2105
2106 // Force sidebar panel to collapse if Welcome Screen or Dashboard is explicitly shown
2107 // This prevents visual overlap/clutter on startup
2110 window_manager_.SetSidebarExpanded(false, /*notify=*/false);
2111 }
2112
2113 if (dashboard_panel_) {
2115 dashboard_panel_->Hide();
2117 dashboard_panel_->Show();
2118 }
2119 }
2120}
2121
2123 ApplyStartupVisibility(config);
2124 // Handle startup editor and panels
2125 std::string panels_str;
2126 for (size_t i = 0; i < config.open_panels.size(); ++i) {
2127 if (i > 0)
2128 panels_str += ",";
2129 panels_str += config.open_panels[i];
2130 }
2131 OpenEditorAndPanelsFromFlags(config.startup_editor, panels_str);
2132
2133 // Handle jump targets
2134 if (config.jump_to_room >= 0) {
2137 }
2138 if (config.jump_to_map >= 0) {
2142 } else {
2143 LOG_WARN("EditorManager",
2144 "Ignoring invalid startup overworld map target %d",
2145 config.jump_to_map);
2146 toast_manager_.Show("Ignored invalid startup overworld map: " +
2147 std::to_string(config.jump_to_map),
2149 }
2150 }
2151}
2152
2153absl::Status EditorManager::LoadAssetsForMode(uint64_t loading_handle) {
2154 switch (asset_load_mode_) {
2156 return LoadAssetsLazy(loading_handle);
2159 default:
2160 return LoadAssets(loading_handle);
2161 }
2162}
2163
2165 if (!session) {
2166 return;
2167 }
2168 session->ResetGraphicsDisplay();
2169 session->game_data_loaded = false;
2170 session->editor_initialized.fill(false);
2171 session->editor_assets_loaded.fill(false);
2172}
2173
2175 EditorType type) {
2176 if (!session) {
2177 return;
2178 }
2179 const size_t index = EditorTypeIndex(type);
2180 if (index < session->editor_initialized.size()) {
2181 session->editor_initialized[index] = true;
2182 }
2183}
2184
2186 if (!session) {
2187 return;
2188 }
2189 const size_t index = EditorTypeIndex(type);
2190 if (index < session->editor_assets_loaded.size()) {
2191 session->editor_assets_loaded[index] = true;
2192 }
2193}
2194
2196 EditorType type) {
2197 if (!session || !layout_manager_) {
2198 return;
2199 }
2200
2201 const size_t index = EditorTypeIndex(type);
2203 index >= session->editor_initialized.size() ||
2204 !session->editor_initialized[index] ||
2206 return;
2207 }
2208
2209 const std::string category = EditorRegistry::GetEditorCategory(type);
2210 // Session switching can happen before a lazily loaded editor registers its
2211 // panels. Repeat the persisted visibility pass once those descriptors exist.
2212 const auto visibility_it =
2214 if (visibility_it != user_settings_.prefs().panel_visibility_state.end()) {
2216 visibility_it->second);
2217 }
2218
2219 // Only the active category owns the shared dock tree. A cross-category
2220 // activation will refresh its layout immediately after the category switch.
2221 if (window_manager_.GetActiveCategory() == category &&
2222 layout_manager_->IsLayoutInitialized(type)) {
2224 }
2225}
2226
2232
2234 switch (type) {
2242 return true;
2243 default:
2244 return false;
2245 }
2246}
2247
2251
2253 EditorSet* editor_set) const {
2254 std::unordered_set<EditorType> types;
2255
2256 auto add_type = [&types](EditorType type) {
2257 switch (type) {
2261 case EditorType::kAgent:
2262 return;
2263 default:
2264 types.insert(type);
2265 return;
2266 }
2267 };
2268
2269 auto add_category = [&](const std::string& category) {
2270 if (category.empty() ||
2272 return;
2273 }
2275 };
2276
2277 const size_t session_id = GetCurrentSessionId();
2278 bool used_startup_hints = false;
2279
2280 if (!startup_editor_hint_.empty()) {
2281 if (auto startup_type = ParseEditorTypeFromString(startup_editor_hint_)) {
2282 add_type(*startup_type);
2283 used_startup_hints = true;
2284 }
2285 }
2286
2287 for (const auto& panel_name : startup_panel_hints_) {
2288 if (panel_name.empty()) {
2289 continue;
2290 }
2291
2292 if (const auto* descriptor =
2293 window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2294 add_category(descriptor->category);
2295 used_startup_hints = true;
2296 continue;
2297 }
2298
2299 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2300 for (const auto& [prefixed_id, descriptor] :
2302 const std::string base_id = StripSessionPrefix(prefixed_id);
2303 const std::string card_lower = absl::AsciiStrToLower(base_id);
2304 const std::string display_lower =
2305 absl::AsciiStrToLower(descriptor.display_name);
2306 if (card_lower == lower_name || display_lower == lower_name) {
2307 add_category(descriptor.category);
2308 used_startup_hints = true;
2309 break;
2310 }
2311 }
2312 }
2313
2314 if (used_startup_hints) {
2315 return std::vector<EditorType>(types.begin(), types.end());
2316 }
2317
2319 return {};
2320 }
2321
2322 if (editor_set) {
2323 for (auto* editor : editor_set->active_editors_) {
2324 if (editor != nullptr && *editor->active()) {
2325 add_type(editor->type());
2326 }
2327 }
2328 }
2329
2330 if (current_editor_ != nullptr) {
2331 add_type(current_editor_->type());
2332 }
2333
2334 add_category(window_manager_.GetActiveCategory());
2335
2336 for (const auto& window_id :
2338 if (const auto* descriptor =
2339 window_manager_.GetWindowDescriptor(session_id, window_id)) {
2340 add_category(descriptor->category);
2341 }
2342 }
2343
2344 return std::vector<EditorType>(types.begin(), types.end());
2345}
2346
2348 EditorSet* editor_set) const {
2349 return editor_set ? editor_set->GetEditor(type) : nullptr;
2350}
2351
2353 if (category.empty() ||
2355 return nullptr;
2356 }
2357
2358 auto* editor_set = GetCurrentEditorSet();
2359 if (!editor_set) {
2360 return nullptr;
2361 }
2362
2364 switch (type) {
2365 case EditorType::kAgent:
2366#ifdef YAZE_BUILD_AGENT_UI
2367 return agent_ui_.GetAgentEditor();
2368#else
2369 return nullptr;
2370#endif
2374 return nullptr;
2375 default:
2376 return GetEditorByType(type, editor_set);
2377 }
2378}
2379
2381 const std::string& category) {
2382 EditorContextSnapshot snapshot;
2383 snapshot.category = category;
2384 Editor* editor = ResolveEditorForCategory(category);
2385 if (editor == nullptr) {
2386 return snapshot;
2387 }
2388
2389 snapshot = editor->BuildContextSnapshot();
2390 if (snapshot.category.empty()) {
2391 snapshot.category = category;
2392 }
2393 if (snapshot.semantic_owner.empty()) {
2394 snapshot.semantic_owner = snapshot.category;
2395 }
2396
2397 const EditorExperimentPolicy policy =
2399 snapshot.experiment.experimental = policy.experimental;
2400 snapshot.experiment.acknowledged =
2402 switch (policy.disabled_save_posture) {
2404 snapshot.experiment.save_posture = "Defensive backup";
2405 break;
2407 snapshot.experiment.save_posture = "Guarded / read-only";
2408 break;
2410 default:
2411 snapshot.experiment.save_posture = "Normal";
2412 break;
2413 }
2416 snapshot.diagnostics.push_back({
2417 .id = "experimental_editor",
2419 .message = absl::StrFormat("%s is experimental; save posture: %s",
2420 snapshot.category,
2421 snapshot.experiment.save_posture),
2422 });
2423 }
2424 return snapshot;
2425}
2426
2427void EditorManager::SyncEditorContextForCategory(const std::string& category) {
2428 if (Editor* resolved = ResolveEditorForCategory(category)) {
2429 SetCurrentEditor(resolved);
2430 } else if (!category.empty() &&
2432 LOG_DEBUG("EditorManager", "No editor context available for category '%s'",
2433 category.c_str());
2434 }
2435}
2436
2438 EditorSet* editor_set,
2439 Rom* rom) {
2440 if (!editor_set) {
2441 return absl::FailedPreconditionError("No editor set available");
2442 }
2443
2444 auto* editor = GetEditorByType(type, editor_set);
2445 if (!editor) {
2446 return absl::OkStatus();
2447 }
2448 editor->Initialize();
2449 return absl::OkStatus();
2450}
2451
2453 auto* session = session_coordinator_
2454 ? session_coordinator_->GetActiveRomSession()
2455 : nullptr;
2456 if (!session) {
2457 return absl::FailedPreconditionError("No active session");
2458 }
2459 if (session->game_data_loaded) {
2460 return absl::OkStatus();
2461 }
2462 if (!session->rom.is_loaded()) {
2463 return absl::FailedPreconditionError("ROM not loaded");
2464 }
2465
2466 RETURN_IF_ERROR(zelda3::LoadGameData(session->rom, session->game_data));
2467 session->ReloadGraphicsDisplay();
2468
2469 auto* game_data = &session->game_data;
2470 auto* editor_set = &session->editors;
2471 editor_set->SetGameData(game_data);
2473
2475 session->game_data_loaded = true;
2476
2477 return absl::OkStatus();
2478}
2479
2481 if (type == EditorType::kUnknown) {
2482 return absl::OkStatus();
2483 }
2484
2485 auto* session = session_coordinator_
2486 ? session_coordinator_->GetActiveRomSession()
2487 : nullptr;
2488 if (!session) {
2489 return absl::OkStatus();
2490 }
2491
2492 if (!session->rom.is_loaded()) {
2494 return absl::OkStatus();
2495 }
2496 const size_t index = EditorTypeIndex(type);
2497 if (index >= session->editor_initialized.size()) {
2498 return absl::InvalidArgumentError("Invalid editor type");
2499 }
2500 if (EditorInitRequiresGameData(type)) {
2501 return absl::OkStatus();
2502 }
2503 bool initialized_now = false;
2504 if (!session->editor_initialized[index]) {
2506 InitializeEditorForType(type, &session->editors, &session->rom));
2507 MarkEditorInitialized(session, type);
2508 initialized_now = true;
2509 }
2510 if (initialized_now) {
2511 RestoreEditorLayoutAfterAssets(session, type);
2512 }
2513 return absl::OkStatus();
2514 }
2515
2516 const size_t index = EditorTypeIndex(type);
2517 if (index >= session->editor_initialized.size()) {
2518 return absl::InvalidArgumentError("Invalid editor type");
2519 }
2520
2521 if (EditorInitRequiresGameData(type)) {
2523 }
2524
2525 bool initialized_now = false;
2526 if (!session->editor_initialized[index]) {
2528 InitializeEditorForType(type, &session->editors, &session->rom));
2529 MarkEditorInitialized(session, type);
2530 initialized_now = true;
2531 }
2532
2533 if (EditorRequiresGameData(type)) {
2535 }
2536
2537 bool loaded_now = false;
2538 if (!session->editor_assets_loaded[index]) {
2539 auto* editor = GetEditorByType(type, &session->editors);
2540 if (editor) {
2541 RETURN_IF_ERROR(editor->Load());
2542 }
2543 MarkEditorLoaded(session, type);
2544 loaded_now = true;
2545 }
2546
2547 if (initialized_now || loaded_now) {
2548 RestoreEditorLayoutAfterAssets(session, type);
2549 }
2550
2551 return absl::OkStatus();
2552}
2553
2560
2586 GetCurrentSessionId()})) {
2587 return absl::OkStatus();
2588 }
2589
2590 return LoadRomInternal();
2591}
2592
2593absl::Status EditorManager::LoadAssets(uint64_t passed_handle) {
2594 auto* current_rom = GetCurrentRom();
2595 auto* current_editor_set = GetCurrentEditorSet();
2596 if (!current_rom || !current_editor_set) {
2597 return absl::FailedPreconditionError("No ROM or editor set loaded");
2598 }
2599
2600 auto* current_session = session_coordinator_->GetActiveRomSession();
2601 if (!current_session) {
2602 return absl::FailedPreconditionError("No active ROM session");
2603 }
2604 ResetAssetState(current_session);
2605
2606 auto start_time = std::chrono::steady_clock::now();
2607
2608#ifdef __EMSCRIPTEN__
2609 // Use passed handle if provided, otherwise create new one
2610 auto loading_handle =
2611 passed_handle != 0
2612 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
2613 passed_handle)
2614 : app::platform::WasmLoadingManager::BeginLoading(
2615 "Loading Editor Assets");
2616
2617 // Progress starts at 10% (ROM already loaded), goes to 100%
2618 constexpr float kStartProgress = 0.10f;
2619 constexpr float kEndProgress = 1.0f;
2620 constexpr int kTotalSteps = 11; // Graphics + 8 editors + profiler + finish
2621 int current_step = 0;
2622 auto update_progress = [&](const std::string& message) {
2623 current_step++;
2624 float progress =
2625 kStartProgress + (kEndProgress - kStartProgress) *
2626 (static_cast<float>(current_step) / kTotalSteps);
2627 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, progress);
2628 app::platform::WasmLoadingManager::UpdateMessage(loading_handle, message);
2629 };
2630 // RAII guard to ensure loading indicator is closed even on early return
2631 auto cleanup_loading = [&]() {
2632 app::platform::WasmLoadingManager::EndLoading(loading_handle);
2633 };
2634 struct LoadingGuard {
2635 std::function<void()> cleanup;
2636 bool dismissed = false;
2637 ~LoadingGuard() {
2638 if (!dismissed)
2639 cleanup();
2640 }
2641 void dismiss() { dismissed = true; }
2642 } loading_guard{cleanup_loading};
2643#else
2644 (void)passed_handle; // Unused on non-WASM
2645#endif
2646
2647 // Set renderer for emulator (lazy initialization happens in Run())
2648 if (renderer_) {
2650 }
2651
2652 const auto preload_editor_list = CollectEditorsToPreload(current_editor_set);
2653 const std::unordered_set<EditorType> preload_types(
2654 preload_editor_list.begin(), preload_editor_list.end());
2655
2656 // Initialize only the editors needed for the current startup surface. This
2657 // registers their windows and sets up editor-specific resources before Load().
2658 struct InitStep {
2659 EditorType type;
2660 bool mark_loaded;
2661 };
2662 const InitStep init_steps[] = {
2664 {EditorType::kGraphics, false}, {EditorType::kScreen, false},
2665 {EditorType::kSprite, false}, {EditorType::kPalette, false},
2666 {EditorType::kAssembly, true}, {EditorType::kMusic, false},
2667 {EditorType::kDungeon, false},
2668 };
2669 for (const auto& step : init_steps) {
2670 if (!preload_types.contains(step.type)) {
2671 continue;
2672 }
2673 if (auto* editor = current_editor_set->GetEditor(step.type)) {
2674 editor->Initialize();
2675 MarkEditorInitialized(current_session, step.type);
2676 if (step.mark_loaded) {
2677 MarkEditorLoaded(current_session, step.type);
2678 }
2679 }
2680 }
2681
2682#ifdef __EMSCRIPTEN__
2683 update_progress("Loading graphics sheets...");
2684#endif
2685 // Load all Zelda3-specific data (metadata, palettes, gfx groups, graphics)
2686 gfx::PaletteManager::Get().ReleaseSession(&current_session->game_data);
2688 zelda3::LoadGameData(*current_rom, current_session->game_data));
2689 current_session->game_data_loaded = true;
2690
2691 current_session->ReloadGraphicsDisplay();
2692
2693 // Propagate GameData to editors that already exist; future editors inherit it
2694 // on first construction via EditorSet.
2695 auto* game_data = &current_session->game_data;
2696 current_editor_set->SetGameData(game_data);
2698
2699 struct LoadStep {
2700 EditorType type;
2701 const char* progress_message;
2702 };
2703 const LoadStep load_steps[] = {
2704 {EditorType::kOverworld, "Loading overworld..."},
2705 {EditorType::kDungeon, "Loading dungeons..."},
2706 {EditorType::kScreen, "Loading screen editor..."},
2707 {EditorType::kGraphics, "Loading graphics editor..."},
2708 {EditorType::kSprite, "Loading sprites..."},
2709 {EditorType::kMessage, "Loading messages..."},
2710 {EditorType::kMusic, "Loading music..."},
2711 {EditorType::kPalette, "Loading palettes..."},
2712 };
2713 for (const auto& step : load_steps) {
2714 if (!preload_types.contains(step.type)) {
2715 continue;
2716 }
2717#ifdef __EMSCRIPTEN__
2718 update_progress(step.progress_message);
2719#endif
2720 if (auto* editor = current_editor_set->GetEditor(step.type)) {
2721 RETURN_IF_ERROR(editor->Load());
2722 MarkEditorLoaded(current_session, step.type);
2723 }
2724 }
2725
2726#ifdef __EMSCRIPTEN__
2727 update_progress("Finishing up...");
2728#endif
2729
2730 // Set up RightDrawerManager with session's settings editor
2732 auto* settings = current_editor_set->GetSettingsPanel();
2733 right_drawer_manager_->SetSettingsPanel(settings);
2734 }
2735
2736 // Apply user preferences to status bar
2738
2739 RestoreActiveEditorLayoutAfterAssets(current_session);
2740
2742
2743#ifdef __EMSCRIPTEN__
2744 // Dismiss the guard and manually close - we completed successfully
2745 loading_guard.dismiss();
2746 app::platform::WasmLoadingManager::EndLoading(loading_handle);
2747#endif
2748
2749 auto end_time = std::chrono::steady_clock::now();
2750 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
2751 end_time - start_time);
2752 LOG_DEBUG("EditorManager", "ROM assets loaded in %lld ms", duration.count());
2753
2754 return absl::OkStatus();
2755}
2756
2757absl::Status EditorManager::LoadAssetsLazy(uint64_t passed_handle) {
2758 auto* current_rom = GetCurrentRom();
2759 auto* current_editor_set = GetCurrentEditorSet();
2760 if (!current_rom || !current_editor_set) {
2761 return absl::FailedPreconditionError("No ROM or editor set loaded");
2762 }
2763
2764 auto* current_session = session_coordinator_->GetActiveRomSession();
2765 if (!current_session) {
2766 return absl::FailedPreconditionError("No active ROM session");
2767 }
2768 ResetAssetState(current_session);
2769
2770#ifdef __EMSCRIPTEN__
2771 // Use passed handle if provided, otherwise create new one
2772 auto loading_handle =
2773 passed_handle != 0
2774 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
2775 passed_handle)
2776 : app::platform::WasmLoadingManager::BeginLoading(
2777 "Loading ROM (lazy assets)");
2778 auto cleanup_loading = [&]() {
2779 app::platform::WasmLoadingManager::EndLoading(loading_handle);
2780 };
2781 struct LoadingGuard {
2782 std::function<void()> cleanup;
2783 bool dismissed = false;
2784 ~LoadingGuard() {
2785 if (!dismissed) {
2786 cleanup();
2787 }
2788 }
2789 void dismiss() { dismissed = true; }
2790 } loading_guard{cleanup_loading};
2791#else
2792 (void)passed_handle; // Unused on non-WASM
2793#endif
2794
2795 // Set renderer for emulator (lazy initialization happens in Run())
2796 if (renderer_) {
2798 }
2799
2800 // Wire settings panel to right panel manager for the current session.
2802 auto* settings = current_editor_set->GetSettingsPanel();
2803 right_drawer_manager_->SetSettingsPanel(settings);
2804 }
2805
2806 // Apply user preferences to status bar
2808
2809#ifdef __EMSCRIPTEN__
2810 loading_guard.dismiss();
2811 app::platform::WasmLoadingManager::EndLoading(loading_handle);
2812#endif
2813
2814 LOG_INFO("EditorManager", "Lazy asset mode: editor assets deferred");
2815 return absl::OkStatus();
2816}
2817
2834 const std::optional<std::string>& target_filename) {
2835 return rom_lifecycle_.CheckRomWritePolicy(GetCurrentRom(), target_filename);
2836}
2837
2841
2847
2849 if (!pending_rom_save_.has_value() || !session_coordinator_) {
2850 return false;
2851 }
2852 return pending_rom_save_->session_index == GetCurrentSessionIndex() &&
2853 pending_rom_save_->rom != nullptr &&
2855}
2856
2858 pending_rom_save_.reset();
2860
2861 if (!hide_popups || !popup_manager_) {
2862 return;
2863 }
2864 for (const char* popup :
2867 if (popup_manager_->IsVisible(popup)) {
2868 popup_manager_->Hide(popup);
2869 }
2870 }
2871}
2872
2874 const std::optional<std::string>& save_as_filename) {
2875 auto* rom = GetCurrentRom();
2876 if (!session_coordinator_ || !session_coordinator_->GetActiveRomSession() ||
2877 !rom) {
2878 return absl::FailedPreconditionError("No active ROM session");
2879 }
2880
2883 PendingRomSave{save_as_filename, GetCurrentSessionIndex(), rom};
2884 return absl::OkStatus();
2885}
2886
2887void EditorManager::FinishPendingRomSaveAttempt(const absl::Status& status) {
2888 // A Cancelled status is resumable only while one of this request's explicit
2889 // confirmation dialogs is pending. Every other outcome is terminal and must
2890 // consume all one-shot bypasses so a later Ctrl+S cannot inherit consent.
2891 if (status.ok() || !absl::IsCancelled(status) ||
2894 }
2895}
2896
2898 // Do not let an autosave or an unrelated Ctrl+S replace a Save As request
2899 // while one of its confirmation dialogs is still open.
2900 if (pending_rom_save_.has_value()) {
2901 const bool unresolved_confirmation =
2906 if (unresolved_confirmation) {
2907 return absl::CancelledError("Save pending confirmation");
2908 }
2909 // Preserve the established request after programmatic confirmation too.
2910 // Older callers use ConfirmRomWrite(); SaveRom() rather than the popup's
2911 // ResumePendingRomSave() helper.
2912 return ResumePendingRomSave();
2913 }
2914 RETURN_IF_ERROR(StartPendingRomSave(std::nullopt));
2915 auto status = SaveRomInternal(std::nullopt);
2917 return status;
2918}
2919
2920absl::Status EditorManager::SaveRomAs(const std::string& filename) {
2921 if (filename.empty()) {
2922 return absl::InvalidArgumentError("No filename provided for save as");
2923 }
2924 if (pending_rom_save_.has_value() && HasPendingRomSaveConfirmation()) {
2925 return absl::CancelledError("Save pending confirmation");
2926 }
2928 auto status = SaveRomInternal(filename);
2930 return status;
2931}
2932
2934 if (!pending_rom_save_.has_value()) {
2936 return absl::FailedPreconditionError("No pending ROM save to resume");
2937 }
2939 CancelPendingRomSave(/*hide_popups=*/true);
2940 return absl::FailedPreconditionError(
2941 "Pending ROM save belongs to a different session");
2942 }
2943
2944 const auto save_as_filename = pending_rom_save_->save_as_filename;
2945 auto status = SaveRomInternal(save_as_filename);
2947 return status;
2948}
2949
2950absl::Status EditorManager::OpenRomOrProject(const std::string& filename) {
2953 GetCurrentSessionId(), SIZE_MAX, filename})) {
2954 return absl::OkStatus();
2955 }
2956
2957 return OpenRomOrProjectInternal(filename);
2958}
2959
2961 const std::string& filename) {
2962 LOG_INFO("EditorManager", "OpenRomOrProject called with: '%s'",
2963 filename.c_str());
2964 if (filename.empty()) {
2965 LOG_INFO("EditorManager", "Empty filename provided, skipping load.");
2966 return absl::OkStatus();
2967 }
2968
2969#ifdef __EMSCRIPTEN__
2970 // Start loading indicator early for WASM builds
2971 auto loading_handle =
2972 app::platform::WasmLoadingManager::BeginLoading("Loading ROM");
2973 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
2974 "Reading ROM file...");
2975 // RAII guard to ensure loading indicator is closed even on early return
2976 struct LoadingGuard {
2977 app::platform::WasmLoadingManager::LoadingHandle handle;
2978 bool dismissed = false;
2979 ~LoadingGuard() {
2980 if (!dismissed)
2981 app::platform::WasmLoadingManager::EndLoading(handle);
2982 }
2983 void dismiss() { dismissed = true; }
2984 } loading_guard{loading_handle};
2985#endif
2986
2987 if (absl::EndsWith(filename, ".yaze") ||
2988 absl::EndsWith(filename, ".zsproj") ||
2989 absl::EndsWith(filename, ".yazeproj")) {
2990 // Parse first so a failed project open cannot destroy the active session's
2991 // working context. Detach only when the incoming project is ready.
2992 project::YazeProject incoming_project;
2993 RETURN_IF_ERROR(incoming_project.Open(filename));
2994 if (!incoming_project.rom_filename.empty()) {
2995 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
2996 incoming_project.rom_filename));
2997 }
2998 const std::optional<size_t> previous_session_id =
3001 pending_project_open_previous_session_id_ = previous_session_id;
3003 current_project_ = std::move(incoming_project);
3006
3007 // Load ROM directly from project - don't prompt user
3008 auto project_status = LoadProjectWithRom();
3009 if (!project_status.ok()) {
3010 RestoreProjectContextAfterFailedOpen(previous_session_id);
3011 }
3012 return project_status;
3013 } else {
3014#ifdef __EMSCRIPTEN__
3015 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.05f);
3016 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
3017 "Loading ROM data...");
3018#endif
3019 Rom temp_rom;
3020 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(filename));
3021 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, filename));
3023
3024 const std::optional<size_t> previous_session_id =
3026 const size_t previous_session_count =
3027 session_coordinator_->GetTotalSessionCount();
3028 auto session_or = session_coordinator_->CreateSessionFromRom(
3029 std::move(temp_rom), filename);
3030 if (!session_or.ok()) {
3031 return session_or.status();
3032 }
3033 RomSession* session = *session_or;
3034
3038
3039 // Apply project feature flags to both session and global singleton
3043
3044 // Keep ResourceLabelProvider in sync with the active ROM session before
3045 // editors register room/sprite labels.
3047
3048 // Update test manager with current ROM for ROM-dependent tests (only when
3049 // tests are enabled)
3050#ifdef YAZE_ENABLE_TESTING
3051 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
3052 (void*)GetCurrentRom(),
3053 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
3055#endif
3056
3057 if (auto* editor_set = GetCurrentEditorSet();
3058 editor_set && !current_project_.code_folder.empty()) {
3059 const std::string absolute_code_folder =
3061 // iOS: avoid blocking the main thread during project open / scene updates.
3062 // Large iCloud-backed projects can trigger watchdog termination if we
3063 // eagerly enumerate folders here.
3064#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
3065 editor_set->OpenAssemblyFolder(absolute_code_folder);
3066#endif
3067 // Also set the sidebar file browser path (refresh happens during UI draw).
3068 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
3069 }
3070
3071#ifdef __EMSCRIPTEN__
3072 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.10f);
3073 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
3074 "Initializing editors...");
3075 // Pass the loading handle to LoadAssets and dismiss our guard
3076 // LoadAssets will manage closing the indicator when done
3077 loading_guard.dismiss();
3078 auto asset_status = LoadAssetsForMode(loading_handle);
3079#else
3080 auto asset_status = LoadAssetsForMode();
3081#endif
3082 if (!asset_status.ok()) {
3083 auto rollback_status =
3084 DiscardProvisionalSessionCreatedSince(previous_session_count);
3085 RestoreProjectContextAfterFailedOpen(previous_session_id);
3086 if (!rollback_status.ok()) {
3087 return absl::InternalError(
3088 absl::StrFormat("%s; session rollback failed: %s",
3089 asset_status.message(), rollback_status.message()));
3090 }
3091 return asset_status;
3092 }
3093
3096 }
3097 return absl::OkStatus();
3098}
3099
3100absl::Status EditorManager::CreateNewProject(const std::string& template_name) {
3102 return absl::FailedPreconditionError(
3103 "Save or discard pending session work before creating a project");
3104 }
3105
3106 if (!ui_coordinator_) {
3107 return absl::FailedPreconditionError(
3108 "Project creation dialog is not available");
3109 }
3110 ui_coordinator_->OpenNewProjectDialog(
3111 template_name.empty() ? "Vanilla ROM Hack" : template_name);
3112 return absl::OkStatus();
3113}
3114
3116 const std::string& template_name, const std::string& rom_path,
3117 const std::string& project_name, const std::string& project_path) {
3118 if (rom_path.empty() || project_name.empty()) {
3119 return absl::InvalidArgumentError("ROM path and project name are required");
3120 }
3121 if (absl::EndsWith(rom_path, ".yaze") ||
3122 absl::EndsWith(rom_path, ".yazeproj") ||
3123 absl::EndsWith(rom_path, ".zsproj")) {
3124 return absl::InvalidArgumentError(
3125 "New projects require a ROM file, not a project descriptor");
3126 }
3128 return absl::FailedPreconditionError(
3129 "Save or discard pending session work before creating a project");
3130 }
3131
3132 auto* reusable_session = session_coordinator_
3133 ? session_coordinator_->GetActiveRomSession()
3134 : nullptr;
3135 const bool reuse_active_raw_session =
3136 reusable_session && reusable_session->rom.is_loaded() &&
3138 rom_path, reusable_session->rom.filename()) &&
3141 const std::optional<size_t> previous_session_id =
3143 const size_t previous_session_count =
3144 session_coordinator_ ? session_coordinator_->GetTotalSessionCount() : 0;
3146 auto rom_status = project_manager_.SetProjectRom(rom_path);
3147 if (!rom_status.ok()) {
3149 return rom_status;
3150 }
3152 project_name, project_path);
3153 if (!target_status.ok()) {
3155 return target_status;
3156 }
3157
3158 if (reuse_active_raw_session) {
3159 return FinalizeNewProject(project_name, project_path);
3160 }
3161
3165
3166 auto open_status = OpenRomOrProjectInternal(rom_path);
3167 if (!open_status.ok()) {
3169 RestoreProjectContextAfterFailedOpen(previous_session_id);
3170 return open_status;
3171 }
3172
3173 auto finalize_status = FinalizeNewProject(project_name, project_path);
3174 if (!finalize_status.ok()) {
3175 auto rollback_status =
3176 DiscardProvisionalSessionCreatedSince(previous_session_count);
3177 RestoreProjectContextAfterFailedOpen(previous_session_id);
3178 if (!rollback_status.ok()) {
3179 return absl::InternalError(absl::StrFormat(
3180 "%s; session rollback failed: %s", finalize_status.message(),
3181 rollback_status.message()));
3182 }
3183 }
3184 return finalize_status;
3185}
3186
3188 const std::string& project_name, const std::string& project_path) {
3189 if (!session_coordinator_) {
3190 return absl::FailedPreconditionError("No session coordinator");
3191 }
3192 auto* session = session_coordinator_->GetActiveRomSession();
3193 if (!session || !session->rom.is_loaded()) {
3194 return absl::FailedPreconditionError(
3195 "Load a ROM before finalizing the project");
3196 }
3197
3198 // The ROM-load-options dialog can create a project directly from an
3199 // already-open raw ROM. Start from a clean shell in that path rather than
3200 // reusing ProjectManager state left by an earlier project/import.
3201 const bool created_project_shell = !project_manager_.IsPendingRomSelection();
3202 if (created_project_shell) {
3204 }
3205
3206 auto project_rom_status =
3207 project_manager_.SetProjectRom(session->rom.filename());
3208 if (!project_rom_status.ok()) {
3210 return project_rom_status;
3211 }
3212 auto& pending_project = project_manager_.GetCurrentProject();
3213 if (created_project_shell) {
3214 // ROM-load options apply their selected flags directly to the raw session.
3215 // Guided creation already configured the pending template, including when
3216 // it reuses an existing raw-ROM session.
3217 pending_project.feature_flags = session->feature_flags;
3218 }
3219 pending_project.workspace_settings.font_global_scale =
3221 pending_project.workspace_settings.autosave_enabled =
3223 pending_project.workspace_settings.autosave_interval_secs =
3225 pending_project.workspace_settings.backup_on_save =
3227
3228 auto finalize_status =
3229 project_manager_.FinalizeProjectCreation(project_name, project_path);
3230 if (!finalize_status.ok()) {
3232 return finalize_status;
3233 }
3234
3237 session->project_dirty = false;
3238 active_project_context_session_id_ = session->session_id();
3239 runtime_feature_flags_session_id_ = session->session_id();
3241 if (version_manager_) {
3242 // Creating a project never creates a Git repository; the Initialize Git
3243 // action in Project Management is the opt-in. Adopt one that exists.
3245 current_project_.git_repository = session->project_context->git_repository;
3247 }
3251 return absl::OkStatus();
3252}
3253
3257 GetCurrentSessionId()})) {
3258 return absl::OkStatus();
3259 }
3260
3261 return OpenProjectInternal();
3262}
3263
3265 auto open_project_from_path =
3266 [this](const std::string& file_path) -> absl::Status {
3267 if (file_path.empty()) {
3268 return absl::OkStatus();
3269 }
3270
3271 project::YazeProject new_project;
3272 RETURN_IF_ERROR(new_project.Open(file_path));
3273
3274 if (!new_project.rom_filename.empty()) {
3275 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
3276 new_project.rom_filename));
3277 }
3278
3279 // Validate project
3280 auto validation_status = new_project.Validate();
3281 if (!validation_status.ok()) {
3282 toast_manager_.Show(absl::StrFormat("Project validation failed: %s",
3283 validation_status.message()),
3285
3286 // Ask user if they want to repair
3287 popup_manager_->Show("Project Repair");
3288 }
3289
3290 const std::optional<size_t> previous_session_id =
3293 pending_project_open_previous_session_id_ = previous_session_id;
3295 current_project_ = std::move(new_project);
3297
3298 auto project_status = LoadProjectWithRom();
3299 if (!project_status.ok()) {
3300 RestoreProjectContextAfterFailedOpen(previous_session_id);
3301 }
3302 return project_status;
3303 };
3304
3305#if defined(__APPLE__) && TARGET_OS_IOS == 1
3306 // On iOS, route project selection through the SwiftUI overlay document picker
3307 // so we get open-in-place + security-scoped access for iCloud Drive bundles.
3308 platform::ios::PostOverlayCommand("open_project");
3309 return absl::OkStatus();
3310#else
3312 return open_project_from_path(file_path);
3313#endif
3314}
3315
3317 const std::string& rom_path) {
3318 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
3319
3320 Rom candidate_rom;
3321 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&candidate_rom, rom_path));
3323
3326 .candidate_path = rom_path,
3327 };
3328 return absl::OkStatus();
3329}
3330
3331absl::Status EditorManager::SwapProjectRom(const std::string& rom_path) {
3332 if (rom_path.empty()) {
3333 return absl::InvalidArgumentError("ROM path cannot be empty");
3334 }
3335 if (!session_coordinator_) {
3336 return absl::FailedPreconditionError("No session coordinator");
3337 }
3338 auto* session = session_coordinator_->GetActiveRomSession();
3339 if (!session || !session->rom.is_loaded() ||
3341 return absl::FailedPreconditionError("No active project ROM session");
3342 }
3344 return absl::FailedPreconditionError(
3345 "Save or discard ROM edits before swapping the project ROM");
3346 }
3348 if (session->project_file_editor_state.initialized &&
3349 session->project_file_editor_state.modified &&
3351 return absl::FailedPreconditionError(
3352 "Preserve the raw project-file draft before swapping ROMs");
3353 }
3354
3355 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
3356 rom_path, session->session_id()));
3357 Rom replacement_rom;
3358 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom, rom_path));
3359
3360 // Save As deliberately leaves the project's collision source untouched.
3361 // Refuse an incompatible copy before saving the descriptor or rebuilding
3362 // live editor models: otherwise neither normal Save nor Build can repair
3363 // the newly mismatched pair. Rebinding never publishes source changes.
3366 &replacement_rom, current_project_.GetAbsolutePath(
3368 }
3370
3371 std::string replacement_path = rom_path;
3376 // The policy proved this is the manifest's exact backing file. Bind its
3377 // canonical path so an accepted alias cannot later fail the write policy
3378 // or be replaced as a separate file by the atomic ROM save.
3379 std::error_code ec;
3380 const auto development_path = std::filesystem::canonical(
3383 ec);
3384 if (ec) {
3385 return absl::FailedPreconditionError(absl::StrFormat(
3386 "Cannot resolve the manifest development ROM before rebinding: %s",
3387 ec.message()));
3388 }
3389 replacement_path = development_path.string();
3390 replacement_rom.set_filename(replacement_path);
3391 }
3392
3393 const std::string old_rom_path = current_project_.rom_filename;
3394 current_project_.rom_filename = replacement_path;
3395 auto save_status = current_project_.Save();
3396 if (!save_status.ok()) {
3397 current_project_.rom_filename = old_rom_path;
3398 return save_status;
3399 }
3400
3402 auto replace_status =
3403 ReplaceActiveSessionRom(std::move(replacement_rom), replacement_path);
3404 if (!replace_status.ok()) {
3405 current_project_.rom_filename = old_rom_path;
3406 auto rollback_status = current_project_.Save();
3409 if (!rollback_status.ok()) {
3410 return absl::InternalError(absl::StrFormat(
3411 "ROM swap failed (%s) and restoring the project descriptor failed "
3412 "(%s)",
3413 replace_status.message(), rollback_status.message()));
3414 }
3415 return replace_status;
3416 }
3417 session->project_dirty = false;
3419 project_management_panel_->SetProjectDirty(false);
3420 }
3423 return absl::OkStatus();
3424}
3425
3427 if (!session_coordinator_) {
3428 return absl::FailedPreconditionError("No session coordinator");
3429 }
3430 auto* session = session_coordinator_->GetActiveRomSession();
3431 if (!session || !session->rom.is_loaded() ||
3434 return absl::FailedPreconditionError("No active project ROM to reload");
3435 }
3437 return absl::FailedPreconditionError(
3438 "Save or discard ROM edits before reloading from disk");
3439 }
3440
3441 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
3442 current_project_.rom_filename, session->session_id()));
3443 Rom replacement_rom;
3444 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom,
3447 return ReplaceActiveSessionRom(std::move(replacement_rom),
3449}
3450
3454 if (auto* session = session_coordinator_->GetActiveRomSession();
3455 session && session->project_file_editor_state.initialized &&
3456 session->project_file_editor_state.modified &&
3458 return absl::FailedPreconditionError(
3459 "Raw project file also has unsaved changes; use raw Save As to "
3460 "preserve that draft before saving project settings");
3461 }
3462 }
3466 return CreateNewProject();
3467 }
3468
3469 // Update project with current settings
3472 auto* session = session_coordinator_->GetActiveRomSession();
3473 if (session) {
3474 current_project_.feature_flags = session->feature_flags;
3475 }
3476 }
3477
3486
3487 // Save recent files
3490 for (const auto& file : manager.GetRecentFiles()) {
3492 }
3493 }
3494
3495 auto status = current_project_.Save();
3496 if (status.ok()) {
3498 project_management_panel_->SetProjectDirty(false);
3499 }
3501 if (auto* session = session_coordinator_->GetActiveRomSession()) {
3502 session->project_dirty = false;
3503 }
3504 }
3506 }
3508 return status;
3509}
3510
3513 project_management_panel_->SetProjectDirty(true);
3514 }
3516 if (auto* session = session_coordinator_->GetActiveRomSession()) {
3517 session->project_dirty = true;
3518 }
3519 }
3520}
3521
3523 if (!session_coordinator_) {
3525 project_management_panel_->IsProjectDirty();
3526 }
3527 const auto* session = session_coordinator_->GetActiveRomSession();
3528 return session != nullptr &&
3529 (session->project_dirty ||
3530 session->editors.HasPendingProjectDraftChanges());
3531}
3532
3534 if (!session_coordinator_) {
3535 return absl::FailedPreconditionError("No session coordinator");
3536 }
3537 auto* session = session_coordinator_->GetActiveRomSession();
3538 if (!session) {
3539 return absl::FailedPreconditionError("No active ROM session");
3540 }
3541
3543 const bool project_dirty = session->project_dirty ||
3544 session->editors.HasPendingProjectDraftChanges();
3545 const bool project_file_dirty =
3546 session->project_file_editor_state.initialized &&
3547 session->project_file_editor_state.modified;
3548
3549 if (project_dirty && project_file_dirty &&
3551 return absl::FailedPreconditionError(
3552 "Project settings and raw project file both have unsaved changes; "
3553 "use raw Save As to preserve the draft, then save project settings");
3554 }
3555
3556 if (project_dirty) {
3558 current_project_.filepath.empty()) {
3559 return absl::FailedPreconditionError(
3560 "Project settings have no project file to save");
3561 }
3563 }
3564
3565 if (project_file_dirty) {
3566 if (project_file_editor_.filepath().empty()) {
3567 return absl::FailedPreconditionError(
3568 "Project file draft has no destination; use Save As first");
3569 }
3571 session->project_file_editor_state = project_file_editor_.CaptureState();
3572 }
3573
3574 return absl::OkStatus();
3575}
3576
3578 if (!session_coordinator_) {
3579 return absl::FailedPreconditionError("No session coordinator");
3580 }
3581 const size_t session_index = GetCurrentSessionIndex();
3583 const auto* session = session_coordinator_->GetActiveRomSession();
3584 if (session != nullptr && session->backup_restore_pending) {
3585 return absl::CancelledError(
3586 "Staged backup restore requires an explicit Save ROM");
3587 }
3588 if (SessionHasPendingRomWork(session_index)) {
3590 }
3591 if (SessionHasPendingUnsavedWork(session_index)) {
3592 return absl::FailedPreconditionError(
3593 "Autosave left unsaved project or ROM work");
3594 }
3595 return absl::OkStatus();
3596}
3597
3599 PotItemSaveDecision decision) {
3600 // Map EditorManager enum to RomLifecycleManager enum
3601 auto lifecycle_decision =
3603 static_cast<int>(decision));
3605
3606 if (decision == PotItemSaveDecision::kCancel) {
3608 toast_manager_.Show("Save cancelled", ToastType::kInfo);
3609 return;
3610 }
3611
3612 auto status = ResumePendingRomSave();
3613 if (!absl::IsCancelled(status) && !status.ok()) {
3615 absl::StrFormat("Failed to save ROM: %s", status.message()),
3617 }
3618}
3619
3621 // Get current project name for default filename
3622 std::string default_name = current_project_.project_opened()
3624 : "untitled_project";
3625
3626 auto file_path =
3627 util::FileDialogWrapper::ShowSaveFileDialog(default_name, "yaze");
3628 if (file_path.empty()) {
3629 return absl::OkStatus();
3630 }
3631
3632 return SaveProjectAs(file_path);
3633}
3634
3635absl::Status EditorManager::SaveProjectAs(const std::string& filepath) {
3636 if (filepath.empty()) {
3637 return absl::InvalidArgumentError("Project file path cannot be empty");
3638 }
3640 std::string file_path = filepath;
3641
3642 // Ensure a project extension.
3643 if (!(absl::EndsWith(file_path, ".yaze") ||
3644 absl::EndsWith(file_path, ".yazeproj"))) {
3645 file_path += ".yaze";
3646 }
3647
3649 if (auto* session = session_coordinator_->GetActiveRomSession();
3650 session && session->project_file_editor_state.initialized &&
3651 session->project_file_editor_state.modified &&
3653 session->project_file_editor_state.filepath, file_path)) {
3654 return absl::FailedPreconditionError(
3655 "Raw project draft targets the selected file; preserve it with "
3656 "raw Save As before overwriting that destination");
3657 }
3658 }
3659
3662
3663 // Update project filepath and save
3664 std::string old_filepath = current_project_.filepath;
3665 current_project_.filepath = file_path;
3666
3667 auto save_status = current_project_.Save();
3668 if (save_status.ok()) {
3670 project_management_panel_->SetProjectDirty(false);
3671 }
3673 if (auto* session = session_coordinator_->GetActiveRomSession()) {
3674 session->project_dirty = false;
3675 }
3676 }
3678 RebaseCleanProjectFileDraft(file_path);
3679
3680 // Add to recent files
3682 manager.AddFile(file_path);
3683 manager.Save();
3684
3685 const auto catalog_path = core::PaletteCatalog::ResolvePathForProject(
3688 absl::IsInvalidArgument(catalog_path.status())) {
3690 absl::StrFormat(
3691 "Project saved as: %s. Its palette catalog cannot load from this "
3692 "folder. Copy the catalog into the new project folder, then "
3693 "update palette_catalog_file in Edit Project File.",
3694 file_path),
3696 } else {
3697 toast_manager_.Show(absl::StrFormat("Project saved as: %s", file_path),
3699 }
3700 } else {
3701 // Restore old filepath on failure
3702 current_project_.filepath = old_filepath;
3704 absl::StrFormat("Failed to save project: %s", save_status.message()),
3706 }
3707
3709
3710 return save_status;
3711}
3712
3713absl::StatusOr<std::string> EditorManager::ResolveProjectBuildCommand() {
3715 return absl::FailedPreconditionError("No project open");
3716 }
3717
3718 // A Build button can run during the panel's Draw() before the frame-end
3719 // project draft capture. Read the live panel and raw editor state now.
3721 if (auto* editors = GetCurrentEditorSet()) {
3722 const auto* dungeon =
3723 editors->GetEditorAs<DungeonEditorV2>(EditorType::kDungeon);
3724 if (dungeon && dungeon->HasUnpublishedMinecartTrackChanges()) {
3725 return absl::FailedPreconditionError(
3726 "Minecart tracks have unpublished changes. Use Publish Tracks, then "
3727 "save remaining project and ROM changes before Build Project. "
3728 "Undo the track edits if you do not want to publish them.");
3729 }
3730 }
3731 const size_t session_index = GetCurrentSessionIndex();
3732 if (SessionHasPendingUnsavedWork(session_index)) {
3733 return absl::FailedPreconditionError(
3734 absl::StrFormat("Save project and ROM changes before building: %s",
3735 DescribePendingUnsavedWork(session_index)));
3736 }
3737
3738 const Rom* active_rom = GetCurrentRom();
3739 if (active_rom != nullptr && active_rom->is_loaded() &&
3740 !current_project_.rom_filename.empty() &&
3742 active_rom->filename(),
3744 return absl::FailedPreconditionError(absl::StrFormat(
3745 "Build source mismatch: active ROM %s differs from project ROM %s. "
3746 "Use Project Management > Swap ROM to bind the saved copy, or reopen "
3747 "the project's ROM before building.",
3748 active_rom->filename(),
3750 }
3751
3752 std::string command = current_project_.build_script;
3753 if (command.empty() && current_project_.hack_manifest.loaded()) {
3754 const auto& pipeline = current_project_.hack_manifest.build_pipeline();
3755 if (!pipeline.dev_rom.empty() && !current_project_.rom_filename.empty() &&
3757 current_project_.GetAbsolutePath(pipeline.dev_rom),
3759 return absl::FailedPreconditionError(absl::StrFormat(
3760 "Build source mismatch: manifest development ROM %s differs from "
3761 "project ROM %s.",
3762 current_project_.GetAbsolutePath(pipeline.dev_rom),
3764 }
3765 command = pipeline.build_script;
3766 // Manifest build_script is normally a path. Do not parse arbitrary shell
3767 // commands here; only check a single, unquoted file-path token.
3768 if (!command.empty() &&
3769 command.find_first_of(" \t\r\n|&;<>()$`'\"\\") == std::string::npos) {
3770 const std::filesystem::path script_path(command);
3771 if (script_path.is_absolute() || script_path.has_parent_path() ||
3772 script_path.extension() == ".sh") {
3773 const std::string absolute_script =
3775 std::error_code ec;
3776 if (!std::filesystem::is_regular_file(absolute_script, ec)) {
3777 return absl::FailedPreconditionError(absl::StrFormat(
3778 "Manifest build script is unavailable: %s. Configure a build "
3779 "script for this project before building.",
3780 absolute_script));
3781 }
3782 }
3783 }
3784 }
3785 if (command.empty()) {
3786 return absl::NotFoundError("Project does not define a build command");
3787 }
3788 return command;
3789}
3790
3791absl::StatusOr<std::string> EditorManager::ResolveProjectRunTarget() const {
3793 return absl::FailedPreconditionError("No project open");
3794 }
3795
3796 std::string target;
3799 }
3800 if (target.empty()) {
3802 }
3803 if (target.empty()) {
3804 return absl::NotFoundError("Project does not define a run target ROM");
3805 }
3806 return current_project_.GetAbsolutePath(target);
3807}
3808
3810 const std::string& summary, const std::string& detail,
3811 ProjectWorkflowState state, const std::string& output_tail,
3812 bool can_cancel) const {
3813 return {.visible = true,
3814 .can_cancel = can_cancel,
3815 .label = "Build",
3816 .summary = summary,
3817 .detail = detail,
3818 .output_tail = output_tail,
3819 .state = state};
3820}
3821
3823 const std::string& summary, const std::string& detail,
3824 ProjectWorkflowState state) const {
3825 return {.visible = true,
3826 .label = "Run",
3827 .summary = summary,
3828 .detail = detail,
3829 .state = state};
3830}
3831
3832absl::StatusOr<std::string> EditorManager::RunProjectBuildCommand() {
3833 auto command_or = ResolveProjectBuildCommand();
3834 if (!command_or.ok()) {
3835 return command_or.status();
3836 }
3837
3838 const std::string command = *command_or;
3839 const std::filesystem::path project_root =
3840 std::filesystem::path(current_project_.filepath).parent_path();
3842 auto start_status = task.Start(command, project_root.string());
3843 if (!start_status.ok()) {
3844 return start_status;
3845 }
3846 auto wait_status = task.Wait();
3847 const auto snapshot = task.GetSnapshot();
3848 if (!wait_status.ok()) {
3849 return wait_status;
3850 }
3851 const std::string summary = LastNonEmptyLine(snapshot.output);
3852 return summary.empty() ? command : summary;
3853}
3854
3856 const std::string running_detail =
3857 "Running the configured project build command";
3858 UpdateBuildWorkflowStatus(
3860 MakeBuildStatus("Build running", running_detail,
3861 ProjectWorkflowState::kRunning, "", false));
3862
3863 auto result_or = RunProjectBuildCommand();
3864 if (!result_or.ok()) {
3865 const bool unavailable =
3866 result_or.status().code() == absl::StatusCode::kFailedPrecondition ||
3867 result_or.status().code() == absl::StatusCode::kNotFound;
3868 UpdateBuildWorkflowStatus(
3870 MakeBuildStatus(unavailable ? "Build unavailable" : "Build failed",
3871 std::string(result_or.status().message()),
3874 absl::StrFormat("%s: %s",
3875 unavailable ? "Build unavailable" : "Build failed",
3876 result_or.status().message()),
3878 return result_or.status();
3879 }
3880
3881 const std::string summary = *result_or;
3882 UpdateBuildWorkflowStatus(&status_bar_, project_management_panel_.get(),
3883 MakeBuildStatus("Build succeeded", summary,
3886 summary.empty() ? "Project build completed"
3887 : absl::StrFormat("Project build completed: %s", summary),
3889 LOG_INFO("EditorManager", "Project build completed: %s", summary.c_str());
3890 return absl::OkStatus();
3891}
3892
3895 const auto snapshot = active_project_build_->GetSnapshot();
3896 if (snapshot.running) {
3897 toast_manager_.Show("A project build is already running",
3899 return;
3900 }
3901 }
3902
3903 auto command_or = ResolveProjectBuildCommand();
3904 if (!command_or.ok()) {
3905 UpdateBuildWorkflowStatus(
3907 MakeBuildStatus("Build unavailable",
3908 std::string(command_or.status().message()),
3911 absl::StrFormat("Build unavailable: %s", command_or.status().message()),
3913 return;
3914 }
3915
3916 const std::filesystem::path project_root =
3917 std::filesystem::path(current_project_.filepath).parent_path();
3918 active_project_build_ = std::make_unique<BackgroundCommandTask>();
3919 auto start_status =
3920 active_project_build_->Start(*command_or, project_root.string());
3921 if (!start_status.ok()) {
3922 UpdateBuildWorkflowStatus(
3924 MakeBuildStatus("Build unavailable",
3925 std::string(start_status.message()),
3928 absl::StrFormat("Build unavailable: %s", start_status.message()),
3930 active_project_build_.reset();
3931 return;
3932 }
3933
3935 UpdateBuildWorkflowStatus(
3937 MakeBuildStatus("Build running",
3938 "Running the configured project build command",
3941 project_management_panel_->SetBuildLogOutput("");
3942 }
3944}
3945
3947 if (!active_project_build_) {
3948 return;
3949 }
3950
3951 const auto snapshot = active_project_build_->GetSnapshot();
3952 if (!snapshot.running) {
3953 return;
3954 }
3955
3956 active_project_build_->Cancel();
3957 UpdateBuildWorkflowStatus(
3959 MakeBuildStatus("Cancelling build", "Stopping the active project build",
3960 ProjectWorkflowState::kRunning, snapshot.output_tail,
3961 false));
3962}
3963
3965 UpdateRunWorkflowStatus(
3967 MakeRunStatus("Run queued",
3968 "Preparing the project output for emulator reload",
3970
3971 auto run_target_or = ResolveProjectRunTarget();
3972 if (!run_target_or.ok()) {
3973 UpdateRunWorkflowStatus(
3975 MakeRunStatus("Run unavailable",
3976 std::string(run_target_or.status().message()),
3978 toast_manager_.Show(absl::StrFormat("Run unavailable: %s",
3979 run_target_or.status().message()),
3981 return run_target_or.status();
3982 }
3983 const std::string run_target = *run_target_or;
3984 if (!std::filesystem::exists(run_target)) {
3985 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
3986 MakeRunStatus("Run target missing", run_target,
3988 toast_manager_.Show("Run target ROM not found. Build the project first.",
3990 return absl::NotFoundError("Run target ROM not found");
3991 }
3992
3993#ifdef YAZE_WITH_GRPC
3994 if (auto* emulator_backend = Application::Instance().GetEmulatorBackend()) {
3995 auto load_status = emulator_backend->LoadRom(run_target);
3996 if (load_status.ok()) {
3997 AppendWorkflowHistoryEntry(
3998 "Run",
3999 MakeRunStatus("Reloaded in backend", run_target,
4001 "");
4002 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
4003 MakeRunStatus("Reloaded in backend", run_target,
4005 if (ui_coordinator_) {
4006 ui_coordinator_->SetEmulatorVisible(true);
4007 }
4009 absl::StrFormat("Reloaded project output in emulator backend: %s",
4010 run_target),
4012 return absl::OkStatus();
4013 }
4014 }
4015#endif
4016
4017 Rom temp_rom;
4018 auto load_rom_status = rom_file_manager_.LoadRom(&temp_rom, run_target);
4019 if (!load_rom_status.ok()) {
4020 UpdateRunWorkflowStatus(
4022 MakeRunStatus("Run load failed", std::string(load_rom_status.message()),
4024 toast_manager_.Show(absl::StrFormat("Failed to load run target ROM: %s",
4025 load_rom_status.message()),
4027 return load_rom_status;
4028 }
4029
4030 auto reload_status = emulator_.ReloadRuntimeRom(temp_rom.vector());
4031 if (!reload_status.ok()) {
4032 UpdateRunWorkflowStatus(
4034 MakeRunStatus("Reload failed", std::string(reload_status.message()),
4036 toast_manager_.Show(absl::StrFormat("Failed to reload emulator runtime: %s",
4037 reload_status.message()),
4039 return reload_status;
4040 }
4041
4042 if (ui_coordinator_) {
4043 ui_coordinator_->SetEmulatorVisible(true);
4044 }
4045 AppendWorkflowHistoryEntry("Run",
4046 MakeRunStatus("Reloaded in emulator", run_target,
4048 "");
4049 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
4050 MakeRunStatus("Reloaded in emulator", run_target,
4053 absl::StrFormat("Reloaded project output in emulator: %s", run_target),
4055 return absl::OkStatus();
4056}
4057
4058absl::Status EditorManager::ImportProject(const std::string& project_path) {
4059 // Delegate to ProjectManager for import logic
4061 // Sync local project reference
4065 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4068 }
4069 }
4072 return absl::OkStatus();
4073}
4074
4077 return absl::FailedPreconditionError("No project is currently open");
4078 }
4079
4082 toast_manager_.Show("Project repaired successfully",
4084
4085 return absl::OkStatus();
4086}
4087
4089 if (auto* editor_set = GetCurrentEditorSet()) {
4090 return editor_set->GetOverworldData();
4091 }
4092 return nullptr;
4093}
4094
4096 if (!rom) {
4097 return absl::InvalidArgumentError("Invalid ROM pointer");
4098 }
4099
4100 // We need to find the session that owns this ROM.
4101 // This is inefficient but SetCurrentRom is rare.
4103 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
4104 auto* session =
4105 static_cast<RomSession*>(session_coordinator_->GetSession(i));
4106 if (session && &session->rom == rom) {
4107 session_coordinator_->SwitchToSession(i);
4108 // Update test manager with current ROM for ROM-dependent tests
4112 return absl::OkStatus();
4113 }
4114 }
4115 }
4116 // If ROM wasn't found in existing sessions, treat as new session.
4117 // Copying an external ROM object is avoided; instead, fail.
4118 return absl::NotFoundError("ROM not found in existing sessions");
4119}
4120
4125
4126void EditorManager::CaptureCurrentRomIdentity(bool preserve_editing_source) {
4128 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4129 session->CaptureRomIdentity(preserve_editing_source);
4130 }
4131 }
4133}
4134
4140
4141// IsRomHashMismatch() is now inline in editor_manager.h delegating to
4142// rom_lifecycle_.
4143
4144std::vector<RomFileManager::BackupEntry> EditorManager::GetRomBackups() const {
4146}
4147
4149 if (!session_coordinator_) {
4150 return false;
4151 }
4152 const auto* session = session_coordinator_->GetActiveRomSession();
4153 return session != nullptr && session->backup_restore_pending;
4154}
4155
4156absl::Status EditorManager::RestoreRomBackup(const std::string& backup_path) {
4157 auto* rom = GetCurrentRom();
4158 if (!rom) {
4159 return absl::FailedPreconditionError("No ROM loaded");
4160 }
4162 return absl::FailedPreconditionError(
4163 "Save or discard ROM edits before restoring a backup");
4164 }
4165
4166 const std::string original_filename = rom->filename();
4167 const project::ResourceLabelManager original_resource_labels =
4168 *rom->resource_label();
4169 const auto backups = rom_file_manager_.ListBackups(original_filename);
4170 const bool is_managed_backup =
4171 std::any_of(backups.begin(), backups.end(), [&](const auto& backup) {
4172 return SessionCoordinator::PathsReferToSameBackingFile(backup.path,
4173 backup_path);
4174 });
4175 if (!is_managed_backup) {
4176 return absl::InvalidArgumentError(
4177 "Selected file is not a managed backup for the active ROM");
4178 }
4179
4180 // Load the backup away from the live session. Backups may legitimately have
4181 // a different size when they predate a ROM expansion or shrink. Reusing the
4182 // ROM-replacement path restores the prior ROM bytes and backing identity if
4183 // rebuilding assets fails partway through.
4184 Rom restored_rom;
4185 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&restored_rom, backup_path));
4186
4187 if (!original_filename.empty()) {
4188 restored_rom.set_filename(original_filename);
4189 }
4191 // Normal ROM backups do not copy the active `.labels` sidecar. Preserve the
4192 // session's loaded and in-memory resource labels instead of replacing them
4193 // with the usually-empty `<backup>.labels` lookup from LoadFromFile().
4194 *restored_rom.resource_label() = original_resource_labels;
4195 // Restore is intentionally staged in memory. Mark it dirty so closing or
4196 // autosave cannot silently treat the on-disk source as already restored.
4197 restored_rom.set_dirty(true);
4199 ReplaceActiveSessionRom(std::move(restored_rom), original_filename));
4200 auto* restored_session = session_coordinator_->GetActiveRomSession();
4201 if (!restored_session) {
4202 return absl::InternalError(
4203 "ROM backup restored without an active session to stage it");
4204 }
4205 restored_session->backup_restore_pending = true;
4206 return absl::OkStatus();
4207}
4208
4210 if (!session_coordinator_) {
4211 return absl::FailedPreconditionError("No session coordinator");
4212 }
4213 auto* session = session_coordinator_->GetActiveRomSession();
4214 if (!session || !session->rom.is_loaded()) {
4215 return absl::FailedPreconditionError("No ROM loaded");
4216 }
4217 if (!session->backup_restore_pending) {
4218 return absl::FailedPreconditionError("No restored backup is staged");
4219 }
4220
4221 const size_t session_index = GetCurrentSessionIndex();
4222 if (session->editors.HasPendingGraphicsChanges() ||
4223 session->HasPendingGfxGroupChanges() ||
4224 session->editors.HasPendingScreenChanges() ||
4225 HasPendingDungeonChangesForSession(session_index) ||
4226 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
4227 return absl::FailedPreconditionError(
4228 "Resolve pending graphics, screen, dungeon, or palette edits before "
4229 "discarding the restored backup");
4230 }
4231
4232 const std::string backing_path = session->rom.filename();
4233 if (backing_path.empty()) {
4234 return absl::FailedPreconditionError("ROM has no backing file to reload");
4235 }
4236
4237 // Load into scratch storage before touching the live session so file I/O
4238 // failures preserve the staged ROM. Resource labels are session work, not
4239 // part of normal ROM backups, so preserve them across the byte-level discard.
4240 const project::ResourceLabelManager resource_labels =
4241 *session->rom.resource_label();
4242 Rom backing_rom;
4243 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&backing_rom, backing_path));
4244 *backing_rom.resource_label() = resource_labels;
4246 ReplaceActiveSessionRom(std::move(backing_rom), backing_path));
4247
4248 session->backup_restore_pending = false;
4249 session->rom.ClearDirty();
4250 return absl::OkStatus();
4251}
4252
4256
4264
4268
4272
4275 session_coordinator_->CreateNewSession();
4276 // Toast messages are now shown by SessionCoordinator
4277 }
4278}
4279
4282 session_coordinator_->DuplicateCurrentSession();
4283 }
4284}
4285
4287 if (!session_coordinator_) {
4288 return;
4289 }
4290
4291 if (!session_coordinator_->HasMultipleSessions()) {
4292 // Preserve the coordinator's existing warning without offering a
4293 // "Close Without Saving" action that cannot close the final session.
4294 session_coordinator_->CloseCurrentSession();
4295 return;
4296 }
4297
4298 const size_t current_session_id = GetCurrentSessionId();
4301 current_session_id})) {
4302 return;
4303 }
4304
4305 session_coordinator_->CloseCurrentSession();
4307}
4308
4310 if (!CanCloseRom()) {
4311 return;
4312 }
4313 const size_t session_id = GetCurrentSessionId();
4316 session_id})) {
4317 return;
4318 }
4320 {PendingUnsavedSessionAction::Type::kCloseRom, session_id, session_id});
4321}
4322
4324 return session_coordinator_ != nullptr &&
4325 session_coordinator_->GetActiveRomSession() != nullptr;
4326}
4327
4329 if (!session_coordinator_) {
4330 return false;
4331 }
4332 const auto* session = session_coordinator_->GetActiveRomSession();
4333 return session != nullptr && session->rom.is_loaded() &&
4334 !session->rom.filename().empty();
4335}
4336
4338 if (!CanRevertRom()) {
4339 return absl::FailedPreconditionError("No ROM with a backing file is open");
4340 }
4341 const size_t session_id = GetCurrentSessionId();
4344 session_id})) {
4345 return absl::OkStatus(); // Confirmation popup is now showing.
4346 }
4348 toast_manager_.Show("Reverted ROM to the saved file", ToastType::kSuccess);
4349 return absl::OkStatus();
4350}
4351
4353 if (!CanRevertRom()) {
4354 return absl::FailedPreconditionError("No ROM with a backing file is open");
4355 }
4356 auto* session = session_coordinator_->GetActiveRomSession();
4357 // Graphics sheet drafts are held by the Graphics editor and are not rebuilt
4358 // by ReplaceActiveSessionRom. Refuse instead of leaving them stale.
4359 if (session->editors.HasPendingGraphicsChanges()) {
4360 return absl::FailedPreconditionError(
4361 "The Graphics editor has unsaved sheet edits; save or undo them "
4362 "before reverting");
4363 }
4364
4365 const std::string backing_path = session->rom.filename();
4366 // Load into scratch storage first so a read failure leaves the live
4367 // session untouched. Resource labels are session work stored beside the
4368 // ROM, not ROM bytes, so keep the in-memory labels (as
4369 // DiscardPendingRomBackupRestore does).
4370 const project::ResourceLabelManager resource_labels =
4371 *session->rom.resource_label();
4372 Rom backing_rom;
4373 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&backing_rom, backing_path));
4374 *backing_rom.resource_label() = resource_labels;
4376 ReplaceActiveSessionRom(std::move(backing_rom), backing_path));
4377
4378 // ReplaceActiveSessionRom may re-resolve the session pointer's contents but
4379 // not its identity; re-read it defensively.
4380 session = session_coordinator_->GetActiveRomSession();
4381 if (session != nullptr) {
4382 session->backup_restore_pending = false;
4383 session->rom.ClearDirty();
4385 return absl::DataLossError(absl::StrFormat(
4386 "ROM reloaded from disk, but the session still reports %s",
4388 }
4389 }
4390 return absl::OkStatus();
4391}
4392
4394 if (!session_coordinator_ ||
4395 !session_coordinator_->IsValidSessionIndex(index)) {
4396 return;
4397 }
4398
4399 const size_t session_id = session_coordinator_->GetSessionId(index);
4402 session_id})) {
4403 return;
4404 }
4405
4406 session_coordinator_->RemoveSession(index);
4408}
4409
4413 return;
4414 }
4415
4416 quit_ = true;
4417}
4418
4422
4424 size_t session_id) const {
4425 if (!session_coordinator_ || session_id == SIZE_MAX) {
4426 return std::nullopt;
4427 }
4428
4429 for (size_t index = 0; index < session_coordinator_->GetTotalSessionCount();
4430 ++index) {
4431 if (session_coordinator_->GetSessionId(index) == session_id) {
4432 return index;
4433 }
4434 }
4435 return std::nullopt;
4436}
4437
4440 return "";
4441 }
4442
4443 const auto& action = *pending_unsaved_session_action_;
4444 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
4445 return absl::StrFormat("%s\n\nQuitting now will discard them.",
4447 }
4448
4449 const auto source_index = ResolveSessionIndexById(action.source_session_id);
4450 const std::string session_name =
4451 source_index.has_value()
4452 ? session_coordinator_->GetSessionDisplayName(*source_index)
4453 : "Closed Session";
4454 const std::string work = source_index.has_value()
4455 ? DescribePendingUnsavedWork(*source_index)
4456 : "unsaved work";
4457
4458 switch (action.type) {
4461 return absl::StrFormat(
4462 "Session '%s' has %s. Opening another ROM or project will not save "
4463 "them automatically.",
4464 session_name, work);
4466 return absl::StrFormat(
4467 "Session '%s' has %s. Opening another project will not save them "
4468 "automatically.",
4469 session_name, work);
4471 return absl::StrFormat(
4472 "Session '%s' has %s. Switching sessions will leave them behind in "
4473 "this session.",
4474 session_name, work);
4476 return absl::StrFormat(
4477 "Session '%s' has %s. Closing it now will discard them.",
4478 session_name, work);
4480 return absl::StrFormat(
4481 "Session '%s' has %s. Closing the ROM now will discard them.",
4482 session_name, work);
4484 return absl::StrFormat(
4485 "Session '%s' has %s. Reverting reloads the ROM from disk and "
4486 "discards all unsaved ROM edits. This cannot be undone.",
4487 session_name, work);
4489 break;
4490 }
4491
4492 return "";
4493}
4494
4497 return "Save Work";
4498 }
4499
4500 switch (pending_unsaved_session_action_->type) {
4503 return "Save Work & Open";
4505 return "Save Work & Open Project";
4507 return "Save Work & Switch";
4509 return "Save Work & Close";
4511 return "Save Work & Close ROM";
4513 // Saving and then reloading from disk is a no-op; the popup hides the
4514 // save button when this label is empty.
4515 return "";
4517 return ModifiedSessionCount() == 1 ? "Save Work & Quit"
4518 : "Save Modified Work & Quit";
4519 }
4520
4521 return "Save Work";
4522}
4523
4526 return "Continue";
4527 }
4528
4529 switch (pending_unsaved_session_action_->type) {
4532 return "Open Without Saving";
4534 return "Open Project Without Saving";
4536 return "Switch Without Saving";
4539 return "Close Without Saving";
4541 return "Discard Edits & Revert";
4543 return "Quit Without Saving";
4544 }
4545
4546 return "Continue";
4547}
4548
4551 return;
4552 }
4553
4554 const auto action = *pending_unsaved_session_action_;
4556
4557 if (popup_manager_) {
4559 }
4560
4561 const size_t original_session_id = GetCurrentSessionId();
4562 auto save_modified_session = [this](size_t session_id) -> absl::Status {
4563 const auto session_index = ResolveSessionIndexById(session_id);
4564 if (!session_index.has_value()) {
4565 return absl::FailedPreconditionError(
4566 "The ROM session is no longer available");
4567 }
4568 if (!SessionHasPendingUnsavedWork(*session_index)) {
4569 return absl::OkStatus();
4570 }
4571
4572 session_coordinator_->SwitchToSession(*session_index);
4574 if (SessionHasPendingRomWork(*session_index)) {
4576 }
4577
4578 // A successful file write does not necessarily mean every pending editor
4579 // domain participated in the save. For example, dungeon palette saving is
4580 // user-configurable. Never execute a destructive follow-up while work that
4581 // the confirmation dialog promised to save is still pending.
4582 if (SessionHasPendingUnsavedWork(*session_index)) {
4583 return absl::FailedPreconditionError(
4584 absl::StrFormat("Save completed, but session still has %s",
4585 DescribePendingUnsavedWork(*session_index)));
4586 }
4587 return absl::OkStatus();
4588 };
4589
4590 absl::Status save_status = absl::OkStatus();
4591 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
4593 std::vector<size_t> session_ids;
4594 session_ids.reserve(session_coordinator_->GetTotalSessionCount());
4595 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount();
4596 ++i) {
4597 session_ids.push_back(session_coordinator_->GetSessionId(i));
4598 }
4599 for (const size_t session_id : session_ids) {
4600 save_status = save_modified_session(session_id);
4601 if (!save_status.ok()) {
4602 break;
4603 }
4604 }
4605 }
4606 } else {
4607 save_status = save_modified_session(action.source_session_id);
4608 }
4609
4610 if (!save_status.ok()) {
4611 const bool resumable_confirmation = absl::IsCancelled(save_status) &&
4614 const auto original_session_index =
4615 ResolveSessionIndexById(original_session_id);
4616 if (!resumable_confirmation && original_session_index.has_value()) {
4617 session_coordinator_->SwitchToSession(*original_session_index);
4618 }
4619
4620 if (absl::IsCancelled(save_status)) {
4621 toast_manager_.Show("Save paused. Finish saving, then retry your action.",
4623 } else {
4625 absl::StrFormat("Failed to save before continuing: %s",
4626 save_status.message()),
4628 }
4629 return;
4630 }
4631
4632 // Saving an inactive close target temporarily activates that session. Put
4633 // the original session back before compacting the target index so the same
4634 // stable session remains active after the close.
4636 original_session_id != action.target_session_id) {
4637 const auto original_session_index =
4638 ResolveSessionIndexById(original_session_id);
4639 if (original_session_index.has_value()) {
4640 session_coordinator_->SwitchToSession(*original_session_index);
4641 }
4642 }
4643
4645}
4646
4659
4666
4670 const auto source_index = ResolveSessionIndexById(action.source_session_id);
4671 bool has_pending_work = false;
4673 has_pending_work = HasAnySessionPendingUnsavedWork();
4675 // Revert only discards ROM-side work; project drafts survive it.
4676 has_pending_work =
4677 source_index.has_value() && SessionHasPendingRomWork(*source_index);
4678 } else {
4679 has_pending_work =
4680 source_index.has_value() && SessionHasPendingUnsavedWork(*source_index);
4681 }
4682 if (!has_pending_work) {
4683 return true;
4684 }
4685
4686 pending_unsaved_session_action_ = std::move(action);
4687 if (popup_manager_) {
4689 }
4690 return false;
4691}
4692
4694 const PendingUnsavedSessionAction& action) {
4695 switch (action.type) {
4697 auto status = LoadRomInternal();
4698 if (!status.ok()) {
4700 absl::StrFormat("Failed to load ROM: %s", status.message()),
4702 }
4703 break;
4704 }
4706 auto status = OpenRomOrProjectInternal(action.path);
4707 if (!status.ok()) {
4708 toast_manager_.Show(absl::StrFormat("Failed to open ROM / project: %s",
4709 status.message()),
4711 }
4712 break;
4713 }
4715 auto status = OpenProjectInternal();
4716 if (!status.ok()) {
4718 absl::StrFormat("Failed to open project: %s", status.message()),
4720 }
4721 break;
4722 }
4725 const auto target_index =
4727 if (target_index.has_value()) {
4728 session_coordinator_->SwitchToSession(*target_index);
4729 }
4730 }
4731 break;
4734 const auto target_index =
4736 if (target_index.has_value()) {
4737 session_coordinator_->RemoveSession(*target_index);
4739 }
4740 }
4741 break;
4744 const auto target_index =
4746 if (target_index.has_value()) {
4747 session_coordinator_->CloseSessionAllowingEmpty(*target_index);
4749 }
4750 }
4751 break;
4753 const auto target_index =
4755 if (!target_index.has_value()) {
4756 break;
4757 }
4759 *target_index != session_coordinator_->GetActiveSessionIndex()) {
4760 session_coordinator_->SwitchToSession(*target_index);
4761 }
4762 auto status = RevertRomToSavedInternal();
4763 if (status.ok()) {
4764 toast_manager_.Show("Reverted ROM to the saved file",
4766 } else {
4768 absl::StrFormat("Revert failed: %s", status.message()),
4769 ToastType::kError, 6.0f);
4770 }
4771 break;
4772 }
4774 quit_ = true;
4775 break;
4776 }
4777}
4778
4779bool EditorManager::SessionHasPendingUnsavedWork(size_t session_index) const {
4780 if (!session_coordinator_ ||
4781 !session_coordinator_->IsValidSessionIndex(session_index)) {
4782 return false;
4783 }
4784
4785 auto* session =
4786 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4787 return session != nullptr &&
4788 (SessionHasPendingRomWork(session_index) || session->project_dirty ||
4789 session->editors.HasPendingProjectDraftChanges() ||
4790 (session->project_file_editor_state.initialized &&
4791 session->project_file_editor_state.modified));
4792}
4793
4794bool EditorManager::SessionHasPendingRomWork(size_t session_index) const {
4795 if (!session_coordinator_ ||
4796 !session_coordinator_->IsValidSessionIndex(session_index)) {
4797 return false;
4798 }
4799
4800 auto* session =
4801 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4802 return session != nullptr &&
4803 ((session->rom.is_loaded() && session->rom.dirty()) ||
4804 session->editors.HasPendingGraphicsChanges() ||
4805 session->HasPendingGfxGroupChanges() ||
4806 session->editors.HasPendingScreenChanges() ||
4807 HasPendingDungeonChangesForSession(session_index) ||
4808 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data));
4809}
4810
4814
4816 size_t session_index) const {
4817 if (!session_coordinator_ ||
4818 !session_coordinator_->IsValidSessionIndex(session_index)) {
4819 return false;
4820 }
4821
4822 auto* session =
4823 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4824 if (!session) {
4825 return false;
4826 }
4827
4828 if (auto* dungeon_editor =
4829 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
4830 return dungeon_editor->HasPendingDungeonChanges();
4831 }
4832 return false;
4833}
4834
4836 size_t session_index) const {
4837 if (!session_coordinator_ ||
4838 !session_coordinator_->IsValidSessionIndex(session_index)) {
4839 return 0;
4840 }
4841
4842 auto* session =
4843 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4844 if (!session) {
4845 return 0;
4846 }
4847
4848 if (auto* dungeon_editor =
4849 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
4850 return dungeon_editor->PendingRoomCount();
4851 }
4852 return 0;
4853}
4854
4856 size_t session_index) const {
4857 if (!session_coordinator_ ||
4858 !session_coordinator_->IsValidSessionIndex(session_index)) {
4859 return 0;
4860 }
4861
4862 auto* session =
4863 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4864 return session != nullptr ? gfx::PaletteManager::Get().GetModifiedColorCount(
4865 &session->game_data)
4866 : 0;
4867}
4868
4870 if (!session_coordinator_) {
4871 return 0;
4872 }
4873
4874 int count = 0;
4875 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
4877 ++count;
4878 }
4879 }
4880 return count;
4881}
4882
4884 size_t session_index) const {
4885 if (!session_coordinator_ ||
4886 !session_coordinator_->IsValidSessionIndex(session_index)) {
4887 return "unsaved work";
4888 }
4889
4890 auto* session =
4891 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4892 const bool rom_dirty =
4893 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
4894 const bool pending_dungeon_changes =
4896 const bool pending_graphics_changes =
4897 session != nullptr && session->editors.HasPendingGraphicsChanges();
4898 const bool pending_gfx_group_changes =
4899 session != nullptr && session->HasPendingGfxGroupChanges();
4900 const bool pending_screen_changes =
4901 session != nullptr && session->editors.HasPendingScreenChanges();
4902 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
4903 const size_t pending_palette_colors =
4905 const bool project_dirty = session != nullptr && session->project_dirty;
4906 const bool project_editor_draft =
4907 session != nullptr && session->editors.HasPendingProjectDraftChanges();
4908 const bool project_file_dirty =
4909 session != nullptr && session->project_file_editor_state.initialized &&
4910 session->project_file_editor_state.modified;
4911
4912 std::vector<std::string> work;
4913 if (pending_rooms > 0) {
4914 work.push_back(absl::StrFormat("%d unapplied dungeon room%s", pending_rooms,
4915 pending_rooms == 1 ? "" : "s"));
4916 } else if (pending_dungeon_changes) {
4917 work.emplace_back("unapplied dungeon metadata");
4918 }
4919 if (pending_palette_colors > 0) {
4920 work.push_back(absl::StrFormat("%zu unapplied palette color%s",
4921 pending_palette_colors,
4922 pending_palette_colors == 1 ? "" : "s"));
4923 }
4924 if (pending_graphics_changes) {
4925 work.emplace_back("unapplied graphics sheet edits");
4926 }
4927 if (pending_gfx_group_changes) {
4928 work.emplace_back("unsaved blockset/spriteset (gfx group) edits");
4929 }
4930 if (pending_screen_changes) {
4931 work.emplace_back("unapplied Screen Editor edits");
4932 }
4933 if (rom_dirty) {
4934 work.emplace_back("unsaved ROM-buffer changes");
4935 }
4936 if (project_dirty) {
4937 work.emplace_back("unsaved project settings");
4938 }
4939 if (project_editor_draft) {
4940 work.emplace_back("an uncommitted project editor draft");
4941 }
4942 if (project_file_dirty) {
4943 work.emplace_back("an unsaved project-file draft");
4944 }
4945 return work.empty() ? "unsaved work" : absl::StrJoin(work, " and ");
4946}
4947
4949 size_t session_index) const {
4950 if (!session_coordinator_ ||
4951 !session_coordinator_->IsValidSessionIndex(session_index)) {
4952 return {};
4953 }
4954
4955 auto* session =
4956 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
4957 const bool rom_dirty =
4958 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
4959 const bool pending_dungeon_changes =
4961 const bool pending_graphics_changes =
4962 session != nullptr && session->editors.HasPendingGraphicsChanges();
4963 const bool pending_gfx_group_changes =
4964 session != nullptr && session->HasPendingGfxGroupChanges();
4965 const bool pending_screen_changes =
4966 session != nullptr && session->editors.HasPendingScreenChanges();
4967 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
4968 const size_t pending_palette_colors =
4970 const bool project_dirty = session != nullptr && session->project_dirty;
4971 const bool project_editor_draft =
4972 session != nullptr && session->editors.HasPendingProjectDraftChanges();
4973 const bool project_file_dirty =
4974 session != nullptr && session->project_file_editor_state.initialized &&
4975 session->project_file_editor_state.modified;
4976
4977 std::vector<std::string> tags;
4978 if (pending_rooms > 0) {
4979 tags.push_back("Rooms");
4980 } else if (pending_dungeon_changes) {
4981 tags.push_back("Dungeon");
4982 }
4983 if (pending_palette_colors > 0) {
4984 tags.push_back("Palette");
4985 }
4986 if (pending_graphics_changes) {
4987 tags.push_back("Gfx");
4988 }
4989 if (pending_gfx_group_changes) {
4990 tags.push_back("GfxGroups");
4991 }
4992 if (pending_screen_changes) {
4993 tags.push_back("Screen");
4994 }
4995 if (rom_dirty) {
4996 tags.push_back("ROM");
4997 }
4998 if (project_dirty || project_editor_draft || project_file_dirty) {
4999 tags.push_back("Project");
5000 }
5001 return absl::StrJoin(tags, "+");
5002}
5003
5005 const int modified_sessions = ModifiedSessionCount();
5006 if (modified_sessions <= 0) {
5007 return "No sessions have unsaved work.";
5008 }
5009
5010 if (modified_sessions == 1 && session_coordinator_) {
5011 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
5013 return absl::StrFormat("Session '%s' has %s.",
5014 session_coordinator_->GetSessionDisplayName(i),
5016 }
5017 }
5018 }
5019
5020 return absl::StrFormat(
5021 "%d sessions have unsaved ROM, graphics, screen, dungeon, palette, or "
5022 "project work.",
5023 modified_sessions);
5024}
5025
5027 return session_coordinator_ ? session_coordinator_->GetActiveSessionIndex()
5028 : 0;
5029}
5030
5032 UiSyncState state;
5033 state.frame_id = ui_sync_frame_id_.load(std::memory_order_relaxed);
5034
5035 int pending_editor =
5036 pending_editor_deferred_actions_.load(std::memory_order_relaxed);
5037 if (pending_editor < 0) {
5038 pending_editor = 0;
5039 }
5040 state.pending_editor_actions = pending_editor;
5041
5042 int pending_layout = layout_coordinator_.PendingDeferredActionCount();
5043 if (pending_layout < 0) {
5044 pending_layout = 0;
5045 }
5046 state.pending_layout_actions = pending_layout;
5048 layout_manager_ ? layout_manager_->IsRebuildRequested() : false;
5049 return state;
5050}
5051
5053 return session_coordinator_ ? session_coordinator_->GetActiveSessionCount()
5054 : 0;
5055}
5056
5058 EditorType type, size_t session_index) const {
5059 const char* base_name = kEditorNames[static_cast<int>(type)];
5060 return session_coordinator_ ? session_coordinator_->GenerateUniqueEditorTitle(
5061 base_name, session_index)
5062 : std::string(base_name);
5063}
5064
5065void EditorManager::SwitchToEditor(EditorType editor_type, bool force_visible,
5066 bool from_dialog) {
5067#ifdef YAZE_BUILD_AGENT_UI
5068 // Agent has its own activation path, including the shared experimental
5069 // warning. Handle it first so one user action cannot emit the warning twice.
5070 if (editor_type == EditorType::kAgent) {
5071 ShowAIAgent();
5072 return;
5073 }
5074#endif
5075
5079 absl::StrFormat(
5080 "%s is experimental. It remains available; enable experimental "
5081 "features in Settings to acknowledge this warning.",
5082 kEditorNames[static_cast<int>(editor_type)]),
5084 }
5085
5086 // Fresh launch has no ROM session, so GetCurrentEditorSet() is null and
5087 // EditorActivator::SwitchToEditor would silently no-op (BUG-020) — that was
5088 // why File->Settings, the "no ROM" welcome buttons, and Tools editors did
5089 // nothing. Create an empty session on demand so ROM-less editors (Settings,
5090 // Assembly, ...) have an editor set to live in. Normal ROM loads create their
5091 // own session, so this only fires when the user reaches an editor first.
5092 if (session_coordinator_ && GetCurrentEditorSet() == nullptr) {
5093 session_coordinator_->CreateNewSession();
5094 }
5095
5096 auto status = EnsureEditorAssetsLoaded(editor_type);
5097 if (!status.ok()) {
5099 absl::StrFormat("Failed to prepare %s: %s",
5100 kEditorNames[static_cast<int>(editor_type)],
5101 status.message()),
5103 }
5104
5106 editor_type != EditorType::kGraphics) {
5107 static_cast<GraphicsEditor*>(current_editor_)->FinishPixelGesture();
5108 }
5109
5110 // Delegate all other editor switching to EditorActivator
5111 editor_activator_.SwitchToEditor(editor_type, force_visible, from_dialog);
5112
5113 switch (editor_type) {
5115 // Show at most one migration tip per editor transition. The next visit
5116 // naturally advances to the more specific Tile16 tip.
5119 }
5120 break;
5123 break;
5126 break;
5127 default:
5128 break;
5129 }
5130}
5131
5133 if (!ui_coordinator_) {
5134 return;
5135 }
5136 ui_coordinator_->SetEditorSelectionVisible(false);
5137 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
5138}
5139
5140bool EditorManager::HasDuplicateSession(const std::string& filepath) {
5141 return session_coordinator_ &&
5142 session_coordinator_->HasDuplicateSession(filepath);
5143}
5144
5170 // Menu actions can run before the next Update() frame captures drawer
5171 // edits. Preserve the panel's current dirty bit before rebinding it.
5174 // Update project panel context before showing
5178 project_management_panel_->SetVersionManager(version_manager_);
5180 }
5181 right_drawer_manager_->ToggleDrawer(
5183 }
5184}
5185
5188 auto* session = session_coordinator_
5189 ? session_coordinator_->GetActiveRomSession()
5190 : nullptr;
5191
5192 // Preserve an existing draft for this session. Only load from disk the first
5193 // time this session opens the project-file editor.
5194 if (session && session->project_file_editor_state.initialized &&
5195 !session->project_file_editor_state.modified &&
5196 !current_project_.filepath.empty() &&
5198 session->project_file_editor_state.filepath,
5201 }
5202
5203 if (session && session->project_file_editor_state.initialized) {
5204 project_file_editor_.RestoreState(session->project_file_editor_state,
5206 } else if (!current_project_.filepath.empty()) {
5208 if (!status.ok()) {
5210 absl::StrFormat("Failed to load project file: %s", status.message()),
5212 return;
5213 }
5214 } else {
5216 }
5217 // Set the project pointer for label import functionality
5219 // Activate the editor window
5221 if (session) {
5222 session->project_file_editor_state = project_file_editor_.CaptureState();
5223 }
5224}
5225
5227 size_t session_id) {
5228 if (!editor_set) {
5229 return;
5230 }
5231
5232 EditorDependencies deps;
5233 deps.rom = rom;
5234 deps.session_id = session_id;
5237 deps.popup_manager = popup_manager_.get();
5242 const auto session_index = ResolveSessionIndexById(session_id);
5243 if (session_index.has_value()) {
5244 auto* session = static_cast<RomSession*>(
5245 session_coordinator_->GetSession(*session_index));
5246 if (session) {
5247 deps.game_data = &session->game_data;
5248 if (session->project_context.has_value()) {
5249 deps.project = &*session->project_context;
5250 deps.version_manager = session->version_manager.get();
5251 }
5252 }
5253 }
5254 }
5255 deps.global_context = editor_context_.get();
5256 deps.status_bar = &status_bar_;
5257 deps.renderer = renderer_;
5258 deps.emulator = &emulator_;
5259 deps.custom_data = this;
5260 deps.gfx_group_workspace = editor_set->gfx_group_workspace();
5261
5262 editor_set->ApplyDependencies(deps);
5263
5264 if (auto* settings_panel = editor_set->GetSettingsPanel()) {
5265 // Settings edit the same active project that CaptureActiveProjectContext
5266 // persists. Editing the session snapshot directly loses changes when the
5267 // next capture replaces that snapshot with current_project_.
5268 settings_panel->SetProjectContextProvider(
5269 [this, session_id]() -> project::YazeProject* {
5270 return IsCurrentProjectContextOwnedBySession(session_id)
5272 : nullptr;
5273 },
5274 [this, session_id]() -> std::string {
5275 return IsCurrentProjectContextOwnedBySession(session_id)
5277 : std::string();
5278 });
5279 settings_panel->SetOpenMinecartTracksCallback(
5280 [this, session_id]() -> absl::Status {
5281 if (!IsCurrentProjectContextOwnedBySession(session_id)) {
5282 return absl::FailedPreconditionError(
5283 "Minecart Tracks requires its project session to be active.");
5284 }
5286 auto* dungeon = GetCurrentEditorSet()->GetEditorAs<DungeonEditorV2>(
5288 if (dungeon == nullptr) {
5289 return absl::NotFoundError(
5290 "Dungeon editor is unavailable in the active project "
5291 "session.");
5292 }
5293 RETURN_IF_ERROR(dungeon->EnsureMinecartTrackEditorPanel());
5294
5298 return absl::NotFoundError(
5299 "Minecart Tracks could not be opened in the active Dungeon "
5300 "editor.");
5301 }
5302 return absl::OkStatus();
5303 });
5304 }
5305
5306 // If configuring the active session, update the properties panel
5307 if (session_id == GetCurrentSessionId()) {
5309 }
5310}
5311
5312} // namespace yaze::editor
static Application & Instance()
void Publish(const T &event)
Definition event_bus.h:35
HandlerId Subscribe(std::function< void(const T &)> handler)
Definition event_bus.h:22
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
project::ResourceLabelManager * resource_label()
Definition rom.h:225
auto filename() const
Definition rom.h:207
void set_dirty(bool dirty)
Definition rom.h:189
const auto & vector() const
Definition rom.h:205
auto set_filename(std::string_view name)
Definition rom.h:208
bool is_loaded() const
Definition rom.h:187
static TimingManager & Get()
Definition timing.h:20
float GetElapsedTime() const
Get total elapsed time since first update.
Definition timing.h:70
static void Initialize(editor::EditorManager *)
static void Initialize(editor::EditorManager *)
static Flags & get()
Definition features.h:119
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
bool loaded() const
Check if the manifest has been loaded.
const BuildPipeline & build_pipeline() const
static absl::StatusOr< std::filesystem::path > ResolvePathForProject(const std::string &project_file, const std::string &catalog_file)
void set_active(bool active)
Definition agent_chat.h:71
void ApplyUserSettingsDefaults(bool force=false)
void Initialize(ToastManager *toast_manager, ProposalDrawer *proposal_drawer, RightDrawerManager *right_drawer_manager, WorkspaceWindowManager *window_manager, UserSettings *user_settings)
absl::Status Start(const std::string &command, const std::string &directory)
DungeonEditorV2 - Simplified dungeon editor using component delegation.
static constexpr const char * kMinecartTrackEditorId
void SwitchToEditor(EditorType type, bool force_visible=false, bool from_dialog=false)
Switch to an editor, optionally forcing visibility.
void Initialize(const Dependencies &deps)
void InitializeEditorLayout(EditorType type)
Initialize the DockBuilder layout for an editor.
std::unique_ptr< SessionCoordinator > session_coordinator_
RomLifecycleManager rom_lifecycle_
StartupVisibility welcome_mode_override_
void ConfirmPendingUnsavedSessionActionSaveAndContinue()
std::optional< PendingRomSave > pending_rom_save_
std::vector< EditorType > CollectEditorsToPreload(EditorSet *editor_set) const
void SwitchToEditor(EditorType editor_type, bool force_visible=false, bool from_dialog=false) override
std::unique_ptr< GlobalEditorContext > editor_context_
bool MaybeGuardPendingSessionAction(PendingUnsavedSessionAction action)
absl::Status SaveRomAs(const std::string &filename)
bool HasAnySessionPendingUnsavedWork() const
project::YazeProject current_project_
void HandleSessionSwitched(size_t new_index, RomSession *session, bool transient=false)
std::unique_ptr< RightDrawerManager > right_drawer_manager_
void SetCurrentEditor(Editor *editor) override
bool SaveLayoutSnapshotAs(const std::string &name)
absl::Status PrepareRawProjectFileSave(const std::string &filepath, const std::string &contents)
absl::Status LoadAssetsForMode(uint64_t loading_handle=0)
std::string GetPreferredStartupCategory(const std::string &saved_category, const std::vector< std::string > &available_categories) const
absl::StatusOr< std::string > RunProjectBuildCommand()
absl::Status CheckRomWritePolicy(const std::optional< std::string > &target_filename=std::nullopt)
Save the current ROM file.
absl::Status RestoreRomBackup(const std::string &backup_path)
bool SessionHasPendingRomWork(size_t session_index) const
Rom * GetCurrentRom() const override
void CancelPendingRomSave(bool hide_popups=false)
int PendingDungeonRoomCountForSession(size_t session_index) const
bool HasDuplicateSession(const std::string &filepath)
std::unique_ptr< LayoutManager > layout_manager_
std::unique_ptr< DashboardPanel > dashboard_panel_
void ResetAssetState(RomSession *session)
void ProcessStartupActions(const AppConfig &config)
std::string GenerateUniqueEditorTitle(EditorType type, size_t session_index) const
core::VersionManager * version_manager_
std::vector< editor::RomFileManager::BackupEntry > GetRomBackups() const
std::string CompactPendingUnsavedWorkLabel(size_t session_index) const
std::vector< std::string > ListLayoutSnapshots() const
SharedClipboard shared_clipboard_
bool EditorInitRequiresGameData(EditorType type) const
void SyncEditorContextForCategory(const std::string &category)
void ShowProjectManagement()
Injects dependencies into all editors within an EditorSet.
Editor * ResolveEditorForCategory(const std::string &category)
void Initialize(gfx::IRenderer *renderer, const std::string &filename="")
std::optional< size_t > runtime_feature_flags_session_id_
absl::Status FinalizeNewProject(const std::string &project_name, const std::string &project_path=std::string())
std::string DescribeAllPendingUnsavedWork() const
void MarkEditorLoaded(RomSession *session, EditorType type)
absl::Status StartPendingRomSave(const std::optional< std::string > &save_as_filename)
absl::Status CreateNewProjectFromRom(const std::string &template_name, const std::string &rom_path, const std::string &project_name, const std::string &project_path=std::string())
absl::Status CreateNewProject(const std::string &template_name="Basic ROM Hack")
void ConfirmPendingUnsavedSessionActionDiscardAndContinue()
absl::Status InitializeEditorForType(EditorType type, EditorSet *editor_set, Rom *rom)
LayoutCoordinator layout_coordinator_
absl::Status LoadAssets(uint64_t loading_handle=0)
std::string GetCurrentRomHash() const
FeatureHintManager feature_hint_manager_
bool HasPendingUnsavedSessionAction() const
void RestoreActiveEditorLayoutAfterAssets(RomSession *session)
absl::Status OpenRomOrProjectInternal(const std::string &filename)
auto GetCurrentEditorSet() const -> EditorSet *
std::string DescribePendingUnsavedWork(size_t session_index) const
EditorContextSnapshot GetEditorContextSnapshot(const std::string &category)
void FinishPendingRomSaveAttempt(const absl::Status &status)
std::unique_ptr< MenuOrchestrator > menu_orchestrator_
void HandleUIActionRequest(UIActionRequestEvent::Action action)
void ResolvePotItemSaveConfirmation(PotItemSaveDecision decision)
absl::Status CommitRawProjectFileSave(const std::string &filepath, const std::string &contents)
absl::Status SwapProjectRom(const std::string &rom_path)
absl::Status SaveActiveProjectEditingWork()
auto GetCurrentEditor() const -> Editor *override
void HandleSessionRomLoaded(size_t index, Rom *rom)
ProjectFileEditor project_file_editor_
void ApplyLayoutPreset(const std::string &preset_name)
void RestoreTemporaryLayoutSnapshot(bool clear_after_restore=false)
void ApplyStartupVisibility(const AppConfig &config)
void ExecutePendingUnsavedSessionAction(const PendingUnsavedSessionAction &action)
WorkspaceWindowManager window_manager_
size_t PendingPaletteColorCountForSession(size_t session_index) const
void RestoreEditorLayoutAfterAssets(RomSession *session, EditorType type)
absl::Status SaveRomInternal(const std::optional< std::string > &save_as_filename)
void SetStartupLoadHints(const AppConfig &config)
void RebaseCleanProjectFileDraft(const std::string &filepath)
std::optional< size_t > pending_project_open_previous_session_id_
absl::Status LoadAssetsLazy(uint64_t loading_handle=0)
bool PendingRomSaveMatchesActiveSession() const
void SetAssetLoadMode(AssetLoadMode mode)
void DismissEditorSelection() override
absl::Status DiscardPendingRomBackupRestore()
std::atomic< uint64_t > ui_sync_frame_id_
std::optional< size_t > active_project_context_session_id_
std::string GetPendingUnsavedSessionActionContinueLabel() const
std::string GetPendingUnsavedSessionActionPrompt() const
bool ApplyLayoutProfile(const std::string &profile_id)
Editor * GetEditorByType(EditorType type, EditorSet *editor_set) const
StartupVisibility sidebar_mode_override_
absl::Status LoadRom()
Load a ROM file into a new or existing session.
std::vector< std::string > startup_panel_hints_
std::vector< std::function< void()> > deferred_actions_
void OpenEditorAndPanelsFromFlags(const std::string &editor_name, const std::string &panels_str)
StartupVisibility dashboard_mode_override_
void BindProjectContextToSession(RomSession *session, const project::YazeProject &project)
std::optional< size_t > ResolveSessionIndexById(size_t session_id) const
static bool IsPanelBasedEditor(EditorType type)
absl::StatusOr< std::string > ResolveProjectRunTarget() const
absl::Status ImportProject(const std::string &project_path)
size_t GetCurrentSessionId() const
Stable workspace identity; unlike the UI index, it does not compact.
bool ProjectFileDraftTargetsCurrentProject() const
absl::Status PrepareActiveProjectEditorDraftsForSave()
ProjectWorkflowStatus MakeRunStatus(const std::string &summary, const std::string &detail, ProjectWorkflowState state) const
void QueueDeferredAction(std::function< void()> action)
absl::Status ValidateProjectRomSelection(const std::string &rom_path)
SelectionPropertiesPanel selection_properties_panel_
bool DeleteLayoutSnapshot(const std::string &name)
std::unique_ptr< ActivityBar > activity_bar_
ShortcutManager shortcut_manager_
bool RestoreLayoutSnapshot(const std::string &name, bool remove_after_restore=false)
bool HasPendingDungeonChangesForSession(size_t session_index) const
std::optional< PendingUnsavedSessionAction > pending_unsaved_session_action_
void MarkEditorInitialized(RomSession *session, EditorType type)
bool SessionHasPendingUnsavedWork(size_t session_index) const
absl::Status RevertRomToSavedInternal()
std::unique_ptr< BackgroundCommandTask > active_project_build_
bool IsCurrentProjectContextOwnedBySession(size_t session_id) const
WorkspaceManager workspace_manager_
yaze::zelda3::Overworld * overworld() const
void ConfigureEditorDependencies(EditorSet *editor_set, Rom *rom, size_t session_id)
absl::StatusOr< const project::YazeProject * > PrepareActiveProjectContextForSave()
std::unique_ptr< ProjectManagementPanel > project_management_panel_
std::unique_ptr< workflow::HackWorkflowBackend > hack_workflow_backend_
absl::Status OpenRomOrProject(const std::string &filename)
absl::StatusOr< std::string > ResolveProjectBuildCommand()
void RestoreProjectContextAfterFailedOpen(std::optional< size_t > previous_session_id)
void RemoveSession(size_t index)
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
UiSyncState GetUiSyncStateSnapshot() const
ProjectWorkflowStatus MakeBuildStatus(const std::string &summary, const std::string &detail, ProjectWorkflowState state, const std::string &output_tail="", bool can_cancel=false) const
absl::Status DiscardProvisionalSessionCreatedSince(size_t previous_session_count)
absl::Status EnsureEditorAssetsLoaded(EditorType type)
std::unique_ptr< PopupManager > popup_manager_
bool RestoreProjectContextForSession(RomSession *session)
void CaptureCurrentRomIdentity(bool preserve_editing_source=false)
std::atomic< int > pending_editor_deferred_actions_
std::unique_ptr< UICoordinator > ui_coordinator_
EditorActivator editor_activator_
absl::Status SetCurrentRom(Rom *rom)
std::string GetPendingUnsavedSessionActionSaveLabel() const
std::optional< PendingProjectRomSelection > pending_project_rom_selection_
std::unique_ptr< WindowHost > window_host_
void HandleSessionCreated(size_t index, RomSession *session)
bool EditorRequiresGameData(EditorType type) const
absl::Status ReplaceActiveSessionRom(Rom &&rom, const std::string &filepath)
static bool UpdateAllowedWithoutLoadedRom(EditorType type)
static EditorType GetEditorTypeFromCategory(const std::string &category)
static bool IsPanelBasedEditor(EditorType type)
static bool ShouldWarnAboutExperimentalEditor(EditorType type, bool experiments_enabled)
static EditorExperimentPolicy GetExperimentPolicy(EditorType type)
static std::string GetEditorName(EditorType type)
static std::string GetEditorCategory(EditorType type)
Contains a complete set of editors for a single ROM instance.
void ApplyDependencies(const EditorDependencies &dependencies)
Editor * GetEditor(EditorType type) const
GfxGroupWorkspaceState * gfx_group_workspace()
SettingsPanel * GetSettingsPanel() const
std::vector< Editor * > active_editors_
Interface for editor classes.
Definition editor.h:249
virtual absl::Status Redo()=0
EditorType type() const
Definition editor.h:343
virtual EditorContextSnapshot BuildContextSnapshot() const
Definition editor.h:332
virtual absl::Status Undo()=0
void SetGuideCallback(std::function< void()> callback)
Opens Help → Coming from ZScream (or equivalent) from actionable tips.
void SetUserSettings(UserSettings *settings)
bool MaybeShow(std::string_view id)
void RegisterBuiltinHints()
Register built-in ZScream migrant tips. Safe to call more than once.
void SetToastManager(ToastManager *toast_manager)
Allows the user to edit graphics sheets from the game or view prototype graphics.
bool RestorePreviousEditorLayout(EditorType type)
void ProcessDeferredActions()
Process all queued deferred actions.
bool HasPreviousEditorLayout(EditorType type) const
void ResetWorkspaceLayout()
Reset the workspace layout to defaults.
void InitializeEditorLayout(EditorType type)
Initialize layout for an editor type on first activation.
void ResetCurrentEditorLayout(EditorType editor_type, size_t session_id)
Reset current editor layout to its default configuration.
int PendingDeferredActionCount() const
Approximate pending deferred layout actions for sync diagnostics.
void ApplyLayoutPreset(const std::string &preset_name, size_t session_id)
Apply a named layout preset.
void Initialize(const Dependencies &deps)
Initialize with all dependencies.
absl::Status LoadFile(const std::string &filepath)
Load a project file into the editor.
void SetProject(project::YazeProject *project)
Set the project pointer for label import operations.
ProjectFileEditorState CaptureState() const
void RestoreState(const ProjectFileEditorState &state, project::YazeProject *project)
void set_active(bool active)
Set whether the editor window is active.
absl::Status SaveFile()
Save the current editor contents to disk.
void ResetForProject(project::YazeProject *project)
void SetSaveCompleteCallback(SaveCompleteCallback callback)
void SetToastManager(ToastManager *toast_manager)
Set toast manager for notifications.
const std::string & filepath() const
Get the current filepath.
void SetSaveGuardCallback(SaveGuardCallback callback)
Panel for managing project settings, ROM versions, and snapshots.
void SetBuildStatus(const ProjectWorkflowStatus &status)
void SetRunStatus(const ProjectWorkflowStatus &status)
absl::Status FinalizeProjectCreation(const std::string &project_name, const std::string &project_path)
Complete project creation after ROM is loaded.
absl::Status ValidateProjectCreationTarget(const std::string &project_name, const std::string &project_path=std::string()) const
absl::Status SetProjectRom(const std::string &rom_path)
Set the ROM for the current project.
absl::Status CreateNewProject(const std::string &template_name="")
void CancelPendingProject()
Cancel pending project creation.
bool IsPendingRomSelection() const
Check if project is waiting for ROM selection.
absl::Status ImportProject(const std::string &project_path)
project::YazeProject & GetCurrentProject()
static float GetDefaultDrawerWidth(DrawerType type, EditorType editor=EditorType::kUnknown)
Get the default width for a specific drawer type.
std::vector< BackupEntry > ListBackups(const std::string &rom_filename) const
absl::Status LoadRom(Rom *rom, const std::string &filename)
std::vector< RomFileManager::BackupEntry > GetRomBackups(Rom *rom) const
absl::Status CheckRomWritePolicy(Rom *rom, const std::optional< std::string > &target_filename=std::nullopt)
Enforce project write policy; may set pending_rom_write_confirm.
void ActivateRomIdentity(const RomSession *session)
absl::Status CheckRomOpenPolicy(Rom *rom)
Validate that the loaded ROM is a safe project target to open/edit.
PotItemSaveDecision ResolvePotItemSaveConfirmation(PotItemSaveDecision decision)
const std::vector< core::WriteConflict > & pending_write_conflicts() const
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
Run Oracle-specific ROM safety preflight before saving.
void ApplyDefaultBackupPolicy(bool enabled, const std::string &folder, int retention_count, bool keep_daily, int keep_daily_days)
Apply default backup policy from user settings.
void SetAgentCallbacks(std::function< void(const std::string &)> send_callback, std::function< void()> focus_callback)
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
Manages the settings UI displayed in the right sidebar.
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:57
void SetBuildStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:209
void SetRunStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:212
void SetEnabled(bool enabled)
Enable or disable the status bar.
Definition status_bar.h:71
void Initialize(GlobalEditorContext *context)
Definition status_bar.cc:91
void SetAgentToggleCallback(std::function< void()> callback)
Definition status_bar.h:219
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)
bool ApplyPanelLayoutDefaultsRevision(int target_revision)
static constexpr int kLatestPanelLayoutDefaultsRevision
static constexpr int kLastWorkspaceResetPanelLayoutDefaultsRevision
void set_apply_preset_callback(std::function< void(const std::string &)> callback)
void set_apply_preset_callback(std::function< void(const std::string &)> callback)
void set_window_manager(WorkspaceWindowManager *manager)
void set_layout_manager(LayoutManager *manager)
void SetSidebarVisible(bool visible, bool notify=true)
void RestoreVisibilityState(size_t session_id, const std::unordered_map< std::string, bool > &state, bool publish_events=false)
Restore panel visibility state from persistence.
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
const std::unordered_map< std::string, WindowDescriptor > & GetAllWindowDescriptors() const
Get all panel descriptors (for layout designer, panel browser, etc.)
void SetActiveCategory(const std::string &category, bool notify=true)
void RegisterRegistryWindowContent(std::unique_ptr< WindowContent > window)
Register a ContentRegistry-managed WindowContent instance.
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
static constexpr const char * kDashboardCategory
std::vector< std::string > GetVisibleWindowIds(size_t session_id) const
Get list of currently visible panel IDs for a session.
void RegisterWindow(size_t session_id, const WindowDescriptor &descriptor)
void SetStoredSidePanelWidth(float width, bool notify=false)
void SetFileBrowserPath(const std::string &category, const std::string &path)
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void SetWindowBrowserCategoryWidth(float width, bool notify=true)
void SetSidebarExpanded(bool expanded, bool notify=true)
void RestorePinnedState(const std::unordered_map< std::string, bool > &state)
Restore pinned panel state from persistence.
void RegisterPanelAlias(const std::string &legacy_base_id, const std::string &canonical_base_id)
Register a legacy panel ID alias that resolves to a canonical ID.
void MarkWindowRecentlyUsed(const std::string &window_id)
void set_window_manager(editor::WorkspaceWindowManager *manager)
Definition emulator.h:51
void set_renderer(gfx::IRenderer *renderer)
Definition emulator.h:100
auto running() const -> bool
Definition emulator.h:61
absl::Status ReloadRuntimeRom(const std::vector< uint8_t > &rom_data)
Definition emulator.cc:265
bool HasPendingTextures() const
Check if there are pending textures to process.
Definition arena.h:92
static Arena & Get()
Definition arena.cc:24
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
bool HasUnsavedChanges() const
Check if there are ANY unsaved changes.
void Initialize(zelda3::GameData *game_data)
Initialize the palette manager with GameData.
size_t GetModifiedColorCount() const
Get count of modified colors across all groups.
static PaletteManager & Get()
Get the singleton instance.
void ReleaseSession(const zelda3::GameData *game_data)
Forget all palette tracking for a closing or reloaded ROM session.
static PerformanceProfiler & Get()
void PrintSummary() const
Print a summary of all operations to console.
static MotionProfile ClampMotionProfile(int raw_profile)
Definition animator.cc:110
void SetMotionPreferences(bool reduced_motion, MotionProfile profile)
Definition animator.cc:120
void ApplyTheme(const std::string &theme_name)
void SetOnThemeChangedCallback(ThemeChangedCallback callback)
static ThemeManager & Get()
void SetLanguage(const std::string &locale)
static LanguageManager & Get()
void SetOnLanguageChangedCallback(LanguageChangedCallback cb)
static RecentFilesManager & GetInstance()
Definition project.h:598
void SetCurrentRom(Rom *rom)
static TestManager & Get()
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
std::unordered_map< std::string, LabelMap > ProjectLabels
void SetHackManifest(const core::HackManifest *manifest)
Set the hack manifest reference for ASM-defined labels.
void SetProjectLabels(ProjectLabels *labels)
Set the project labels reference (typically from YazeProject)
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_STOP
Definition icons.h:1862
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_SPEED
Definition icons.h:1817
#define ICON_MD_AUDIOTRACK
Definition icons.h:213
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_SPORTS_ESPORTS
Definition icons.h:1826
#define ICON_MD_AUDIO_FILE
Definition icons.h:212
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
void SetGlobalContext(GlobalEditorContext *ctx)
void SetShowWorkflowOutputCallback(std::function< void()> callback)
void SetEventBus(::yaze::EventBus *bus)
Set the current EventBus instance.
void SetGameData(::yaze::zelda3::GameData *data)
Set the current game data instance.
void AppendWorkflowHistory(const ProjectWorkflowHistoryEntry &entry)
void SetRunProjectWorkflowCallback(std::function< void()> callback)
void SetBuildWorkflowStatus(const ProjectWorkflowStatus &status)
void SetCancelBuildWorkflowCallback(std::function< void()> callback)
void SetLayoutManager(LayoutManager *manager)
void SetBuildWorkflowLog(const std::string &output)
void SetStartBuildWorkflowCallback(std::function< void()> callback)
void SetRunWorkflowStatus(const ProjectWorkflowStatus &status)
void SetHackWorkflowBackend(workflow::HackWorkflowBackend *backend)
void SetUserSettings(UserSettings *settings)
void Clear()
Clear all context state.
std::vector< std::unique_ptr< WindowContent > > CreateAll()
Create new instances of all registered panels.
constexpr const char * kZScreamDungeon
constexpr const char * kZScreamOverworld
constexpr const char * kZScreamTile16
constexpr const char * kZScreamScreenMap
constexpr const char * kAbout
constexpr const char * kComingFromZScream
constexpr const char * kSaveAs
constexpr const char * kWriteConflictWarning
constexpr const char * kDungeonPotItemSaveConfirm
constexpr const char * kUnsavedSessionChanges
constexpr const char * kRomWriteConfirm
void UpdateBuildWorkflowStatus(StatusBar *status_bar, ProjectManagementPanel *project_panel, const ProjectWorkflowStatus &status)
void UpdateRunWorkflowStatus(StatusBar *status_bar, ProjectManagementPanel *project_panel, const ProjectWorkflowStatus &status)
std::string LastNonEmptyLine(const std::string &text)
void MergeRightDrawerWidthPreferences(std::unordered_map< std::string, float > *preferences, const std::unordered_map< std::string, float > &drawer_widths)
void AppendWorkflowHistoryEntry(const std::string &kind, const ProjectWorkflowStatus &status, const std::string &output_log)
std::string StripSessionPrefix(absl::string_view panel_id)
std::unique_ptr< HackWorkflowBackend > CreateHackWorkflowBackendForProject(const project::YazeProject *project)
Editors are the view controllers for the application.
std::string PreferStartupCategory(const std::string &saved_category, const std::vector< std::string > &available_categories)
bool IsTransientPanelVisibilityId(absl::string_view panel_id)
std::optional< EditorType > ParseEditorTypeFromString(absl::string_view name)
constexpr std::array< const char *, 15 > kEditorNames
Definition editor.h:229
void ConfigureMenuShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
void RegisterDefaultEditorFactories(EditorRegistry *registry)
void ConfigurePanelShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
Final configuration pass: apply user rebinds, then register panel toggle shortcuts.
size_t EditorTypeIndex(EditorType type)
Definition editor.h:239
void ConfigureEditorShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
constexpr size_t kEditorTypeCount
Definition editor.h:236
absl::Status ValidateRomCollisionSourceBinding(Rom *replacement_rom, const fs::path &json_path)
Animator & GetAnimator()
Definition animator.cc:318
void PostOverlayCommand(const char *command)
void RegisterZ3edTestSuites()
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
Definition file_util.cc:87
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
constexpr int kNumOverworldMaps
Definition common.h:85
void SetPreferHmagicSpriteNames(bool prefer)
Definition sprite.cc:276
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
AssetLoadMode
Asset loading mode for editor resources.
void SetActiveFontIndex(int index)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Configuration options for the application startup.
Definition application.h:26
std::string startup_editor
Definition application.h:38
StartupVisibility welcome_mode
Definition application.h:32
std::vector< std::string > open_panels
Definition application.h:40
StartupVisibility sidebar_mode
Definition application.h:34
StartupVisibility dashboard_mode
Definition application.h:33
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
std::function< EditorSet *()> get_current_editor_set
std::function< void(std::function< void()>)> queue_deferred_action
std::function< absl::Status(EditorType)> ensure_editor_assets_loaded
std::vector< EditorContextDiagnostic > diagnostics
Unified dependency container for all editor types.
Definition editor.h:170
project::YazeProject * project
Definition editor.h:174
GlobalEditorContext * global_context
Definition editor.h:176
SharedClipboard * shared_clipboard
Definition editor.h:187
gfx::IRenderer * renderer
Definition editor.h:192
ShortcutManager * shortcut_manager
Definition editor.h:186
core::VersionManager * version_manager
Definition editor.h:175
zelda3::GameData * game_data
Definition editor.h:173
WorkspaceWindowManager * window_manager
Definition editor.h:182
GfxGroupWorkspaceState * gfx_group_workspace
Definition editor.h:179
ExperimentalSavePosture disabled_save_posture
Published after ImGui::NewFrame and dockspace creation.
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
All dependencies required by LayoutCoordinator.
Built-in workflow-oriented layout profiles.
Published when panel visibility changes.
Published when a ROM is successfully loaded into a session.
Definition core_events.h:27
Represents a single session, containing a ROM and its associated editors.
core::FeatureFlags::Flags feature_flags
std::array< bool, kEditorTypeCount > editor_initialized
std::array< bool, kEditorTypeCount > editor_assets_loaded
Published when a session is closed.
Published when a new session is created.
Published when the active session changes.
Definition core_events.h:89
Activity bar or menu action request.
std::unordered_map< std::string, float > right_panel_widths
std::unordered_map< std::string, std::unordered_map< std::string, bool > > panel_visibility_state
std::unordered_map< std::string, bool > pinned_panels
Declarative registration contract for editor windows.
Definition panel_host.h:22
WindowPresentationPolicy presentation
Definition panel_host.h:37
Metadata for a dockable editor window (formerly PanelInfo)
static WindowPresentationPolicy Diagnostic()
static WindowPresentationPolicy CoreWorkspace()
WindowPresentationPolicy WithHost(std::string host_name) const
Returns a copy that names the hosting surface.
static WindowPresentationPolicy OptionalPopOut()
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:589
RomWritePolicy write_policy
Definition project.h:225
std::vector< std::string > recent_files
Definition project.h:86
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
absl::Status RepairProject()
Definition project.cc:1967
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:746
bool project_opened() const
Definition project.h:505
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
Definition project.cc:703
std::string git_repository
Definition project.h:374
core::HackManifest hack_manifest
Definition project.h:360
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:353
std::string assets_folder
Definition project.h:312
std::string GetDisplayName() const
Definition project.cc:2001
WorkspaceSettings workspace_settings
Definition project.h:346
std::string custom_collision_json
Definition project.h:325
std::string palette_catalog_file
Definition project.h:339
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2028
absl::Status Open(const std::string &project_path)
Definition project.cc:537
absl::Status Validate() const
Definition project.cc:1897
core::FeatureFlags::Flags feature_flags
Definition project.h:345