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 <chrono>
14#include <cstdlib>
15#include <exception>
16#include <filesystem>
17#include <fstream>
18#include <functional>
19#include <initializer_list>
20#include <memory>
21#include <optional>
22#include <sstream>
23#include <string>
24#include <unordered_set>
25#include <utility>
26#include <vector>
27
28#ifdef __APPLE__
29#include <TargetConditionals.h>
30#endif
31
32// Third-party library headers
33#define IMGUI_DEFINE_MATH_OPERATORS
34#include "absl/status/status.h"
35#include "absl/status/statusor.h"
36#include "absl/strings/ascii.h"
37#include "absl/strings/match.h"
38#include "absl/strings/str_format.h"
39#include "absl/strings/str_join.h"
40#include "absl/strings/str_split.h"
41#include "absl/strings/string_view.h"
42#include "imgui/imgui.h"
43
44// Project headers
45#include "app/application.h"
47#include "app/editor/editor.h"
67#include "app/emu/emulator.h"
73#include "app/gui/core/icons.h"
78#include "app/platform/timing.h"
80#include "core/features.h"
81#include "core/project.h"
82#include "core/rom_settings.h"
91#include "rom/rom.h"
92#include "rom/rom_diff.h"
93#include "rom/transaction.h"
94#include "startup_flags.h"
95#include "util/file_util.h"
97#include "util/log.h"
98#include "util/macro.h"
99#include "util/rom_hash.h"
100#include "yaze_config.h"
106#include "zelda3/game_data.h"
112#include "zelda3/sprite/sprite.h"
113
114// Conditional platform headers
115#ifdef __EMSCRIPTEN__
119#endif
120
121// Conditional test headers
122#ifdef YAZE_ENABLE_TESTING
128#endif
129#ifdef YAZE_ENABLE_GTEST
131#endif
132#ifdef YAZE_WITH_GRPC
134#endif
135
136// Conditional agent UI headers
137#ifdef YAZE_BUILD_AGENT_UI
139#endif
140
141namespace yaze::editor {
142
143namespace {
145 public:
149 delete;
150
152 if (!committed_) {
153 for (auto it = editors_.rbegin(); it != editors_.rend(); ++it) {
154 (*it)->RollbackSaveTransaction();
155 }
156 }
157 }
158
159 absl::Status Begin(Editor* editor) {
160 if (editor == nullptr) {
161 return absl::OkStatus();
162 }
164 editors_.push_back(editor);
165 return absl::OkStatus();
166 }
167
168 void Commit() {
169 for (Editor* editor : editors_) {
170 editor->CommitSaveTransaction();
171 }
172 committed_ = true;
173 }
174
175 private:
176 std::vector<Editor*> editors_;
177 bool committed_ = false;
178};
179
181 std::initializer_list<const char*> keys) {
182 for (const char* key : keys) {
183 if (overrides.addresses.find(key) != overrides.addresses.end()) {
184 return true;
185 }
186 }
187 return false;
188}
189
190bool IsTransientPanelVisibilityId(absl::string_view panel_id) {
191 constexpr absl::string_view kRoomPanelPrefix = "dungeon.room_";
192 if (!absl::StartsWith(panel_id, kRoomPanelPrefix) ||
193 panel_id.size() <= kRoomPanelPrefix.size()) {
194 return false;
195 }
196 for (char ch : panel_id.substr(kRoomPanelPrefix.size())) {
197 if (ch < '0' || ch > '9') {
198 return false;
199 }
200 }
201 return true;
202}
203
204std::string LastNonEmptyLine(const std::string& text) {
205 std::vector<std::string> lines = absl::StrSplit(text, '\n');
206 for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
207 std::string line = std::string(*it);
208 absl::StripAsciiWhitespace(&line);
209 if (!line.empty()) {
210 return line;
211 }
212 }
213 return "";
214}
215
217 ProjectManagementPanel* project_panel,
218 const ProjectWorkflowStatus& status) {
220 if (status_bar != nullptr) {
221 status_bar->SetBuildStatus(status);
222 }
223 if (project_panel != nullptr) {
224 project_panel->SetBuildStatus(status);
225 }
226}
227
229 ProjectManagementPanel* project_panel,
230 const ProjectWorkflowStatus& status) {
232 if (status_bar != nullptr) {
233 status_bar->SetRunStatus(status);
234 }
235 if (project_panel != nullptr) {
236 project_panel->SetRunStatus(status);
237 }
238}
239
240void AppendWorkflowHistoryEntry(const std::string& kind,
241 const ProjectWorkflowStatus& status,
242 const std::string& output_log) {
244 {.kind = kind,
245 .status = status,
246 .output_log = output_log,
247 .timestamp = std::chrono::system_clock::now()});
248}
249
251 return !project.custom_objects_folder.empty() ||
252 !project.custom_object_files.empty();
253}
254
255constexpr int kTrackCustomObjectId = 0x31;
256
258 std::string* warning) {
259 if (project == nullptr) {
260 return false;
261 }
262 if (project->custom_objects_folder.empty()) {
263 return false;
264 }
265 if (project->custom_object_files.find(kTrackCustomObjectId) !=
266 project->custom_object_files.end()) {
267 return false;
268 }
269
270 const auto& defaults =
273 if (defaults.empty()) {
274 return false;
275 }
276
277 project->custom_object_files[kTrackCustomObjectId] = defaults;
278 if (warning != nullptr) {
279 *warning = absl::StrFormat(
280 "Project defines custom_objects_folder but is missing "
281 "custom_object_files[0x%02X]. Seeded default mapping (%d entries).",
282 kTrackCustomObjectId, static_cast<int>(defaults.size()));
283 }
284 return true;
285}
286
287std::vector<std::string> ValidateRomAddressOverrides(
288 const core::RomAddressOverrides& overrides, const Rom& rom) {
289 std::vector<std::string> warnings;
290 if (overrides.addresses.empty()) {
291 return warnings;
292 }
293
294 const auto rom_size = rom.size();
295 auto warn = [&](const std::string& message) {
296 warnings.push_back(message);
297 };
298
299 auto check_range = [&](const std::string& label, uint32_t addr, size_t span) {
300 const size_t addr_size = static_cast<size_t>(addr);
301 if (addr_size >= rom_size || addr_size + span > rom_size) {
302 warn(absl::StrFormat("ROM override '%s' out of range: 0x%X (size 0x%X)",
303 label, addr, rom_size));
304 }
305 };
306
307 for (const auto& [key, value] : overrides.addresses) {
308 check_range(key, value, 1);
309 }
310
313 // Defaults match the Oracle expanded message bank ($2F8000-$2FFFFF).
314 // Kept local to avoid pulling message editor constants into core validation.
315 constexpr uint32_t kExpandedMessageStartDefault = 0x178000;
316 constexpr uint32_t kExpandedMessageEndDefault = 0x17FFFF;
317 const uint32_t start =
319 .value_or(kExpandedMessageStartDefault);
320 const uint32_t end =
322 .value_or(kExpandedMessageEndDefault);
323 if (start >= rom_size || end >= rom_size) {
324 warn(absl::StrFormat(
325 "Expanded message range out of ROM bounds: 0x%X-0x%X", start, end));
326 } else if (end < start) {
327 warn(absl::StrFormat("Expanded message range invalid: 0x%X-0x%X", start,
328 end));
329 }
330 }
331
332 if (auto hook =
334 check_range(core::RomAddressKey::kExpandedMusicHook, *hook, 4);
335 if (*hook < rom_size) {
336 auto opcode = rom.ReadByte(*hook);
337 if (opcode.ok() && opcode.value() != 0x22) {
338 warn(absl::StrFormat(
339 "Expanded music hook at 0x%X is not a JSL opcode (0x%02X)", *hook,
340 opcode.value()));
341 }
342 }
343 }
344
345 if (auto main =
348 }
349
350 if (auto aux = overrides.GetAddress(core::RomAddressKey::kExpandedMusicAux)) {
351 check_range(core::RomAddressKey::kExpandedMusicAux, *aux, 4);
352 }
353
354 if (HasAnyOverride(overrides,
359 const uint32_t marker =
362 const uint32_t magic =
365 check_range("overworld_ptr_marker", marker, 1);
366 if (marker < rom_size) {
367 if (rom.data()[marker] != static_cast<uint8_t>(magic)) {
368 warn(absl::StrFormat(
369 "Overworld expanded pointer marker mismatch at 0x%X: expected "
370 "0x%02X, found 0x%02X",
371 marker, static_cast<uint8_t>(magic), rom.data()[marker]));
372 }
373 }
374 }
375
376 if (HasAnyOverride(overrides,
381 const uint32_t flag_addr =
382 overrides
385 check_range("overworld_entrance_flag_expanded", flag_addr, 1);
386 if (flag_addr < rom_size && rom.data()[flag_addr] == 0xB8) {
387 warn(
388 absl::StrFormat("Overworld entrance flag at 0x%X is still 0xB8 "
389 "(vanilla); expanded entrance tables may be ignored",
390 flag_addr));
391 }
392 }
393
394 return warnings;
395}
396
397std::string StripSessionPrefix(absl::string_view panel_id) {
398 if (panel_id.size() > 2 && panel_id[0] == 's' &&
399 absl::ascii_isdigit(panel_id[1])) {
400 const size_t dot = panel_id.find('.');
401 if (dot != absl::string_view::npos) {
402 return std::string(panel_id.substr(dot + 1));
403 }
404 }
405 return std::string(panel_id);
406}
407
409 namespace fs = std::filesystem;
410
411 std::vector<fs::path> roots;
412 if (const char* home = std::getenv("HOME");
413 home != nullptr && std::strlen(home) > 0) {
414 roots.push_back(fs::path(home) / "src" / "hobby" / "oracle-of-secrets");
415 }
416
417 std::vector<fs::path> candidates;
418 std::unordered_set<std::string> seen;
419 auto add_candidate = [&](const fs::path& path) {
420 std::error_code ec;
421 if (!fs::exists(path, ec) || ec) {
422 return;
423 }
424 std::string ext = path.extension().string();
425 absl::AsciiStrToLower(&ext);
426 if (ext != ".yaze" && ext != ".yazeproj") {
427 return;
428 }
429 const std::string normalized = fs::weakly_canonical(path, ec).string();
430 const std::string key = normalized.empty() ? path.string() : normalized;
431 if (key.empty() || seen.count(key) > 0) {
432 return;
433 }
434 seen.insert(key);
435 candidates.push_back(path);
436 };
437
438 for (const auto& root : roots) {
439 std::error_code ec;
440 if (!fs::exists(root, ec) || ec) {
441 continue;
442 }
443
444 // Priority candidates first.
445 add_candidate(root / "Oracle-of-Secrets.yaze");
446 add_candidate(root / "Oracle of Secrets.yaze");
447 add_candidate(root / "Oracle-of-Secrets.yazeproj");
448 add_candidate(root / "Oracle of Secrets.yazeproj");
449 add_candidate(root / "Roms" / "Oracle of Secrets.yaze");
450
451 // Also detect additional local project files nearby.
452 fs::recursive_directory_iterator it(
453 root, fs::directory_options::skip_permission_denied, ec);
454 fs::recursive_directory_iterator end;
455 if (ec) {
456 continue;
457 }
458 for (; it != end; it.increment(ec)) {
459 if (ec) {
460 ec.clear();
461 continue;
462 }
463 if (it.depth() > 2) {
464 it.disable_recursion_pending();
465 continue;
466 }
467 add_candidate(it->path());
468 }
469 }
470
471 if (candidates.empty()) {
472 return;
473 }
474
476 // Add in reverse so the first candidate remains most recent.
477 for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
478 manager.AddFile(it->string());
479 }
480 manager.Save();
481}
482
483} // namespace
484
485std::optional<EditorType> ParseEditorTypeFromString(absl::string_view name) {
486 std::string normalized = std::string(name);
487 absl::StripAsciiWhitespace(&normalized);
488 const std::string lower = absl::AsciiStrToLower(normalized);
489 for (int i = 0; i < static_cast<int>(EditorType::kSettings) + 1; ++i) {
490 const auto type = static_cast<EditorType>(i);
491 const std::string name_candidate =
492 absl::AsciiStrToLower(EditorRegistry::GetEditorName(type));
493 const std::string category_candidate =
494 absl::AsciiStrToLower(EditorRegistry::GetEditorCategory(type));
495 if (name_candidate == lower || category_candidate == lower) {
496 return type;
497 }
498 }
499 return std::nullopt;
500}
501
502// Static registry of editors that use the card-based layout system
503// These editors register their cards with WorkspaceWindowManager and manage their
504// own windows They do NOT need the traditional ImGui::Begin/End wrapper - they
505// create cards internally
509
513
514void EditorManager::ApplyLayoutPreset(const std::string& preset_name) {
516}
517
518bool EditorManager::ApplyLayoutProfile(const std::string& profile_id) {
519 if (!layout_manager_) {
520 return false;
521 }
522
523 const size_t session_id = GetCurrentSessionId();
524 const EditorType current_type =
526
527 LayoutProfile applied_profile;
528 if (!layout_manager_->ApplyBuiltInProfile(profile_id, session_id,
529 current_type, &applied_profile)) {
530 return false;
531 }
532
533 if (applied_profile.open_agent_chat && right_drawer_manager_) {
534 right_drawer_manager_->OpenDrawer(
536 const float default_width = RightDrawerManager::GetDefaultDrawerWidth(
538 right_drawer_manager_->SetDrawerWidth(
540 std::max(default_width, 480.0f));
541 }
542
544 absl::StrFormat("Layout Profile: %s", applied_profile.label),
546 return true;
547}
548
550 if (!layout_manager_) {
551 return;
552 }
553 layout_manager_->CaptureTemporarySessionLayout(GetCurrentSessionId());
554 toast_manager_.Show("Captured temporary layout snapshot", ToastType::kInfo);
555}
556
557void EditorManager::RestoreTemporaryLayoutSnapshot(bool clear_after_restore) {
558 if (!layout_manager_) {
559 return;
560 }
561
562 if (layout_manager_->RestoreTemporarySessionLayout(GetCurrentSessionId(),
563 clear_after_restore)) {
564 toast_manager_.Show("Restored temporary layout snapshot",
566 } else {
567 toast_manager_.Show("No temporary layout snapshot available",
569 }
570}
571
573 if (!layout_manager_) {
574 return;
575 }
576 layout_manager_->ClearTemporarySessionLayout();
577 toast_manager_.Show("Cleared temporary layout snapshot", ToastType::kInfo);
578}
579
580bool EditorManager::SaveLayoutSnapshotAs(const std::string& name) {
581 if (!layout_manager_)
582 return false;
583 if (name.empty()) {
584 toast_manager_.Show("Snapshot name cannot be empty", ToastType::kWarning);
585 return false;
586 }
587 const bool ok =
588 layout_manager_->SaveNamedSnapshot(name, GetCurrentSessionId());
589 if (ok) {
590 toast_manager_.Show(absl::StrFormat("Saved snapshot '%s'", name),
592 } else {
593 toast_manager_.Show(absl::StrFormat("Failed to save snapshot '%s'", name),
595 }
596 return ok;
597}
598
599bool EditorManager::RestoreLayoutSnapshot(const std::string& name,
600 bool remove_after_restore) {
601 if (!layout_manager_)
602 return false;
603 const bool ok = layout_manager_->RestoreNamedSnapshot(
604 name, GetCurrentSessionId(), remove_after_restore);
605 if (ok) {
606 toast_manager_.Show(absl::StrFormat("Restored snapshot '%s'", name),
608 } else {
609 toast_manager_.Show(absl::StrFormat("Snapshot '%s' not available", name),
611 }
612 return ok;
613}
614
615bool EditorManager::DeleteLayoutSnapshot(const std::string& name) {
616 if (!layout_manager_)
617 return false;
618 const bool ok = layout_manager_->DeleteNamedSnapshot(name);
619 if (ok) {
620 toast_manager_.Show(absl::StrFormat("Deleted snapshot '%s'", name),
622 }
623 return ok;
624}
625
626std::vector<std::string> EditorManager::ListLayoutSnapshots() const {
627 if (!layout_manager_)
628 return {};
629 return layout_manager_->ListNamedSnapshots(GetCurrentSessionId());
630}
631
633 if (!layout_manager_) {
634 return;
635 }
636
638 layout_manager_->SetProjectLayoutKey(
640 } else {
641 layout_manager_->UseGlobalLayouts();
642 }
643}
644
653
654#ifdef YAZE_BUILD_AGENT_UI
655void EditorManager::ShowAIAgent() {
656 // Apply saved agent settings from the current project when opening the Agent
657 // UI to respect the user's preferred provider/model.
658 // TODO: Implement LoadAgentSettingsFromProject in AgentChat or AgentEditor
662 for (const auto& window_id :
663 LayoutPresets::GetDefaultWindows(EditorType::kAgent)) {
664 window_manager_.OpenWindow(window_id);
665 }
666}
667
668void EditorManager::ShowChatHistory() {
670}
671#endif
672
674 : project_manager_(&toast_manager_), rom_file_manager_(&toast_manager_) {
675 std::stringstream ss;
676 ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "."
677 << YAZE_VERSION_PATCH;
678 ss >> version_;
679
680 // Initialize Core Context
681 editor_context_ = std::make_unique<GlobalEditorContext>(event_bus_);
684
688
689 // STEP 5: ShortcutConfigurator created later in Initialize() method
690 // It depends on all above coordinators being available
691}
692
695
696 // STEP 1: Initialize PopupManager FIRST
697 popup_manager_ = std::make_unique<PopupManager>(this);
698 popup_manager_->Initialize(); // Registers all popups with PopupID constants
699
700 // STEP 2: Initialize SessionCoordinator (independent of popups)
701 session_coordinator_ = std::make_unique<SessionCoordinator>(
703 session_coordinator_->SetEditorRegistry(&editor_registry_);
704
705 // STEP 3: Initialize MenuOrchestrator (depends on popup_manager_,
706 // session_coordinator_)
707 menu_orchestrator_ = std::make_unique<MenuOrchestrator>(
710
711 // Wire up the window manager for the View menu window listing
712 menu_orchestrator_->SetWindowManager(&window_manager_);
713 menu_orchestrator_->SetStatusBar(&status_bar_);
714 menu_orchestrator_->SetUserSettings(&user_settings_);
715
716 session_coordinator_->SetEditorManager(this);
717 session_coordinator_->SetEventBus(&event_bus_); // Enable event publishing
719 &event_bus_); // Global event bus access
720
721 // Expose UserSettings to cross-editor panels that don't get constructor
722 // injection (e.g. Layout Designer, which reads named_layouts). The
723 // LayoutManager setter happens below once the unique_ptr is constructed.
725
726 // STEP 3.5: Initialize RomLifecycleManager (depends on popup_manager_,
727 // session_coordinator_, rom_file_manager_, toast_manager_)
729 .rom_file_manager = &rom_file_manager_,
730 .session_coordinator = session_coordinator_.get(),
733 .project = &current_project_,
734 });
735
736 // STEP 4: Initialize UICoordinator (depends on popup_manager_,
737 // session_coordinator_, window_manager_)
738 ui_coordinator_ = std::make_unique<UICoordinator>(
742
743 // STEP 4.5: Initialize LayoutManager (DockBuilder layouts for editors)
744 layout_manager_ = std::make_unique<LayoutManager>();
745 layout_manager_->SetWindowManager(&window_manager_);
746 layout_manager_->UseGlobalLayouts();
747 // Expose to cross-editor panels that drive the live dockspace (Layout
748 // Designer). Must land after the unique_ptr is constructed.
752 [this](const std::string& preset_name) {
753 ApplyLayoutPreset(preset_name);
754 });
756 [this](const std::string& preset_name) {
757 ApplyLayoutPreset(preset_name);
758 });
759
760 // STEP 4.6: Initialize RightDrawerManager (right-side sliding drawers)
761 right_drawer_manager_ = std::make_unique<RightDrawerManager>();
762 right_drawer_manager_->SetToastManager(&toast_manager_);
763 right_drawer_manager_->SetProposalDrawer(&proposal_drawer_);
765 right_drawer_manager_->SetShortcutManager(&shortcut_manager_);
767 [this](const std::string& prompt) {
768#if defined(YAZE_BUILD_AGENT_UI)
769 auto* agent_editor = agent_ui_.GetAgentEditor();
770 if (!agent_editor) {
771 return;
772 }
773 auto* chat = agent_editor->GetAgentChat();
774 if (!chat) {
775 return;
776 }
777 chat->set_active(true);
778 chat->SendMessage(prompt);
779#else
780 (void)prompt;
781#endif
782 },
783 [this]() {
784#if defined(YAZE_BUILD_AGENT_UI)
786 right_drawer_manager_->OpenDrawer(
788 }
789#endif
790 });
793 right_drawer_manager_->ToggleDrawer(
795 } else {
797 }
798 });
799
800 // Initialize ProjectManagementPanel for project/version management
801 project_management_panel_ = std::make_unique<ProjectManagementPanel>();
802 project_management_panel_->SetToastManager(&toast_manager_);
804 [this](const std::string& filepath, const std::string& contents) {
805 return PrepareRawProjectFileSave(filepath, contents);
806 });
808 [this](const std::string& filepath, const std::string& contents) {
809 return CommitRawProjectFileSave(filepath, contents);
810 });
812 std::make_unique<workflow::ProjectWorkflowOutputPanel>());
813 project_management_panel_->SetSwapRomCallback([this]() {
814 // Prompt user to select a new ROM for the project
817 if (!rom_path.empty()) {
818 auto status = SwapProjectRom(rom_path);
819 if (status.ok()) {
820 toast_manager_.Show("Project ROM updated and reloaded.",
822 } else {
823 toast_manager_.Show(absl::StrFormat("Failed to update project ROM: %s",
824 status.message()),
826 }
827 }
828 });
829 project_management_panel_->SetReloadRomCallback([this]() {
832 auto status = ReloadProjectRom();
833 if (!status.ok()) {
835 absl::StrFormat("Failed to reload ROM: %s", status.message()),
837 }
838 }
839 });
840 project_management_panel_->SetSaveProjectCallback([this]() {
841 auto status = SaveProject();
842 if (status.ok()) {
843 toast_manager_.Show("Project saved", ToastType::kSuccess);
844 } else {
846 absl::StrFormat("Failed to save project: %s", status.message()),
848 }
849 return status;
850 });
851 project_management_panel_->SetBrowseFolderCallback(
852 [this](const std::string& type) {
854 if (!folder_path.empty()) {
855 if (type == "code") {
856 current_project_.code_folder = folder_path;
857 // Update assembly editor path
858 if (auto* editor_set = GetCurrentEditorSet()) {
859// iOS: avoid blocking folder enumeration on the UI thread.
860#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
861 editor_set->OpenAssemblyFolder(folder_path);
862#endif
863 window_manager_.SetFileBrowserPath("Assembly", folder_path);
864 }
865 } else if (type == "assets") {
866 current_project_.assets_folder = folder_path;
867 }
871 }
872 toast_manager_.Show(absl::StrFormat("%s folder set: %s", type.c_str(),
873 folder_path.c_str()),
875 }
876 });
877 project_management_panel_->SetBuildProjectCallback(
878 [this]() { QueueBuildCurrentProject(); });
879 project_management_panel_->SetCancelBuildCallback(
880 [this]() { CancelQueuedProjectBuild(); });
881 project_management_panel_->SetRunProjectCallback(
882 [this]() { (void)RunCurrentProject(); });
884 [this]() { QueueBuildCurrentProject(); });
886 [this]() { (void)RunCurrentProject(); });
888 window_manager_.OpenWindow("workflow.output");
889 window_manager_.MarkWindowRecentlyUsed("workflow.output");
890 });
892 [this]() { CancelQueuedProjectBuild(); });
893 right_drawer_manager_->SetProjectManagementPanel(
895
896 // STEP 4.6.1: Initialize LayoutCoordinator (facade for layout operations)
898 layout_deps.layout_manager = layout_manager_.get();
899 layout_deps.window_manager = &window_manager_;
900 layout_deps.ui_coordinator = ui_coordinator_.get();
901 layout_deps.toast_manager = &toast_manager_;
902 layout_deps.status_bar = &status_bar_;
904 layout_coordinator_.Initialize(layout_deps);
905
906 // STEP 4.6.2: Initialize EditorActivator (editor switching and jump navigation)
907 EditorActivator::Dependencies activator_deps;
908 activator_deps.window_manager = &window_manager_;
909 activator_deps.layout_manager = layout_manager_.get();
910 activator_deps.ui_coordinator = ui_coordinator_.get();
911 activator_deps.right_drawer_manager = right_drawer_manager_.get();
912 activator_deps.toast_manager = &toast_manager_;
913 activator_deps.event_bus = &event_bus_;
914 activator_deps.ensure_editor_assets_loaded = [this](EditorType type) {
915 return EnsureEditorAssetsLoaded(type);
916 };
917 activator_deps.get_current_editor_set = [this]() {
918 return GetCurrentEditorSet();
919 };
920 activator_deps.get_current_session_id = [this]() {
921 return GetCurrentSessionId();
922 };
923 activator_deps.queue_deferred_action = [this](std::function<void()> action) {
924 QueueDeferredAction(std::move(action));
925 };
926 editor_activator_.Initialize(activator_deps);
927
928 // STEP 4.7: Initialize ActivityBar
929 activity_bar_ = std::make_unique<ActivityBar>(
931 [this]() -> bool {
932 if (auto* editor_set = GetCurrentEditorSet()) {
933 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
935 return dungeon_editor->IsWorkbenchWorkflowEnabled();
936 }
937 }
938 return false;
939 },
940 [this](bool enabled) {
941 if (auto* editor_set = GetCurrentEditorSet()) {
942 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
944 dungeon_editor->QueueWorkbenchWorkflowMode(enabled);
945 }
946 }
947 });
948
949 // Wire per-user sidebar prefs so right-click / drag mutate persisted state.
950 activity_bar_->SetUserSettings(&user_settings_);
951
952 // Populate the MoreActions registry with the default rail entries. External
953 // callers can Register/Unregister further entries at runtime.
954 auto& registry = activity_bar_->actions_registry();
955 registry.Register({"command_palette",
956 "Command Palette",
959 {}});
960 registry.Register({"keyboard_shortcuts",
961 "Keyboard Shortcuts",
964 {}});
965 registry.Register({"open_rom_project",
966 "Open ROM / Project",
968 [this]() { window_manager_.TriggerOpenRom(); },
969 {}});
970 registry.Register({"settings",
971 "Settings",
973 [this]() { window_manager_.TriggerShowSettings(); },
974 {}});
975
976 // WindowHost is the declarative window registration surface used by
977 // editor/runtime systems.
978 window_host_ = std::make_unique<WindowHost>(&window_manager_);
979
980 // Wire up EventBus to WorkspaceWindowManager for action event publishing
982}
983
985 // Auto-register panels from ContentRegistry (Unified Panel System)
986 auto registry_panels = ContentRegistry::Panels::CreateAll();
987 for (auto& panel : registry_panels) {
989 }
990
991 // STEP 4.8: Initialize DashboardPanel
992 dashboard_panel_ = std::make_unique<DashboardPanel>(this);
993
994 if (window_host_) {
995 WindowDefinition dashboard_definition;
996 dashboard_definition.id = "dashboard.main";
997 dashboard_definition.display_name = "Dashboard";
998 dashboard_definition.icon = ICON_MD_DASHBOARD;
999 dashboard_definition.category = "Dashboard";
1000 dashboard_definition.window_title = " Dashboard";
1001 dashboard_definition.shortcut_hint = "F1";
1002 dashboard_definition.priority = 0;
1003 dashboard_definition.visibility_flag = dashboard_panel_->visibility_flag();
1004 window_host_->RegisterWindow(dashboard_definition);
1005 } else {
1007 {.card_id = "dashboard.main",
1008 .display_name = "Dashboard",
1009 .window_title = " Dashboard",
1010 .icon = ICON_MD_DASHBOARD,
1011 .category = "Dashboard",
1012 .shortcut_hint = "F1",
1013 .visibility_flag = dashboard_panel_->visibility_flag(),
1014 .priority = 0});
1015 }
1016}
1017
1019 // Subscribe to session lifecycle events via EventBus
1020 // (replaces SessionObserver pattern)
1022 [this](const SessionSwitchedEvent& e) {
1023 HandleSessionSwitched(e.new_index, e.session, e.transient);
1024 });
1025
1027 [this](const SessionCreatedEvent& e) {
1028 HandleSessionCreated(e.index, e.session);
1029 });
1030
1032 [this](const SessionClosedEvent& e) { HandleSessionClosed(e.index); });
1033
1035 HandleSessionRomLoaded(e.session_id, e.rom);
1036 });
1037
1038 // Subscribe to FrameGuiBeginEvent for ImGui-safe deferred action processing
1039 // This replaces scattered manual processing calls with event-driven execution
1041 ui_sync_frame_id_.fetch_add(1, std::memory_order_relaxed);
1042
1043 // Process LayoutCoordinator's deferred actions
1045
1046 // Process EditorManager's deferred actions
1047 if (!deferred_actions_.empty()) {
1048 std::vector<std::function<void()>> actions_to_execute;
1049 actions_to_execute.swap(deferred_actions_);
1050 const int processed_count = static_cast<int>(actions_to_execute.size());
1051 for (auto& action : actions_to_execute) {
1052 action();
1053 }
1054 if (processed_count > 0) {
1055 const int remaining = pending_editor_deferred_actions_.fetch_sub(
1056 processed_count, std::memory_order_relaxed) -
1057 processed_count;
1058 if (remaining < 0) {
1059 pending_editor_deferred_actions_.store(0, std::memory_order_relaxed);
1060 }
1061 }
1062 }
1063 });
1064
1065 // Subscribe to UIActionRequestEvent for activity bar actions
1066 // This replaces direct callbacks from WorkspaceWindowManager
1068 [this](const UIActionRequestEvent& e) {
1069 HandleUIActionRequest(e.action);
1070 });
1071
1073 [this](const PanelVisibilityChangedEvent& e) {
1074 if (e.category.empty() ||
1076 return;
1077 }
1078 if (IsTransientPanelVisibilityId(e.base_panel_id)) {
1079 return;
1080 }
1081 auto& prefs = user_settings_.prefs();
1082 prefs.panel_visibility_state[e.category][e.base_panel_id] = e.visible;
1083 settings_dirty_ = true;
1085 });
1086}
1087
1089 // ContentRegistry::Context holds non-owning pointers into our members
1090 // (event_bus_, editor_context_, user_settings_, layout_manager_, callbacks).
1091 // Clear them before our members destruct so widgets that read the singleton
1092 // (e.g. Canvas::set_global_scale via Context::event_bus()) don't see
1093 // dangling pointers in tests that instantiate multiple EditorManagers.
1094 // Clear() resets fallback state but intentionally keeps global_context for
1095 // session-close semantics, so null it explicitly here.
1098
1099 // GetResourceLabels() is a process-wide singleton. RefreshResourceLabelProvider
1100 // hands it a pointer to current_project_.hack_manifest, which dangles after
1101 // we destruct. Clear before our members go out of scope.
1104
1105 // ThemeManager is a singleton that outlives us. Clear the theme-changed
1106 // callback so it stops calling back into a destroyed EditorManager via the
1107 // captured `this` pointer. Matters for unit tests that construct and destroy
1108 // multiple EditorManager instances in the same process.
1111
1112 // EventBus subscriptions are automatically cleaned up when event_bus_ is
1113 // destroyed (owned by this class). No manual unsubscription needed.
1114}
1115
1116// ============================================================================
1117// Session Event Handlers (EventBus subscribers)
1118// ============================================================================
1119
1121 auto& provider = zelda3::GetResourceLabels();
1124
1125 auto merge_labels =
1128 for (const auto& [type, labels] : src) {
1129 auto& out = (*dst)[type];
1130 for (const auto& [key, value] : labels) {
1131 out[key] = value;
1132 }
1133 }
1134 };
1135
1136 merged_labels.clear();
1137 std::vector<std::string> source_parts;
1138
1139 // 1) ROM-local labels as baseline for active session.
1140 if (auto* rom = GetCurrentRom();
1141 rom && rom->resource_label() && !rom->resource_label()->labels_.empty()) {
1142 merge_labels(&merged_labels, rom->resource_label()->labels_);
1143 source_parts.push_back("rom");
1144 }
1145
1146 // 2) Project registry labels from the active hack manifest.
1150 merge_labels(
1151 &merged_labels,
1153 source_parts.push_back("registry");
1154 }
1155
1156 // 3) Explicit project labels override all other sources.
1159 merge_labels(&merged_labels, current_project_.resource_labels);
1160 source_parts.push_back("project");
1161 }
1162
1163 auto* label_source = merged_labels.empty() ? &empty_labels : &merged_labels;
1164 std::string source =
1165 source_parts.empty() ? "empty" : absl::StrJoin(source_parts, "+");
1166
1167 provider.SetProjectLabels(label_source);
1168 provider.SetHackManifest(current_project_.project_opened() &&
1171 : nullptr);
1172
1173 const bool prefer_hmagic =
1177 provider.SetPreferHMagicNames(prefer_hmagic);
1178
1179 LOG_DEBUG("EditorManager",
1180 "ResourceLabelProvider refreshed (source=%s, project_open=%s)",
1181 source.c_str(),
1182 current_project_.project_opened() ? "true" : "false");
1183}
1184
1187 return;
1188 }
1189
1190 const auto index =
1192 if (!index.has_value()) {
1194 return;
1195 }
1196
1197 auto* session =
1198 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1199 if (!session) {
1201 return;
1202 }
1203
1205 if (session->project_context.has_value()) {
1206 session->project_context->feature_flags = session->feature_flags;
1207 }
1209 current_project_.feature_flags = session->feature_flags;
1210 }
1211}
1212
1214 if (!active_project_context_session_id_.has_value() ||
1216 return;
1217 }
1218
1221 const auto index =
1223 if (!index.has_value()) {
1225 return;
1226 }
1227
1228 auto* session =
1229 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1230 if (!session) {
1232 return;
1233 }
1234
1235 // The singleton can temporarily belong to another session while all editor
1236 // windows are ticked. Only use that value when it still belongs to this
1237 // project owner; otherwise its session-owned copy is authoritative.
1240 }
1241 // These preferences are exposed through global runtime settings, but they
1242 // are project workspace state. Capture them before leaving the session so a
1243 // later SaveProject cannot copy another session's values into this project.
1252 current_project_.feature_flags = session->feature_flags;
1254}
1255
1257 if (!active_project_context_session_id_.has_value() ||
1259 return;
1260 }
1261
1262 const auto index =
1264 if (!index.has_value()) {
1265 return;
1266 }
1267 auto* session =
1268 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1269 if (!session) {
1270 return;
1271 }
1272
1274 session->project_dirty = project_management_panel_->IsProjectDirty();
1275 }
1276 session->project_file_editor_state = project_file_editor_.CaptureState();
1277}
1278
1280 if (!session_coordinator_) {
1281 return absl::OkStatus();
1282 }
1283 auto* session = session_coordinator_->GetActiveRomSession();
1284 if (session == nullptr || !session->editors.HasPendingProjectDraftChanges()) {
1285 return absl::OkStatus();
1286 }
1287 if (!IsCurrentProjectContextOwnedBySession(session->session_id())) {
1288 return absl::FailedPreconditionError(
1289 "Project editor drafts require their project context to be active");
1290 }
1291 return session->editors.PrepareProjectSave();
1292}
1293
1303
1305 std::optional<size_t> previous_session_id) {
1308 if (previous_session_id.has_value() && session_coordinator_) {
1309 const auto index = ResolveSessionIndexById(*previous_session_id);
1310 if (index.has_value()) {
1311 auto* previous_session =
1312 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1313 if (previous_session) {
1314 if (session_coordinator_->GetActiveSessionIndex() != *index) {
1315 session_coordinator_->SwitchToSession(*index);
1316 } else {
1317 RestoreProjectContextForSession(previous_session);
1318 }
1319 return;
1320 }
1321 }
1322 }
1323
1327 version_manager_ = nullptr;
1330}
1331
1333 size_t previous_session_count) {
1334 if (!session_coordinator_ ||
1335 session_coordinator_->GetTotalSessionCount() <= previous_session_count) {
1336 return absl::OkStatus();
1337 }
1338 auto* provisional_session = session_coordinator_->GetActiveRomSession();
1339 if (!provisional_session) {
1340 return absl::InternalError(
1341 "A provisional session was created but is not active");
1342 }
1343 return session_coordinator_->DiscardProvisionalSession(
1344 provisional_session->session_id());
1345}
1346
1355
1357 const std::string& filepath, const std::string& contents) {
1359 const bool targets_current = !current_project_.filepath.empty() &&
1361 filepath, current_project_.filepath);
1362 if (!targets_current) {
1363 // Save As to a separate descriptor is the deterministic escape hatch when
1364 // both the structured panel and raw document have edits.
1365 return absl::OkStatus();
1366 }
1367 if (IsCurrentProjectDirty()) {
1368 return absl::FailedPreconditionError(
1369 "Project settings also have unsaved changes; use raw Save As to "
1370 "preserve this draft, then save the project settings");
1371 }
1372
1373 auto* session = session_coordinator_
1374 ? session_coordinator_->GetActiveRomSession()
1375 : nullptr;
1376 if (!session || !session->rom.is_loaded()) {
1377 return absl::FailedPreconditionError(
1378 "No active ROM session for this project file");
1379 }
1380
1381 project::YazeProject parsed_project;
1382 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1383 if (!parsed_project.project_opened()) {
1384 return absl::InvalidArgumentError(
1385 "Raw project file must contain a non-empty project name");
1386 }
1387 if (parsed_project.rom_filename.empty() ||
1389 parsed_project.rom_filename, session->filepath) ||
1391 parsed_project.rom_filename, session->rom.filename()))) {
1392 return absl::FailedPreconditionError(
1393 "Raw project file cannot change the ROM backing file of a loaded "
1394 "session; use Project Management > Swap ROM");
1395 }
1396 return session_coordinator_->CheckBackingFileAvailable(
1397 parsed_project.rom_filename, session->session_id());
1398}
1399
1401 const std::string& filepath, const std::string& contents) {
1402 if (current_project_.filepath.empty() ||
1404 filepath, current_project_.filepath)) {
1405 return absl::OkStatus();
1406 }
1407
1408 auto* session = session_coordinator_
1409 ? session_coordinator_->GetActiveRomSession()
1410 : nullptr;
1411 if (!session) {
1412 return absl::FailedPreconditionError("No active ROM session");
1413 }
1414
1415 project::YazeProject parsed_project;
1416 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1417 current_project_ = std::move(parsed_project);
1419 session->project_dirty = false;
1420 active_project_context_session_id_ = session->session_id();
1421 runtime_feature_flags_session_id_ = session->session_id();
1422 version_manager_ = session->version_manager.get();
1425 project_management_panel_->SetProject(&current_project_, false);
1426 }
1427 return absl::OkStatus();
1428}
1429
1430void EditorManager::RebaseCleanProjectFileDraft(const std::string& filepath) {
1431 if (!session_coordinator_ || filepath.empty()) {
1432 return;
1433 }
1434 auto* session = session_coordinator_->GetActiveRomSession();
1435 if (!session || !session->project_file_editor_state.initialized ||
1436 session->project_file_editor_state.modified) {
1437 return;
1438 }
1439
1440 const bool was_active = project_file_editor_.is_active();
1441 auto status = project_file_editor_.LoadFile(filepath);
1442 if (!status.ok()) {
1443 LOG_WARN("EditorManager", "Failed to rebase raw project editor: %s",
1444 status.message());
1445 return;
1446 }
1448 project_file_editor_.set_active(was_active);
1449 session->project_file_editor_state = project_file_editor_.CaptureState();
1450}
1451
1453 RomSession* session, const project::YazeProject& project) {
1454 if (!session) {
1455 return;
1456 }
1457 const bool needs_version_manager =
1458 !session->project_context.has_value() || !session->version_manager;
1459 if (session->project_context.has_value()) {
1460 *session->project_context = project;
1461 } else {
1462 session->project_context.emplace(project);
1463 }
1464 if (needs_version_manager) {
1465 session->version_manager =
1466 std::make_unique<core::VersionManager>(&*session->project_context);
1467 }
1468 session->feature_flags = project.feature_flags;
1469 // Configure after the stable context and VersionManager exist. EditorSet
1470 // instances are created before SessionCreatedEvent, so their first
1471 // dependency pass can legitimately have no project yet.
1472 ConfigureSession(session);
1473}
1474
1478
1487 if (ImGui::GetCurrentContext() != nullptr) {
1488 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
1489 }
1490
1495 } else {
1498 }
1501 ? ""
1504
1512 } else {
1514 }
1515
1521 const auto* session = session_coordinator_
1522 ? session_coordinator_->GetActiveRomSession()
1523 : nullptr;
1524 project_management_panel_->SetProject(
1525 &current_project_, session != nullptr && session->project_dirty);
1526 project_management_panel_->SetVersionManager(version_manager_);
1528 }
1529}
1530
1532 if (!session || !session->project_context.has_value()) {
1536 version_manager_ = nullptr;
1538 project_management_panel_->SetVersionManager(nullptr);
1539 }
1540 return false;
1541 }
1542
1543 if (!session->version_manager) {
1544 session->version_manager =
1545 std::make_unique<core::VersionManager>(&*session->project_context);
1546 ConfigureSession(session);
1547 }
1549 version_manager_ = session->version_manager.get();
1555 return true;
1556}
1557
1559 if (!session) {
1561 project_management_panel_->SetProject(nullptr);
1562 }
1564 return;
1565 }
1566
1569 session->project_dirty);
1570 }
1574 } else {
1576 }
1577}
1578
1579absl::StatusOr<const project::YazeProject*>
1581 if (!session_coordinator_) {
1582 return absl::FailedPreconditionError("No session coordinator");
1583 }
1584 auto* session = session_coordinator_->GetActiveRomSession();
1585 if (!session || !session->rom.is_loaded()) {
1586 return absl::FailedPreconditionError("No active ROM session");
1587 }
1588
1590 if (!active_project_context_session_id_.has_value() ||
1591 *active_project_context_session_id_ != session->session_id()) {
1592 return absl::FailedPreconditionError(
1593 "Active ROM session project context is not restored");
1594 }
1596 if (!session->project_context.has_value()) {
1597 return absl::FailedPreconditionError(
1598 "Active ROM session has no project/save context");
1599 }
1600 return &*session->project_context;
1601}
1602
1603void EditorManager::HandleSessionSwitched(size_t new_index, RomSession* session,
1604 bool transient) {
1605 // Confirmation consent and Save As targets belong to the ROM that started
1606 // the request. Never carry them across a user session switch. Frame-level
1607 // context switches are temporary and must not consume the active request.
1608 if (!transient) {
1613 CancelPendingRomSave(/*hide_popups=*/true);
1616 if (!RestoreProjectContextForSession(session) && session != nullptr) {
1617 LOG_ERROR("EditorManager",
1618 "Session %zu has no project context; saves are disabled",
1619 new_index);
1620 }
1621 } else {
1622 // Frame iteration must not copy full projects or reset project-wide caches.
1623 // Feature flags are cheap and directly affect per-editor draw/save paths.
1625 if (session) {
1628 } else {
1630 }
1631 }
1632
1633 // Palette edit history and dirty tracking are session-owned. Select the
1634 // matching state before any session-specific editor or save action runs.
1636 : nullptr);
1637
1638 // Update RightDrawerManager with the new session's settings editor
1640 right_drawer_manager_->SetSettingsPanel(
1641 session ? session->editors.GetSettingsPanel() : nullptr);
1642 }
1643
1644 // Update properties panel with new ROM
1645 if (session) {
1646 if (!transient) {
1648 }
1650 } else {
1653 }
1654
1655 // Update ContentRegistry context with current session's ROM and GameData
1656 ContentRegistry::Context::SetRom(session ? &session->rom : nullptr);
1658 : nullptr);
1659
1660 // current_editor_ is also session-owned. Resolve it from the newly active
1661 // EditorSet, or clear it when the current category has no editor backing.
1662 const std::string active_category = window_manager_.GetActiveCategory();
1663 Editor* active_editor = ResolveEditorForCategory(active_category);
1664 SetCurrentEditor(active_editor);
1666 active_editor);
1667
1668 // Keep room/sprite labels in sync with active session context.
1670
1671 const std::string category = window_manager_.GetActiveCategory();
1672 if (session != nullptr && !category.empty() &&
1674 auto it = user_settings_.prefs().panel_visibility_state.find(category);
1675 if (it != user_settings_.prefs().panel_visibility_state.end()) {
1676 const size_t session_id = session ? session->session_id() : new_index;
1677 window_manager_.RestoreVisibilityState(session_id, it->second);
1678 }
1679 }
1680
1681#ifdef YAZE_ENABLE_TESTING
1682 test::TestManager::Get().SetCurrentRom(session ? &session->rom : nullptr);
1683#endif
1684
1685 // RomLifecycleManager caches both identity hash and path; refresh them for
1686 // the newly active session before its next save-policy check.
1688
1689 LOG_DEBUG("EditorManager", "Session switched to %zu via EventBus", new_index);
1690}
1691
1694 CancelPendingRomSave(/*hide_popups=*/true);
1695 }
1697 if (session && !session->project_context.has_value()) {
1698 // A parsed project deliberately detaches the prior owner before creating
1699 // its ROM session. Normal raw-ROM and empty-session opens must instead get
1700 // a neutral context, not a copy of whichever project happened to be active.
1701 if (!active_project_context_session_id_.has_value() &&
1704 } else {
1705 project::YazeProject neutral_project;
1706 BindProjectContextToSession(session, neutral_project);
1707 }
1708 }
1709
1710 const size_t session_id = session ? session->session_id() : index;
1714 index == session_coordinator_->GetActiveSessionIndex()) {
1717 : nullptr);
1719 }
1720 LOG_INFO("EditorManager", "Session %zu created via EventBus", index);
1721}
1722
1724 CancelPendingRomSave(/*hide_popups=*/true);
1725
1726 // SessionClosedEvent is emitted before the owning RomSession is erased and
1727 // before the active index is adjusted. Bind the surviving session now so the
1728 // closing session's destructor cannot leave PaletteManager unbound.
1729 RomSession* palette_session = nullptr;
1731 const size_t session_count = session_coordinator_->GetTotalSessionCount();
1732 auto* active_session = session_coordinator_->GetActiveRomSession();
1733 auto* closing_session =
1734 session_coordinator_->IsValidSessionIndex(index)
1735 ? static_cast<RomSession*>(session_coordinator_->GetSession(index))
1736 : nullptr;
1737 if (closing_session != nullptr &&
1738 active_project_context_session_id_ == closing_session->session_id()) {
1741 if (runtime_feature_flags_session_id_ == closing_session->session_id()) {
1743 }
1744 version_manager_ = nullptr;
1746 project_management_panel_->SetVersionManager(nullptr);
1747 }
1748 }
1749 palette_session = active_session;
1750 if (closing_session != nullptr && active_session == closing_session &&
1751 session_count > 1) {
1752 const size_t replacement_index = index > 0 ? index - 1 : 1;
1753 palette_session = static_cast<RomSession*>(
1754 session_coordinator_->GetSession(replacement_index));
1755 }
1756 }
1758 palette_session ? &palette_session->game_data : nullptr);
1759
1760 // Update ContentRegistry - it will be set to new active ROM on next switch
1761 // If no sessions remain, clear the context
1763 session_coordinator_->GetTotalSessionCount() == 0) {
1765 }
1766
1767 // Avoid dangling/stale label map pointers after session close.
1769
1770#ifdef YAZE_ENABLE_TESTING
1771 // Update test manager - it will get the new current ROM on next switch
1773#endif
1774
1775 LOG_INFO("EditorManager", "Session %zu closed via EventBus", index);
1776}
1777
1779 auto* session =
1780 static_cast<RomSession*>(session_coordinator_->GetSession(index));
1781 ResetAssetState(session);
1782
1783 // Update ContentRegistry when ROM is loaded (if this is the active session)
1784 if (rom && session_coordinator_ &&
1785 index == session_coordinator_->GetActiveSessionIndex()) {
1787 // Also update GameData from the session
1788 if (session) {
1789 ContentRegistry::Context::SetGameData(&session->game_data);
1790 }
1792 }
1793
1794#ifdef YAZE_ENABLE_TESTING
1795 if (rom) {
1797 }
1798#endif
1799
1800 LOG_INFO("EditorManager", "ROM loaded in session %zu via EventBus", index);
1801}
1802
1804 using Action = UIActionRequestEvent::Action;
1805 switch (action) {
1806 case Action::kShowEmulator:
1807 if (ui_coordinator_) {
1808 ui_coordinator_->SetEmulatorVisible(true);
1809 }
1810 break;
1811
1812 case Action::kShowSettings:
1813 // Toggle Settings panel in sidebar
1815 right_drawer_manager_->ToggleDrawer(
1817 } else {
1819 }
1820 break;
1821
1822 case Action::kShowPanelBrowser:
1823 if (ui_coordinator_) {
1824 ui_coordinator_->ShowWindowBrowser();
1825 }
1826 break;
1827
1828 case Action::kShowSearch:
1829 if (ui_coordinator_) {
1830 ui_coordinator_->ShowGlobalSearch();
1831 }
1832 break;
1833
1834 case Action::kShowShortcuts:
1835 // Shortcut configuration is part of Settings
1837 break;
1838
1839 case Action::kShowCommandPalette:
1840 if (ui_coordinator_) {
1841 ui_coordinator_->ShowCommandPalette();
1842 }
1843 break;
1844
1845 case Action::kShowHelp:
1847 // Toggle Help panel in sidebar
1848 right_drawer_manager_->ToggleDrawer(
1850 } else if (popup_manager_) {
1851 // Fallback to "About" dialog if sidebar not available
1853 }
1854 break;
1855
1856 case Action::kShowAgentChatSidebar:
1858 right_drawer_manager_->OpenDrawer(
1860 } else {
1862 }
1863 break;
1864
1865 case Action::kShowAgentProposalsSidebar:
1867 right_drawer_manager_->OpenDrawer(
1869 }
1870 break;
1871
1872 case Action::kOpenRom: {
1873 auto status = LoadRom();
1874 if (!status.ok()) {
1876 std::string("Open failed: ") + std::string(status.message()),
1878 }
1879 } break;
1880
1881 case Action::kSaveRom:
1882 if (GetCurrentRom() && GetCurrentRom()->is_loaded()) {
1883 auto status = SaveRom();
1884 if (!absl::IsCancelled(status) && !status.ok()) {
1886 std::string("Save failed: ") + std::string(status.message()),
1888 }
1889 }
1890 break;
1891
1892 case Action::kUndo:
1893 if (auto* current_editor = GetCurrentEditor()) {
1894 auto status = current_editor->Undo();
1895 if (!status.ok()) {
1897 std::string("Undo failed: ") + std::string(status.message()),
1899 }
1900 }
1901 break;
1902
1903 case Action::kRedo:
1904 if (auto* current_editor = GetCurrentEditor()) {
1905 auto status = current_editor->Redo();
1906 if (!status.ok()) {
1908 std::string("Redo failed: ") + std::string(status.message()),
1910 }
1911 }
1912 break;
1913
1914 case Action::kResetLayout:
1916 break;
1917 }
1918}
1919
1921 auto& test_manager = test::TestManager::Get();
1922
1923#ifdef YAZE_ENABLE_TESTING
1924 // Register comprehensive test suites
1925 test_manager.RegisterTestSuite(
1926 std::make_unique<test::CoreSystemsTestSuite>());
1927 test_manager.RegisterTestSuite(std::make_unique<test::IntegratedTestSuite>());
1928 test_manager.RegisterTestSuite(
1929 std::make_unique<test::PerformanceTestSuite>());
1930 test_manager.RegisterTestSuite(std::make_unique<test::UITestSuite>());
1931 test_manager.RegisterTestSuite(
1932 std::make_unique<test::RomDependentTestSuite>());
1933
1934 // Register new E2E and ZSCustomOverworld test suites
1935 test_manager.RegisterTestSuite(std::make_unique<test::E2ETestSuite>());
1936 test_manager.RegisterTestSuite(
1937 std::make_unique<test::ZSCustomOverworldTestSuite>());
1938#endif
1939
1940 // Register Google Test suite if available
1941#ifdef YAZE_ENABLE_GTEST
1942 test_manager.RegisterTestSuite(std::make_unique<test::UnitTestSuite>());
1943#endif
1944
1945 // Register z3ed AI Agent test suites (requires gRPC)
1946#ifdef YAZE_WITH_GRPC
1948#endif
1949
1950 // Update resource monitoring to track Arena state
1951 test_manager.UpdateResourceStats();
1952}
1953
1955 const std::string& filename) {
1956 renderer_ = renderer;
1958 SeedOracleProjectInRecents();
1959
1960 // Inject the window manager into emulator and workspace_manager
1963
1964 // Point to a blank editor set when no ROM is loaded
1965 // current_editor_set_ = &blank_editor_set_;
1966
1967 if (!filename.empty()) {
1969 }
1970
1971 // Note: PopupManager is now initialized in constructor before
1972 // MenuOrchestrator This ensures all menu callbacks can safely call
1973 // popup_manager_.Show()
1974
1978
1979 // Apply sidebar state from settings AFTER registering callbacks
1980 // This triggers the callbacks but they should be safe now
1982 /*notify=*/false);
1985 /*notify=*/false);
1987 user_settings_.prefs().sidebar_panel_width, /*notify=*/false);
1990 /*notify=*/false);
1991 {
1992 const bool prefer_dashboard_only =
1994 startup_editor_hint_.empty() && startup_panel_hints_.empty();
1995 if (!prefer_dashboard_only) {
1996 const std::string category = GetPreferredStartupCategory(
1998 if (!category.empty()) {
1999 window_manager_.SetActiveCategory(category, /*notify=*/false);
2001 auto it = user_settings_.prefs().panel_visibility_state.find(category);
2002 if (it != user_settings_.prefs().panel_visibility_state.end()) {
2004 window_manager_.GetActiveSessionId(), it->second);
2005 }
2006 }
2007 }
2008 }
2009
2013 }
2014
2015 // Initialize testing system only when tests are enabled
2016#ifdef YAZE_ENABLE_TESTING
2018#endif
2019
2020 // TestManager will be updated when ROMs are loaded via SetCurrentRom calls
2021
2023}
2024
2026 // Register emulator panels early (emulator Initialize might not be called).
2027 const std::vector<WindowDefinition> panel_definitions = {
2028 {.id = "emulator.cpu_debugger",
2029 .display_name = "CPU Debugger",
2030 .icon = ICON_MD_BUG_REPORT,
2031 .category = "Emulator",
2032 .priority = 10},
2033 {.id = "emulator.ppu_viewer",
2034 .display_name = "PPU Viewer",
2036 .category = "Emulator",
2037 .priority = 20},
2038 {.id = "emulator.memory_viewer",
2039 .display_name = "Memory Viewer",
2040 .icon = ICON_MD_MEMORY,
2041 .category = "Emulator",
2042 .priority = 30},
2043 {.id = "emulator.breakpoints",
2044 .display_name = "Breakpoints",
2045 .icon = ICON_MD_STOP,
2046 .category = "Emulator",
2047 .priority = 40},
2048 {.id = "emulator.performance",
2049 .display_name = "Performance",
2050 .icon = ICON_MD_SPEED,
2051 .category = "Emulator",
2052 .priority = 50},
2053 {.id = "emulator.ai_agent",
2054 .display_name = "AI Agent",
2055 .icon = ICON_MD_SMART_TOY,
2056 .category = "Emulator",
2057 .priority = 60},
2058 {.id = "emulator.save_states",
2059 .display_name = "Save States",
2060 .icon = ICON_MD_SAVE,
2061 .category = "Emulator",
2062 .priority = 70},
2063 {.id = "emulator.keyboard_config",
2064 .display_name = "Keyboard Config",
2065 .icon = ICON_MD_KEYBOARD,
2066 .category = "Emulator",
2067 .priority = 80},
2068 {.id = "emulator.virtual_controller",
2069 .display_name = "Virtual Controller",
2070 .icon = ICON_MD_SPORTS_ESPORTS,
2071 .category = "Emulator",
2072 .priority = 85},
2073 {.id = "emulator.apu_debugger",
2074 .display_name = "APU Debugger",
2075 .icon = ICON_MD_AUDIOTRACK,
2076 .category = "Emulator",
2077 .priority = 90},
2078 {.id = "emulator.audio_mixer",
2079 .display_name = "Audio Mixer",
2080 .icon = ICON_MD_AUDIO_FILE,
2081 .category = "Emulator",
2082 .priority = 100},
2083 {.id = "memory.hex_editor",
2084 .display_name = "Hex Editor",
2085 .icon = ICON_MD_MEMORY,
2086 .category = "Memory",
2087 .window_title = ICON_MD_MEMORY " Hex Editor",
2088 .priority = 10,
2089 .legacy_ids = {"Memory Editor"}},
2090 };
2091
2092 if (window_host_) {
2093 window_host_->RegisterPanels(panel_definitions);
2094 return;
2095 }
2096
2097 for (const auto& definition : panel_definitions) {
2098 WindowDescriptor descriptor;
2099 descriptor.card_id = definition.id;
2100 descriptor.display_name = definition.display_name;
2101 descriptor.window_title = definition.window_title;
2102 descriptor.icon = definition.icon;
2103 descriptor.category = definition.category;
2104 descriptor.shortcut_hint = definition.shortcut_hint;
2105 descriptor.priority = definition.priority;
2106 descriptor.scope = definition.scope;
2107 descriptor.window_lifecycle = definition.window_lifecycle;
2108 descriptor.context_scope = definition.context_scope;
2109 descriptor.visibility_flag = definition.visibility_flag;
2110 descriptor.on_show = definition.on_show;
2111 descriptor.on_hide = definition.on_hide;
2112
2113 for (const auto& legacy_id : definition.legacy_ids) {
2114 window_manager_.RegisterPanelAlias(legacy_id, definition.id);
2115 }
2116
2117 window_manager_.RegisterWindow(descriptor);
2118 if (definition.visible_by_default) {
2119 window_manager_.OpenWindow(definition.id);
2120 }
2121 }
2122}
2123
2125 // Initialize project file editor
2127
2128 // Initialize agent UI (no-op when agent UI is disabled)
2132
2133 // Note: Unified gRPC Server is started from Application::Initialize()
2134 // after gRPC infrastructure is properly set up
2135
2136 // Load critical user settings first
2138 if (!status_.ok()) {
2139 LOG_WARN("EditorManager", "Failed to load user settings: %s",
2140 status_.ToString().c_str());
2141 }
2142
2143 // Wire theme persistence. Any successful theme application (selector click,
2144 // command-palette switch, programmatic call) stamps the name into prefs and
2145 // rides the existing debounced-save path. Decouples ThemeManager (singleton
2146 // in app/gui) from UserSettings (editor-layer) — ThemeManager holds only a
2147 // std::function.
2149 [this](const std::string& theme_name) {
2150 user_settings_.prefs().last_theme_name = theme_name;
2151 settings_dirty_ = true;
2153 });
2155 [this](const std::string& locale) {
2156 user_settings_.prefs().language_locale = locale;
2157 settings_dirty_ = true;
2159 });
2160
2161 auto& prefs = user_settings_.prefs();
2162 i18n::LanguageManager::Get().SetLanguage(prefs.language_locale);
2163
2164 // Apply the persisted font selection (defaults to index 0 = Karla).
2165 // Fonts were loaded by the window backend before EditorManager init, so
2166 // ImGui::GetIO().Fonts->Fonts is fully populated here.
2167 ::yaze::SetActiveFontIndex(prefs.font_family_index);
2168
2169 prefs.switch_motion_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
2171 prefs.reduced_motion,
2172 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
2173
2175
2178 right_drawer_manager_->ResetDrawerWidths();
2180 right_drawer_manager_->SerializeDrawerWidths();
2181 } else {
2182 right_drawer_manager_->RestoreDrawerWidths(
2184 }
2185 right_drawer_manager_->SetDrawerWidthChangedCallback(
2186 [this](RightDrawerManager::DrawerType, float) {
2187 if (!right_drawer_manager_) {
2188 return;
2189 }
2190 user_settings_.prefs().right_panel_widths =
2191 right_drawer_manager_->SerializeDrawerWidths();
2192 settings_dirty_ = true;
2194 });
2195 }
2197 // Apply sprite naming preference globally.
2200
2202
2203 // Apply font scale after loading (only if ImGui context exists)
2204 if (ImGui::GetCurrentContext() != nullptr) {
2205 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
2206 } else {
2207 LOG_WARN("EditorManager",
2208 "ImGui context not available; skipping FontGlobalScale update");
2209 }
2210
2211 // Restore the user's last theme. Empty (first run) or "Custom Accent"
2212 // (transient generated themes without a matching discovered preset) both
2213 // skip restoration and keep the built-in default (Classic YAZE / YAZE Tre).
2214 const auto& last_theme = user_settings_.prefs().last_theme_name;
2215 if (!last_theme.empty() && last_theme != "Custom Accent") {
2216 gui::ThemeManager::Get().ApplyTheme(last_theme);
2217 }
2218
2219 // Initialize WASM control and session APIs for browser/agent integration
2220#ifdef __EMSCRIPTEN__
2223 LOG_INFO("EditorManager", "WASM Control and Session APIs initialized");
2224#endif
2225}
2226
2232
2234 // Initialize ROM load options dialog callbacks
2236 [this](const RomLoadOptionsDialog::LoadOptions& options) {
2237 // Apply feature flags from dialog
2238 auto& flags = core::FeatureFlags::get();
2239 flags.overworld.kSaveOverworldMaps = options.save_overworld_maps;
2240 flags.overworld.kSaveOverworldEntrances =
2242 flags.overworld.kSaveOverworldExits = options.save_overworld_exits;
2243 flags.overworld.kSaveOverworldItems = options.save_overworld_items;
2244 flags.overworld.kLoadCustomOverworld = options.enable_custom_overworld;
2245 flags.kSaveDungeonMaps = options.save_dungeon_maps;
2246 flags.kSaveAllPalettes = options.save_all_palettes;
2247 flags.kSaveGfxGroups = options.save_gfx_groups;
2249 if (auto* session = session_coordinator_->GetActiveRomSession()) {
2250 session->feature_flags = flags;
2251 }
2252 }
2253
2254 // Create project if requested
2255 if (options.create_project && !options.project_name.empty()) {
2256 auto status =
2258 if (!status.ok()) {
2259 toast_manager_.Show(absl::StrFormat("Failed to create project: %s",
2260 status.message()),
2262 } else {
2263 toast_manager_.Show("Project created: " + options.project_name,
2265 }
2266 }
2267
2268 // Close dialog and show editor selection
2269 show_rom_load_options_ = false;
2270 if (ui_coordinator_) {
2271 ui_coordinator_->SetEditorSelectionVisible(true);
2272 }
2273
2274 LOG_INFO("EditorManager", "ROM load options applied: preset=%s",
2275 options.selected_preset.c_str());
2276 });
2277}
2278
2280 // Initialize welcome screen callbacks
2282
2284 [this]() { status_ = CreateNewProject(); });
2285
2287 [this](const std::string& template_name) {
2288 status_ = CreateNewProject(template_name);
2289 });
2290
2291 welcome_screen_.SetOpenProjectCallback([this](const std::string& filepath) {
2292 status_ = OpenRomOrProject(filepath);
2293 if (status_.ok() && ui_coordinator_) {
2294 ui_coordinator_->SetWelcomeScreenVisible(false);
2295 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2296 }
2297 });
2298
2300#ifdef YAZE_BUILD_AGENT_UI
2301 ShowAIAgent();
2302#endif
2303 });
2304
2307 window_manager_.OpenWindow("graphics.prototype_viewer");
2308 if (ui_coordinator_) {
2309 ui_coordinator_->SetWelcomeScreenVisible(false);
2310 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2311 }
2312 });
2313
2316 window_manager_.OpenWindow("assembly.code_editor");
2317 if (ui_coordinator_) {
2318 ui_coordinator_->SetWelcomeScreenVisible(false);
2319 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2320 }
2321 });
2322
2324 status_ = OpenProject();
2325 if (status_.ok() && ui_coordinator_) {
2326 ui_coordinator_->SetWelcomeScreenVisible(false);
2327 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2328 } else if (!status_.ok()) {
2330 absl::StrFormat("Failed to open project: %s", status_.message()),
2332 }
2333 });
2334
2336 [this]() { ShowProjectManagement(); });
2337
2339 if (current_project_.filepath.empty()) {
2340 toast_manager_.Show("No project file to edit", ToastType::kInfo);
2341 return;
2342 }
2344 });
2345
2346 // Apply welcome screen preference
2348 ui_coordinator_->SetWelcomeScreenVisible(false);
2349 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2350 }
2351}
2352
2354 // Utility callbacks removed - now handled via EventBus
2355
2357 [this](bool visible, bool expanded) {
2358 user_settings_.prefs().sidebar_visible = visible;
2359 user_settings_.prefs().sidebar_panel_expanded = expanded;
2360 settings_dirty_ = true;
2362 });
2365 settings_dirty_ = true;
2367 });
2369 [this](float width) {
2371 settings_dirty_ = true;
2373 });
2374
2376 const std::string& category) {
2377 if (category.empty() ||
2379 return;
2380 }
2383
2384 const auto& prefs = user_settings_.prefs();
2385 auto it = prefs.panel_visibility_state.find(category);
2386 if (it != prefs.panel_visibility_state.end()) {
2388 window_manager_.GetActiveSessionId(), it->second);
2389 } else {
2390 // No saved visibility state for this category yet.
2391 //
2392 // Apply LayoutPresets defaults only when this category has *no*
2393 // visible panels (editors may have already shown their own defaults,
2394 // e.g. Dungeon Workbench).
2395 const size_t session_id = window_manager_.GetActiveSessionId();
2396 bool any_visible = false;
2397 for (const auto& desc :
2398 window_manager_.GetWindowsInCategory(session_id, category)) {
2399 if (desc.visibility_flag && *desc.visibility_flag) {
2400 any_visible = true;
2401 break;
2402 }
2403 }
2404
2405 if (!any_visible) {
2406 const EditorType type =
2408 for (const auto& window_id : LayoutPresets::GetDefaultWindows(type)) {
2409 window_manager_.OpenWindow(session_id, window_id);
2410 }
2411 }
2412 }
2413
2414 settings_dirty_ = true;
2416 });
2417
2419 [this](const std::string& category) -> Editor* {
2420 Editor* editor = ResolveEditorForCategory(category);
2422 return editor;
2423 });
2424
2426 [this](const std::string& category) {
2428 if (type != EditorType::kSettings && type != EditorType::kUnknown) {
2429 SwitchToEditor(type, true);
2430 }
2431 });
2432
2434 [this](const std::string& category) {
2435 if (ui_coordinator_) {
2436 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
2437 }
2438
2439 if (category == "Agent") {
2440#ifdef YAZE_BUILD_AGENT_UI
2441 ShowAIAgent();
2442#endif
2443 return;
2444 }
2445
2447 if (type != EditorType::kSettings) {
2448 SwitchToEditor(type, true);
2449 }
2450 });
2451
2453
2455 [this](const std::string& category, const std::string& path) {
2456 if (category == "Assembly") {
2457 if (auto* editor_set = GetCurrentEditorSet()) {
2458 editor_set->ChangeActiveAssemblyFile(path);
2460 }
2461 }
2462 });
2463}
2464
2466 ShortcutDependencies shortcut_deps;
2467 shortcut_deps.editor_manager = this;
2468 shortcut_deps.editor_registry = &editor_registry_;
2469 shortcut_deps.menu_orchestrator = menu_orchestrator_.get();
2470 shortcut_deps.rom_file_manager = &rom_file_manager_;
2471 shortcut_deps.project_manager = &project_manager_;
2472 shortcut_deps.session_coordinator = session_coordinator_.get();
2473 shortcut_deps.ui_coordinator = ui_coordinator_.get();
2474 shortcut_deps.workspace_manager = &workspace_manager_;
2475 shortcut_deps.popup_manager = popup_manager_.get();
2476 shortcut_deps.toast_manager = &toast_manager_;
2477 shortcut_deps.window_manager = &window_manager_;
2478 shortcut_deps.user_settings = &user_settings_;
2479
2483}
2484
2486 const std::string& editor_name, const std::string& panels_str) {
2487 const bool has_editor = !editor_name.empty();
2488 const bool has_panels = !panels_str.empty();
2489
2490 if (!has_editor && !has_panels) {
2491 return;
2492 }
2493
2494 LOG_INFO("EditorManager",
2495 "Processing startup flags: editor='%s', panels='%s'",
2496 editor_name.c_str(), panels_str.c_str());
2497
2498 std::optional<EditorType> editor_type_to_open =
2499 has_editor ? ParseEditorTypeFromString(editor_name) : std::nullopt;
2500 if (has_editor && !editor_type_to_open.has_value()) {
2501 LOG_WARN("EditorManager", "Unknown editor specified via flag: %s",
2502 editor_name.c_str());
2503 } else if (editor_type_to_open.has_value()) {
2504 // Use EditorActivator to ensure layouts and default panels are initialized
2505 SwitchToEditor(*editor_type_to_open, true, /*from_dialog=*/true);
2506 }
2507
2508 // Open windows via WorkspaceWindowManager - works for any editor type
2509 if (!has_panels) {
2510 return;
2511 }
2512
2513 const size_t session_id = GetCurrentSessionId();
2514 std::string last_known_category = window_manager_.GetActiveCategory();
2515 bool applied_category_from_panel = false;
2516
2517 for (absl::string_view token :
2518 absl::StrSplit(panels_str, ',', absl::SkipWhitespace())) {
2519 if (token.empty()) {
2520 continue;
2521 }
2522 std::string panel_name = std::string(absl::StripAsciiWhitespace(token));
2523 LOG_DEBUG("EditorManager", "Attempting to open panel: '%s'",
2524 panel_name.c_str());
2525
2526 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2527 if (lower_name == "welcome" || lower_name == "welcome_screen") {
2528 if (ui_coordinator_) {
2529 ui_coordinator_->SetWelcomeScreenBehavior(StartupVisibility::kShow);
2530 }
2531 continue;
2532 }
2533 if (lower_name == "dashboard" || lower_name == "dashboard.main" ||
2534 lower_name == "editor_selection") {
2535 if (dashboard_panel_) {
2536 dashboard_panel_->Show();
2537 }
2538 if (ui_coordinator_) {
2539 ui_coordinator_->SetDashboardBehavior(StartupVisibility::kShow);
2540 }
2543 /*notify=*/false);
2544 continue;
2545 }
2546
2547 // Special case: "Room <id>" opens a dungeon room
2548 if (absl::StartsWith(panel_name, "Room ")) {
2549 if (auto* editor_set = GetCurrentEditorSet()) {
2550 try {
2551 int room_id = std::stoi(panel_name.substr(5));
2553 JumpToRoomRequestEvent::Create(room_id, session_id));
2554 } catch (const std::exception& e) {
2555 LOG_WARN("EditorManager", "Invalid room ID format: %s",
2556 panel_name.c_str());
2557 }
2558 }
2559 continue;
2560 }
2561
2562 std::optional<std::string> resolved_panel;
2563 if (window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2564 resolved_panel = panel_name;
2565 } else {
2566 for (const auto& [prefixed_id, descriptor] :
2568 const std::string base_id = StripSessionPrefix(prefixed_id);
2569 const std::string card_lower = absl::AsciiStrToLower(base_id);
2570 const std::string display_lower =
2571 absl::AsciiStrToLower(descriptor.display_name);
2572
2573 if (card_lower == lower_name || display_lower == lower_name) {
2574 resolved_panel = base_id;
2575 break;
2576 }
2577 }
2578 }
2579
2580 if (!resolved_panel.has_value()) {
2581 LOG_WARN("EditorManager",
2582 "Unknown panel '%s' from --open_panels (known count: %zu)",
2583 panel_name.c_str(),
2585 continue;
2586 }
2587
2588 if (window_manager_.OpenWindow(session_id, *resolved_panel)) {
2589 const auto* descriptor =
2590 window_manager_.GetWindowDescriptor(session_id, *resolved_panel);
2591 if (descriptor != nullptr) {
2592 const EditorType type =
2593 EditorRegistry::GetEditorTypeFromCategory(descriptor->category);
2594 const auto ensure_status = EnsureEditorAssetsLoaded(type);
2595 if (!ensure_status.ok()) {
2596 LOG_WARN("EditorManager", "Failed to load assets for panel '%s': %s",
2597 resolved_panel->c_str(), ensure_status.message().data());
2598 }
2599 }
2600 if (descriptor && !applied_category_from_panel &&
2601 descriptor->category != WorkspaceWindowManager::kDashboardCategory) {
2602 window_manager_.SetActiveCategory(descriptor->category);
2603 applied_category_from_panel = true;
2604 } else if (!applied_category_from_panel && descriptor &&
2605 descriptor->category.empty() && !last_known_category.empty()) {
2606 window_manager_.SetActiveCategory(last_known_category);
2607 }
2608 } else {
2609 LOG_WARN("EditorManager", "Failed to show panel '%s'",
2610 resolved_panel->c_str());
2611 }
2612 }
2613}
2614
2621
2629
2631 constexpr int kTargetRevision =
2633 if (!user_settings_.ApplyPanelLayoutDefaultsRevision(kTargetRevision)) {
2634 return;
2635 }
2636
2638 settings_dirty_ = true;
2640
2641 LOG_INFO("EditorManager",
2642 "Applied panel layout defaults migration revision %d",
2643 kTargetRevision);
2644}
2645
2647 const std::string& saved_category,
2648 const std::vector<std::string>& available_categories) const {
2649 // If saved category is valid and not Emulator, use it directly
2650 if (!saved_category.empty() && saved_category != "Emulator") {
2651 // Validate it exists in available_categories if the list is provided
2652 if (available_categories.empty()) {
2653 return saved_category;
2654 }
2655 for (const auto& cat : available_categories) {
2656 if (cat == saved_category)
2657 return saved_category;
2658 }
2659 }
2660 // Pick first non-Emulator category from available list
2661 for (const auto& cat : available_categories) {
2662 if (cat != "Emulator")
2663 return cat;
2664 }
2665 return {};
2666}
2667
2671
2673 if (ui_coordinator_) {
2674 ui_coordinator_->SetWelcomeScreenBehavior(welcome_mode_override_);
2675 ui_coordinator_->SetDashboardBehavior(dashboard_mode_override_);
2676 }
2677
2679 const bool sidebar_visible =
2681 window_manager_.SetSidebarVisible(sidebar_visible, /*notify=*/false);
2682 if (ui_coordinator_) {
2683 ui_coordinator_->SetPanelSidebarVisible(sidebar_visible);
2684 }
2685 }
2686
2687 // Force sidebar panel to collapse if Welcome Screen or Dashboard is explicitly shown
2688 // This prevents visual overlap/clutter on startup
2691 window_manager_.SetSidebarExpanded(false, /*notify=*/false);
2692 }
2693
2694 if (dashboard_panel_) {
2696 dashboard_panel_->Hide();
2698 dashboard_panel_->Show();
2699 }
2700 }
2701}
2702
2704 ApplyStartupVisibility(config);
2705 // Handle startup editor and panels
2706 std::string panels_str;
2707 for (size_t i = 0; i < config.open_panels.size(); ++i) {
2708 if (i > 0)
2709 panels_str += ",";
2710 panels_str += config.open_panels[i];
2711 }
2712 OpenEditorAndPanelsFromFlags(config.startup_editor, panels_str);
2713
2714 // Handle jump targets
2715 if (config.jump_to_room >= 0) {
2718 }
2719 if (config.jump_to_map >= 0) {
2723 } else {
2724 LOG_WARN("EditorManager",
2725 "Ignoring invalid startup overworld map target %d",
2726 config.jump_to_map);
2727 toast_manager_.Show("Ignored invalid startup overworld map: " +
2728 std::to_string(config.jump_to_map),
2730 }
2731 }
2732}
2733
2734absl::Status EditorManager::LoadAssetsForMode(uint64_t loading_handle) {
2735 switch (asset_load_mode_) {
2737 return LoadAssetsLazy(loading_handle);
2740 default:
2741 return LoadAssets(loading_handle);
2742 }
2743}
2744
2746 if (!session) {
2747 return;
2748 }
2749 session->game_data_loaded = false;
2750 session->editor_initialized.fill(false);
2751 session->editor_assets_loaded.fill(false);
2752}
2753
2755 EditorType type) {
2756 if (!session) {
2757 return;
2758 }
2759 const size_t index = EditorTypeIndex(type);
2760 if (index < session->editor_initialized.size()) {
2761 session->editor_initialized[index] = true;
2762 }
2763}
2764
2766 if (!session) {
2767 return;
2768 }
2769 const size_t index = EditorTypeIndex(type);
2770 if (index < session->editor_assets_loaded.size()) {
2771 session->editor_assets_loaded[index] = true;
2772 }
2773}
2774
2776 switch (type) {
2784 return true;
2785 default:
2786 return false;
2787 }
2788}
2789
2793
2795 EditorSet* editor_set) const {
2796 std::unordered_set<EditorType> types;
2797
2798 auto add_type = [&types](EditorType type) {
2799 switch (type) {
2803 case EditorType::kAgent:
2804 return;
2805 default:
2806 types.insert(type);
2807 return;
2808 }
2809 };
2810
2811 auto add_category = [&](const std::string& category) {
2812 if (category.empty() ||
2814 return;
2815 }
2817 };
2818
2819 const size_t session_id = GetCurrentSessionId();
2820 bool used_startup_hints = false;
2821
2822 if (!startup_editor_hint_.empty()) {
2823 if (auto startup_type = ParseEditorTypeFromString(startup_editor_hint_)) {
2824 add_type(*startup_type);
2825 used_startup_hints = true;
2826 }
2827 }
2828
2829 for (const auto& panel_name : startup_panel_hints_) {
2830 if (panel_name.empty()) {
2831 continue;
2832 }
2833
2834 if (const auto* descriptor =
2835 window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2836 add_category(descriptor->category);
2837 used_startup_hints = true;
2838 continue;
2839 }
2840
2841 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2842 for (const auto& [prefixed_id, descriptor] :
2844 const std::string base_id = StripSessionPrefix(prefixed_id);
2845 const std::string card_lower = absl::AsciiStrToLower(base_id);
2846 const std::string display_lower =
2847 absl::AsciiStrToLower(descriptor.display_name);
2848 if (card_lower == lower_name || display_lower == lower_name) {
2849 add_category(descriptor.category);
2850 used_startup_hints = true;
2851 break;
2852 }
2853 }
2854 }
2855
2856 if (used_startup_hints) {
2857 return std::vector<EditorType>(types.begin(), types.end());
2858 }
2859
2861 return {};
2862 }
2863
2864 if (editor_set) {
2865 for (auto* editor : editor_set->active_editors_) {
2866 if (editor != nullptr && *editor->active()) {
2867 add_type(editor->type());
2868 }
2869 }
2870 }
2871
2872 if (current_editor_ != nullptr) {
2873 add_type(current_editor_->type());
2874 }
2875
2876 add_category(window_manager_.GetActiveCategory());
2877
2878 for (const auto& window_id :
2880 if (const auto* descriptor =
2881 window_manager_.GetWindowDescriptor(session_id, window_id)) {
2882 add_category(descriptor->category);
2883 }
2884 }
2885
2886 return std::vector<EditorType>(types.begin(), types.end());
2887}
2888
2890 EditorSet* editor_set) const {
2891 return editor_set ? editor_set->GetEditor(type) : nullptr;
2892}
2893
2895 if (category.empty() ||
2897 return nullptr;
2898 }
2899
2900 auto* editor_set = GetCurrentEditorSet();
2901 if (!editor_set) {
2902 return nullptr;
2903 }
2904
2906 switch (type) {
2907 case EditorType::kAgent:
2908#ifdef YAZE_BUILD_AGENT_UI
2909 return agent_ui_.GetAgentEditor();
2910#else
2911 return nullptr;
2912#endif
2916 return nullptr;
2917 default:
2918 return GetEditorByType(type, editor_set);
2919 }
2920}
2921
2922void EditorManager::SyncEditorContextForCategory(const std::string& category) {
2923 if (Editor* resolved = ResolveEditorForCategory(category)) {
2924 SetCurrentEditor(resolved);
2925 } else if (!category.empty() &&
2927 LOG_DEBUG("EditorManager", "No editor context available for category '%s'",
2928 category.c_str());
2929 }
2930}
2931
2933 EditorSet* editor_set,
2934 Rom* rom) {
2935 if (!editor_set) {
2936 return absl::FailedPreconditionError("No editor set available");
2937 }
2938
2939 auto* editor = GetEditorByType(type, editor_set);
2940 if (!editor) {
2941 return absl::OkStatus();
2942 }
2943 editor->Initialize();
2944 return absl::OkStatus();
2945}
2946
2948 auto* session = session_coordinator_
2949 ? session_coordinator_->GetActiveRomSession()
2950 : nullptr;
2951 if (!session) {
2952 return absl::FailedPreconditionError("No active session");
2953 }
2954 if (session->game_data_loaded) {
2955 return absl::OkStatus();
2956 }
2957 if (!session->rom.is_loaded()) {
2958 return absl::FailedPreconditionError("ROM not loaded");
2959 }
2960
2961 RETURN_IF_ERROR(zelda3::LoadGameData(session->rom, session->game_data));
2962 *gfx::Arena::Get().mutable_gfx_sheets() = session->game_data.gfx_bitmaps;
2963
2964 auto* game_data = &session->game_data;
2965 auto* editor_set = &session->editors;
2966 editor_set->SetGameData(game_data);
2968
2970 session->game_data_loaded = true;
2971
2972 return absl::OkStatus();
2973}
2974
2976 if (type == EditorType::kUnknown) {
2977 return absl::OkStatus();
2978 }
2979
2980 auto* session = session_coordinator_
2981 ? session_coordinator_->GetActiveRomSession()
2982 : nullptr;
2983 if (!session) {
2984 return absl::OkStatus();
2985 }
2986
2987 if (!session->rom.is_loaded()) {
2989 return absl::OkStatus();
2990 }
2991 const size_t index = EditorTypeIndex(type);
2992 if (index >= session->editor_initialized.size()) {
2993 return absl::InvalidArgumentError("Invalid editor type");
2994 }
2995 if (EditorInitRequiresGameData(type)) {
2996 return absl::OkStatus();
2997 }
2998 if (!session->editor_initialized[index]) {
3000 InitializeEditorForType(type, &session->editors, &session->rom));
3001 MarkEditorInitialized(session, type);
3002 }
3003 return absl::OkStatus();
3004 }
3005
3006 const size_t index = EditorTypeIndex(type);
3007 if (index >= session->editor_initialized.size()) {
3008 return absl::InvalidArgumentError("Invalid editor type");
3009 }
3010
3011 if (EditorInitRequiresGameData(type)) {
3013 }
3014
3015 if (!session->editor_initialized[index]) {
3017 InitializeEditorForType(type, &session->editors, &session->rom));
3018 MarkEditorInitialized(session, type);
3019 }
3020
3021 if (EditorRequiresGameData(type)) {
3023 }
3024
3025 if (!session->editor_assets_loaded[index]) {
3026 auto* editor = GetEditorByType(type, &session->editors);
3027 if (editor) {
3028 RETURN_IF_ERROR(editor->Load());
3029 }
3030 MarkEditorLoaded(session, type);
3031 }
3032
3033 return absl::OkStatus();
3034}
3035
3053 ProcessInput();
3055 DrawInterface();
3056
3057 return status_;
3058}
3059
3061 // Space/focus transitions can leave mid-animation surfaces ghosted.
3062 // Reset transient animation state to a stable endpoint.
3065 right_drawer_manager_->OnHostVisibilityChanged(visible);
3066 }
3067}
3068
3070 // Update timing manager for accurate delta time across the application
3072
3073 // Execute keyboard shortcuts (registered via ShortcutConfigurator)
3075}
3076
3078 status_ = absl::OkStatus();
3080 if (ui_coordinator_) {
3081 ui_coordinator_->RefreshWorkflowActions();
3082 }
3083 // Check for layout rebuild requests and execute if needed (delegated to LayoutCoordinator)
3084 bool is_emulator_visible =
3085 ui_coordinator_ && ui_coordinator_->IsEmulatorVisible();
3086 EditorType current_type =
3088 layout_coordinator_.ProcessLayoutRebuild(current_type, is_emulator_visible);
3089
3090 // Periodic user settings auto-save
3091 if (settings_dirty_) {
3092 const float elapsed = TimingManager::Get().GetElapsedTime();
3093 if (elapsed - settings_dirty_timestamp_ >= 1.0f) {
3094 auto save_status = user_settings_.Save();
3095 if (!save_status.ok()) {
3096 LOG_WARN("EditorManager", "Failed to save user settings: %s",
3097 save_status.ToString().c_str());
3098 }
3099 settings_dirty_ = false;
3100 }
3101 }
3102
3103 // Update agent editor dashboard
3105
3106 // Ensure TestManager always has the current ROM
3107 static Rom* last_test_rom = nullptr;
3108 auto* current_rom = GetCurrentRom();
3109 if (last_test_rom != current_rom) {
3110 LOG_DEBUG(
3111 "EditorManager",
3112 "EditorManager::Update - ROM changed, updating TestManager: %p -> %p",
3113 (void*)last_test_rom, (void*)current_rom);
3115 last_test_rom = current_rom;
3116 }
3117
3118 // Autosave uses the same guarded serialization, validation, backup, and
3119 // atomic commit pipeline as an explicit save. Writing Rom::SaveToFile here
3120 // would skip editor model serialization and could persist a partially
3121 // failed buffer while clearing its dirty bit.
3122 const bool current_session_has_unsaved_work =
3124 const auto* current_session =
3125 session_coordinator_ ? session_coordinator_->GetActiveRomSession()
3126 : nullptr;
3127 const bool backup_restore_requires_explicit_save =
3128 current_session != nullptr && current_session->backup_restore_pending;
3130 current_session_has_unsaved_work &&
3131 !backup_restore_requires_explicit_save) {
3132 autosave_timer_ += ImGui::GetIO().DeltaTime;
3134 autosave_timer_ = 0.0f;
3135 auto st = AutosaveActiveSession();
3136 if (st.ok()) {
3137 toast_manager_.Show("Autosave completed", editor::ToastType::kSuccess);
3138 } else if (absl::IsCancelled(st)) {
3139 toast_manager_.Show("Autosave paused: confirmation required",
3141 } else {
3143 absl::StrFormat("Autosave failed: %s", st.message()),
3145 }
3146 }
3147 } else {
3148 autosave_timer_ = 0.0f;
3149 }
3150
3151 // Update ROM context for agent UI
3152 if (current_rom && current_rom->is_loaded()) {
3153 agent_ui_.SetRomContext(current_rom);
3155 if (auto* editor_set = GetCurrentEditorSet()) {
3156 agent_ui_.SetAsarWrapperContext(editor_set->GetAsarWrapper());
3157 // Backend-agnostic symbol feed. Works for ASAR and z3dk alike; tools
3158 // prefer this pointer over reaching through the wrapper.
3160 &editor_set->GetAssemblySymbols());
3161 }
3162 }
3163
3164 // Delegate session updates to SessionCoordinator
3166 session_coordinator_->UpdateSessions();
3167 }
3168}
3169
3171 if (!active_project_build_) {
3172 return;
3173 }
3174
3175 const auto snapshot = active_project_build_->GetSnapshot();
3176 UpdateBuildWorkflowStatus(
3179 snapshot.running
3180 ? "Build running"
3181 : (snapshot.status.ok() ? "Build succeeded" : "Build failed"),
3182 snapshot.output_tail.empty()
3183 ? (snapshot.running
3184 ? std::string(
3185 "Running the configured project build command")
3186 : std::string(snapshot.status.message()))
3187 : snapshot.output_tail,
3188 snapshot.running
3190 : (snapshot.status.ok() ? ProjectWorkflowState::kSuccess
3192 snapshot.output_tail, snapshot.running));
3194 project_management_panel_->SetBuildLogOutput(snapshot.output);
3195 }
3197
3198 if (snapshot.running || active_project_build_reported_) {
3199 return;
3200 }
3201
3203 if (!snapshot.status.ok()) {
3204 AppendWorkflowHistoryEntry(
3205 "Build",
3206 MakeBuildStatus("Build failed", std::string(snapshot.status.message()),
3207 ProjectWorkflowState::kFailure, snapshot.output_tail,
3208 false),
3209 snapshot.output);
3211 absl::StrFormat("Build failed: %s", snapshot.status.message()),
3212 snapshot.status.code() == absl::StatusCode::kCancelled
3215 return;
3216 }
3217
3218 AppendWorkflowHistoryEntry(
3219 "Build",
3221 "Build succeeded",
3222 snapshot.output_tail.empty() ? std::string("Project build completed")
3223 : snapshot.output_tail,
3224 ProjectWorkflowState::kSuccess, snapshot.output_tail, false),
3225 snapshot.output);
3226 toast_manager_.Show(snapshot.output_tail.empty()
3227 ? "Project build completed"
3228 : absl::StrFormat("Project build completed: %s",
3229 snapshot.output_tail),
3231}
3232
3234
3235 // Draw editor selection dialog (managed by UICoordinator)
3236 if (ui_coordinator_ && ui_coordinator_->IsEditorSelectionVisible()) {
3237 dashboard_panel_->Show();
3238 dashboard_panel_->Draw();
3239 if (!dashboard_panel_->IsVisible()) {
3240 ui_coordinator_->SetEditorSelectionVisible(false);
3241 }
3242 }
3243
3244 // Draw ROM load options dialog (ZSCustomOverworld, feature flags, project)
3247 }
3248
3249 // Draw window browser (managed by UICoordinator)
3250 if (ui_coordinator_ && ui_coordinator_->IsWindowBrowserVisible()) {
3251 bool show = true;
3252 if (activity_bar_) {
3253 activity_bar_->DrawWindowBrowser(GetCurrentSessionId(), &show);
3254 }
3255 if (!show) {
3256 ui_coordinator_->SetWindowBrowserVisible(false);
3257 }
3258 }
3259
3260 // Draw background grid effects
3261 if (ui_coordinator_) {
3262 ui_coordinator_->DrawBackground();
3263 }
3264
3265 // Draw UICoordinator UI components (Welcome Screen, Command Palette, etc.)
3266 if (ui_coordinator_) {
3267 ui_coordinator_->DrawAllUI();
3268 }
3269
3270 // Handle Welcome screen early-exit for rendering
3271 if (ui_coordinator_ && ui_coordinator_->ShouldShowWelcome()) {
3273 right_drawer_manager_->CloseDrawer();
3274 }
3275 return;
3276 }
3277
3280 RunEmulator();
3281
3282 // Draw sidebar
3283 if (ui_coordinator_ && ui_coordinator_->IsPanelSidebarVisible()) {
3284 auto all_categories = EditorRegistry::GetAllEditorCategories();
3285 std::unordered_set<std::string> active_editor_categories;
3286
3287 if (auto* current_editor_set = GetCurrentEditorSet()) {
3289 for (size_t session_idx = 0;
3290 session_idx < session_coordinator_->GetTotalSessionCount();
3291 ++session_idx) {
3292 auto* session = static_cast<RomSession*>(
3293 session_coordinator_->GetSession(session_idx));
3294 if (!session || !session->rom.is_loaded()) {
3295 continue;
3296 }
3297
3298 for (auto* editor : session->editors.active_editors_) {
3299 if (*editor->active() && IsPanelBasedEditor(editor->type())) {
3300 std::string category =
3301 EditorRegistry::GetEditorCategory(editor->type());
3302 active_editor_categories.insert(category);
3303 }
3304 }
3305 }
3306
3307 if (ui_coordinator_->IsEmulatorVisible()) {
3308 active_editor_categories.insert("Emulator");
3309 }
3310 }
3311 }
3312
3313 const bool prefer_dashboard_only =
3315 startup_editor_hint_.empty() && startup_panel_hints_.empty();
3316 std::string sidebar_category = window_manager_.GetActiveCategory();
3317 if (!prefer_dashboard_only && sidebar_category.empty() &&
3318 !all_categories.empty()) {
3319 sidebar_category = GetPreferredStartupCategory("", all_categories);
3320 if (!sidebar_category.empty()) {
3321 window_manager_.SetActiveCategory(sidebar_category, /*notify=*/false);
3322 SyncEditorContextForCategory(sidebar_category);
3324 sidebar_category);
3325 if (it != user_settings_.prefs().panel_visibility_state.end()) {
3327 window_manager_.GetActiveSessionId(), it->second);
3328 }
3329 }
3330 }
3331
3332 auto has_rom_callback = [this]() -> bool {
3333 auto* rom = GetCurrentRom();
3334 return rom && rom->is_loaded();
3335 };
3336
3337 if (activity_bar_ && ui_coordinator_->ShouldShowActivityBar()) {
3338 auto is_rom_dirty_callback = [this]() -> bool {
3339 auto* rom = GetCurrentRom();
3340 return rom && rom->is_loaded() && rom->dirty();
3341 };
3342 auto pending_dungeon_rooms_callback = [this]() -> int {
3343 if (auto* editor_set = GetCurrentEditorSet()) {
3344 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
3346 return dungeon_editor->PendingRoomCount();
3347 }
3348 }
3349 return 0;
3350 };
3351 activity_bar_->Render(GetCurrentSessionId(), sidebar_category,
3352 all_categories, active_editor_categories,
3353 has_rom_callback, is_rom_dirty_callback,
3354 pending_dungeon_rooms_callback);
3355 }
3356 }
3357
3358 // Draw right panel
3361 right_drawer_manager_->Draw();
3362 }
3363
3364 // Update and draw status bar
3368 session_coordinator_->GetActiveSessionCount());
3369 }
3370
3371 bool has_agent_info = false;
3372#if defined(YAZE_BUILD_AGENT_UI)
3373 if (auto* agent_editor = agent_ui_.GetAgentEditor()) {
3374 auto* chat = agent_editor->GetAgentChat();
3375 const auto* ctx = agent_ui_.GetContext();
3376 if (ctx) {
3377 const auto& config = ctx->agent_config();
3378 bool active = chat && *chat->active();
3379 status_bar_.SetAgentInfo(config.ai_provider, config.ai_model, active);
3380 has_agent_info = true;
3381 }
3382 }
3383#endif
3384 if (!has_agent_info) {
3386 }
3389 }
3390
3391 // Editor-aware context: let the active editor push its own mode/custom
3392 // segments for this frame without wiping event-driven cursor/selection/zoom
3393 // state that older editors still publish through the event bus.
3395 if (current_editor_) {
3397 }
3398
3399 if (auto* current_editor_set = GetCurrentEditorSet()) {
3400 if (auto* dungeon_editor = current_editor_set->GetEditorAs<DungeonEditorV2>(
3402 const int pending_rooms = dungeon_editor->PendingRoomCount();
3403 if (pending_rooms > 0) {
3404 StatusBarSegmentOptions pending_opts;
3405 pending_opts.tooltip = absl::StrFormat(
3406 "%d dungeon room%s still ha%s pending editor changes. Apply them "
3407 "to the ROM buffer before File > Save ROM if needed.",
3408 pending_rooms, pending_rooms == 1 ? "" : "s",
3409 pending_rooms == 1 ? "s" : "ve");
3411 "Dungeon", absl::StrFormat("%d pending", pending_rooms),
3412 std::move(pending_opts));
3413 }
3414 }
3415 }
3416
3417 status_bar_.Draw();
3418
3419 // Check if ROM is loaded before drawing panels
3420 auto* current_editor_set = GetCurrentEditorSet();
3421 if (!current_editor_set || !GetCurrentRom()) {
3422 if (window_manager_.GetActiveCategory() == "Agent") {
3424 }
3425 return;
3426 }
3427
3428 // Central workspace window drawing
3430
3431 if (ui_coordinator_ && ui_coordinator_->IsPerformanceDashboardVisible()) {
3433 }
3434
3435 // Draw SessionCoordinator UI components
3437 session_coordinator_->DrawSessionSwitcher();
3438 session_coordinator_->DrawSessionManager();
3439 session_coordinator_->DrawSessionRenameDialog();
3440 }
3441}
3442
3444 if (ImGui::BeginMenuBar()) {
3445 // Consistent button styling for sidebar toggle
3446 {
3447 const bool sidebar_visible = window_manager_.IsSidebarVisible();
3448 gui::StyleColorGuard sidebar_btn_guard(
3449 {{ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
3450 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
3451 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
3452 {ImGuiCol_Text, sidebar_visible ? gui::GetPrimaryVec4()
3454
3455 const char* icon = sidebar_visible ? ICON_MD_MENU_OPEN : ICON_MD_MENU;
3456 if (ImGui::SmallButton(icon)) {
3458 }
3459 }
3460
3461 if (ImGui::IsItemHovered()) {
3462 const char* tooltip = window_manager_.IsSidebarVisible()
3463 ? "Hide Activity Bar (Ctrl+B)"
3464 : "Show Activity Bar (Ctrl+B)";
3465 ImGui::SetTooltip("%s", tooltip);
3466 }
3467
3468 // Delegate menu building to MenuOrchestrator
3469 if (menu_orchestrator_) {
3470 menu_orchestrator_->BuildMainMenu();
3471 }
3472
3473 // Delegate menu bar extras to UICoordinator
3474 if (ui_coordinator_) {
3475 ui_coordinator_->DrawMenuBarExtras();
3476 }
3477
3478 ImGui::EndMenuBar();
3479 }
3480}
3481
3483
3484 // ImGui debug windows
3485 if (ui_coordinator_) {
3486 if (ui_coordinator_->IsImGuiDemoVisible()) {
3487 bool visible = true;
3488 ImGui::ShowDemoWindow(&visible);
3489 if (!visible)
3490 ui_coordinator_->SetImGuiDemoVisible(false);
3491 }
3492
3493 if (ui_coordinator_->IsImGuiMetricsVisible()) {
3494 bool visible = true;
3495 ImGui::ShowMetricsWindow(&visible);
3496 if (!visible)
3497 ui_coordinator_->SetImGuiMetricsVisible(false);
3498 }
3499 }
3500
3501 // Legacy window-based editors
3502 if (auto* editor_set = GetCurrentEditorSet()) {
3503 bool* hex_visibility =
3504 window_manager_.GetWindowVisibilityFlag("memory.hex_editor");
3505 if (hex_visibility) {
3506 if (auto* editor = editor_set->GetEditor(EditorType::kHex)) {
3507 // Keep the legacy panel visibility flag in sync with the window close
3508 // button (ImGui::Begin will toggle Editor::active_).
3509 editor->set_active(*hex_visibility);
3510 editor->Update();
3511 *hex_visibility = *editor->active();
3512 }
3513 }
3514
3515 if (ui_coordinator_ && ui_coordinator_->IsAsmEditorVisible()) {
3516 if (auto* editor = editor_set->GetEditor(EditorType::kAssembly)) {
3517 editor->set_active(true);
3518 editor->Update();
3519 if (!*editor->active()) {
3520 ui_coordinator_->SetAsmEditorVisible(false);
3521 }
3522 }
3523 }
3524 }
3525
3526 // Project and performance tools
3529
3530 if (ui_coordinator_ && ui_coordinator_->IsPerformanceDashboardVisible()) {
3534 if (!gfx::PerformanceDashboard::Get().IsVisible()) {
3535 ui_coordinator_->SetPerformanceDashboardVisible(false);
3536 }
3537 }
3538
3539#ifdef YAZE_ENABLE_TESTING
3540 if (show_test_dashboard_) {
3542 test::TestManager::Get().DrawTestDashboard(&show_test_dashboard_);
3543 }
3544#endif
3545}
3546
3548 // Update proposal drawer context
3550
3551 // Agent UI popups
3553
3554 // Resource label management
3555 if (ui_coordinator_ && ui_coordinator_->IsResourceLabelManagerVisible() &&
3556 GetCurrentRom()) {
3557 bool visible = true;
3563 }
3564 if (!visible)
3565 ui_coordinator_->SetResourceLabelManagerVisible(false);
3566 }
3567
3568 // Layout presets
3569 if (ui_coordinator_) {
3570 ui_coordinator_->DrawLayoutPresets();
3571 }
3572}
3573
3575 auto* current_rom = GetCurrentRom();
3576 if (!current_rom)
3577 return;
3578
3579 // Visibility gates *rendering*, not *ticking*. Run(rom) is the lazy-init +
3580 // render path; the SNES only starts (running_=true, snes_initialized_=true)
3581 // after Run(rom) fires while the emulator panel is visible. Once running,
3582 // switching to another editor hides the panel but must NOT freeze the game —
3583 // the tick-only branch below keeps audio + frame state alive.
3584 if (ui_coordinator_ && ui_coordinator_->IsEmulatorVisible()) {
3585 emulator_.Run(current_rom);
3586 } else if (emulator_.running() && emulator_.is_snes_initialized()) {
3589 } else {
3591 }
3592 }
3593}
3594
3601
3627 GetCurrentSessionId()})) {
3628 return absl::OkStatus();
3629 }
3630
3631 return LoadRomInternal();
3632}
3633
3635 auto load_from_path = [this](const std::string& file_name) -> absl::Status {
3636 if (file_name.empty()) {
3637 return absl::OkStatus();
3638 }
3639
3640 // Check if this is a project file - route to project loading
3641 if (absl::EndsWith(file_name, ".yaze") ||
3642 absl::EndsWith(file_name, ".zsproj") ||
3643 absl::EndsWith(file_name, ".yazeproj")) {
3644 return OpenRomOrProjectInternal(file_name);
3645 }
3646
3647 auto path_status =
3648 session_coordinator_->CheckBackingFileAvailable(file_name);
3649 if (!path_status.ok()) {
3650 toast_manager_.Show(std::string(path_status.message()),
3652 return path_status;
3653 }
3654
3655 // Delegate ROM loading to RomFileManager
3656 Rom temp_rom;
3657 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, file_name));
3658
3659 const std::optional<size_t> previous_session_id =
3661 const size_t previous_session_count =
3662 session_coordinator_->GetTotalSessionCount();
3663 auto session_or = session_coordinator_->CreateSessionFromRom(
3664 std::move(temp_rom), file_name);
3665 if (!session_or.ok()) {
3666 return session_or.status();
3667 }
3668
3672
3673 // HandleSessionCreated binds the project context that governed this open.
3674 // Reapply it here rather than partially clearing global runtime state.
3677
3678 // Keep ResourceLabelProvider in sync with the newly-active ROM session
3679 // before any editors/assets query room/sprite names.
3681
3682#ifdef YAZE_ENABLE_TESTING
3684#endif
3685
3687 const auto& recent_files = manager.GetRecentFiles();
3688 const bool is_first_time_rom_path =
3689 std::find(recent_files.begin(), recent_files.end(), file_name) ==
3690 recent_files.end();
3691 auto asset_status = LoadAssetsForMode();
3692 if (!asset_status.ok()) {
3693 auto rollback_status =
3694 DiscardProvisionalSessionCreatedSince(previous_session_count);
3695 RestoreProjectContextAfterFailedOpen(previous_session_id);
3696 if (!rollback_status.ok()) {
3697 return absl::InternalError(
3698 absl::StrFormat("%s; session rollback failed: %s",
3699 asset_status.message(), rollback_status.message()));
3700 }
3701 return asset_status;
3702 }
3703
3704 manager.AddFile(file_name);
3705 manager.Save();
3706
3707 if (ui_coordinator_) {
3708 ui_coordinator_->SetWelcomeScreenVisible(false);
3709
3710 // Show ROM load options and bias new ROM paths toward project creation.
3711 rom_load_options_dialog_.Open(GetCurrentRom(), is_first_time_rom_path);
3713 }
3714
3715 return absl::OkStatus();
3716 };
3717
3718#if defined(__APPLE__) && TARGET_OS_IOS == 1
3719 // On iOS, route through the SwiftUI overlay document picker to get proper
3720 // security-scoped access to iCloud Drive and Files app locations. This
3721 // mirrors how OpenProject() works on iOS and supports cloud ROMs.
3722 // The SwiftUI picker calls YazeIOSBridge.loadRomAtPath: after importing the
3723 // ROM to a persistent sandbox directory.
3725 return absl::OkStatus();
3726#else
3729 return load_from_path(file_name);
3730#endif
3731}
3732
3733absl::Status EditorManager::LoadAssets(uint64_t passed_handle) {
3734 auto* current_rom = GetCurrentRom();
3735 auto* current_editor_set = GetCurrentEditorSet();
3736 if (!current_rom || !current_editor_set) {
3737 return absl::FailedPreconditionError("No ROM or editor set loaded");
3738 }
3739
3740 auto* current_session = session_coordinator_->GetActiveRomSession();
3741 if (!current_session) {
3742 return absl::FailedPreconditionError("No active ROM session");
3743 }
3744 ResetAssetState(current_session);
3745
3746 auto start_time = std::chrono::steady_clock::now();
3747
3748#ifdef __EMSCRIPTEN__
3749 // Use passed handle if provided, otherwise create new one
3750 auto loading_handle =
3751 passed_handle != 0
3752 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3753 passed_handle)
3754 : app::platform::WasmLoadingManager::BeginLoading(
3755 "Loading Editor Assets");
3756
3757 // Progress starts at 10% (ROM already loaded), goes to 100%
3758 constexpr float kStartProgress = 0.10f;
3759 constexpr float kEndProgress = 1.0f;
3760 constexpr int kTotalSteps = 11; // Graphics + 8 editors + profiler + finish
3761 int current_step = 0;
3762 auto update_progress = [&](const std::string& message) {
3763 current_step++;
3764 float progress =
3765 kStartProgress + (kEndProgress - kStartProgress) *
3766 (static_cast<float>(current_step) / kTotalSteps);
3767 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, progress);
3768 app::platform::WasmLoadingManager::UpdateMessage(loading_handle, message);
3769 };
3770 // RAII guard to ensure loading indicator is closed even on early return
3771 auto cleanup_loading = [&]() {
3772 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3773 };
3774 struct LoadingGuard {
3775 std::function<void()> cleanup;
3776 bool dismissed = false;
3777 ~LoadingGuard() {
3778 if (!dismissed)
3779 cleanup();
3780 }
3781 void dismiss() { dismissed = true; }
3782 } loading_guard{cleanup_loading};
3783#else
3784 (void)passed_handle; // Unused on non-WASM
3785#endif
3786
3787 // Set renderer for emulator (lazy initialization happens in Run())
3788 if (renderer_) {
3790 }
3791
3792 const auto preload_editor_list = CollectEditorsToPreload(current_editor_set);
3793 const std::unordered_set<EditorType> preload_types(
3794 preload_editor_list.begin(), preload_editor_list.end());
3795
3796 // Initialize only the editors needed for the current startup surface. This
3797 // registers their windows and sets up editor-specific resources before Load().
3798 struct InitStep {
3799 EditorType type;
3800 bool mark_loaded;
3801 };
3802 const InitStep init_steps[] = {
3804 {EditorType::kGraphics, false}, {EditorType::kScreen, false},
3805 {EditorType::kSprite, false}, {EditorType::kPalette, false},
3806 {EditorType::kAssembly, true}, {EditorType::kMusic, false},
3807 {EditorType::kDungeon, false},
3808 };
3809 for (const auto& step : init_steps) {
3810 if (!preload_types.contains(step.type)) {
3811 continue;
3812 }
3813 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3814 editor->Initialize();
3815 MarkEditorInitialized(current_session, step.type);
3816 if (step.mark_loaded) {
3817 MarkEditorLoaded(current_session, step.type);
3818 }
3819 }
3820 }
3821
3822#ifdef __EMSCRIPTEN__
3823 update_progress("Loading graphics sheets...");
3824#endif
3825 // Load all Zelda3-specific data (metadata, palettes, gfx groups, graphics)
3826 gfx::PaletteManager::Get().ReleaseSession(&current_session->game_data);
3828 zelda3::LoadGameData(*current_rom, current_session->game_data));
3829 current_session->game_data_loaded = true;
3830
3831 // Copy loaded graphics to Arena for global access
3833 current_session->game_data.gfx_bitmaps;
3834
3835 // Propagate GameData to editors that already exist; future editors inherit it
3836 // on first construction via EditorSet.
3837 auto* game_data = &current_session->game_data;
3838 current_editor_set->SetGameData(game_data);
3840
3841 struct LoadStep {
3842 EditorType type;
3843 const char* progress_message;
3844 };
3845 const LoadStep load_steps[] = {
3846 {EditorType::kOverworld, "Loading overworld..."},
3847 {EditorType::kDungeon, "Loading dungeons..."},
3848 {EditorType::kScreen, "Loading screen editor..."},
3849 {EditorType::kGraphics, "Loading graphics editor..."},
3850 {EditorType::kSprite, "Loading sprites..."},
3851 {EditorType::kMessage, "Loading messages..."},
3852 {EditorType::kMusic, "Loading music..."},
3853 {EditorType::kPalette, "Loading palettes..."},
3854 };
3855 for (const auto& step : load_steps) {
3856 if (!preload_types.contains(step.type)) {
3857 continue;
3858 }
3859#ifdef __EMSCRIPTEN__
3860 update_progress(step.progress_message);
3861#endif
3862 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3863 RETURN_IF_ERROR(editor->Load());
3864 MarkEditorLoaded(current_session, step.type);
3865 }
3866 }
3867
3868#ifdef __EMSCRIPTEN__
3869 update_progress("Finishing up...");
3870#endif
3871
3872 // Set up RightDrawerManager with session's settings editor
3874 auto* settings = current_editor_set->GetSettingsPanel();
3875 right_drawer_manager_->SetSettingsPanel(settings);
3876 }
3877
3878 // Apply user preferences to status bar
3880
3882
3883#ifdef __EMSCRIPTEN__
3884 // Dismiss the guard and manually close - we completed successfully
3885 loading_guard.dismiss();
3886 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3887#endif
3888
3889 auto end_time = std::chrono::steady_clock::now();
3890 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
3891 end_time - start_time);
3892 LOG_DEBUG("EditorManager", "ROM assets loaded in %lld ms", duration.count());
3893
3894 return absl::OkStatus();
3895}
3896
3897absl::Status EditorManager::LoadAssetsLazy(uint64_t passed_handle) {
3898 auto* current_rom = GetCurrentRom();
3899 auto* current_editor_set = GetCurrentEditorSet();
3900 if (!current_rom || !current_editor_set) {
3901 return absl::FailedPreconditionError("No ROM or editor set loaded");
3902 }
3903
3904 auto* current_session = session_coordinator_->GetActiveRomSession();
3905 if (!current_session) {
3906 return absl::FailedPreconditionError("No active ROM session");
3907 }
3908 ResetAssetState(current_session);
3909
3910#ifdef __EMSCRIPTEN__
3911 // Use passed handle if provided, otherwise create new one
3912 auto loading_handle =
3913 passed_handle != 0
3914 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3915 passed_handle)
3916 : app::platform::WasmLoadingManager::BeginLoading(
3917 "Loading ROM (lazy assets)");
3918 auto cleanup_loading = [&]() {
3919 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3920 };
3921 struct LoadingGuard {
3922 std::function<void()> cleanup;
3923 bool dismissed = false;
3924 ~LoadingGuard() {
3925 if (!dismissed) {
3926 cleanup();
3927 }
3928 }
3929 void dismiss() { dismissed = true; }
3930 } loading_guard{cleanup_loading};
3931#else
3932 (void)passed_handle; // Unused on non-WASM
3933#endif
3934
3935 // Set renderer for emulator (lazy initialization happens in Run())
3936 if (renderer_) {
3938 }
3939
3940 // Wire settings panel to right panel manager for the current session.
3942 auto* settings = current_editor_set->GetSettingsPanel();
3943 right_drawer_manager_->SetSettingsPanel(settings);
3944 }
3945
3946 // Apply user preferences to status bar
3948
3949#ifdef __EMSCRIPTEN__
3950 loading_guard.dismiss();
3951 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3952#endif
3953
3954 LOG_INFO("EditorManager", "Lazy asset mode: editor assets deferred");
3955 return absl::OkStatus();
3956}
3957
3974 const std::optional<std::string>& target_filename) {
3975 return rom_lifecycle_.CheckRomWritePolicy(GetCurrentRom(), target_filename);
3976}
3977
3981
3987
3989 if (!pending_rom_save_.has_value() || !session_coordinator_) {
3990 return false;
3991 }
3992 return pending_rom_save_->session_index == GetCurrentSessionIndex() &&
3993 pending_rom_save_->rom != nullptr &&
3995}
3996
3998 pending_rom_save_.reset();
4000
4001 if (!hide_popups || !popup_manager_) {
4002 return;
4003 }
4004 for (const char* popup :
4007 if (popup_manager_->IsVisible(popup)) {
4008 popup_manager_->Hide(popup);
4009 }
4010 }
4011}
4012
4014 const std::optional<std::string>& save_as_filename) {
4015 auto* rom = GetCurrentRom();
4016 if (!session_coordinator_ || !session_coordinator_->GetActiveRomSession() ||
4017 !rom) {
4018 return absl::FailedPreconditionError("No active ROM session");
4019 }
4020
4023 PendingRomSave{save_as_filename, GetCurrentSessionIndex(), rom};
4024 return absl::OkStatus();
4025}
4026
4027void EditorManager::FinishPendingRomSaveAttempt(const absl::Status& status) {
4028 // A Cancelled status is resumable only while one of this request's explicit
4029 // confirmation dialogs is pending. Every other outcome is terminal and must
4030 // consume all one-shot bypasses so a later Ctrl+S cannot inherit consent.
4031 if (status.ok() || !absl::IsCancelled(status) ||
4034 }
4035}
4036
4038 // Do not let an autosave or an unrelated Ctrl+S replace a Save As request
4039 // while one of its confirmation dialogs is still open.
4040 if (pending_rom_save_.has_value()) {
4041 const bool unresolved_confirmation =
4046 if (unresolved_confirmation) {
4047 return absl::CancelledError("Save pending confirmation");
4048 }
4049 // Preserve the established request after programmatic confirmation too.
4050 // Older callers use ConfirmRomWrite(); SaveRom() rather than the popup's
4051 // ResumePendingRomSave() helper.
4052 return ResumePendingRomSave();
4053 }
4054 RETURN_IF_ERROR(StartPendingRomSave(std::nullopt));
4055 auto status = SaveRomInternal(std::nullopt);
4057 return status;
4058}
4059
4061 const std::optional<std::string>& save_as_filename) {
4062 auto* current_rom = GetCurrentRom();
4063 auto* current_editor_set = GetCurrentEditorSet();
4064 if (!current_rom || !current_editor_set) {
4065 return absl::FailedPreconditionError("No ROM or editor set loaded");
4066 }
4067 if (save_as_filename.has_value() && save_as_filename->empty()) {
4068 return absl::InvalidArgumentError("No filename provided for save as");
4069 }
4070 if (save_as_filename.has_value()) {
4071 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4072 *save_as_filename, GetCurrentSessionId()));
4073 }
4074
4075 ASSIGN_OR_RETURN(const project::YazeProject* save_project,
4077 // Hash/write/safety checks must read the same immutable session snapshot as
4078 // the rest of this save, never a process-global project left by another ROM.
4079 rom_lifecycle_.SetProjectContext(save_project);
4080 struct LifecycleProjectContextGuard {
4081 RomLifecycleManager* lifecycle = nullptr;
4082 project::YazeProject* restore = nullptr;
4083 ~LifecycleProjectContextGuard() {
4084 if (lifecycle) {
4085 lifecycle->SetProjectContext(restore);
4086 }
4087 }
4088 } lifecycle_project_guard{&rom_lifecycle_, &current_project_};
4089
4090 // GraphicsEditor tracks pixel edits in its model, not in the ROM buffer.
4091 // The previous global graphics writer was a success-returning stub, and
4092 // the editor-specific writer is not yet safe to join this coordinated save.
4093 // Block before any serializer can mutate the ROM rather than report a save
4094 // that silently omitted the pending sheets.
4095 if (current_editor_set->HasPendingGraphicsChanges()) {
4096 return absl::FailedPreconditionError(absl::StrFormat(
4097 "Save blocked: graphics sheet edits are pending, but graphics ROM "
4098 "persistence is not safely available (Save Graphics Sheets is %s). "
4099 "Discard the graphics sheet edits before saving the ROM.",
4100 core::FeatureFlags::get().kSaveGraphicsSheet ? "enabled" : "disabled"));
4101 }
4102
4103 // ScreenEditor keeps dungeon-map, Tile16, title-screen, and pause-map edits
4104 // in editor-owned models. Not every domain can safely participate in the
4105 // coordinated ROM transaction yet, so fail before any serializer mutates
4106 // the ROM instead of reporting a partial save as successful.
4107 if (current_editor_set->HasPendingScreenChanges()) {
4108 return absl::FailedPreconditionError(absl::StrFormat(
4109 "Save blocked: Screen Editor edits are pending, but they cannot all "
4110 "participate safely in the coordinated ROM save (Save Dungeon Maps is "
4111 "%s). Title-screen and pause-map ROM writes remain disabled until "
4112 "write/reopen/readback verification exists; discard the pending Screen "
4113 "Editor edits before saving the ROM.",
4114 core::FeatureFlags::get().kSaveDungeonMaps ? "enabled" : "disabled"));
4115 }
4116
4117 // --- State machine checks (delegated to RomLifecycleManager) ---
4119 return absl::CancelledError("Save pending confirmation");
4120 }
4121
4122 RETURN_IF_ERROR(CheckRomWritePolicy(save_as_filename));
4123
4125 return absl::CancelledError("Save pending confirmation");
4126 }
4127
4128 const bool pot_items_enabled =
4131 !rom_lifecycle_.ShouldSuppressPotItemSave() && pot_items_enabled) {
4132 const int loaded_rooms = current_editor_set->LoadedDungeonRoomCount();
4133 const int total_rooms = current_editor_set->TotalDungeonRoomCount();
4134 if (loaded_rooms < total_rooms) {
4135 rom_lifecycle_.SetPotItemConfirmPending(total_rooms - loaded_rooms,
4136 total_rooms);
4137 if (popup_manager_) {
4139 }
4141 absl::StrFormat(
4142 "Save paused: pot items enabled with %d unloaded rooms",
4145 return absl::CancelledError("Pot item save confirmation required");
4146 }
4147 }
4148
4149 const bool bypass_confirm = rom_lifecycle_.ShouldBypassPotItemConfirm();
4150 const bool suppress_pot_items = rom_lifecycle_.ShouldSuppressPotItemSave();
4152
4153 struct PotItemFlagGuard {
4154 bool restore = false;
4155 bool previous = false;
4156 ~PotItemFlagGuard() {
4157 if (restore) {
4158 core::FeatureFlags::get().dungeon.kSavePotItems = previous;
4159 }
4160 }
4161 } pot_item_guard;
4162
4163 if (suppress_pot_items) {
4164 pot_item_guard.previous = core::FeatureFlags::get().dungeon.kSavePotItems;
4165 pot_item_guard.restore = true;
4167 } else if (bypass_confirm) {
4168 // Explicitly allow pot item save once after confirmation.
4169 }
4170
4171 // --- Backup policy setup ---
4172 if (save_project->project_opened()) {
4174 save_project->workspace_settings.backup_on_save,
4175 save_project->GetAbsolutePath(save_project->rom_backup_folder),
4179 } else {
4181 user_settings_.prefs().backup_before_save, "", 20, true, 14);
4182 }
4183
4184 // Reject an already-invalid Oracle layout before any editor serializer can
4185 // mutate the ROM buffer or clear its own dirty tracking. The post-save check
4186 // below remains mandatory because serializers may introduce a violation.
4188
4189 // Serializers clear their dirty flags after a successful in-memory write.
4190 // Capture their conservative write prediction first so conflict protection
4191 // still has a fail-closed fallback when the on-disk ROM cannot be read.
4192 const auto predicted_dungeon_write_ranges =
4193 current_editor_set->CollectDungeonWriteRanges();
4194
4195 ScopedRomTransaction rom_transaction(*current_rom);
4196 ScopedEditorSaveTransactions editor_transactions;
4197
4198 auto save_editor = [&editor_transactions](Editor* editor) -> absl::Status {
4199 if (editor == nullptr) {
4200 return absl::OkStatus();
4201 }
4202 RETURN_IF_ERROR(editor_transactions.Begin(editor));
4203 return editor->Save();
4204 };
4205
4206 // --- Save editor-specific data ---
4207 auto* screen_editor = static_cast<ScreenEditor*>(
4208 current_editor_set->GetExistingEditor(EditorType::kScreen));
4209 // A failed lazy Screen load leaves an invalid, clean editor behind. It has
4210 // nothing to serialize and must not poison unrelated ROM saves.
4211 if (screen_editor != nullptr && (screen_editor->IsRomBackedStateValid() ||
4212 screen_editor->HasPendingScreenChanges())) {
4213 RETURN_IF_ERROR(save_editor(screen_editor));
4214 }
4215 // A coordinated save must not materialize an unrelated lazy editor. Its
4216 // serializer can project broad writes (and correctly trip manifest guards)
4217 // even though the user never opened or edited that domain.
4219 save_editor(current_editor_set->GetExistingEditor(EditorType::kDungeon)));
4220 RETURN_IF_ERROR(save_editor(
4221 current_editor_set->GetExistingEditor(EditorType::kOverworld)));
4222
4223 if (core::FeatureFlags::get().kSaveMessages) {
4226 save_editor(current_editor_set->GetEditor(EditorType::kMessage)));
4227 }
4228
4229 // Oracle guardrails: refuse to write obviously corrupted ROM layouts.
4231
4232 // --- Write conflict check (ASM-owned address protection) ---
4233 if (save_project->project_opened() && save_project->hack_manifest.loaded()) {
4235 std::vector<std::pair<uint32_t, uint32_t>> write_ranges;
4236 bool diff_computed = false;
4237
4238 if (!current_rom->filename().empty()) {
4239 std::ifstream file(current_rom->filename(), std::ios::binary);
4240 if (file.is_open()) {
4241 file.seekg(0, std::ios::end);
4242 const std::streampos end = file.tellg();
4243 if (end >= 0) {
4244 std::vector<uint8_t> disk_data(static_cast<size_t>(end));
4245 file.seekg(0, std::ios::beg);
4246 file.read(reinterpret_cast<char*>(disk_data.data()),
4247 static_cast<std::streamsize>(disk_data.size()));
4248 if (file) {
4249 diff_computed = true;
4250 auto diff = yaze::rom::ComputeDiffRanges(disk_data,
4251 current_rom->vector());
4252 if (!diff.ranges.empty()) {
4253 LOG_DEBUG("EditorManager",
4254 "ROM save diff: %zu bytes changed in %zu range(s)",
4255 diff.total_bytes_changed, diff.ranges.size());
4256 write_ranges = std::move(diff.ranges);
4257 }
4258 }
4259 }
4260 }
4261 }
4262
4263 if (write_ranges.empty() && !diff_computed) {
4264 write_ranges = predicted_dungeon_write_ranges;
4265 }
4266
4267 if (!write_ranges.empty()) {
4268 auto conflicts =
4269 save_project->hack_manifest.AnalyzePcWriteRanges(write_ranges);
4270 if (!conflicts.empty()) {
4271 rom_lifecycle_.SetPendingWriteConflicts(std::move(conflicts));
4272 if (popup_manager_) {
4274 }
4276 absl::StrFormat(
4277 "Save paused: %zu write conflict(s) with ASM hooks",
4280 return absl::CancelledError("Write conflict confirmation required");
4281 }
4282 }
4283 } else {
4284 // Bypass is single-use, set by the warning popup.
4286 }
4287 }
4288
4289 // Delegate the final atomic disk write to RomFileManager. Save As is part of
4290 // this same transaction and writes only the requested target path.
4291 auto save_status =
4292 save_as_filename.has_value()
4293 ? rom_file_manager_.SaveRomAs(current_rom, *save_as_filename)
4294 : rom_file_manager_.SaveRom(current_rom);
4295 if (save_status.ok()) {
4296 editor_transactions.Commit();
4297 rom_transaction.Commit();
4300 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4301 session->backup_restore_pending = false;
4302 }
4303 }
4304 // Write-confirm bypass is single-use. Clear it after a successful save.
4306
4307 if (save_as_filename.has_value()) {
4309 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4310 session->filepath = *save_as_filename;
4311 }
4312 }
4313
4315 manager.AddFile(*save_as_filename);
4316 manager.Save();
4317 if (popup_manager_) {
4319 }
4320 }
4321 }
4322 return save_status;
4323}
4324
4325absl::Status EditorManager::SaveRomAs(const std::string& filename) {
4326 if (filename.empty()) {
4327 return absl::InvalidArgumentError("No filename provided for save as");
4328 }
4329 if (pending_rom_save_.has_value() && HasPendingRomSaveConfirmation()) {
4330 return absl::CancelledError("Save pending confirmation");
4331 }
4333 auto status = SaveRomInternal(filename);
4335 return status;
4336}
4337
4339 if (!pending_rom_save_.has_value()) {
4341 return absl::FailedPreconditionError("No pending ROM save to resume");
4342 }
4344 CancelPendingRomSave(/*hide_popups=*/true);
4345 return absl::FailedPreconditionError(
4346 "Pending ROM save belongs to a different session");
4347 }
4348
4349 const auto save_as_filename = pending_rom_save_->save_as_filename;
4350 auto status = SaveRomInternal(save_as_filename);
4352 return status;
4353}
4354
4355absl::Status EditorManager::OpenRomOrProject(const std::string& filename) {
4358 GetCurrentSessionId(), SIZE_MAX, filename})) {
4359 return absl::OkStatus();
4360 }
4361
4362 return OpenRomOrProjectInternal(filename);
4363}
4364
4366 const std::string& filename) {
4367 LOG_INFO("EditorManager", "OpenRomOrProject called with: '%s'",
4368 filename.c_str());
4369 if (filename.empty()) {
4370 LOG_INFO("EditorManager", "Empty filename provided, skipping load.");
4371 return absl::OkStatus();
4372 }
4373
4374#ifdef __EMSCRIPTEN__
4375 // Start loading indicator early for WASM builds
4376 auto loading_handle =
4377 app::platform::WasmLoadingManager::BeginLoading("Loading ROM");
4378 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4379 "Reading ROM file...");
4380 // RAII guard to ensure loading indicator is closed even on early return
4381 struct LoadingGuard {
4382 app::platform::WasmLoadingManager::LoadingHandle handle;
4383 bool dismissed = false;
4384 ~LoadingGuard() {
4385 if (!dismissed)
4386 app::platform::WasmLoadingManager::EndLoading(handle);
4387 }
4388 void dismiss() { dismissed = true; }
4389 } loading_guard{loading_handle};
4390#endif
4391
4392 if (absl::EndsWith(filename, ".yaze") ||
4393 absl::EndsWith(filename, ".zsproj") ||
4394 absl::EndsWith(filename, ".yazeproj")) {
4395 // Parse first so a failed project open cannot destroy the active session's
4396 // working context. Detach only when the incoming project is ready.
4397 project::YazeProject incoming_project;
4398 RETURN_IF_ERROR(incoming_project.Open(filename));
4399 if (!incoming_project.rom_filename.empty()) {
4400 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4401 incoming_project.rom_filename));
4402 }
4403 const std::optional<size_t> previous_session_id =
4406 pending_project_open_previous_session_id_ = previous_session_id;
4408 current_project_ = std::move(incoming_project);
4411
4412 // Load ROM directly from project - don't prompt user
4413 auto project_status = LoadProjectWithRom();
4414 if (!project_status.ok()) {
4415 RestoreProjectContextAfterFailedOpen(previous_session_id);
4416 }
4417 return project_status;
4418 } else {
4419#ifdef __EMSCRIPTEN__
4420 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.05f);
4421 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4422 "Loading ROM data...");
4423#endif
4424 Rom temp_rom;
4425 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(filename));
4426 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, filename));
4428
4429 const std::optional<size_t> previous_session_id =
4431 const size_t previous_session_count =
4432 session_coordinator_->GetTotalSessionCount();
4433 auto session_or = session_coordinator_->CreateSessionFromRom(
4434 std::move(temp_rom), filename);
4435 if (!session_or.ok()) {
4436 return session_or.status();
4437 }
4438 RomSession* session = *session_or;
4439
4443
4444 // Apply project feature flags to both session and global singleton
4448
4449 // Keep ResourceLabelProvider in sync with the active ROM session before
4450 // editors register room/sprite labels.
4452
4453 // Update test manager with current ROM for ROM-dependent tests (only when
4454 // tests are enabled)
4455#ifdef YAZE_ENABLE_TESTING
4456 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
4457 (void*)GetCurrentRom(),
4458 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
4460#endif
4461
4462 if (auto* editor_set = GetCurrentEditorSet();
4463 editor_set && !current_project_.code_folder.empty()) {
4464 const std::string absolute_code_folder =
4466 // iOS: avoid blocking the main thread during project open / scene updates.
4467 // Large iCloud-backed projects can trigger watchdog termination if we
4468 // eagerly enumerate folders here.
4469#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
4470 editor_set->OpenAssemblyFolder(absolute_code_folder);
4471#endif
4472 // Also set the sidebar file browser path (refresh happens during UI draw).
4473 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
4474 }
4475
4476#ifdef __EMSCRIPTEN__
4477 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.10f);
4478 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4479 "Initializing editors...");
4480 // Pass the loading handle to LoadAssets and dismiss our guard
4481 // LoadAssets will manage closing the indicator when done
4482 loading_guard.dismiss();
4483 auto asset_status = LoadAssetsForMode(loading_handle);
4484#else
4485 auto asset_status = LoadAssetsForMode();
4486#endif
4487 if (!asset_status.ok()) {
4488 auto rollback_status =
4489 DiscardProvisionalSessionCreatedSince(previous_session_count);
4490 RestoreProjectContextAfterFailedOpen(previous_session_id);
4491 if (!rollback_status.ok()) {
4492 return absl::InternalError(
4493 absl::StrFormat("%s; session rollback failed: %s",
4494 asset_status.message(), rollback_status.message()));
4495 }
4496 return asset_status;
4497 }
4498
4499 // Hide welcome screen and show editor selection when ROM is loaded
4500 ui_coordinator_->SetWelcomeScreenVisible(false);
4501 // dashboard_panel_->ClearRecentEditors();
4502 ui_coordinator_->SetEditorSelectionVisible(true);
4503
4504 // Set Dashboard category to suppress panel drawing until user selects an editor
4507 /*notify=*/false);
4508 }
4509 return absl::OkStatus();
4510}
4511
4512absl::Status EditorManager::CreateNewProject(const std::string& template_name) {
4514 return absl::FailedPreconditionError(
4515 "Save or discard pending session work before creating a project");
4516 }
4517
4518 if (!ui_coordinator_) {
4519 return absl::FailedPreconditionError(
4520 "Project creation dialog is not available");
4521 }
4522 ui_coordinator_->OpenNewProjectDialog(
4523 template_name.empty() ? "Vanilla ROM Hack" : template_name);
4524 return absl::OkStatus();
4525}
4526
4528 const std::string& template_name, const std::string& rom_path,
4529 const std::string& project_name, const std::string& project_path) {
4530 if (rom_path.empty() || project_name.empty()) {
4531 return absl::InvalidArgumentError("ROM path and project name are required");
4532 }
4533 if (absl::EndsWith(rom_path, ".yaze") ||
4534 absl::EndsWith(rom_path, ".yazeproj") ||
4535 absl::EndsWith(rom_path, ".zsproj")) {
4536 return absl::InvalidArgumentError(
4537 "New projects require a ROM file, not a project descriptor");
4538 }
4540 return absl::FailedPreconditionError(
4541 "Save or discard pending session work before creating a project");
4542 }
4543
4544 auto* reusable_session = session_coordinator_
4545 ? session_coordinator_->GetActiveRomSession()
4546 : nullptr;
4547 const bool reuse_active_raw_session =
4548 reusable_session && reusable_session->rom.is_loaded() &&
4550 rom_path, reusable_session->rom.filename()) &&
4553 const std::optional<size_t> previous_session_id =
4555 const size_t previous_session_count =
4556 session_coordinator_ ? session_coordinator_->GetTotalSessionCount() : 0;
4558 auto rom_status = project_manager_.SetProjectRom(rom_path);
4559 if (!rom_status.ok()) {
4561 return rom_status;
4562 }
4564 project_name, project_path);
4565 if (!target_status.ok()) {
4567 return target_status;
4568 }
4569
4570 if (reuse_active_raw_session) {
4571 return FinalizeNewProject(project_name, project_path);
4572 }
4573
4577
4578 auto open_status = OpenRomOrProjectInternal(rom_path);
4579 if (!open_status.ok()) {
4581 RestoreProjectContextAfterFailedOpen(previous_session_id);
4582 return open_status;
4583 }
4584
4585 auto finalize_status = FinalizeNewProject(project_name, project_path);
4586 if (!finalize_status.ok()) {
4587 auto rollback_status =
4588 DiscardProvisionalSessionCreatedSince(previous_session_count);
4589 RestoreProjectContextAfterFailedOpen(previous_session_id);
4590 if (!rollback_status.ok()) {
4591 return absl::InternalError(absl::StrFormat(
4592 "%s; session rollback failed: %s", finalize_status.message(),
4593 rollback_status.message()));
4594 }
4595 }
4596 return finalize_status;
4597}
4598
4600 const std::string& project_name, const std::string& project_path) {
4601 if (!session_coordinator_) {
4602 return absl::FailedPreconditionError("No session coordinator");
4603 }
4604 auto* session = session_coordinator_->GetActiveRomSession();
4605 if (!session || !session->rom.is_loaded()) {
4606 return absl::FailedPreconditionError(
4607 "Load a ROM before finalizing the project");
4608 }
4609
4610 // The ROM-load-options dialog can create a project directly from an
4611 // already-open raw ROM. Start from a clean shell in that path rather than
4612 // reusing ProjectManager state left by an earlier project/import.
4613 const bool created_project_shell = !project_manager_.IsPendingRomSelection();
4614 if (created_project_shell) {
4616 }
4617
4618 auto project_rom_status =
4619 project_manager_.SetProjectRom(session->rom.filename());
4620 if (!project_rom_status.ok()) {
4622 return project_rom_status;
4623 }
4624 auto& pending_project = project_manager_.GetCurrentProject();
4625 if (created_project_shell) {
4626 // ROM-load options apply their selected flags directly to the raw session.
4627 // Guided creation already configured the pending template, including when
4628 // it reuses an existing raw-ROM session.
4629 pending_project.feature_flags = session->feature_flags;
4630 }
4631 pending_project.workspace_settings.font_global_scale =
4633 pending_project.workspace_settings.autosave_enabled =
4635 pending_project.workspace_settings.autosave_interval_secs =
4637 pending_project.workspace_settings.backup_on_save =
4639
4640 auto finalize_status =
4641 project_manager_.FinalizeProjectCreation(project_name, project_path);
4642 if (!finalize_status.ok()) {
4644 return finalize_status;
4645 }
4646
4649 session->project_dirty = false;
4650 active_project_context_session_id_ = session->session_id();
4651 runtime_feature_flags_session_id_ = session->session_id();
4653 if (version_manager_) {
4655 current_project_.git_repository = session->project_context->git_repository;
4657 }
4661 return absl::OkStatus();
4662}
4663
4667 GetCurrentSessionId()})) {
4668 return absl::OkStatus();
4669 }
4670
4671 return OpenProjectInternal();
4672}
4673
4675 auto open_project_from_path =
4676 [this](const std::string& file_path) -> absl::Status {
4677 if (file_path.empty()) {
4678 return absl::OkStatus();
4679 }
4680
4681 project::YazeProject new_project;
4682 RETURN_IF_ERROR(new_project.Open(file_path));
4683
4684 if (!new_project.rom_filename.empty()) {
4685 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4686 new_project.rom_filename));
4687 }
4688
4689 // Validate project
4690 auto validation_status = new_project.Validate();
4691 if (!validation_status.ok()) {
4692 toast_manager_.Show(absl::StrFormat("Project validation failed: %s",
4693 validation_status.message()),
4695
4696 // Ask user if they want to repair
4697 popup_manager_->Show("Project Repair");
4698 }
4699
4700 const std::optional<size_t> previous_session_id =
4703 pending_project_open_previous_session_id_ = previous_session_id;
4705 current_project_ = std::move(new_project);
4707
4708 auto project_status = LoadProjectWithRom();
4709 if (!project_status.ok()) {
4710 RestoreProjectContextAfterFailedOpen(previous_session_id);
4711 }
4712 return project_status;
4713 };
4714
4715#if defined(__APPLE__) && TARGET_OS_IOS == 1
4716 // On iOS, route project selection through the SwiftUI overlay document picker
4717 // so we get open-in-place + security-scoped access for iCloud Drive bundles.
4718 platform::ios::PostOverlayCommand("open_project");
4719 return absl::OkStatus();
4720#else
4722 return open_project_from_path(file_path);
4723#endif
4724}
4725
4727 const std::string& rom_path) {
4728 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4729
4730 Rom candidate_rom;
4731 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&candidate_rom, rom_path));
4733
4736 .candidate_path = rom_path,
4737 };
4738 return absl::OkStatus();
4739}
4740
4742 auto pending_selection = std::move(pending_project_rom_selection_);
4744 std::string project_rom_path = pending_selection.has_value()
4745 ? pending_selection->candidate_path
4747 const std::string previous_project_rom_path =
4748 pending_selection.has_value() ? pending_selection->previous_path
4750 bool persist_selected_rom = pending_selection.has_value();
4751 auto abandon_selected_rom = [&]() {
4752 if (!persist_selected_rom) {
4753 return;
4754 }
4755 current_project_.rom_filename = previous_project_rom_path;
4757 };
4758
4759 // Check if project has a ROM file specified
4760 if (project_rom_path.empty()) {
4761 // No ROM specified - prompt user to select one
4763 "Project has no ROM file configured. Please select a ROM.",
4765#if defined(__APPLE__) && TARGET_OS_IOS == 1
4766 // Guard: if the project lives inside a .yazeproj bundle the ROM path
4767 // defaults to bundle/rom, which may not exist yet because iCloud hasn't
4768 // finished the download. Popping the file picker here would let a
4769 // temporary path overwrite the correct bundle path in project.yaze.
4770 // Show guidance and let the user reopen the project once the download
4771 // is complete.
4772 {
4773 auto bundle_parent =
4774 std::filesystem::path(current_project_.filepath).parent_path();
4775 if (bundle_parent.extension() == ".yazeproj") {
4777 "ROM is downloading from iCloud. Reopen the project in a few "
4778 "seconds.",
4779 ToastType::kInfo, 6.0f);
4780 return absl::UnavailableError(
4781 "Project ROM is not available from iCloud yet");
4782 }
4783 }
4784 const std::string target_project_filepath = current_project_.filepath;
4785 const std::optional<size_t> target_session_id =
4787 const uint64_t selection_generation = ++project_rom_selection_generation_;
4790 [this, target_project_filepath, target_session_id,
4791 selection_generation](const std::string& rom_path) {
4792 if (selection_generation != project_rom_selection_generation_ ||
4793 current_project_.filepath != target_project_filepath ||
4794 active_project_context_session_id_ != target_session_id) {
4795 toast_manager_.Show(
4796 "ROM selection ignored because the active project changed",
4797 ToastType::kWarning);
4798 return;
4799 }
4800 const bool restore_pending_transition =
4802 const std::optional<size_t> previous_session_id =
4804 auto restore_after_failure = [&]() {
4805 if (restore_pending_transition) {
4806 RestoreProjectContextAfterFailedOpen(previous_session_id);
4807 }
4808 };
4809 if (rom_path.empty()) {
4810 restore_after_failure();
4811 return;
4812 }
4813 auto path_status =
4814 session_coordinator_->CheckBackingFileAvailable(rom_path);
4815 if (!path_status.ok()) {
4816 toast_manager_.Show(std::string(path_status.message()),
4818 restore_after_failure();
4819 return;
4820 }
4821 auto selection_status = ValidateProjectRomSelection(rom_path);
4822 if (!selection_status.ok()) {
4824 absl::StrFormat("Failed to update project ROM: %s",
4825 selection_status.message()),
4827 restore_after_failure();
4828 return;
4829 }
4830 auto status = LoadProjectWithRom();
4831 if (!status.ok()) {
4833 absl::StrFormat("Failed to load project ROM: %s",
4834 status.message()),
4836 restore_after_failure();
4837 }
4838 });
4839 return absl::OkStatus();
4840#else
4843 if (rom_path.empty()) {
4844 return absl::CancelledError("Project ROM selection cancelled");
4845 }
4846 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4847 project_rom_path = rom_path;
4848 persist_selected_rom = true;
4849#endif
4850 }
4851
4852 auto backing_status =
4853 session_coordinator_->CheckBackingFileAvailable(project_rom_path);
4854 if (!backing_status.ok()) {
4855 abandon_selected_rom();
4856 return backing_status;
4857 }
4858
4859 // Load ROM from project
4860 Rom temp_rom;
4861 auto load_status = rom_file_manager_.LoadRom(&temp_rom, project_rom_path);
4862 if (!load_status.ok()) {
4863 // ROM file not found or invalid - prompt user to select new ROM
4865 absl::StrFormat("Could not load ROM '%s': %s. Please select a new ROM.",
4866 project_rom_path, load_status.message()),
4868#if defined(__APPLE__) && TARGET_OS_IOS == 1
4869 abandon_selected_rom();
4870 // If the ROM is inside a .yazeproj bundle its path may not be readable
4871 // yet because iCloud hasn't finished downloading it. Saving any other
4872 // path here would corrupt the project file with a temporary location.
4873 // Show guidance and bail without touching current_project_.rom_filename.
4874 {
4875 auto rom_parent = std::filesystem::path(project_rom_path).parent_path();
4876 if (rom_parent.extension() == ".yazeproj") {
4878 "ROM is still downloading from iCloud. Try reopening the project "
4879 "in a moment.",
4880 ToastType::kInfo, 6.0f);
4881 return absl::UnavailableError(
4882 "Project ROM is still downloading from iCloud");
4883 }
4884 }
4885 const std::string target_project_filepath = current_project_.filepath;
4886 const std::optional<size_t> target_session_id =
4888 const uint64_t selection_generation = ++project_rom_selection_generation_;
4891 [this, target_project_filepath, target_session_id,
4892 selection_generation](const std::string& rom_path) {
4893 if (selection_generation != project_rom_selection_generation_ ||
4894 current_project_.filepath != target_project_filepath ||
4895 active_project_context_session_id_ != target_session_id) {
4896 toast_manager_.Show(
4897 "ROM selection ignored because the active project changed",
4898 ToastType::kWarning);
4899 return;
4900 }
4901 const bool restore_pending_transition =
4903 const std::optional<size_t> previous_session_id =
4905 auto restore_after_failure = [&]() {
4906 if (restore_pending_transition) {
4907 RestoreProjectContextAfterFailedOpen(previous_session_id);
4908 }
4909 };
4910 if (rom_path.empty()) {
4911 restore_after_failure();
4912 return;
4913 }
4914 auto path_status =
4915 session_coordinator_->CheckBackingFileAvailable(rom_path);
4916 if (!path_status.ok()) {
4917 toast_manager_.Show(std::string(path_status.message()),
4919 restore_after_failure();
4920 return;
4921 }
4922 auto selection_status = ValidateProjectRomSelection(rom_path);
4923 if (!selection_status.ok()) {
4925 absl::StrFormat("Failed to update project ROM: %s",
4926 selection_status.message()),
4928 restore_after_failure();
4929 return;
4930 }
4931 auto status = LoadProjectWithRom();
4932 if (!status.ok()) {
4934 absl::StrFormat("Failed to load project ROM: %s",
4935 status.message()),
4937 restore_after_failure();
4938 }
4939 });
4940 return absl::OkStatus();
4941#else
4944 if (rom_path.empty()) {
4945 return absl::CancelledError("Project ROM selection cancelled");
4946 }
4947 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4948 project_rom_path = rom_path;
4949 persist_selected_rom = true;
4950 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, rom_path));
4951#endif
4952 }
4953
4954 auto policy_status = rom_lifecycle_.CheckRomOpenPolicy(&temp_rom);
4955 if (!policy_status.ok()) {
4956 abandon_selected_rom();
4957 return policy_status;
4958 }
4959
4960 if (persist_selected_rom) {
4961 current_project_.rom_filename = project_rom_path;
4962 }
4963
4964 const size_t previous_session_count =
4965 session_coordinator_->GetTotalSessionCount();
4966 auto session_or = session_coordinator_->CreateSessionFromRom(
4967 std::move(temp_rom), project_rom_path);
4968 if (!session_or.ok()) {
4969 abandon_selected_rom();
4970 return session_or.status();
4971 }
4972 RomSession* session = *session_or;
4973
4977
4978 // Auto-enable custom object rendering when a project defines custom object
4979 // data but the stale feature flag is off.
4980 if (ProjectUsesCustomObjects(current_project_) &&
4983 LOG_WARN("EditorManager",
4984 "Project has custom object data but 'enable_custom_objects' was "
4985 "disabled. Enabling at runtime.");
4986 toast_manager_.Show("Custom object rendering auto-enabled for this project",
4988 }
4989 std::string legacy_mapping_warning;
4990 if (SeedLegacyTrackObjectMapping(&current_project_,
4991 &legacy_mapping_warning)) {
4992 LOG_WARN("EditorManager", "%s", legacy_mapping_warning.c_str());
4994 "Seeded default custom object mapping for object 0x31 (save project "
4995 "to persist)",
4997 }
4998
4999 // Apply project feature flags to both session and global singleton.
5003#if !defined(NDEBUG)
5004 LOG_INFO(
5005 "EditorManager",
5006 "Feature flags applied: kEnableCustomObjects=%s, "
5007 "custom_objects_folder='%s', custom_object_files=%zu entries",
5011#endif
5012
5015 if (version_manager_) {
5016 // Preserve the existing best-effort Git initialization behavior, now
5017 // against the stable session-owned project object.
5019 current_project_.git_repository = session->project_context->git_repository;
5020 }
5021
5022 if (auto* rom = GetCurrentRom(); rom && rom->is_loaded()) {
5023 if (IsRomHashMismatch()) {
5025 "Project ROM hash mismatch detected. Check ROM Identity settings.",
5027 }
5028 auto warnings = ValidateRomAddressOverrides(
5030 if (!warnings.empty()) {
5031 for (const auto& warning : warnings) {
5032 LOG_WARN("EditorManager", "%s", warning.c_str());
5033 }
5034 toast_manager_.Show(absl::StrFormat("ROM override warnings: %d (see log)",
5035 warnings.size()),
5037 }
5038 }
5039
5040 // Update test manager with current ROM for ROM-dependent tests (only when
5041 // tests are enabled)
5042#ifdef YAZE_ENABLE_TESTING
5043 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
5044 (void*)GetCurrentRom(),
5045 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
5047#endif
5048
5049 if (auto* editor_set = GetCurrentEditorSet();
5050 editor_set && !current_project_.code_folder.empty()) {
5051 const std::string absolute_code_folder =
5053 // iOS: avoid blocking the main thread during project open / scene updates.
5054#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
5055 editor_set->OpenAssemblyFolder(absolute_code_folder);
5056#endif
5057 // Also set the sidebar file browser path (refresh happens during UI draw).
5058 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
5059 }
5060
5061 // Initialize labels before loading editor assets so room lists / command
5062 // palette entries resolve project registry labels on first render.
5064
5065 auto asset_status = LoadAssetsForMode();
5066 if (!asset_status.ok()) {
5067 abandon_selected_rom();
5068 auto rollback_status =
5069 DiscardProvisionalSessionCreatedSince(previous_session_count);
5070 if (!rollback_status.ok()) {
5071 return absl::InternalError(
5072 absl::StrFormat("%s; session rollback failed: %s",
5073 asset_status.message(), rollback_status.message()));
5074 }
5075 return asset_status;
5076 }
5077
5078 if (persist_selected_rom) {
5079 auto save_status = current_project_.Save();
5080 if (!save_status.ok()) {
5081 abandon_selected_rom();
5082 auto rollback_status =
5083 DiscardProvisionalSessionCreatedSince(previous_session_count);
5084 if (!rollback_status.ok()) {
5085 return absl::InternalError(
5086 absl::StrFormat("%s; session rollback failed: %s",
5087 save_status.message(), rollback_status.message()));
5088 }
5089 return save_status;
5090 }
5092 }
5093
5094 // Hide welcome screen and show editor selection when project ROM is loaded
5095 if (ui_coordinator_) {
5096 ui_coordinator_->SetWelcomeScreenVisible(false);
5097 ui_coordinator_->SetEditorSelectionVisible(true);
5098 }
5099
5100 // Set Dashboard category to suppress panel drawing until user selects an editor
5102 /*notify=*/false);
5103
5104 // Apply workspace settings
5113 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
5114
5117
5125 project_management_panel_->SetBuildLogOutput("");
5126 }
5128 active_project_build_.reset();
5130
5131 // Publish project context for hack workflow panels and other consumers
5133
5134 // Add to recent files
5136 manager.AddFile(current_project_.filepath);
5137 manager.Save();
5138
5139 // Update project management panel with loaded project
5142 project_management_panel_->SetVersionManager(version_manager_);
5144 }
5145
5146 toast_manager_.Show(absl::StrFormat("Project '%s' loaded successfully",
5149
5152 return absl::OkStatus();
5153}
5154
5156 Rom&& rom, const std::string& filepath) {
5157 if (!session_coordinator_) {
5158 return absl::FailedPreconditionError("No session coordinator");
5159 }
5160 auto* session = session_coordinator_->GetActiveRomSession();
5161 if (!session) {
5162 return absl::FailedPreconditionError("No active ROM session");
5163 }
5164
5165 Rom previous_rom = session->rom;
5166 const std::string previous_filepath = session->filepath;
5167 const bool game_data_was_loaded = session->game_data_loaded;
5168 auto* dungeon_editor = static_cast<DungeonEditorV2*>(
5169 session->editors.GetExistingEditor(EditorType::kDungeon));
5170 const size_t dungeon_index = EditorTypeIndex(EditorType::kDungeon);
5171 const bool dungeon_was_initialized =
5172 session->editor_initialized[dungeon_index];
5173 const bool dungeon_assets_were_loaded =
5174 session->editor_assets_loaded[dungeon_index];
5175 auto* screen_editor = static_cast<ScreenEditor*>(
5176 session->editors.GetExistingEditor(EditorType::kScreen));
5177 const size_t screen_index = EditorTypeIndex(EditorType::kScreen);
5178 const bool screen_was_initialized = session->editor_initialized[screen_index];
5179 const bool screen_assets_were_loaded =
5180 session->editor_assets_loaded[screen_index];
5181 auto restore_dungeon_asset_state = [&]() {
5182 if (dungeon_editor) {
5183 session->editor_initialized[dungeon_index] =
5184 session->editor_initialized[dungeon_index] || dungeon_was_initialized;
5185 session->editor_assets_loaded[dungeon_index] =
5186 session->editor_assets_loaded[dungeon_index] ||
5187 dungeon_assets_were_loaded;
5188 }
5189 };
5190 auto restore_screen_asset_state = [&]() {
5191 if (screen_editor) {
5192 session->editor_initialized[screen_index] =
5193 session->editor_initialized[screen_index] || screen_was_initialized;
5194 session->editor_assets_loaded[screen_index] =
5195 session->editor_assets_loaded[screen_index] ||
5196 screen_assets_were_loaded;
5197 }
5198 };
5199
5200 session->editors.InvalidateScreenRomBackedState();
5201 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5202 session->rom = std::move(rom);
5203 session->filepath = filepath;
5204 session->game_data.Clear();
5205 session->game_data.set_rom(&session->rom);
5206 session->editors.SetGameData(&session->game_data);
5207 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5208 ResetAssetState(session);
5209 ConfigureSession(session);
5212 auto load_status = LoadAssetsForMode();
5213 if (load_status.ok() && game_data_was_loaded && !session->game_data_loaded) {
5214 load_status = EnsureGameDataLoaded();
5215 }
5216 if (load_status.ok() && screen_editor && screen_assets_were_loaded) {
5217 load_status = screen_editor->RefreshRomBackedState();
5218 }
5219 if (load_status.ok()) {
5220 restore_screen_asset_state();
5221 }
5222 if (load_status.ok() && dungeon_editor) {
5223 load_status = dungeon_editor->RefreshRomBackedState();
5224 if (load_status.ok()) {
5225 restore_dungeon_asset_state();
5226 }
5227 }
5228 if (!load_status.ok()) {
5229 // Loading editors is fallible. Restore the prior ROM and rebuild its
5230 // assets so a failed reload never leaves a half-initialized live session.
5231 session->editors.InvalidateScreenRomBackedState();
5232 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5233 session->rom = std::move(previous_rom);
5234 session->filepath = previous_filepath;
5235 session->game_data.Clear();
5236 session->game_data.set_rom(&session->rom);
5237 session->editors.SetGameData(&session->game_data);
5238 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5239 ResetAssetState(session);
5240 ConfigureSession(session);
5243 auto rollback_status = LoadAssetsForMode();
5244 if (game_data_was_loaded && !session->game_data_loaded) {
5245 auto game_data_status = EnsureGameDataLoaded();
5246 if (rollback_status.ok()) {
5247 rollback_status = game_data_status;
5248 }
5249 }
5250 if (screen_editor && screen_assets_were_loaded) {
5251 auto screen_status = screen_editor->RefreshRomBackedState();
5252 if (screen_status.ok()) {
5253 restore_screen_asset_state();
5254 }
5255 if (rollback_status.ok()) {
5256 rollback_status = screen_status;
5257 }
5258 } else {
5259 restore_screen_asset_state();
5260 }
5261 if (dungeon_editor) {
5262 auto dungeon_status = dungeon_editor->RefreshRomBackedState();
5263 if (dungeon_status.ok()) {
5264 restore_dungeon_asset_state();
5265 }
5266 if (rollback_status.ok()) {
5267 rollback_status = dungeon_status;
5268 }
5269 }
5271 if (!rollback_status.ok()) {
5272 return absl::InternalError(absl::StrFormat(
5273 "ROM reload failed (%s) and restoring prior assets failed (%s)",
5274 load_status.message(), rollback_status.message()));
5275 }
5276 return load_status;
5277 }
5279 return absl::OkStatus();
5280}
5281
5282absl::Status EditorManager::SwapProjectRom(const std::string& rom_path) {
5283 if (rom_path.empty()) {
5284 return absl::InvalidArgumentError("ROM path cannot be empty");
5285 }
5286 if (!session_coordinator_) {
5287 return absl::FailedPreconditionError("No session coordinator");
5288 }
5289 auto* session = session_coordinator_->GetActiveRomSession();
5290 if (!session || !session->rom.is_loaded() ||
5292 return absl::FailedPreconditionError("No active project ROM session");
5293 }
5295 return absl::FailedPreconditionError(
5296 "Save or discard ROM edits before swapping the project ROM");
5297 }
5299 if (session->project_file_editor_state.initialized &&
5300 session->project_file_editor_state.modified &&
5302 return absl::FailedPreconditionError(
5303 "Preserve the raw project-file draft before swapping ROMs");
5304 }
5305
5306 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5307 rom_path, session->session_id()));
5308 Rom replacement_rom;
5309 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom, rom_path));
5311
5312 const std::string old_rom_path = current_project_.rom_filename;
5313 current_project_.rom_filename = rom_path;
5314 auto save_status = current_project_.Save();
5315 if (!save_status.ok()) {
5316 current_project_.rom_filename = old_rom_path;
5317 return save_status;
5318 }
5319
5321 auto replace_status =
5322 ReplaceActiveSessionRom(std::move(replacement_rom), rom_path);
5323 if (!replace_status.ok()) {
5324 current_project_.rom_filename = old_rom_path;
5325 auto rollback_status = current_project_.Save();
5328 if (!rollback_status.ok()) {
5329 return absl::InternalError(absl::StrFormat(
5330 "ROM swap failed (%s) and restoring the project descriptor failed "
5331 "(%s)",
5332 replace_status.message(), rollback_status.message()));
5333 }
5334 return replace_status;
5335 }
5336 session->project_dirty = false;
5338 project_management_panel_->SetProjectDirty(false);
5339 }
5342 return absl::OkStatus();
5343}
5344
5346 if (!session_coordinator_) {
5347 return absl::FailedPreconditionError("No session coordinator");
5348 }
5349 auto* session = session_coordinator_->GetActiveRomSession();
5350 if (!session || !session->rom.is_loaded() ||
5353 return absl::FailedPreconditionError("No active project ROM to reload");
5354 }
5356 return absl::FailedPreconditionError(
5357 "Save or discard ROM edits before reloading from disk");
5358 }
5359
5360 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5361 current_project_.rom_filename, session->session_id()));
5362 Rom replacement_rom;
5363 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom,
5366 return ReplaceActiveSessionRom(std::move(replacement_rom),
5368}
5369
5373 if (auto* session = session_coordinator_->GetActiveRomSession();
5374 session && session->project_file_editor_state.initialized &&
5375 session->project_file_editor_state.modified &&
5377 return absl::FailedPreconditionError(
5378 "Raw project file also has unsaved changes; use raw Save As to "
5379 "preserve that draft before saving project settings");
5380 }
5381 }
5385 return CreateNewProject();
5386 }
5387
5388 // Update project with current settings
5391 auto* session = session_coordinator_->GetActiveRomSession();
5392 if (session) {
5393 current_project_.feature_flags = session->feature_flags;
5394 }
5395 }
5396
5405
5406 // Save recent files
5409 for (const auto& file : manager.GetRecentFiles()) {
5411 }
5412 }
5413
5414 auto status = current_project_.Save();
5415 if (status.ok()) {
5417 project_management_panel_->SetProjectDirty(false);
5418 }
5420 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5421 session->project_dirty = false;
5422 }
5423 }
5425 }
5427 return status;
5428}
5429
5432 project_management_panel_->SetProjectDirty(true);
5433 }
5435 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5436 session->project_dirty = true;
5437 }
5438 }
5439}
5440
5442 if (!session_coordinator_) {
5444 project_management_panel_->IsProjectDirty();
5445 }
5446 const auto* session = session_coordinator_->GetActiveRomSession();
5447 return session != nullptr &&
5448 (session->project_dirty ||
5449 session->editors.HasPendingProjectDraftChanges());
5450}
5451
5453 if (!session_coordinator_) {
5454 return absl::FailedPreconditionError("No session coordinator");
5455 }
5456 auto* session = session_coordinator_->GetActiveRomSession();
5457 if (!session) {
5458 return absl::FailedPreconditionError("No active ROM session");
5459 }
5460
5462 const bool project_dirty = session->project_dirty ||
5463 session->editors.HasPendingProjectDraftChanges();
5464 const bool project_file_dirty =
5465 session->project_file_editor_state.initialized &&
5466 session->project_file_editor_state.modified;
5467
5468 if (project_dirty && project_file_dirty &&
5470 return absl::FailedPreconditionError(
5471 "Project settings and raw project file both have unsaved changes; "
5472 "use raw Save As to preserve the draft, then save project settings");
5473 }
5474
5475 if (project_dirty) {
5477 current_project_.filepath.empty()) {
5478 return absl::FailedPreconditionError(
5479 "Project settings have no project file to save");
5480 }
5482 }
5483
5484 if (project_file_dirty) {
5485 if (project_file_editor_.filepath().empty()) {
5486 return absl::FailedPreconditionError(
5487 "Project file draft has no destination; use Save As first");
5488 }
5490 session->project_file_editor_state = project_file_editor_.CaptureState();
5491 }
5492
5493 return absl::OkStatus();
5494}
5495
5497 if (!session_coordinator_) {
5498 return absl::FailedPreconditionError("No session coordinator");
5499 }
5500 const size_t session_index = GetCurrentSessionIndex();
5502 const auto* session = session_coordinator_->GetActiveRomSession();
5503 if (session != nullptr && session->backup_restore_pending) {
5504 return absl::CancelledError(
5505 "Staged backup restore requires an explicit Save ROM");
5506 }
5507 if (SessionHasPendingRomWork(session_index)) {
5509 }
5510 if (SessionHasPendingUnsavedWork(session_index)) {
5511 return absl::FailedPreconditionError(
5512 "Autosave left unsaved project or ROM work");
5513 }
5514 return absl::OkStatus();
5515}
5516
5518 PotItemSaveDecision decision) {
5519 // Map EditorManager enum to RomLifecycleManager enum
5520 auto lifecycle_decision =
5522 static_cast<int>(decision));
5524
5525 if (decision == PotItemSaveDecision::kCancel) {
5527 toast_manager_.Show("Save cancelled", ToastType::kInfo);
5528 return;
5529 }
5530
5531 auto status = ResumePendingRomSave();
5532 if (!absl::IsCancelled(status) && !status.ok()) {
5534 absl::StrFormat("Failed to save ROM: %s", status.message()),
5536 }
5537}
5538
5540 // Get current project name for default filename
5541 std::string default_name = current_project_.project_opened()
5543 : "untitled_project";
5544
5545 auto file_path =
5546 util::FileDialogWrapper::ShowSaveFileDialog(default_name, "yaze");
5547 if (file_path.empty()) {
5548 return absl::OkStatus();
5549 }
5550
5551 return SaveProjectAs(file_path);
5552}
5553
5554absl::Status EditorManager::SaveProjectAs(const std::string& filepath) {
5555 if (filepath.empty()) {
5556 return absl::InvalidArgumentError("Project file path cannot be empty");
5557 }
5559 std::string file_path = filepath;
5560
5561 // Ensure a project extension.
5562 if (!(absl::EndsWith(file_path, ".yaze") ||
5563 absl::EndsWith(file_path, ".yazeproj"))) {
5564 file_path += ".yaze";
5565 }
5566
5568 if (auto* session = session_coordinator_->GetActiveRomSession();
5569 session && session->project_file_editor_state.initialized &&
5570 session->project_file_editor_state.modified &&
5572 session->project_file_editor_state.filepath, file_path)) {
5573 return absl::FailedPreconditionError(
5574 "Raw project draft targets the selected file; preserve it with "
5575 "raw Save As before overwriting that destination");
5576 }
5577 }
5578
5581
5582 // Update project filepath and save
5583 std::string old_filepath = current_project_.filepath;
5584 current_project_.filepath = file_path;
5585
5586 auto save_status = current_project_.Save();
5587 if (save_status.ok()) {
5589 project_management_panel_->SetProjectDirty(false);
5590 }
5592 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5593 session->project_dirty = false;
5594 }
5595 }
5597 RebaseCleanProjectFileDraft(file_path);
5598
5599 // Add to recent files
5601 manager.AddFile(file_path);
5602 manager.Save();
5603
5604 toast_manager_.Show(absl::StrFormat("Project saved as: %s", file_path),
5606 } else {
5607 // Restore old filepath on failure
5608 current_project_.filepath = old_filepath;
5610 absl::StrFormat("Failed to save project: %s", save_status.message()),
5612 }
5613
5615
5616 return save_status;
5617}
5618
5619absl::StatusOr<std::string> EditorManager::ResolveProjectBuildCommand() const {
5621 return absl::FailedPreconditionError("No project open");
5622 }
5623
5624 std::string command = current_project_.build_script;
5625 if (command.empty() && current_project_.hack_manifest.loaded()) {
5627 }
5628 if (command.empty()) {
5629 return absl::NotFoundError("Project does not define a build command");
5630 }
5631 return command;
5632}
5633
5634absl::StatusOr<std::string> EditorManager::ResolveProjectRunTarget() const {
5636 return absl::FailedPreconditionError("No project open");
5637 }
5638
5639 std::string target;
5642 }
5643 if (target.empty()) {
5645 }
5646 if (target.empty()) {
5647 return absl::NotFoundError("Project does not define a run target ROM");
5648 }
5649 return current_project_.GetAbsolutePath(target);
5650}
5651
5653 const std::string& summary, const std::string& detail,
5654 ProjectWorkflowState state, const std::string& output_tail,
5655 bool can_cancel) const {
5656 return {.visible = true,
5657 .can_cancel = can_cancel,
5658 .label = "Build",
5659 .summary = summary,
5660 .detail = detail,
5661 .output_tail = output_tail,
5662 .state = state};
5663}
5664
5666 const std::string& summary, const std::string& detail,
5667 ProjectWorkflowState state) const {
5668 return {.visible = true,
5669 .label = "Run",
5670 .summary = summary,
5671 .detail = detail,
5672 .state = state};
5673}
5674
5675absl::StatusOr<std::string> EditorManager::RunProjectBuildCommand() {
5676 auto command_or = ResolveProjectBuildCommand();
5677 if (!command_or.ok()) {
5678 return command_or.status();
5679 }
5680
5681 const std::string command = *command_or;
5682 const std::filesystem::path project_root =
5683 std::filesystem::path(current_project_.filepath).parent_path();
5685 auto start_status = task.Start(command, project_root.string());
5686 if (!start_status.ok()) {
5687 return start_status;
5688 }
5689 auto wait_status = task.Wait();
5690 const auto snapshot = task.GetSnapshot();
5691 if (!wait_status.ok()) {
5692 return wait_status;
5693 }
5694 const std::string summary = LastNonEmptyLine(snapshot.output);
5695 return summary.empty() ? command : summary;
5696}
5697
5699 const std::string running_detail =
5700 "Running the configured project build command";
5701 UpdateBuildWorkflowStatus(
5703 MakeBuildStatus("Build running", running_detail,
5704 ProjectWorkflowState::kRunning, "", false));
5705
5706 auto result_or = RunProjectBuildCommand();
5707 if (!result_or.ok()) {
5708 UpdateBuildWorkflowStatus(
5710 MakeBuildStatus("Build failed",
5711 std::string(result_or.status().message()),
5714 absl::StrFormat("Build unavailable: %s", result_or.status().message()),
5716 return result_or.status();
5717 }
5718
5719 const std::string summary = *result_or;
5720 UpdateBuildWorkflowStatus(&status_bar_, project_management_panel_.get(),
5721 MakeBuildStatus("Build succeeded", summary,
5724 summary.empty() ? "Project build completed"
5725 : absl::StrFormat("Project build completed: %s", summary),
5727 LOG_INFO("EditorManager", "Project build completed: %s", summary.c_str());
5728 return absl::OkStatus();
5729}
5730
5733 const auto snapshot = active_project_build_->GetSnapshot();
5734 if (snapshot.running) {
5735 toast_manager_.Show("A project build is already running",
5737 return;
5738 }
5739 }
5740
5741 auto command_or = ResolveProjectBuildCommand();
5742 if (!command_or.ok()) {
5743 UpdateBuildWorkflowStatus(
5745 MakeBuildStatus("Build unavailable",
5746 std::string(command_or.status().message()),
5749 absl::StrFormat("Build unavailable: %s", command_or.status().message()),
5751 return;
5752 }
5753
5754 const std::filesystem::path project_root =
5755 std::filesystem::path(current_project_.filepath).parent_path();
5756 active_project_build_ = std::make_unique<BackgroundCommandTask>();
5757 auto start_status =
5758 active_project_build_->Start(*command_or, project_root.string());
5759 if (!start_status.ok()) {
5760 UpdateBuildWorkflowStatus(
5762 MakeBuildStatus("Build unavailable",
5763 std::string(start_status.message()),
5766 absl::StrFormat("Build unavailable: %s", start_status.message()),
5768 active_project_build_.reset();
5769 return;
5770 }
5771
5773 UpdateBuildWorkflowStatus(
5775 MakeBuildStatus("Build running",
5776 "Running the configured project build command",
5779 project_management_panel_->SetBuildLogOutput("");
5780 }
5782}
5783
5785 if (!active_project_build_) {
5786 return;
5787 }
5788
5789 const auto snapshot = active_project_build_->GetSnapshot();
5790 if (!snapshot.running) {
5791 return;
5792 }
5793
5794 active_project_build_->Cancel();
5795 UpdateBuildWorkflowStatus(
5797 MakeBuildStatus("Cancelling build", "Stopping the active project build",
5798 ProjectWorkflowState::kRunning, snapshot.output_tail,
5799 false));
5800}
5801
5803 UpdateRunWorkflowStatus(
5805 MakeRunStatus("Run queued",
5806 "Preparing the project output for emulator reload",
5808
5809 auto run_target_or = ResolveProjectRunTarget();
5810 if (!run_target_or.ok()) {
5811 UpdateRunWorkflowStatus(
5813 MakeRunStatus("Run unavailable",
5814 std::string(run_target_or.status().message()),
5816 toast_manager_.Show(absl::StrFormat("Run unavailable: %s",
5817 run_target_or.status().message()),
5819 return run_target_or.status();
5820 }
5821 const std::string run_target = *run_target_or;
5822 if (!std::filesystem::exists(run_target)) {
5823 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5824 MakeRunStatus("Run target missing", run_target,
5826 toast_manager_.Show("Run target ROM not found. Build the project first.",
5828 return absl::NotFoundError("Run target ROM not found");
5829 }
5830
5831#ifdef YAZE_WITH_GRPC
5832 if (auto* emulator_backend = Application::Instance().GetEmulatorBackend()) {
5833 auto load_status = emulator_backend->LoadRom(run_target);
5834 if (load_status.ok()) {
5835 AppendWorkflowHistoryEntry(
5836 "Run",
5837 MakeRunStatus("Reloaded in backend", run_target,
5839 "");
5840 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5841 MakeRunStatus("Reloaded in backend", run_target,
5843 if (ui_coordinator_) {
5844 ui_coordinator_->SetEmulatorVisible(true);
5845 }
5847 absl::StrFormat("Reloaded project output in emulator backend: %s",
5848 run_target),
5850 return absl::OkStatus();
5851 }
5852 }
5853#endif
5854
5855 Rom temp_rom;
5856 auto load_rom_status = rom_file_manager_.LoadRom(&temp_rom, run_target);
5857 if (!load_rom_status.ok()) {
5858 UpdateRunWorkflowStatus(
5860 MakeRunStatus("Run load failed", std::string(load_rom_status.message()),
5862 toast_manager_.Show(absl::StrFormat("Failed to load run target ROM: %s",
5863 load_rom_status.message()),
5865 return load_rom_status;
5866 }
5867
5868 auto reload_status = emulator_.ReloadRuntimeRom(temp_rom.vector());
5869 if (!reload_status.ok()) {
5870 UpdateRunWorkflowStatus(
5872 MakeRunStatus("Reload failed", std::string(reload_status.message()),
5874 toast_manager_.Show(absl::StrFormat("Failed to reload emulator runtime: %s",
5875 reload_status.message()),
5877 return reload_status;
5878 }
5879
5880 if (ui_coordinator_) {
5881 ui_coordinator_->SetEmulatorVisible(true);
5882 }
5883 AppendWorkflowHistoryEntry("Run",
5884 MakeRunStatus("Reloaded in emulator", run_target,
5886 "");
5887 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5888 MakeRunStatus("Reloaded in emulator", run_target,
5891 absl::StrFormat("Reloaded project output in emulator: %s", run_target),
5893 return absl::OkStatus();
5894}
5895
5896absl::Status EditorManager::ImportProject(const std::string& project_path) {
5897 // Delegate to ProjectManager for import logic
5899 // Sync local project reference
5903 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5906 }
5907 }
5910 return absl::OkStatus();
5911}
5912
5915 return absl::FailedPreconditionError("No project is currently open");
5916 }
5917
5920 toast_manager_.Show("Project repaired successfully",
5922
5923 return absl::OkStatus();
5924}
5925
5927 if (auto* editor_set = GetCurrentEditorSet()) {
5928 return editor_set->GetOverworldData();
5929 }
5930 return nullptr;
5931}
5932
5934 if (!rom) {
5935 return absl::InvalidArgumentError("Invalid ROM pointer");
5936 }
5937
5938 // We need to find the session that owns this ROM.
5939 // This is inefficient but SetCurrentRom is rare.
5941 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
5942 auto* session =
5943 static_cast<RomSession*>(session_coordinator_->GetSession(i));
5944 if (session && &session->rom == rom) {
5945 session_coordinator_->SwitchToSession(i);
5946 // Update test manager with current ROM for ROM-dependent tests
5950 return absl::OkStatus();
5951 }
5952 }
5953 }
5954 // If ROM wasn't found in existing sessions, treat as new session.
5955 // Copying an external ROM object is avoided; instead, fail.
5956 return absl::NotFoundError("ROM not found in existing sessions");
5957}
5958
5963
5967
5968// IsRomHashMismatch() is now inline in editor_manager.h delegating to
5969// rom_lifecycle_.
5970
5971std::vector<RomFileManager::BackupEntry> EditorManager::GetRomBackups() const {
5973}
5974
5976 if (!session_coordinator_) {
5977 return false;
5978 }
5979 const auto* session = session_coordinator_->GetActiveRomSession();
5980 return session != nullptr && session->backup_restore_pending;
5981}
5982
5983absl::Status EditorManager::RestoreRomBackup(const std::string& backup_path) {
5984 auto* rom = GetCurrentRom();
5985 if (!rom) {
5986 return absl::FailedPreconditionError("No ROM loaded");
5987 }
5989 return absl::FailedPreconditionError(
5990 "Save or discard ROM edits before restoring a backup");
5991 }
5992
5993 const std::string original_filename = rom->filename();
5994 const project::ResourceLabelManager original_resource_labels =
5995 *rom->resource_label();
5996 const auto backups = rom_file_manager_.ListBackups(original_filename);
5997 const bool is_managed_backup =
5998 std::any_of(backups.begin(), backups.end(), [&](const auto& backup) {
5999 return SessionCoordinator::PathsReferToSameBackingFile(backup.path,
6000 backup_path);
6001 });
6002 if (!is_managed_backup) {
6003 return absl::InvalidArgumentError(
6004 "Selected file is not a managed backup for the active ROM");
6005 }
6006
6007 // Load the backup away from the live session. Backups may legitimately have
6008 // a different size when they predate a ROM expansion or shrink. Reusing the
6009 // ROM-replacement path restores the prior ROM bytes and backing identity if
6010 // rebuilding assets fails partway through.
6011 Rom restored_rom;
6012 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&restored_rom, backup_path));
6013
6014 if (!original_filename.empty()) {
6015 restored_rom.set_filename(original_filename);
6016 }
6018 // Normal ROM backups do not copy the active `.labels` sidecar. Preserve the
6019 // session's loaded and in-memory resource labels instead of replacing them
6020 // with the usually-empty `<backup>.labels` lookup from LoadFromFile().
6021 *restored_rom.resource_label() = original_resource_labels;
6022 // Restore is intentionally staged in memory. Mark it dirty so closing or
6023 // autosave cannot silently treat the on-disk source as already restored.
6024 restored_rom.set_dirty(true);
6026 ReplaceActiveSessionRom(std::move(restored_rom), original_filename));
6027 auto* restored_session = session_coordinator_->GetActiveRomSession();
6028 if (!restored_session) {
6029 return absl::InternalError(
6030 "ROM backup restored without an active session to stage it");
6031 }
6032 restored_session->backup_restore_pending = true;
6033 return absl::OkStatus();
6034}
6035
6037 if (!session_coordinator_) {
6038 return absl::FailedPreconditionError("No session coordinator");
6039 }
6040 auto* session = session_coordinator_->GetActiveRomSession();
6041 if (!session || !session->rom.is_loaded()) {
6042 return absl::FailedPreconditionError("No ROM loaded");
6043 }
6044 if (!session->backup_restore_pending) {
6045 return absl::FailedPreconditionError("No restored backup is staged");
6046 }
6047
6048 const size_t session_index = GetCurrentSessionIndex();
6049 if (session->editors.HasPendingGraphicsChanges() ||
6050 session->editors.HasPendingScreenChanges() ||
6051 HasPendingDungeonChangesForSession(session_index) ||
6052 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
6053 return absl::FailedPreconditionError(
6054 "Resolve pending graphics, screen, dungeon, or palette edits before "
6055 "discarding the restored backup");
6056 }
6057
6058 const std::string backing_path = session->rom.filename();
6059 if (backing_path.empty()) {
6060 return absl::FailedPreconditionError("ROM has no backing file to reload");
6061 }
6062
6063 // Load into scratch storage before touching the live session so file I/O
6064 // failures preserve the staged ROM. Resource labels are session work, not
6065 // part of normal ROM backups, so preserve them across the byte-level discard.
6066 const project::ResourceLabelManager resource_labels =
6067 *session->rom.resource_label();
6068 Rom backing_rom;
6069 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&backing_rom, backing_path));
6070 *backing_rom.resource_label() = resource_labels;
6072 ReplaceActiveSessionRom(std::move(backing_rom), backing_path));
6073
6074 session->backup_restore_pending = false;
6075 session->rom.ClearDirty();
6076 return absl::OkStatus();
6077}
6078
6082
6090
6094
6098
6101 session_coordinator_->CreateNewSession();
6102 // Toast messages are now shown by SessionCoordinator
6103 }
6104}
6105
6108 session_coordinator_->DuplicateCurrentSession();
6109 }
6110}
6111
6113 if (!session_coordinator_) {
6114 return;
6115 }
6116
6117 if (!session_coordinator_->HasMultipleSessions()) {
6118 // Preserve the coordinator's existing warning without offering a
6119 // "Close Without Saving" action that cannot close the final session.
6120 session_coordinator_->CloseCurrentSession();
6121 return;
6122 }
6123
6124 const size_t current_session_id = GetCurrentSessionId();
6127 current_session_id})) {
6128 return;
6129 }
6130
6131 session_coordinator_->CloseCurrentSession();
6133}
6134
6136 if (!session_coordinator_ ||
6137 !session_coordinator_->IsValidSessionIndex(index)) {
6138 return;
6139 }
6140
6141 const size_t session_id = session_coordinator_->GetSessionId(index);
6144 session_id})) {
6145 return;
6146 }
6147
6148 session_coordinator_->RemoveSession(index);
6150}
6151
6153 if (!session_coordinator_) {
6154 return;
6155 }
6156
6157 const size_t current_index = GetCurrentSessionIndex();
6158 if (index == current_index ||
6159 !session_coordinator_->IsValidSessionIndex(index)) {
6160 return;
6161 }
6162
6163 const size_t current_session_id = GetCurrentSessionId();
6164 const size_t target_session_id = session_coordinator_->GetSessionId(index);
6167 current_session_id, target_session_id})) {
6168 return;
6169 }
6170
6171 session_coordinator_->SwitchToSession(index);
6172}
6173
6177 return;
6178 }
6179
6180 quit_ = true;
6181}
6182
6186
6188 size_t session_id) const {
6189 if (!session_coordinator_ || session_id == SIZE_MAX) {
6190 return std::nullopt;
6191 }
6192
6193 for (size_t index = 0; index < session_coordinator_->GetTotalSessionCount();
6194 ++index) {
6195 if (session_coordinator_->GetSessionId(index) == session_id) {
6196 return index;
6197 }
6198 }
6199 return std::nullopt;
6200}
6201
6204 return "";
6205 }
6206
6207 const auto& action = *pending_unsaved_session_action_;
6208 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6209 return absl::StrFormat("%s\n\nQuitting now will discard them.",
6211 }
6212
6213 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6214 const std::string session_name =
6215 source_index.has_value()
6216 ? session_coordinator_->GetSessionDisplayName(*source_index)
6217 : "Closed Session";
6218 const std::string work = source_index.has_value()
6219 ? DescribePendingUnsavedWork(*source_index)
6220 : "unsaved work";
6221
6222 switch (action.type) {
6225 return absl::StrFormat(
6226 "Session '%s' has %s. Opening another ROM or project will not save "
6227 "them automatically.",
6228 session_name, work);
6230 return absl::StrFormat(
6231 "Session '%s' has %s. Opening another project will not save them "
6232 "automatically.",
6233 session_name, work);
6235 return absl::StrFormat(
6236 "Session '%s' has %s. Switching sessions will leave them behind in "
6237 "this session.",
6238 session_name, work);
6240 return absl::StrFormat(
6241 "Session '%s' has %s. Closing it now will discard them.",
6242 session_name, work);
6244 break;
6245 }
6246
6247 return "";
6248}
6249
6252 return "Save Work";
6253 }
6254
6255 switch (pending_unsaved_session_action_->type) {
6258 return "Save Work & Open";
6260 return "Save Work & Open Project";
6262 return "Save Work & Switch";
6264 return "Save Work & Close";
6266 return ModifiedSessionCount() == 1 ? "Save Work & Quit"
6267 : "Save Modified Work & Quit";
6268 }
6269
6270 return "Save Work";
6271}
6272
6275 return "Continue";
6276 }
6277
6278 switch (pending_unsaved_session_action_->type) {
6281 return "Open Without Saving";
6283 return "Open Project Without Saving";
6285 return "Switch Without Saving";
6287 return "Close Without Saving";
6289 return "Quit Without Saving";
6290 }
6291
6292 return "Continue";
6293}
6294
6297 return;
6298 }
6299
6300 const auto action = *pending_unsaved_session_action_;
6302
6303 if (popup_manager_) {
6305 }
6306
6307 const size_t original_session_id = GetCurrentSessionId();
6308 auto save_modified_session = [this](size_t session_id) -> absl::Status {
6309 const auto session_index = ResolveSessionIndexById(session_id);
6310 if (!session_index.has_value()) {
6311 return absl::FailedPreconditionError(
6312 "The ROM session is no longer available");
6313 }
6314 if (!SessionHasPendingUnsavedWork(*session_index)) {
6315 return absl::OkStatus();
6316 }
6317
6318 session_coordinator_->SwitchToSession(*session_index);
6320 if (SessionHasPendingRomWork(*session_index)) {
6322 }
6323
6324 // A successful file write does not necessarily mean every pending editor
6325 // domain participated in the save. For example, dungeon palette saving is
6326 // user-configurable. Never execute a destructive follow-up while work that
6327 // the confirmation dialog promised to save is still pending.
6328 if (SessionHasPendingUnsavedWork(*session_index)) {
6329 return absl::FailedPreconditionError(
6330 absl::StrFormat("Save completed, but session still has %s",
6331 DescribePendingUnsavedWork(*session_index)));
6332 }
6333 return absl::OkStatus();
6334 };
6335
6336 absl::Status save_status = absl::OkStatus();
6337 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6339 std::vector<size_t> session_ids;
6340 session_ids.reserve(session_coordinator_->GetTotalSessionCount());
6341 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount();
6342 ++i) {
6343 session_ids.push_back(session_coordinator_->GetSessionId(i));
6344 }
6345 for (const size_t session_id : session_ids) {
6346 save_status = save_modified_session(session_id);
6347 if (!save_status.ok()) {
6348 break;
6349 }
6350 }
6351 }
6352 } else {
6353 save_status = save_modified_session(action.source_session_id);
6354 }
6355
6356 if (!save_status.ok()) {
6357 const bool resumable_confirmation = absl::IsCancelled(save_status) &&
6360 const auto original_session_index =
6361 ResolveSessionIndexById(original_session_id);
6362 if (!resumable_confirmation && original_session_index.has_value()) {
6363 session_coordinator_->SwitchToSession(*original_session_index);
6364 }
6365
6366 if (absl::IsCancelled(save_status)) {
6367 toast_manager_.Show("Save paused. Finish saving, then retry your action.",
6369 } else {
6371 absl::StrFormat("Failed to save before continuing: %s",
6372 save_status.message()),
6374 }
6375 return;
6376 }
6377
6378 // Saving an inactive close target temporarily activates that session. Put
6379 // the original session back before compacting the target index so the same
6380 // stable session remains active after the close.
6382 original_session_id != action.target_session_id) {
6383 const auto original_session_index =
6384 ResolveSessionIndexById(original_session_id);
6385 if (original_session_index.has_value()) {
6386 session_coordinator_->SwitchToSession(*original_session_index);
6387 }
6388 }
6389
6391}
6392
6405
6412
6416 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6417 const bool has_pending_work =
6420 : source_index.has_value() &&
6421 SessionHasPendingUnsavedWork(*source_index);
6422 if (!has_pending_work) {
6423 return true;
6424 }
6425
6426 pending_unsaved_session_action_ = std::move(action);
6427 if (popup_manager_) {
6429 }
6430 return false;
6431}
6432
6434 const PendingUnsavedSessionAction& action) {
6435 switch (action.type) {
6437 auto status = LoadRomInternal();
6438 if (!status.ok()) {
6440 absl::StrFormat("Failed to load ROM: %s", status.message()),
6442 }
6443 break;
6444 }
6446 auto status = OpenRomOrProjectInternal(action.path);
6447 if (!status.ok()) {
6448 toast_manager_.Show(absl::StrFormat("Failed to open ROM / project: %s",
6449 status.message()),
6451 }
6452 break;
6453 }
6455 auto status = OpenProjectInternal();
6456 if (!status.ok()) {
6458 absl::StrFormat("Failed to open project: %s", status.message()),
6460 }
6461 break;
6462 }
6465 const auto target_index =
6467 if (target_index.has_value()) {
6468 session_coordinator_->SwitchToSession(*target_index);
6469 }
6470 }
6471 break;
6474 const auto target_index =
6476 if (target_index.has_value()) {
6477 session_coordinator_->RemoveSession(*target_index);
6479 }
6480 }
6481 break;
6483 quit_ = true;
6484 break;
6485 }
6486}
6487
6488bool EditorManager::SessionHasPendingUnsavedWork(size_t session_index) const {
6489 if (!session_coordinator_ ||
6490 !session_coordinator_->IsValidSessionIndex(session_index)) {
6491 return false;
6492 }
6493
6494 auto* session =
6495 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6496 return session != nullptr &&
6497 (SessionHasPendingRomWork(session_index) || session->project_dirty ||
6498 session->editors.HasPendingProjectDraftChanges() ||
6499 (session->project_file_editor_state.initialized &&
6500 session->project_file_editor_state.modified));
6501}
6502
6503bool EditorManager::SessionHasPendingRomWork(size_t session_index) const {
6504 if (!session_coordinator_ ||
6505 !session_coordinator_->IsValidSessionIndex(session_index)) {
6506 return false;
6507 }
6508
6509 auto* session =
6510 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6511 return session != nullptr &&
6512 ((session->rom.is_loaded() && session->rom.dirty()) ||
6513 session->editors.HasPendingGraphicsChanges() ||
6514 session->editors.HasPendingScreenChanges() ||
6515 HasPendingDungeonChangesForSession(session_index) ||
6516 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data));
6517}
6518
6522
6524 size_t session_index) const {
6525 if (!session_coordinator_ ||
6526 !session_coordinator_->IsValidSessionIndex(session_index)) {
6527 return false;
6528 }
6529
6530 auto* session =
6531 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6532 if (!session) {
6533 return false;
6534 }
6535
6536 if (auto* dungeon_editor =
6537 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6538 return dungeon_editor->HasPendingDungeonChanges();
6539 }
6540 return false;
6541}
6542
6544 size_t session_index) const {
6545 if (!session_coordinator_ ||
6546 !session_coordinator_->IsValidSessionIndex(session_index)) {
6547 return 0;
6548 }
6549
6550 auto* session =
6551 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6552 if (!session) {
6553 return 0;
6554 }
6555
6556 if (auto* dungeon_editor =
6557 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6558 return dungeon_editor->PendingRoomCount();
6559 }
6560 return 0;
6561}
6562
6564 size_t session_index) const {
6565 if (!session_coordinator_ ||
6566 !session_coordinator_->IsValidSessionIndex(session_index)) {
6567 return 0;
6568 }
6569
6570 auto* session =
6571 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6572 return session != nullptr ? gfx::PaletteManager::Get().GetModifiedColorCount(
6573 &session->game_data)
6574 : 0;
6575}
6576
6578 if (!session_coordinator_) {
6579 return 0;
6580 }
6581
6582 int count = 0;
6583 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6585 ++count;
6586 }
6587 }
6588 return count;
6589}
6590
6592 size_t session_index) const {
6593 if (!session_coordinator_ ||
6594 !session_coordinator_->IsValidSessionIndex(session_index)) {
6595 return "unsaved work";
6596 }
6597
6598 auto* session =
6599 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6600 const bool rom_dirty =
6601 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
6602 const bool pending_dungeon_changes =
6604 const bool pending_graphics_changes =
6605 session != nullptr && session->editors.HasPendingGraphicsChanges();
6606 const bool pending_screen_changes =
6607 session != nullptr && session->editors.HasPendingScreenChanges();
6608 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
6609 const size_t pending_palette_colors =
6611 const bool project_dirty = session != nullptr && session->project_dirty;
6612 const bool project_editor_draft =
6613 session != nullptr && session->editors.HasPendingProjectDraftChanges();
6614 const bool project_file_dirty =
6615 session != nullptr && session->project_file_editor_state.initialized &&
6616 session->project_file_editor_state.modified;
6617
6618 std::vector<std::string> work;
6619 if (pending_rooms > 0) {
6620 work.push_back(absl::StrFormat("%d unapplied dungeon room%s", pending_rooms,
6621 pending_rooms == 1 ? "" : "s"));
6622 } else if (pending_dungeon_changes) {
6623 work.emplace_back("unapplied dungeon metadata");
6624 }
6625 if (pending_palette_colors > 0) {
6626 work.push_back(absl::StrFormat("%zu unapplied palette color%s",
6627 pending_palette_colors,
6628 pending_palette_colors == 1 ? "" : "s"));
6629 }
6630 if (pending_graphics_changes) {
6631 work.emplace_back("unapplied graphics sheet edits");
6632 }
6633 if (pending_screen_changes) {
6634 work.emplace_back("unapplied Screen Editor edits");
6635 }
6636 if (rom_dirty) {
6637 work.emplace_back("unsaved ROM-buffer changes");
6638 }
6639 if (project_dirty) {
6640 work.emplace_back("unsaved project settings");
6641 }
6642 if (project_editor_draft) {
6643 work.emplace_back("an uncommitted project editor draft");
6644 }
6645 if (project_file_dirty) {
6646 work.emplace_back("an unsaved project-file draft");
6647 }
6648 return work.empty() ? "unsaved work" : absl::StrJoin(work, " and ");
6649}
6650
6652 const int modified_sessions = ModifiedSessionCount();
6653 if (modified_sessions <= 0) {
6654 return "No sessions have unsaved work.";
6655 }
6656
6657 if (modified_sessions == 1 && session_coordinator_) {
6658 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6660 return absl::StrFormat("Session '%s' has %s.",
6661 session_coordinator_->GetSessionDisplayName(i),
6663 }
6664 }
6665 }
6666
6667 return absl::StrFormat(
6668 "%d sessions have unsaved ROM, graphics, screen, dungeon, palette, or "
6669 "project work.",
6670 modified_sessions);
6671}
6672
6674 return session_coordinator_ ? session_coordinator_->GetActiveSessionIndex()
6675 : 0;
6676}
6677
6679 UiSyncState state;
6680 state.frame_id = ui_sync_frame_id_.load(std::memory_order_relaxed);
6681
6682 int pending_editor =
6683 pending_editor_deferred_actions_.load(std::memory_order_relaxed);
6684 if (pending_editor < 0) {
6685 pending_editor = 0;
6686 }
6687 state.pending_editor_actions = pending_editor;
6688
6689 int pending_layout = layout_coordinator_.PendingDeferredActionCount();
6690 if (pending_layout < 0) {
6691 pending_layout = 0;
6692 }
6693 state.pending_layout_actions = pending_layout;
6695 layout_manager_ ? layout_manager_->IsRebuildRequested() : false;
6696 return state;
6697}
6698
6700 return session_coordinator_ ? session_coordinator_->GetActiveSessionCount()
6701 : 0;
6702}
6703
6705 EditorType type, size_t session_index) const {
6706 const char* base_name = kEditorNames[static_cast<int>(type)];
6707 return session_coordinator_ ? session_coordinator_->GenerateUniqueEditorTitle(
6708 base_name, session_index)
6709 : std::string(base_name);
6710}
6711
6712void EditorManager::SwitchToEditor(EditorType editor_type, bool force_visible,
6713 bool from_dialog) {
6714 // Special case: Agent editor requires EditorManager-specific handling
6715#ifdef YAZE_BUILD_AGENT_UI
6716 if (editor_type == EditorType::kAgent) {
6717 ShowAIAgent();
6718 return;
6719 }
6720#endif
6721
6722 // Fresh launch has no ROM session, so GetCurrentEditorSet() is null and
6723 // EditorActivator::SwitchToEditor would silently no-op (BUG-020) — that was
6724 // why File->Settings, the "no ROM" welcome buttons, and Tools editors did
6725 // nothing. Create an empty session on demand so ROM-less editors (Settings,
6726 // Assembly, ...) have an editor set to live in. Normal ROM loads create their
6727 // own session, so this only fires when the user reaches an editor first.
6728 if (session_coordinator_ && GetCurrentEditorSet() == nullptr) {
6729 session_coordinator_->CreateNewSession();
6730 }
6731
6732 auto status = EnsureEditorAssetsLoaded(editor_type);
6733 if (!status.ok()) {
6735 absl::StrFormat("Failed to prepare %s: %s",
6736 kEditorNames[static_cast<int>(editor_type)],
6737 status.message()),
6739 }
6740
6741 // Delegate all other editor switching to EditorActivator
6742 editor_activator_.SwitchToEditor(editor_type, force_visible, from_dialog);
6743}
6744
6746 if (!ui_coordinator_) {
6747 return;
6748 }
6749 ui_coordinator_->SetEditorSelectionVisible(false);
6750 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
6751}
6752
6754 if (!session)
6755 return;
6757 ConfigureEditorDependencies(&session->editors, &session->rom,
6758 session->editors.session_id());
6759}
6760
6761// SessionScope implementation
6763 size_t session_index)
6764 : manager_(manager),
6765 prev_rom_(manager->GetCurrentRom()),
6766 prev_editor_set_(manager->GetCurrentEditorSet()),
6767 prev_session_index_(manager->GetCurrentSessionIndex()) {
6768 // Set new session context
6769 manager_->session_coordinator_->SwitchToSession(session_index);
6770}
6771
6773 // Restore previous context
6774 manager_->session_coordinator_->SwitchToSession(prev_session_index_);
6775}
6776
6777bool EditorManager::HasDuplicateSession(const std::string& filepath) {
6778 return session_coordinator_ &&
6779 session_coordinator_->HasDuplicateSession(filepath);
6780}
6781
6807 // Menu actions can run before the next Update() frame captures drawer
6808 // edits. Preserve the panel's current dirty bit before rebinding it.
6811 // Update project panel context before showing
6815 project_management_panel_->SetVersionManager(version_manager_);
6817 }
6818 right_drawer_manager_->ToggleDrawer(
6820 }
6821}
6822
6825 auto* session = session_coordinator_
6826 ? session_coordinator_->GetActiveRomSession()
6827 : nullptr;
6828
6829 // Preserve an existing draft for this session. Only load from disk the first
6830 // time this session opens the project-file editor.
6831 if (session && session->project_file_editor_state.initialized &&
6832 !session->project_file_editor_state.modified &&
6833 !current_project_.filepath.empty() &&
6835 session->project_file_editor_state.filepath,
6838 }
6839
6840 if (session && session->project_file_editor_state.initialized) {
6841 project_file_editor_.RestoreState(session->project_file_editor_state,
6843 } else if (!current_project_.filepath.empty()) {
6845 if (!status.ok()) {
6847 absl::StrFormat("Failed to load project file: %s", status.message()),
6849 return;
6850 }
6851 } else {
6853 }
6854 // Set the project pointer for label import functionality
6856 // Activate the editor window
6858 if (session) {
6859 session->project_file_editor_state = project_file_editor_.CaptureState();
6860 }
6861}
6862
6864 size_t session_id) {
6865 if (!editor_set) {
6866 return;
6867 }
6868
6869 EditorDependencies deps;
6870 deps.rom = rom;
6871 deps.session_id = session_id;
6874 deps.popup_manager = popup_manager_.get();
6879 const auto session_index = ResolveSessionIndexById(session_id);
6880 if (session_index.has_value()) {
6881 auto* session = static_cast<RomSession*>(
6882 session_coordinator_->GetSession(*session_index));
6883 if (session) {
6884 deps.game_data = &session->game_data;
6885 if (session->project_context.has_value()) {
6886 deps.project = &*session->project_context;
6887 deps.version_manager = session->version_manager.get();
6888 }
6889 }
6890 }
6891 }
6892 deps.global_context = editor_context_.get();
6893 deps.status_bar = &status_bar_;
6894 deps.renderer = renderer_;
6895 deps.emulator = &emulator_;
6896 deps.custom_data = this;
6897 deps.gfx_group_workspace = editor_set->gfx_group_workspace();
6898
6899 editor_set->ApplyDependencies(deps);
6900
6901 // If configuring the active session, update the properties panel
6902 if (session_id == GetCurrentSessionId()) {
6904 }
6905}
6906
6907} // 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:28
project::ResourceLabelManager * resource_label()
Definition rom.h:180
auto filename() const
Definition rom.h:175
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:563
void set_dirty(bool dirty)
Definition rom.h:157
const auto & vector() const
Definition rom.h:173
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool dirty() const
Definition rom.h:156
auto set_filename(std::string_view name)
Definition rom.h:176
bool is_loaded() const
Definition rom.h:155
static TimingManager & Get()
Definition timing.h:20
float Update()
Update the timing manager (call once per frame)
Definition timing.h:29
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
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
bool loaded() const
Check if the manifest has been loaded.
const BuildPipeline & build_pipeline() const
static RomSettings & Get()
void SetAddressOverrides(const RomAddressOverrides &overrides)
void set_active(bool active)
Definition agent_chat.h:71
AgentConfigState & agent_config()
void SetProjectContext(project::YazeProject *project)
void ApplyUserSettingsDefaults(bool force=false)
void SetAssemblySymbolTableContext(const std::map< std::string, core::AsarSymbol > *table)
void Initialize(ToastManager *toast_manager, ProposalDrawer *proposal_drawer, RightDrawerManager *right_drawer_manager, WorkspaceWindowManager *window_manager, UserSettings *user_settings)
void SetAsarWrapperContext(core::AsarWrapper *asar_wrapper)
absl::Status Start(const std::string &command, const std::string &directory)
DungeonEditorV2 - Simplified dungeon editor using component delegation.
void SwitchToEditor(EditorType type, bool force_visible=false, bool from_dialog=false)
Switch to an editor, optionally forcing visibility.
void Initialize(const Dependencies &deps)
SessionScope(EditorManager *manager, size_t session_index)
The EditorManager controls the main editor window and manages the various editor classes.
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_
void RestoreProjectEditingStateForSession(RomSession *session)
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.
void SwitchToSession(size_t index)
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 HandleSessionClosed(size_t index)
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::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)
bool HasPendingUnsavedSessionAction() const
absl::Status OpenRomOrProjectInternal(const std::string &filename)
auto GetCurrentEditorSet() const -> EditorSet *
std::string DescribePendingUnsavedWork(size_t session_index) const
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 HandleHostVisibilityChanged(bool visible)
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
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
absl::StatusOr< std::string > ResolveProjectBuildCommand() 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_
RomLoadOptionsDialog rom_load_options_dialog_
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::Status Update()
Main update loop for the editor application.
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)
void ConfigureSession(RomSession *session) override
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
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)
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)
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 std::vector< std::string > GetAllEditorCategories()
Get all editor categories in display order for sidebar.
static bool IsPanelBasedEditor(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.
size_t session_id() const
void ApplyDependencies(const EditorDependencies &dependencies)
Editor * GetEditor(EditorType type) const
void set_user_settings(UserSettings *settings)
GfxGroupWorkspaceState * gfx_group_workspace()
SettingsPanel * GetSettingsPanel() const
std::vector< Editor * > active_editors_
Interface for editor classes.
Definition editor.h:245
virtual absl::Status BeginSaveTransaction()
Definition editor.h:272
virtual void ContributeStatus(StatusBar *)
Definition editor.h:304
virtual absl::Status Redo()=0
EditorType type() const
Definition editor.h:306
virtual absl::Status Undo()=0
void ProcessDeferredActions()
Process all queued deferred actions.
void ResetWorkspaceLayout()
Reset the workspace layout to defaults.
void ProcessLayoutRebuild(EditorType current_editor_type, bool is_emulator_visible)
Process pending layout rebuild requests.
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.
Centralized definition of default layouts per editor.
static std::vector< std::string > GetDefaultWindows(EditorType type)
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)
absl::Status SaveRom(Rom *rom)
absl::Status SaveRomAs(Rom *rom, const std::string &filename)
Manages ROM and project persistence state.
void SetProjectContext(const project::YazeProject *project)
void UpdateCurrentRomHash(Rom *rom)
Recompute the hash of the current ROM.
std::vector< RomFileManager::BackupEntry > GetRomBackups(Rom *rom) const
void SetPotItemConfirmPending(int unloaded_rooms, int total_rooms)
Set pot-item confirmation pending (called by SaveRom when needed).
absl::Status CheckRomWritePolicy(Rom *rom, const std::optional< std::string > &target_filename=std::nullopt)
Enforce project write policy; may set pending_rom_write_confirm.
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
void SetPendingWriteConflicts(std::vector< core::WriteConflict > conflicts)
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 Open(Rom *rom, const std::string &rom_filename)
Open the dialog after ROM detection.
void SetConfirmCallback(std::function< void(const LoadOptions &)> callback)
Set callback for when options are confirmed.
void Draw(bool *p_open)
Draw the dialog (wrapper around Show)
The ScreenEditor class allows the user to edit a variety of screens in the game or create a custom me...
void SetAgentCallbacks(std::function< void(const std::string &)> send_callback, std::function< void()> focus_callback)
void ClearSelection()
Clear the current selection.
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:54
void SetSessionInfo(size_t session_id, size_t total_sessions)
Set session information.
void SetBuildStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:174
void SetRunStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:177
void SetRom(Rom *rom)
Set the current ROM for dirty status and filename display.
Definition status_bar.h:74
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEnabled(bool enabled)
Enable or disable the status bar.
Definition status_bar.h:68
void Initialize(GlobalEditorContext *context)
Definition status_bar.cc:91
void ClearEditorContributions()
Clear frame-scoped editor contributions.
void Draw()
Draw the status bar.
void SetAgentToggleCallback(std::function< void()> callback)
Definition status_bar.h:182
void SetAgentInfo(const std::string &provider, const std::string &model, bool active)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
bool ApplyPanelLayoutDefaultsRevision(int target_revision)
static constexpr int kLatestPanelLayoutDefaultsRevision
void SetNewProjectCallback(std::function< void()> callback)
Set callback for creating new project.
void SetOpenAgentCallback(std::function< void()> callback)
Set callback for opening AI Agent.
void SetOpenAssemblyEditorNoRomCallback(std::function< void()> callback)
Open the assembly editor for file/folder work (no ROM required).
void SetOpenRomCallback(std::function< void()> callback)
Set callback for opening ROM.
void SetOpenPrototypeResearchCallback(std::function< void()> callback)
Open the graphics editor focused on prototype research (no ROM).
void SetOpenProjectDialogCallback(std::function< void()> callback)
Set callback for opening the project file dialog.
void SetOpenProjectManagementCallback(std::function< void()> callback)
Set callback for showing project management.
void SetOpenProjectFileEditorCallback(std::function< void()> callback)
Set callback for showing the project file editor.
void SetNewProjectWithTemplateCallback(std::function< void(const std::string &)> callback)
Set callback for creating project with template.
void SetOpenProjectCallback(std::function< void(const std::string &)> callback)
Set callback for opening project.
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 EnableFileBrowser(const std::string &category, const std::string &root_path="")
void SetSidebarVisible(bool visible, bool notify=true)
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
void SetEditorResolver(std::function< Editor *(const std::string &)> resolver)
void RestoreVisibilityState(size_t session_id, const std::unordered_map< std::string, bool > &state, bool publish_events=false)
Restore panel visibility state from persistence.
void SetPanelBrowserCategoryWidthChangedCallback(std::function< void(float)> cb)
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 SetSidebarStateChangedCallback(std::function< void(bool, bool)> cb)
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 SetCategoryChangedCallback(std::function< void(const std::string &)> cb)
void SetFileBrowserPath(const std::string &category, const std::string &path)
void SetOnWindowCategorySelectedCallback(std::function< void(const std::string &)> callback)
void SetOnWindowClickedCallback(std::function< void(const std::string &)> callback)
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void SetSidePanelWidthChangedCallback(std::function< void(float)> cb)
void SetWindowBrowserCategoryWidth(float width, bool notify=true)
void RegisterRegistryWindowContentsForSession(size_t session_id)
Register descriptors for all registry window contents in a session.
void SetFileClickedCallback(std::function< void(const std::string &category, const std::string &path)> callback)
void SetSidebarExpanded(bool expanded, bool notify=true)
bool * GetWindowVisibilityFlag(size_t session_id, const std::string &base_window_id)
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)
ScopedEditorSaveTransactions & operator=(const ScopedEditorSaveTransactions &)=delete
void set_window_manager(editor::WorkspaceWindowManager *manager)
Definition emulator.h:51
bool is_audio_focus_mode() const
Definition emulator.h:111
void set_renderer(gfx::IRenderer *renderer)
Definition emulator.h:100
bool is_snes_initialized() const
Definition emulator.h:129
void Run(Rom *rom)
Definition emulator.cc:469
auto running() const -> bool
Definition emulator.h:61
absl::Status ReloadRuntimeRom(const std::vector< uint8_t > &rom_data)
Definition emulator.cc:265
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
Definition arena.h:178
static Arena & Get()
Definition arena.cc:21
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
bool HasUnsavedChanges() const
Check if there are ANY unsaved changes.
void ActivateSession(zelda3::GameData *game_data)
Select the palette state for a ROM session without resnapshotting it.
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 PerformanceDashboard & Get()
void SetVisible(bool visible)
Show/hide the dashboard.
void Update()
Update dashboard with current performance data.
void Render()
Render the performance dashboard UI.
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 ClearWorkspaceTransitionState()
Definition animator.cc:89
RAII guard for ImGui style colors.
Definition style_guard.h:27
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:441
void SetCurrentRom(Rom *rom)
void DrawTestDashboard(bool *show_dashboard=nullptr)
static TestManager & Get()
static void ShowOpenFileDialogAsync(const FileDialogOptions &options, std::function< void(const std::string &)> callback)
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...
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
void SetObjectFileMap(const std::unordered_map< int, std::vector< std::string > > &map)
static CustomObjectManager & Get()
void Initialize(const std::string &custom_objects_folder)
static DrawRoutineRegistry & Get()
Represents the full Overworld data, light and dark world.
Definition overworld.h:389
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)
int main(int argc, char **argv)
Definition emu.cc:43
#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_MENU
Definition icons.h:1196
#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 ICON_MD_MENU_OPEN
Definition icons.h:1198
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define PRINT_IF_ERROR(expression)
Definition macro.h:28
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr char kOverworldExpandedPtrHigh[]
constexpr char kOverworldEntrancePosExpanded[]
constexpr char kExpandedMusicHook[]
constexpr char kExpandedMusicMain[]
constexpr char kOverworldExpandedPtrMagic[]
constexpr char kExpandedMessageEnd[]
constexpr char kOverworldExpandedPtrMarker[]
constexpr char kOverworldEntranceMapExpanded[]
constexpr char kOverworldEntranceFlagExpanded[]
constexpr char kExpandedMessageStart[]
constexpr char kOverworldEntranceIdExpanded[]
constexpr char kOverworldExpandedPtrLow[]
constexpr char kExpandedMusicAux[]
void SetGlobalContext(GlobalEditorContext *ctx)
void SetShowWorkflowOutputCallback(std::function< void()> callback)
void SetEditorWindowContext(const std::string &category, Editor *editor)
void SetEventBus(::yaze::EventBus *bus)
Set the current EventBus instance.
void SetRom(Rom *rom)
Set the current ROM 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.
void SetCurrentProject(::yaze::project::YazeProject *project)
Set the current project instance.
std::vector< std::unique_ptr< WindowContent > > CreateAll()
Create new instances of all registered panels.
constexpr const char * kAbout
constexpr const char * kSaveAs
constexpr const char * kWriteConflictWarning
constexpr const char * kDungeonPotItemSaveConfirm
constexpr const char * kUnsavedSessionChanges
constexpr const char * kRomWriteConfirm
bool IsTransientPanelVisibilityId(absl::string_view panel_id)
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)
bool HasAnyOverride(const core::RomAddressOverrides &overrides, std::initializer_list< const char * > keys)
bool ProjectUsesCustomObjects(const project::YazeProject &project)
void AppendWorkflowHistoryEntry(const std::string &kind, const ProjectWorkflowStatus &status, const std::string &output_log)
std::string StripSessionPrefix(absl::string_view panel_id)
bool SeedLegacyTrackObjectMapping(project::YazeProject *project, std::string *warning)
std::vector< std::string > ValidateRomAddressOverrides(const core::RomAddressOverrides &overrides, const Rom &rom)
std::unique_ptr< HackWorkflowBackend > CreateHackWorkflowBackendForProject(const project::YazeProject *project)
Editors are the view controllers for the application.
constexpr std::array< const char *, 14 > kEditorNames
Definition editor.h:226
std::optional< EditorType > ParseEditorTypeFromString(absl::string_view name)
void ConfigureMenuShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
void RegisterDefaultEditorFactories(EditorRegistry *registry)
void ConfigurePanelShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
Register configurable panel shortcuts from user settings.
size_t EditorTypeIndex(EditorType type)
Definition editor.h:235
void ConfigureEditorShortcuts(const ShortcutDependencies &deps, ShortcutManager *shortcut_manager)
void ExecuteShortcuts(const ShortcutManager &shortcut_manager)
ImVec4 GetSurfaceContainerHighestVec4()
ImVec4 GetPrimaryVec4()
ImVec4 GetTextSecondaryVec4()
Animator & GetAnimator()
Definition animator.cc:318
ImVec4 GetSurfaceContainerHighVec4()
void PostOverlayCommand(const char *command)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
Definition rom_diff.cc:11
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 kExpandedPtrTableMarker
Definition overworld.h:184
constexpr int kNumOverworldMaps
Definition common.h:85
void SetPreferHmagicSpriteNames(bool prefer)
Definition sprite.cc:273
constexpr uint8_t kExpandedPtrTableMagic
Definition overworld.h:185
constexpr int kOverworldEntranceExpandedFlagPos
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
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
std::unordered_map< std::string, uint32_t > addresses
std::optional< uint32_t > GetAddress(const std::string &key) const
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
Unified dependency container for all editor types.
Definition editor.h:169
project::YazeProject * project
Definition editor.h:173
GlobalEditorContext * global_context
Definition editor.h:175
SharedClipboard * shared_clipboard
Definition editor.h:186
gfx::IRenderer * renderer
Definition editor.h:191
ShortcutManager * shortcut_manager
Definition editor.h:185
core::VersionManager * version_manager
Definition editor.h:174
zelda3::GameData * game_data
Definition editor.h:172
WorkspaceWindowManager * window_manager
Definition editor.h:181
GfxGroupWorkspaceState * gfx_group_workspace
Definition editor.h:178
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
ProjectFileEditorState project_file_editor_state
std::array< bool, kEditorTypeCount > editor_initialized
zelda3::GameData game_data
std::unique_ptr< core::VersionManager > version_manager
std::array< bool, kEditorTypeCount > editor_assets_loaded
std::optional< project::YazeProject > project_context
Published when a session is closed.
Published when a new session is created.
Published when the active session changes.
Definition core_events.h:89
Optional behavior for an interactive status bar segment.
Definition status_bar.h:27
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
Metadata for a dockable editor window (formerly PanelInfo)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:432
std::vector< std::string > recent_files
Definition project.h:85
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
std::string rom_backup_folder
Definition project.h:181
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:197
std::string custom_objects_folder
Definition project.h:192
absl::Status RepairProject()
Definition project.cc:1430
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:638
bool project_opened() const
Definition project.h:348
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
Definition project.cc:595
std::string git_repository
Definition project.h:226
core::HackManifest hack_manifest
Definition project.h:212
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:205
std::string assets_folder
Definition project.h:187
std::string labels_filename
Definition project.h:189
std::string GetDisplayName() const
Definition project.cc:1464
WorkspaceSettings workspace_settings
Definition project.h:201
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1491
absl::Status Open(const std::string &project_path)
Definition project.cc:429
absl::Status Validate() const
Definition project.cc:1360
core::FeatureFlags::Flags feature_flags
Definition project.h:200
core::RomAddressOverrides rom_address_overrides
Definition project.h:203