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"
66#include "app/emu/emulator.h"
72#include "app/gui/core/icons.h"
77#include "app/platform/timing.h"
79#include "core/features.h"
80#include "core/project.h"
81#include "core/rom_settings.h"
90#include "rom/rom.h"
91#include "rom/rom_diff.h"
92#include "rom/transaction.h"
93#include "startup_flags.h"
94#include "util/file_util.h"
96#include "util/log.h"
97#include "util/macro.h"
98#include "util/rom_hash.h"
99#include "yaze_config.h"
105#include "zelda3/game_data.h"
111#include "zelda3/sprite/sprite.h"
112
113// Conditional platform headers
114#ifdef __EMSCRIPTEN__
118#endif
119
120// Conditional test headers
121#ifdef YAZE_ENABLE_TESTING
127#endif
128#ifdef YAZE_ENABLE_GTEST
130#endif
131#ifdef YAZE_WITH_GRPC
133#endif
134
135// Conditional agent UI headers
136#ifdef YAZE_BUILD_AGENT_UI
138#endif
139
140namespace yaze::editor {
141
142namespace {
144 public:
148 delete;
149
151 if (!committed_) {
152 for (auto it = editors_.rbegin(); it != editors_.rend(); ++it) {
153 (*it)->RollbackSaveTransaction();
154 }
155 }
156 }
157
158 absl::Status Begin(Editor* editor) {
159 if (editor == nullptr) {
160 return absl::OkStatus();
161 }
163 editors_.push_back(editor);
164 return absl::OkStatus();
165 }
166
167 void Commit() {
168 for (Editor* editor : editors_) {
169 editor->CommitSaveTransaction();
170 }
171 committed_ = true;
172 }
173
174 private:
175 std::vector<Editor*> editors_;
176 bool committed_ = false;
177};
178
180 std::initializer_list<const char*> keys) {
181 for (const char* key : keys) {
182 if (overrides.addresses.find(key) != overrides.addresses.end()) {
183 return true;
184 }
185 }
186 return false;
187}
188
189bool IsTransientPanelVisibilityId(absl::string_view panel_id) {
190 constexpr absl::string_view kRoomPanelPrefix = "dungeon.room_";
191 if (!absl::StartsWith(panel_id, kRoomPanelPrefix) ||
192 panel_id.size() <= kRoomPanelPrefix.size()) {
193 return false;
194 }
195 for (char ch : panel_id.substr(kRoomPanelPrefix.size())) {
196 if (ch < '0' || ch > '9') {
197 return false;
198 }
199 }
200 return true;
201}
202
203std::string LastNonEmptyLine(const std::string& text) {
204 std::vector<std::string> lines = absl::StrSplit(text, '\n');
205 for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
206 std::string line = std::string(*it);
207 absl::StripAsciiWhitespace(&line);
208 if (!line.empty()) {
209 return line;
210 }
211 }
212 return "";
213}
214
216 ProjectManagementPanel* project_panel,
217 const ProjectWorkflowStatus& status) {
219 if (status_bar != nullptr) {
220 status_bar->SetBuildStatus(status);
221 }
222 if (project_panel != nullptr) {
223 project_panel->SetBuildStatus(status);
224 }
225}
226
228 ProjectManagementPanel* project_panel,
229 const ProjectWorkflowStatus& status) {
231 if (status_bar != nullptr) {
232 status_bar->SetRunStatus(status);
233 }
234 if (project_panel != nullptr) {
235 project_panel->SetRunStatus(status);
236 }
237}
238
239void AppendWorkflowHistoryEntry(const std::string& kind,
240 const ProjectWorkflowStatus& status,
241 const std::string& output_log) {
243 {.kind = kind,
244 .status = status,
245 .output_log = output_log,
246 .timestamp = std::chrono::system_clock::now()});
247}
248
250 return !project.custom_objects_folder.empty() ||
251 !project.custom_object_files.empty();
252}
253
254constexpr int kTrackCustomObjectId = 0x31;
255
257 std::string* warning) {
258 if (project == nullptr) {
259 return false;
260 }
261 if (project->custom_objects_folder.empty()) {
262 return false;
263 }
264 if (project->custom_object_files.find(kTrackCustomObjectId) !=
265 project->custom_object_files.end()) {
266 return false;
267 }
268
269 const auto& defaults =
272 if (defaults.empty()) {
273 return false;
274 }
275
276 project->custom_object_files[kTrackCustomObjectId] = defaults;
277 if (warning != nullptr) {
278 *warning = absl::StrFormat(
279 "Project defines custom_objects_folder but is missing "
280 "custom_object_files[0x%02X]. Seeded default mapping (%d entries).",
281 kTrackCustomObjectId, static_cast<int>(defaults.size()));
282 }
283 return true;
284}
285
286std::vector<std::string> ValidateRomAddressOverrides(
287 const core::RomAddressOverrides& overrides, const Rom& rom) {
288 std::vector<std::string> warnings;
289 if (overrides.addresses.empty()) {
290 return warnings;
291 }
292
293 const auto rom_size = rom.size();
294 auto warn = [&](const std::string& message) {
295 warnings.push_back(message);
296 };
297
298 auto check_range = [&](const std::string& label, uint32_t addr, size_t span) {
299 const size_t addr_size = static_cast<size_t>(addr);
300 if (addr_size >= rom_size || addr_size + span > rom_size) {
301 warn(absl::StrFormat("ROM override '%s' out of range: 0x%X (size 0x%X)",
302 label, addr, rom_size));
303 }
304 };
305
306 for (const auto& [key, value] : overrides.addresses) {
307 check_range(key, value, 1);
308 }
309
312 // Defaults match the Oracle expanded message bank ($2F8000-$2FFFFF).
313 // Kept local to avoid pulling message editor constants into core validation.
314 constexpr uint32_t kExpandedMessageStartDefault = 0x178000;
315 constexpr uint32_t kExpandedMessageEndDefault = 0x17FFFF;
316 const uint32_t start =
318 .value_or(kExpandedMessageStartDefault);
319 const uint32_t end =
321 .value_or(kExpandedMessageEndDefault);
322 if (start >= rom_size || end >= rom_size) {
323 warn(absl::StrFormat(
324 "Expanded message range out of ROM bounds: 0x%X-0x%X", start, end));
325 } else if (end < start) {
326 warn(absl::StrFormat("Expanded message range invalid: 0x%X-0x%X", start,
327 end));
328 }
329 }
330
331 if (auto hook =
333 check_range(core::RomAddressKey::kExpandedMusicHook, *hook, 4);
334 if (*hook < rom_size) {
335 auto opcode = rom.ReadByte(*hook);
336 if (opcode.ok() && opcode.value() != 0x22) {
337 warn(absl::StrFormat(
338 "Expanded music hook at 0x%X is not a JSL opcode (0x%02X)", *hook,
339 opcode.value()));
340 }
341 }
342 }
343
344 if (auto main =
347 }
348
349 if (auto aux = overrides.GetAddress(core::RomAddressKey::kExpandedMusicAux)) {
350 check_range(core::RomAddressKey::kExpandedMusicAux, *aux, 4);
351 }
352
353 if (HasAnyOverride(overrides,
358 const uint32_t marker =
361 const uint32_t magic =
364 check_range("overworld_ptr_marker", marker, 1);
365 if (marker < rom_size) {
366 if (rom.data()[marker] != static_cast<uint8_t>(magic)) {
367 warn(absl::StrFormat(
368 "Overworld expanded pointer marker mismatch at 0x%X: expected "
369 "0x%02X, found 0x%02X",
370 marker, static_cast<uint8_t>(magic), rom.data()[marker]));
371 }
372 }
373 }
374
375 if (HasAnyOverride(overrides,
380 const uint32_t flag_addr =
381 overrides
384 check_range("overworld_entrance_flag_expanded", flag_addr, 1);
385 if (flag_addr < rom_size && rom.data()[flag_addr] == 0xB8) {
386 warn(
387 absl::StrFormat("Overworld entrance flag at 0x%X is still 0xB8 "
388 "(vanilla); expanded entrance tables may be ignored",
389 flag_addr));
390 }
391 }
392
393 return warnings;
394}
395
396std::string StripSessionPrefix(absl::string_view panel_id) {
397 if (panel_id.size() > 2 && panel_id[0] == 's' &&
398 absl::ascii_isdigit(panel_id[1])) {
399 const size_t dot = panel_id.find('.');
400 if (dot != absl::string_view::npos) {
401 return std::string(panel_id.substr(dot + 1));
402 }
403 }
404 return std::string(panel_id);
405}
406
408 namespace fs = std::filesystem;
409
410 std::vector<fs::path> roots;
411 if (const char* home = std::getenv("HOME");
412 home != nullptr && std::strlen(home) > 0) {
413 roots.push_back(fs::path(home) / "src" / "hobby" / "oracle-of-secrets");
414 }
415
416 std::vector<fs::path> candidates;
417 std::unordered_set<std::string> seen;
418 auto add_candidate = [&](const fs::path& path) {
419 std::error_code ec;
420 if (!fs::exists(path, ec) || ec) {
421 return;
422 }
423 std::string ext = path.extension().string();
424 absl::AsciiStrToLower(&ext);
425 if (ext != ".yaze" && ext != ".yazeproj") {
426 return;
427 }
428 const std::string normalized = fs::weakly_canonical(path, ec).string();
429 const std::string key = normalized.empty() ? path.string() : normalized;
430 if (key.empty() || seen.count(key) > 0) {
431 return;
432 }
433 seen.insert(key);
434 candidates.push_back(path);
435 };
436
437 for (const auto& root : roots) {
438 std::error_code ec;
439 if (!fs::exists(root, ec) || ec) {
440 continue;
441 }
442
443 // Priority candidates first.
444 add_candidate(root / "Oracle-of-Secrets.yaze");
445 add_candidate(root / "Oracle of Secrets.yaze");
446 add_candidate(root / "Oracle-of-Secrets.yazeproj");
447 add_candidate(root / "Oracle of Secrets.yazeproj");
448 add_candidate(root / "Roms" / "Oracle of Secrets.yaze");
449
450 // Also detect additional local project files nearby.
451 fs::recursive_directory_iterator it(
452 root, fs::directory_options::skip_permission_denied, ec);
453 fs::recursive_directory_iterator end;
454 if (ec) {
455 continue;
456 }
457 for (; it != end; it.increment(ec)) {
458 if (ec) {
459 ec.clear();
460 continue;
461 }
462 if (it.depth() > 2) {
463 it.disable_recursion_pending();
464 continue;
465 }
466 add_candidate(it->path());
467 }
468 }
469
470 if (candidates.empty()) {
471 return;
472 }
473
475 // Add in reverse so the first candidate remains most recent.
476 for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
477 manager.AddFile(it->string());
478 }
479 manager.Save();
480}
481
482} // namespace
483
484std::optional<EditorType> ParseEditorTypeFromString(absl::string_view name) {
485 std::string normalized = std::string(name);
486 absl::StripAsciiWhitespace(&normalized);
487 const std::string lower = absl::AsciiStrToLower(normalized);
488 for (int i = 0; i < static_cast<int>(EditorType::kSettings) + 1; ++i) {
489 const auto type = static_cast<EditorType>(i);
490 const std::string name_candidate =
491 absl::AsciiStrToLower(EditorRegistry::GetEditorName(type));
492 const std::string category_candidate =
493 absl::AsciiStrToLower(EditorRegistry::GetEditorCategory(type));
494 if (name_candidate == lower || category_candidate == lower) {
495 return type;
496 }
497 }
498 return std::nullopt;
499}
500
501// Static registry of editors that use the card-based layout system
502// These editors register their cards with WorkspaceWindowManager and manage their
503// own windows They do NOT need the traditional ImGui::Begin/End wrapper - they
504// create cards internally
508
512
513void EditorManager::ApplyLayoutPreset(const std::string& preset_name) {
515}
516
517bool EditorManager::ApplyLayoutProfile(const std::string& profile_id) {
518 if (!layout_manager_) {
519 return false;
520 }
521
522 const size_t session_id = GetCurrentSessionId();
523 const EditorType current_type =
525
526 LayoutProfile applied_profile;
527 if (!layout_manager_->ApplyBuiltInProfile(profile_id, session_id,
528 current_type, &applied_profile)) {
529 return false;
530 }
531
532 if (applied_profile.open_agent_chat && right_drawer_manager_) {
533 right_drawer_manager_->OpenDrawer(
535 const float default_width = RightDrawerManager::GetDefaultDrawerWidth(
537 right_drawer_manager_->SetDrawerWidth(
539 std::max(default_width, 480.0f));
540 }
541
543 absl::StrFormat("Layout Profile: %s", applied_profile.label),
545 return true;
546}
547
549 if (!layout_manager_) {
550 return;
551 }
552 layout_manager_->CaptureTemporarySessionLayout(GetCurrentSessionId());
553 toast_manager_.Show("Captured temporary layout snapshot", ToastType::kInfo);
554}
555
556void EditorManager::RestoreTemporaryLayoutSnapshot(bool clear_after_restore) {
557 if (!layout_manager_) {
558 return;
559 }
560
561 if (layout_manager_->RestoreTemporarySessionLayout(GetCurrentSessionId(),
562 clear_after_restore)) {
563 toast_manager_.Show("Restored temporary layout snapshot",
565 } else {
566 toast_manager_.Show("No temporary layout snapshot available",
568 }
569}
570
572 if (!layout_manager_) {
573 return;
574 }
575 layout_manager_->ClearTemporarySessionLayout();
576 toast_manager_.Show("Cleared temporary layout snapshot", ToastType::kInfo);
577}
578
579bool EditorManager::SaveLayoutSnapshotAs(const std::string& name) {
580 if (!layout_manager_)
581 return false;
582 if (name.empty()) {
583 toast_manager_.Show("Snapshot name cannot be empty", ToastType::kWarning);
584 return false;
585 }
586 const bool ok =
587 layout_manager_->SaveNamedSnapshot(name, GetCurrentSessionId());
588 if (ok) {
589 toast_manager_.Show(absl::StrFormat("Saved snapshot '%s'", name),
591 } else {
592 toast_manager_.Show(absl::StrFormat("Failed to save snapshot '%s'", name),
594 }
595 return ok;
596}
597
598bool EditorManager::RestoreLayoutSnapshot(const std::string& name,
599 bool remove_after_restore) {
600 if (!layout_manager_)
601 return false;
602 const bool ok = layout_manager_->RestoreNamedSnapshot(
603 name, GetCurrentSessionId(), remove_after_restore);
604 if (ok) {
605 toast_manager_.Show(absl::StrFormat("Restored snapshot '%s'", name),
607 } else {
608 toast_manager_.Show(absl::StrFormat("Snapshot '%s' not available", name),
610 }
611 return ok;
612}
613
614bool EditorManager::DeleteLayoutSnapshot(const std::string& name) {
615 if (!layout_manager_)
616 return false;
617 const bool ok = layout_manager_->DeleteNamedSnapshot(name);
618 if (ok) {
619 toast_manager_.Show(absl::StrFormat("Deleted snapshot '%s'", name),
621 }
622 return ok;
623}
624
625std::vector<std::string> EditorManager::ListLayoutSnapshots() const {
626 if (!layout_manager_)
627 return {};
628 return layout_manager_->ListNamedSnapshots(GetCurrentSessionId());
629}
630
632 if (!layout_manager_) {
633 return;
634 }
635
637 layout_manager_->SetProjectLayoutKey(
639 } else {
640 layout_manager_->UseGlobalLayouts();
641 }
642}
643
652
653#ifdef YAZE_BUILD_AGENT_UI
654void EditorManager::ShowAIAgent() {
655 // Apply saved agent settings from the current project when opening the Agent
656 // UI to respect the user's preferred provider/model.
657 // TODO: Implement LoadAgentSettingsFromProject in AgentChat or AgentEditor
661 for (const auto& window_id :
662 LayoutPresets::GetDefaultWindows(EditorType::kAgent)) {
663 window_manager_.OpenWindow(window_id);
664 }
665}
666
667void EditorManager::ShowChatHistory() {
669}
670#endif
671
673 : project_manager_(&toast_manager_), rom_file_manager_(&toast_manager_) {
674 std::stringstream ss;
675 ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "."
676 << YAZE_VERSION_PATCH;
677 ss >> version_;
678
679 // Initialize Core Context
680 editor_context_ = std::make_unique<GlobalEditorContext>(event_bus_);
683
687
688 // STEP 5: ShortcutConfigurator created later in Initialize() method
689 // It depends on all above coordinators being available
690}
691
694
695 // STEP 1: Initialize PopupManager FIRST
696 popup_manager_ = std::make_unique<PopupManager>(this);
697 popup_manager_->Initialize(); // Registers all popups with PopupID constants
698
699 // STEP 2: Initialize SessionCoordinator (independent of popups)
700 session_coordinator_ = std::make_unique<SessionCoordinator>(
702 session_coordinator_->SetEditorRegistry(&editor_registry_);
703
704 // STEP 3: Initialize MenuOrchestrator (depends on popup_manager_,
705 // session_coordinator_)
706 menu_orchestrator_ = std::make_unique<MenuOrchestrator>(
709
710 // Wire up the window manager for the View menu window listing
711 menu_orchestrator_->SetWindowManager(&window_manager_);
712 menu_orchestrator_->SetStatusBar(&status_bar_);
713 menu_orchestrator_->SetUserSettings(&user_settings_);
714
715 session_coordinator_->SetEditorManager(this);
716 session_coordinator_->SetEventBus(&event_bus_); // Enable event publishing
718 &event_bus_); // Global event bus access
719
720 // Expose UserSettings to cross-editor panels that don't get constructor
721 // injection (e.g. Layout Designer, which reads named_layouts). The
722 // LayoutManager setter happens below once the unique_ptr is constructed.
724
725 // STEP 3.5: Initialize RomLifecycleManager (depends on popup_manager_,
726 // session_coordinator_, rom_file_manager_, toast_manager_)
728 .rom_file_manager = &rom_file_manager_,
729 .session_coordinator = session_coordinator_.get(),
732 .project = &current_project_,
733 });
734
735 // STEP 4: Initialize UICoordinator (depends on popup_manager_,
736 // session_coordinator_, window_manager_)
737 ui_coordinator_ = std::make_unique<UICoordinator>(
741
742 // STEP 4.5: Initialize LayoutManager (DockBuilder layouts for editors)
743 layout_manager_ = std::make_unique<LayoutManager>();
744 layout_manager_->SetWindowManager(&window_manager_);
745 layout_manager_->UseGlobalLayouts();
746 // Expose to cross-editor panels that drive the live dockspace (Layout
747 // Designer). Must land after the unique_ptr is constructed.
751 [this](const std::string& preset_name) {
752 ApplyLayoutPreset(preset_name);
753 });
755 [this](const std::string& preset_name) {
756 ApplyLayoutPreset(preset_name);
757 });
758
759 // STEP 4.6: Initialize RightDrawerManager (right-side sliding drawers)
760 right_drawer_manager_ = std::make_unique<RightDrawerManager>();
761 right_drawer_manager_->SetToastManager(&toast_manager_);
762 right_drawer_manager_->SetProposalDrawer(&proposal_drawer_);
764 right_drawer_manager_->SetShortcutManager(&shortcut_manager_);
766 [this](const std::string& prompt) {
767#if defined(YAZE_BUILD_AGENT_UI)
768 auto* agent_editor = agent_ui_.GetAgentEditor();
769 if (!agent_editor) {
770 return;
771 }
772 auto* chat = agent_editor->GetAgentChat();
773 if (!chat) {
774 return;
775 }
776 chat->set_active(true);
777 chat->SendMessage(prompt);
778#else
779 (void)prompt;
780#endif
781 },
782 [this]() {
783#if defined(YAZE_BUILD_AGENT_UI)
785 right_drawer_manager_->OpenDrawer(
787 }
788#endif
789 });
792 right_drawer_manager_->ToggleDrawer(
794 } else {
796 }
797 });
798
799 // Initialize ProjectManagementPanel for project/version management
800 project_management_panel_ = std::make_unique<ProjectManagementPanel>();
801 project_management_panel_->SetToastManager(&toast_manager_);
803 [this](const std::string& filepath, const std::string& contents) {
804 return PrepareRawProjectFileSave(filepath, contents);
805 });
807 [this](const std::string& filepath, const std::string& contents) {
808 return CommitRawProjectFileSave(filepath, contents);
809 });
811 std::make_unique<workflow::ProjectWorkflowOutputPanel>());
812 project_management_panel_->SetSwapRomCallback([this]() {
813 // Prompt user to select a new ROM for the project
816 if (!rom_path.empty()) {
817 auto status = SwapProjectRom(rom_path);
818 if (status.ok()) {
819 toast_manager_.Show("Project ROM updated and reloaded.",
821 } else {
822 toast_manager_.Show(absl::StrFormat("Failed to update project ROM: %s",
823 status.message()),
825 }
826 }
827 });
828 project_management_panel_->SetReloadRomCallback([this]() {
831 auto status = ReloadProjectRom();
832 if (!status.ok()) {
834 absl::StrFormat("Failed to reload ROM: %s", status.message()),
836 }
837 }
838 });
839 project_management_panel_->SetSaveProjectCallback([this]() {
840 auto status = SaveProject();
841 if (status.ok()) {
842 toast_manager_.Show("Project saved", ToastType::kSuccess);
843 } else {
845 absl::StrFormat("Failed to save project: %s", status.message()),
847 }
848 return status;
849 });
850 project_management_panel_->SetBrowseFolderCallback(
851 [this](const std::string& type) {
853 if (!folder_path.empty()) {
854 if (type == "code") {
855 current_project_.code_folder = folder_path;
856 // Update assembly editor path
857 if (auto* editor_set = GetCurrentEditorSet()) {
858// iOS: avoid blocking folder enumeration on the UI thread.
859#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
860 editor_set->OpenAssemblyFolder(folder_path);
861#endif
862 window_manager_.SetFileBrowserPath("Assembly", folder_path);
863 }
864 } else if (type == "assets") {
865 current_project_.assets_folder = folder_path;
866 }
870 }
871 toast_manager_.Show(absl::StrFormat("%s folder set: %s", type.c_str(),
872 folder_path.c_str()),
874 }
875 });
876 project_management_panel_->SetBuildProjectCallback(
877 [this]() { QueueBuildCurrentProject(); });
878 project_management_panel_->SetCancelBuildCallback(
879 [this]() { CancelQueuedProjectBuild(); });
880 project_management_panel_->SetRunProjectCallback(
881 [this]() { (void)RunCurrentProject(); });
883 [this]() { QueueBuildCurrentProject(); });
885 [this]() { (void)RunCurrentProject(); });
887 window_manager_.OpenWindow("workflow.output");
888 window_manager_.MarkWindowRecentlyUsed("workflow.output");
889 });
891 [this]() { CancelQueuedProjectBuild(); });
892 right_drawer_manager_->SetProjectManagementPanel(
894
895 // STEP 4.6.1: Initialize LayoutCoordinator (facade for layout operations)
897 layout_deps.layout_manager = layout_manager_.get();
898 layout_deps.window_manager = &window_manager_;
899 layout_deps.ui_coordinator = ui_coordinator_.get();
900 layout_deps.toast_manager = &toast_manager_;
901 layout_deps.status_bar = &status_bar_;
903 layout_coordinator_.Initialize(layout_deps);
904
905 // STEP 4.6.2: Initialize EditorActivator (editor switching and jump navigation)
906 EditorActivator::Dependencies activator_deps;
907 activator_deps.window_manager = &window_manager_;
908 activator_deps.layout_manager = layout_manager_.get();
909 activator_deps.ui_coordinator = ui_coordinator_.get();
910 activator_deps.right_drawer_manager = right_drawer_manager_.get();
911 activator_deps.toast_manager = &toast_manager_;
912 activator_deps.event_bus = &event_bus_;
913 activator_deps.ensure_editor_assets_loaded = [this](EditorType type) {
914 return EnsureEditorAssetsLoaded(type);
915 };
916 activator_deps.get_current_editor_set = [this]() {
917 return GetCurrentEditorSet();
918 };
919 activator_deps.get_current_session_id = [this]() {
920 return GetCurrentSessionId();
921 };
922 activator_deps.queue_deferred_action = [this](std::function<void()> action) {
923 QueueDeferredAction(std::move(action));
924 };
925 editor_activator_.Initialize(activator_deps);
926
927 // STEP 4.7: Initialize ActivityBar
928 activity_bar_ = std::make_unique<ActivityBar>(
930 [this]() -> bool {
931 if (auto* editor_set = GetCurrentEditorSet()) {
932 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
934 return dungeon_editor->IsWorkbenchWorkflowEnabled();
935 }
936 }
937 return false;
938 },
939 [this](bool enabled) {
940 if (auto* editor_set = GetCurrentEditorSet()) {
941 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
943 dungeon_editor->QueueWorkbenchWorkflowMode(enabled);
944 }
945 }
946 });
947
948 // Wire per-user sidebar prefs so right-click / drag mutate persisted state.
949 activity_bar_->SetUserSettings(&user_settings_);
950
951 // Populate the MoreActions registry with the default rail entries. External
952 // callers can Register/Unregister further entries at runtime.
953 auto& registry = activity_bar_->actions_registry();
954 registry.Register({"command_palette",
955 "Command Palette",
958 {}});
959 registry.Register({"keyboard_shortcuts",
960 "Keyboard Shortcuts",
963 {}});
964 registry.Register({"open_rom_project",
965 "Open ROM / Project",
967 [this]() { window_manager_.TriggerOpenRom(); },
968 {}});
969 registry.Register({"settings",
970 "Settings",
972 [this]() { window_manager_.TriggerShowSettings(); },
973 {}});
974
975 // WindowHost is the declarative window registration surface used by
976 // editor/runtime systems.
977 window_host_ = std::make_unique<WindowHost>(&window_manager_);
978
979 // Wire up EventBus to WorkspaceWindowManager for action event publishing
981}
982
984 // Auto-register panels from ContentRegistry (Unified Panel System)
985 auto registry_panels = ContentRegistry::Panels::CreateAll();
986 for (auto& panel : registry_panels) {
988 }
989
990 // STEP 4.8: Initialize DashboardPanel
991 dashboard_panel_ = std::make_unique<DashboardPanel>(this);
992
993 if (window_host_) {
994 WindowDefinition dashboard_definition;
995 dashboard_definition.id = "dashboard.main";
996 dashboard_definition.display_name = "Dashboard";
997 dashboard_definition.icon = ICON_MD_DASHBOARD;
998 dashboard_definition.category = "Dashboard";
999 dashboard_definition.window_title = " Dashboard";
1000 dashboard_definition.shortcut_hint = "F1";
1001 dashboard_definition.priority = 0;
1002 dashboard_definition.visibility_flag = dashboard_panel_->visibility_flag();
1003 window_host_->RegisterWindow(dashboard_definition);
1004 } else {
1006 {.card_id = "dashboard.main",
1007 .display_name = "Dashboard",
1008 .window_title = " Dashboard",
1009 .icon = ICON_MD_DASHBOARD,
1010 .category = "Dashboard",
1011 .shortcut_hint = "F1",
1012 .visibility_flag = dashboard_panel_->visibility_flag(),
1013 .priority = 0});
1014 }
1015}
1016
1018 // Subscribe to session lifecycle events via EventBus
1019 // (replaces SessionObserver pattern)
1021 [this](const SessionSwitchedEvent& e) {
1022 HandleSessionSwitched(e.new_index, e.session, e.transient);
1023 });
1024
1026 [this](const SessionCreatedEvent& e) {
1027 HandleSessionCreated(e.index, e.session);
1028 });
1029
1031 [this](const SessionClosedEvent& e) { HandleSessionClosed(e.index); });
1032
1034 HandleSessionRomLoaded(e.session_id, e.rom);
1035 });
1036
1037 // Subscribe to FrameGuiBeginEvent for ImGui-safe deferred action processing
1038 // This replaces scattered manual processing calls with event-driven execution
1040 ui_sync_frame_id_.fetch_add(1, std::memory_order_relaxed);
1041
1042 // Process LayoutCoordinator's deferred actions
1044
1045 // Process EditorManager's deferred actions
1046 if (!deferred_actions_.empty()) {
1047 std::vector<std::function<void()>> actions_to_execute;
1048 actions_to_execute.swap(deferred_actions_);
1049 const int processed_count = static_cast<int>(actions_to_execute.size());
1050 for (auto& action : actions_to_execute) {
1051 action();
1052 }
1053 if (processed_count > 0) {
1054 const int remaining = pending_editor_deferred_actions_.fetch_sub(
1055 processed_count, std::memory_order_relaxed) -
1056 processed_count;
1057 if (remaining < 0) {
1058 pending_editor_deferred_actions_.store(0, std::memory_order_relaxed);
1059 }
1060 }
1061 }
1062 });
1063
1064 // Subscribe to UIActionRequestEvent for activity bar actions
1065 // This replaces direct callbacks from WorkspaceWindowManager
1067 [this](const UIActionRequestEvent& e) {
1068 HandleUIActionRequest(e.action);
1069 });
1070
1072 [this](const PanelVisibilityChangedEvent& e) {
1073 if (e.category.empty() ||
1075 return;
1076 }
1077 if (IsTransientPanelVisibilityId(e.base_panel_id)) {
1078 return;
1079 }
1080 auto& prefs = user_settings_.prefs();
1081 prefs.panel_visibility_state[e.category][e.base_panel_id] = e.visible;
1082 settings_dirty_ = true;
1084 });
1085}
1086
1088 // ContentRegistry::Context holds non-owning pointers into our members
1089 // (event_bus_, editor_context_, user_settings_, layout_manager_, callbacks).
1090 // Clear them before our members destruct so widgets that read the singleton
1091 // (e.g. Canvas::set_global_scale via Context::event_bus()) don't see
1092 // dangling pointers in tests that instantiate multiple EditorManagers.
1093 // Clear() resets fallback state but intentionally keeps global_context for
1094 // session-close semantics, so null it explicitly here.
1097
1098 // GetResourceLabels() is a process-wide singleton. RefreshResourceLabelProvider
1099 // hands it a pointer to current_project_.hack_manifest, which dangles after
1100 // we destruct. Clear before our members go out of scope.
1103
1104 // ThemeManager is a singleton that outlives us. Clear the theme-changed
1105 // callback so it stops calling back into a destroyed EditorManager via the
1106 // captured `this` pointer. Matters for unit tests that construct and destroy
1107 // multiple EditorManager instances in the same process.
1110
1111 // EventBus subscriptions are automatically cleaned up when event_bus_ is
1112 // destroyed (owned by this class). No manual unsubscription needed.
1113}
1114
1115// ============================================================================
1116// Session Event Handlers (EventBus subscribers)
1117// ============================================================================
1118
1120 auto& provider = zelda3::GetResourceLabels();
1123
1124 auto merge_labels =
1127 for (const auto& [type, labels] : src) {
1128 auto& out = (*dst)[type];
1129 for (const auto& [key, value] : labels) {
1130 out[key] = value;
1131 }
1132 }
1133 };
1134
1135 merged_labels.clear();
1136 std::vector<std::string> source_parts;
1137
1138 // 1) ROM-local labels as baseline for active session.
1139 if (auto* rom = GetCurrentRom();
1140 rom && rom->resource_label() && !rom->resource_label()->labels_.empty()) {
1141 merge_labels(&merged_labels, rom->resource_label()->labels_);
1142 source_parts.push_back("rom");
1143 }
1144
1145 // 2) Project registry labels from the active hack manifest.
1149 merge_labels(
1150 &merged_labels,
1152 source_parts.push_back("registry");
1153 }
1154
1155 // 3) Explicit project labels override all other sources.
1158 merge_labels(&merged_labels, current_project_.resource_labels);
1159 source_parts.push_back("project");
1160 }
1161
1162 auto* label_source = merged_labels.empty() ? &empty_labels : &merged_labels;
1163 std::string source =
1164 source_parts.empty() ? "empty" : absl::StrJoin(source_parts, "+");
1165
1166 provider.SetProjectLabels(label_source);
1167 provider.SetHackManifest(current_project_.project_opened() &&
1170 : nullptr);
1171
1172 const bool prefer_hmagic =
1176 provider.SetPreferHMagicNames(prefer_hmagic);
1177
1178 LOG_DEBUG("EditorManager",
1179 "ResourceLabelProvider refreshed (source=%s, project_open=%s)",
1180 source.c_str(),
1181 current_project_.project_opened() ? "true" : "false");
1182}
1183
1186 return;
1187 }
1188
1189 const auto index =
1191 if (!index.has_value()) {
1193 return;
1194 }
1195
1196 auto* session =
1197 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1198 if (!session) {
1200 return;
1201 }
1202
1204 if (session->project_context.has_value()) {
1205 session->project_context->feature_flags = session->feature_flags;
1206 }
1208 current_project_.feature_flags = session->feature_flags;
1209 }
1210}
1211
1213 if (!active_project_context_session_id_.has_value() ||
1215 return;
1216 }
1217
1220 const auto index =
1222 if (!index.has_value()) {
1224 return;
1225 }
1226
1227 auto* session =
1228 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1229 if (!session) {
1231 return;
1232 }
1233
1234 // The singleton can temporarily belong to another session while all editor
1235 // windows are ticked. Only use that value when it still belongs to this
1236 // project owner; otherwise its session-owned copy is authoritative.
1239 }
1240 // These preferences are exposed through global runtime settings, but they
1241 // are project workspace state. Capture them before leaving the session so a
1242 // later SaveProject cannot copy another session's values into this project.
1251 current_project_.feature_flags = session->feature_flags;
1253}
1254
1256 if (!active_project_context_session_id_.has_value() ||
1258 return;
1259 }
1260
1261 const auto index =
1263 if (!index.has_value()) {
1264 return;
1265 }
1266 auto* session =
1267 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1268 if (!session) {
1269 return;
1270 }
1271
1273 session->project_dirty = project_management_panel_->IsProjectDirty();
1274 }
1275 session->project_file_editor_state = project_file_editor_.CaptureState();
1276}
1277
1287
1289 std::optional<size_t> previous_session_id) {
1292 if (previous_session_id.has_value() && session_coordinator_) {
1293 const auto index = ResolveSessionIndexById(*previous_session_id);
1294 if (index.has_value()) {
1295 auto* previous_session =
1296 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1297 if (previous_session) {
1298 if (session_coordinator_->GetActiveSessionIndex() != *index) {
1299 session_coordinator_->SwitchToSession(*index);
1300 } else {
1301 RestoreProjectContextForSession(previous_session);
1302 }
1303 return;
1304 }
1305 }
1306 }
1307
1311 version_manager_ = nullptr;
1314}
1315
1317 size_t previous_session_count) {
1318 if (!session_coordinator_ ||
1319 session_coordinator_->GetTotalSessionCount() <= previous_session_count) {
1320 return absl::OkStatus();
1321 }
1322 auto* provisional_session = session_coordinator_->GetActiveRomSession();
1323 if (!provisional_session) {
1324 return absl::InternalError(
1325 "A provisional session was created but is not active");
1326 }
1327 return session_coordinator_->DiscardProvisionalSession(
1328 provisional_session->session_id());
1329}
1330
1339
1341 const std::string& filepath, const std::string& contents) {
1343 const bool targets_current = !current_project_.filepath.empty() &&
1345 filepath, current_project_.filepath);
1346 if (!targets_current) {
1347 // Save As to a separate descriptor is the deterministic escape hatch when
1348 // both the structured panel and raw document have edits.
1349 return absl::OkStatus();
1350 }
1351 if (IsCurrentProjectDirty()) {
1352 return absl::FailedPreconditionError(
1353 "Project settings also have unsaved changes; use raw Save As to "
1354 "preserve this draft, then save the project settings");
1355 }
1356
1357 auto* session = session_coordinator_
1358 ? session_coordinator_->GetActiveRomSession()
1359 : nullptr;
1360 if (!session || !session->rom.is_loaded()) {
1361 return absl::FailedPreconditionError(
1362 "No active ROM session for this project file");
1363 }
1364
1365 project::YazeProject parsed_project;
1366 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1367 if (!parsed_project.project_opened()) {
1368 return absl::InvalidArgumentError(
1369 "Raw project file must contain a non-empty project name");
1370 }
1371 if (parsed_project.rom_filename.empty() ||
1373 parsed_project.rom_filename, session->filepath) ||
1375 parsed_project.rom_filename, session->rom.filename()))) {
1376 return absl::FailedPreconditionError(
1377 "Raw project file cannot change the ROM backing file of a loaded "
1378 "session; use Project Management > Swap ROM");
1379 }
1380 return session_coordinator_->CheckBackingFileAvailable(
1381 parsed_project.rom_filename, session->session_id());
1382}
1383
1385 const std::string& filepath, const std::string& contents) {
1386 if (current_project_.filepath.empty() ||
1388 filepath, current_project_.filepath)) {
1389 return absl::OkStatus();
1390 }
1391
1392 auto* session = session_coordinator_
1393 ? session_coordinator_->GetActiveRomSession()
1394 : nullptr;
1395 if (!session) {
1396 return absl::FailedPreconditionError("No active ROM session");
1397 }
1398
1399 project::YazeProject parsed_project;
1400 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1401 current_project_ = std::move(parsed_project);
1403 session->project_dirty = false;
1404 active_project_context_session_id_ = session->session_id();
1405 runtime_feature_flags_session_id_ = session->session_id();
1406 version_manager_ = session->version_manager.get();
1409 project_management_panel_->SetProject(&current_project_, false);
1410 }
1411 return absl::OkStatus();
1412}
1413
1414void EditorManager::RebaseCleanProjectFileDraft(const std::string& filepath) {
1415 if (!session_coordinator_ || filepath.empty()) {
1416 return;
1417 }
1418 auto* session = session_coordinator_->GetActiveRomSession();
1419 if (!session || !session->project_file_editor_state.initialized ||
1420 session->project_file_editor_state.modified) {
1421 return;
1422 }
1423
1424 const bool was_active = project_file_editor_.is_active();
1425 auto status = project_file_editor_.LoadFile(filepath);
1426 if (!status.ok()) {
1427 LOG_WARN("EditorManager", "Failed to rebase raw project editor: %s",
1428 status.message());
1429 return;
1430 }
1432 project_file_editor_.set_active(was_active);
1433 session->project_file_editor_state = project_file_editor_.CaptureState();
1434}
1435
1437 RomSession* session, const project::YazeProject& project) {
1438 if (!session) {
1439 return;
1440 }
1441 const bool needs_version_manager =
1442 !session->project_context.has_value() || !session->version_manager;
1443 if (session->project_context.has_value()) {
1444 *session->project_context = project;
1445 } else {
1446 session->project_context.emplace(project);
1447 }
1448 if (needs_version_manager) {
1449 session->version_manager =
1450 std::make_unique<core::VersionManager>(&*session->project_context);
1451 }
1452 session->feature_flags = project.feature_flags;
1453 // Configure after the stable context and VersionManager exist. EditorSet
1454 // instances are created before SessionCreatedEvent, so their first
1455 // dependency pass can legitimately have no project yet.
1456 ConfigureSession(session);
1457}
1458
1462
1471 if (ImGui::GetCurrentContext() != nullptr) {
1472 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
1473 }
1474
1479 } else {
1482 }
1485 ? ""
1488
1496 } else {
1498 }
1499
1505 const auto* session = session_coordinator_
1506 ? session_coordinator_->GetActiveRomSession()
1507 : nullptr;
1508 project_management_panel_->SetProject(
1509 &current_project_, session != nullptr && session->project_dirty);
1510 project_management_panel_->SetVersionManager(version_manager_);
1512 }
1513}
1514
1516 if (!session || !session->project_context.has_value()) {
1520 version_manager_ = nullptr;
1522 project_management_panel_->SetVersionManager(nullptr);
1523 }
1524 return false;
1525 }
1526
1527 if (!session->version_manager) {
1528 session->version_manager =
1529 std::make_unique<core::VersionManager>(&*session->project_context);
1530 ConfigureSession(session);
1531 }
1533 version_manager_ = session->version_manager.get();
1539 return true;
1540}
1541
1543 if (!session) {
1545 project_management_panel_->SetProject(nullptr);
1546 }
1548 return;
1549 }
1550
1553 session->project_dirty);
1554 }
1558 } else {
1560 }
1561}
1562
1563absl::StatusOr<const project::YazeProject*>
1565 if (!session_coordinator_) {
1566 return absl::FailedPreconditionError("No session coordinator");
1567 }
1568 auto* session = session_coordinator_->GetActiveRomSession();
1569 if (!session || !session->rom.is_loaded()) {
1570 return absl::FailedPreconditionError("No active ROM session");
1571 }
1572
1574 if (!active_project_context_session_id_.has_value() ||
1575 *active_project_context_session_id_ != session->session_id()) {
1576 return absl::FailedPreconditionError(
1577 "Active ROM session project context is not restored");
1578 }
1580 if (!session->project_context.has_value()) {
1581 return absl::FailedPreconditionError(
1582 "Active ROM session has no project/save context");
1583 }
1584 return &*session->project_context;
1585}
1586
1587void EditorManager::HandleSessionSwitched(size_t new_index, RomSession* session,
1588 bool transient) {
1589 // Confirmation consent and Save As targets belong to the ROM that started
1590 // the request. Never carry them across a user session switch. Frame-level
1591 // context switches are temporary and must not consume the active request.
1592 if (!transient) {
1597 CancelPendingRomSave(/*hide_popups=*/true);
1600 if (!RestoreProjectContextForSession(session) && session != nullptr) {
1601 LOG_ERROR("EditorManager",
1602 "Session %zu has no project context; saves are disabled",
1603 new_index);
1604 }
1605 } else {
1606 // Frame iteration must not copy full projects or reset project-wide caches.
1607 // Feature flags are cheap and directly affect per-editor draw/save paths.
1609 if (session) {
1612 } else {
1614 }
1615 }
1616
1617 // Palette edit history and dirty tracking are session-owned. Select the
1618 // matching state before any session-specific editor or save action runs.
1620 : nullptr);
1621
1622 // Update RightDrawerManager with the new session's settings editor
1624 right_drawer_manager_->SetSettingsPanel(
1625 session ? session->editors.GetSettingsPanel() : nullptr);
1626 }
1627
1628 // Update properties panel with new ROM
1629 if (session) {
1630 if (!transient) {
1632 }
1634 } else {
1637 }
1638
1639 // Update ContentRegistry context with current session's ROM and GameData
1640 ContentRegistry::Context::SetRom(session ? &session->rom : nullptr);
1642 : nullptr);
1643
1644 // current_editor_ is also session-owned. Resolve it from the newly active
1645 // EditorSet, or clear it when the current category has no editor backing.
1646 const std::string active_category = window_manager_.GetActiveCategory();
1647 Editor* active_editor = ResolveEditorForCategory(active_category);
1648 SetCurrentEditor(active_editor);
1650 active_editor);
1651
1652 // Keep room/sprite labels in sync with active session context.
1654
1655 const std::string category = window_manager_.GetActiveCategory();
1656 if (session != nullptr && !category.empty() &&
1658 auto it = user_settings_.prefs().panel_visibility_state.find(category);
1659 if (it != user_settings_.prefs().panel_visibility_state.end()) {
1660 const size_t session_id = session ? session->session_id() : new_index;
1661 window_manager_.RestoreVisibilityState(session_id, it->second);
1662 }
1663 }
1664
1665#ifdef YAZE_ENABLE_TESTING
1666 test::TestManager::Get().SetCurrentRom(session ? &session->rom : nullptr);
1667#endif
1668
1669 // RomLifecycleManager caches both identity hash and path; refresh them for
1670 // the newly active session before its next save-policy check.
1672
1673 LOG_DEBUG("EditorManager", "Session switched to %zu via EventBus", new_index);
1674}
1675
1678 CancelPendingRomSave(/*hide_popups=*/true);
1679 }
1681 if (session && !session->project_context.has_value()) {
1682 // A parsed project deliberately detaches the prior owner before creating
1683 // its ROM session. Normal raw-ROM and empty-session opens must instead get
1684 // a neutral context, not a copy of whichever project happened to be active.
1685 if (!active_project_context_session_id_.has_value() &&
1688 } else {
1689 project::YazeProject neutral_project;
1690 BindProjectContextToSession(session, neutral_project);
1691 }
1692 }
1693
1694 const size_t session_id = session ? session->session_id() : index;
1698 index == session_coordinator_->GetActiveSessionIndex()) {
1701 : nullptr);
1703 }
1704 LOG_INFO("EditorManager", "Session %zu created via EventBus", index);
1705}
1706
1708 CancelPendingRomSave(/*hide_popups=*/true);
1709
1710 // SessionClosedEvent is emitted before the owning RomSession is erased and
1711 // before the active index is adjusted. Bind the surviving session now so the
1712 // closing session's destructor cannot leave PaletteManager unbound.
1713 RomSession* palette_session = nullptr;
1715 const size_t session_count = session_coordinator_->GetTotalSessionCount();
1716 auto* active_session = session_coordinator_->GetActiveRomSession();
1717 auto* closing_session =
1718 session_coordinator_->IsValidSessionIndex(index)
1719 ? static_cast<RomSession*>(session_coordinator_->GetSession(index))
1720 : nullptr;
1721 if (closing_session != nullptr &&
1722 active_project_context_session_id_ == closing_session->session_id()) {
1725 if (runtime_feature_flags_session_id_ == closing_session->session_id()) {
1727 }
1728 version_manager_ = nullptr;
1730 project_management_panel_->SetVersionManager(nullptr);
1731 }
1732 }
1733 palette_session = active_session;
1734 if (closing_session != nullptr && active_session == closing_session &&
1735 session_count > 1) {
1736 const size_t replacement_index = index > 0 ? index - 1 : 1;
1737 palette_session = static_cast<RomSession*>(
1738 session_coordinator_->GetSession(replacement_index));
1739 }
1740 }
1742 palette_session ? &palette_session->game_data : nullptr);
1743
1744 // Update ContentRegistry - it will be set to new active ROM on next switch
1745 // If no sessions remain, clear the context
1747 session_coordinator_->GetTotalSessionCount() == 0) {
1749 }
1750
1751 // Avoid dangling/stale label map pointers after session close.
1753
1754#ifdef YAZE_ENABLE_TESTING
1755 // Update test manager - it will get the new current ROM on next switch
1757#endif
1758
1759 LOG_INFO("EditorManager", "Session %zu closed via EventBus", index);
1760}
1761
1763 auto* session =
1764 static_cast<RomSession*>(session_coordinator_->GetSession(index));
1765 ResetAssetState(session);
1766
1767 // Update ContentRegistry when ROM is loaded (if this is the active session)
1768 if (rom && session_coordinator_ &&
1769 index == session_coordinator_->GetActiveSessionIndex()) {
1771 // Also update GameData from the session
1772 if (session) {
1773 ContentRegistry::Context::SetGameData(&session->game_data);
1774 }
1776 }
1777
1778#ifdef YAZE_ENABLE_TESTING
1779 if (rom) {
1781 }
1782#endif
1783
1784 LOG_INFO("EditorManager", "ROM loaded in session %zu via EventBus", index);
1785}
1786
1788 using Action = UIActionRequestEvent::Action;
1789 switch (action) {
1790 case Action::kShowEmulator:
1791 if (ui_coordinator_) {
1792 ui_coordinator_->SetEmulatorVisible(true);
1793 }
1794 break;
1795
1796 case Action::kShowSettings:
1797 // Toggle Settings panel in sidebar
1799 right_drawer_manager_->ToggleDrawer(
1801 } else {
1803 }
1804 break;
1805
1806 case Action::kShowPanelBrowser:
1807 if (ui_coordinator_) {
1808 ui_coordinator_->ShowWindowBrowser();
1809 }
1810 break;
1811
1812 case Action::kShowSearch:
1813 if (ui_coordinator_) {
1814 ui_coordinator_->ShowGlobalSearch();
1815 }
1816 break;
1817
1818 case Action::kShowShortcuts:
1819 // Shortcut configuration is part of Settings
1821 break;
1822
1823 case Action::kShowCommandPalette:
1824 if (ui_coordinator_) {
1825 ui_coordinator_->ShowCommandPalette();
1826 }
1827 break;
1828
1829 case Action::kShowHelp:
1831 // Toggle Help panel in sidebar
1832 right_drawer_manager_->ToggleDrawer(
1834 } else if (popup_manager_) {
1835 // Fallback to "About" dialog if sidebar not available
1837 }
1838 break;
1839
1840 case Action::kShowAgentChatSidebar:
1842 right_drawer_manager_->OpenDrawer(
1844 } else {
1846 }
1847 break;
1848
1849 case Action::kShowAgentProposalsSidebar:
1851 right_drawer_manager_->OpenDrawer(
1853 }
1854 break;
1855
1856 case Action::kOpenRom: {
1857 auto status = LoadRom();
1858 if (!status.ok()) {
1860 std::string("Open failed: ") + std::string(status.message()),
1862 }
1863 } break;
1864
1865 case Action::kSaveRom:
1866 if (GetCurrentRom() && GetCurrentRom()->is_loaded()) {
1867 auto status = SaveRom();
1868 if (!absl::IsCancelled(status) && !status.ok()) {
1870 std::string("Save failed: ") + std::string(status.message()),
1872 }
1873 }
1874 break;
1875
1876 case Action::kUndo:
1877 if (auto* current_editor = GetCurrentEditor()) {
1878 auto status = current_editor->Undo();
1879 if (!status.ok()) {
1881 std::string("Undo failed: ") + std::string(status.message()),
1883 }
1884 }
1885 break;
1886
1887 case Action::kRedo:
1888 if (auto* current_editor = GetCurrentEditor()) {
1889 auto status = current_editor->Redo();
1890 if (!status.ok()) {
1892 std::string("Redo failed: ") + std::string(status.message()),
1894 }
1895 }
1896 break;
1897
1898 case Action::kResetLayout:
1900 break;
1901 }
1902}
1903
1905 auto& test_manager = test::TestManager::Get();
1906
1907#ifdef YAZE_ENABLE_TESTING
1908 // Register comprehensive test suites
1909 test_manager.RegisterTestSuite(
1910 std::make_unique<test::CoreSystemsTestSuite>());
1911 test_manager.RegisterTestSuite(std::make_unique<test::IntegratedTestSuite>());
1912 test_manager.RegisterTestSuite(
1913 std::make_unique<test::PerformanceTestSuite>());
1914 test_manager.RegisterTestSuite(std::make_unique<test::UITestSuite>());
1915 test_manager.RegisterTestSuite(
1916 std::make_unique<test::RomDependentTestSuite>());
1917
1918 // Register new E2E and ZSCustomOverworld test suites
1919 test_manager.RegisterTestSuite(std::make_unique<test::E2ETestSuite>());
1920 test_manager.RegisterTestSuite(
1921 std::make_unique<test::ZSCustomOverworldTestSuite>());
1922#endif
1923
1924 // Register Google Test suite if available
1925#ifdef YAZE_ENABLE_GTEST
1926 test_manager.RegisterTestSuite(std::make_unique<test::UnitTestSuite>());
1927#endif
1928
1929 // Register z3ed AI Agent test suites (requires gRPC)
1930#ifdef YAZE_WITH_GRPC
1932#endif
1933
1934 // Update resource monitoring to track Arena state
1935 test_manager.UpdateResourceStats();
1936}
1937
1939 const std::string& filename) {
1940 renderer_ = renderer;
1942 SeedOracleProjectInRecents();
1943
1944 // Inject the window manager into emulator and workspace_manager
1947
1948 // Point to a blank editor set when no ROM is loaded
1949 // current_editor_set_ = &blank_editor_set_;
1950
1951 if (!filename.empty()) {
1953 }
1954
1955 // Note: PopupManager is now initialized in constructor before
1956 // MenuOrchestrator This ensures all menu callbacks can safely call
1957 // popup_manager_.Show()
1958
1962
1963 // Apply sidebar state from settings AFTER registering callbacks
1964 // This triggers the callbacks but they should be safe now
1966 /*notify=*/false);
1969 /*notify=*/false);
1971 user_settings_.prefs().sidebar_panel_width, /*notify=*/false);
1974 /*notify=*/false);
1975 {
1976 const bool prefer_dashboard_only =
1978 startup_editor_hint_.empty() && startup_panel_hints_.empty();
1979 if (!prefer_dashboard_only) {
1980 const std::string category = GetPreferredStartupCategory(
1982 if (!category.empty()) {
1983 window_manager_.SetActiveCategory(category, /*notify=*/false);
1985 auto it = user_settings_.prefs().panel_visibility_state.find(category);
1986 if (it != user_settings_.prefs().panel_visibility_state.end()) {
1988 window_manager_.GetActiveSessionId(), it->second);
1989 }
1990 }
1991 }
1992 }
1993
1997 }
1998
1999 // Initialize testing system only when tests are enabled
2000#ifdef YAZE_ENABLE_TESTING
2002#endif
2003
2004 // TestManager will be updated when ROMs are loaded via SetCurrentRom calls
2005
2007}
2008
2010 // Register emulator panels early (emulator Initialize might not be called).
2011 const std::vector<WindowDefinition> panel_definitions = {
2012 {.id = "emulator.cpu_debugger",
2013 .display_name = "CPU Debugger",
2014 .icon = ICON_MD_BUG_REPORT,
2015 .category = "Emulator",
2016 .priority = 10},
2017 {.id = "emulator.ppu_viewer",
2018 .display_name = "PPU Viewer",
2020 .category = "Emulator",
2021 .priority = 20},
2022 {.id = "emulator.memory_viewer",
2023 .display_name = "Memory Viewer",
2024 .icon = ICON_MD_MEMORY,
2025 .category = "Emulator",
2026 .priority = 30},
2027 {.id = "emulator.breakpoints",
2028 .display_name = "Breakpoints",
2029 .icon = ICON_MD_STOP,
2030 .category = "Emulator",
2031 .priority = 40},
2032 {.id = "emulator.performance",
2033 .display_name = "Performance",
2034 .icon = ICON_MD_SPEED,
2035 .category = "Emulator",
2036 .priority = 50},
2037 {.id = "emulator.ai_agent",
2038 .display_name = "AI Agent",
2039 .icon = ICON_MD_SMART_TOY,
2040 .category = "Emulator",
2041 .priority = 60},
2042 {.id = "emulator.save_states",
2043 .display_name = "Save States",
2044 .icon = ICON_MD_SAVE,
2045 .category = "Emulator",
2046 .priority = 70},
2047 {.id = "emulator.keyboard_config",
2048 .display_name = "Keyboard Config",
2049 .icon = ICON_MD_KEYBOARD,
2050 .category = "Emulator",
2051 .priority = 80},
2052 {.id = "emulator.virtual_controller",
2053 .display_name = "Virtual Controller",
2054 .icon = ICON_MD_SPORTS_ESPORTS,
2055 .category = "Emulator",
2056 .priority = 85},
2057 {.id = "emulator.apu_debugger",
2058 .display_name = "APU Debugger",
2059 .icon = ICON_MD_AUDIOTRACK,
2060 .category = "Emulator",
2061 .priority = 90},
2062 {.id = "emulator.audio_mixer",
2063 .display_name = "Audio Mixer",
2064 .icon = ICON_MD_AUDIO_FILE,
2065 .category = "Emulator",
2066 .priority = 100},
2067 {.id = "memory.hex_editor",
2068 .display_name = "Hex Editor",
2069 .icon = ICON_MD_MEMORY,
2070 .category = "Memory",
2071 .window_title = ICON_MD_MEMORY " Hex Editor",
2072 .priority = 10,
2073 .legacy_ids = {"Memory Editor"}},
2074 };
2075
2076 if (window_host_) {
2077 window_host_->RegisterPanels(panel_definitions);
2078 return;
2079 }
2080
2081 for (const auto& definition : panel_definitions) {
2082 WindowDescriptor descriptor;
2083 descriptor.card_id = definition.id;
2084 descriptor.display_name = definition.display_name;
2085 descriptor.window_title = definition.window_title;
2086 descriptor.icon = definition.icon;
2087 descriptor.category = definition.category;
2088 descriptor.shortcut_hint = definition.shortcut_hint;
2089 descriptor.priority = definition.priority;
2090 descriptor.scope = definition.scope;
2091 descriptor.window_lifecycle = definition.window_lifecycle;
2092 descriptor.context_scope = definition.context_scope;
2093 descriptor.visibility_flag = definition.visibility_flag;
2094 descriptor.on_show = definition.on_show;
2095 descriptor.on_hide = definition.on_hide;
2096
2097 for (const auto& legacy_id : definition.legacy_ids) {
2098 window_manager_.RegisterPanelAlias(legacy_id, definition.id);
2099 }
2100
2101 window_manager_.RegisterWindow(descriptor);
2102 if (definition.visible_by_default) {
2103 window_manager_.OpenWindow(definition.id);
2104 }
2105 }
2106}
2107
2109 // Initialize project file editor
2111
2112 // Initialize agent UI (no-op when agent UI is disabled)
2116
2117 // Note: Unified gRPC Server is started from Application::Initialize()
2118 // after gRPC infrastructure is properly set up
2119
2120 // Load critical user settings first
2122 if (!status_.ok()) {
2123 LOG_WARN("EditorManager", "Failed to load user settings: %s",
2124 status_.ToString().c_str());
2125 }
2126
2127 // Wire theme persistence. Any successful theme application (selector click,
2128 // command-palette switch, programmatic call) stamps the name into prefs and
2129 // rides the existing debounced-save path. Decouples ThemeManager (singleton
2130 // in app/gui) from UserSettings (editor-layer) — ThemeManager holds only a
2131 // std::function.
2133 [this](const std::string& theme_name) {
2134 user_settings_.prefs().last_theme_name = theme_name;
2135 settings_dirty_ = true;
2137 });
2139 [this](const std::string& locale) {
2140 user_settings_.prefs().language_locale = locale;
2141 settings_dirty_ = true;
2143 });
2144
2145 auto& prefs = user_settings_.prefs();
2146 i18n::LanguageManager::Get().SetLanguage(prefs.language_locale);
2147
2148 // Apply the persisted font selection (defaults to index 0 = Karla).
2149 // Fonts were loaded by the window backend before EditorManager init, so
2150 // ImGui::GetIO().Fonts->Fonts is fully populated here.
2151 ::yaze::SetActiveFontIndex(prefs.font_family_index);
2152
2153 prefs.switch_motion_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
2155 prefs.reduced_motion,
2156 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
2157
2159
2162 right_drawer_manager_->ResetDrawerWidths();
2164 right_drawer_manager_->SerializeDrawerWidths();
2165 } else {
2166 right_drawer_manager_->RestoreDrawerWidths(
2168 }
2169 right_drawer_manager_->SetDrawerWidthChangedCallback(
2170 [this](RightDrawerManager::DrawerType, float) {
2171 if (!right_drawer_manager_) {
2172 return;
2173 }
2174 user_settings_.prefs().right_panel_widths =
2175 right_drawer_manager_->SerializeDrawerWidths();
2176 settings_dirty_ = true;
2178 });
2179 }
2181 // Apply sprite naming preference globally.
2184
2186
2187 // Apply font scale after loading (only if ImGui context exists)
2188 if (ImGui::GetCurrentContext() != nullptr) {
2189 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
2190 } else {
2191 LOG_WARN("EditorManager",
2192 "ImGui context not available; skipping FontGlobalScale update");
2193 }
2194
2195 // Restore the user's last theme. Empty (first run) or "Custom Accent"
2196 // (transient generated themes without a matching discovered preset) both
2197 // skip restoration and keep the built-in default (Classic YAZE / YAZE Tre).
2198 const auto& last_theme = user_settings_.prefs().last_theme_name;
2199 if (!last_theme.empty() && last_theme != "Custom Accent") {
2200 gui::ThemeManager::Get().ApplyTheme(last_theme);
2201 }
2202
2203 // Initialize WASM control and session APIs for browser/agent integration
2204#ifdef __EMSCRIPTEN__
2207 LOG_INFO("EditorManager", "WASM Control and Session APIs initialized");
2208#endif
2209}
2210
2216
2218 // Initialize ROM load options dialog callbacks
2220 [this](const RomLoadOptionsDialog::LoadOptions& options) {
2221 // Apply feature flags from dialog
2222 auto& flags = core::FeatureFlags::get();
2223 flags.overworld.kSaveOverworldMaps = options.save_overworld_maps;
2224 flags.overworld.kSaveOverworldEntrances =
2226 flags.overworld.kSaveOverworldExits = options.save_overworld_exits;
2227 flags.overworld.kSaveOverworldItems = options.save_overworld_items;
2228 flags.overworld.kLoadCustomOverworld = options.enable_custom_overworld;
2229 flags.kSaveDungeonMaps = options.save_dungeon_maps;
2230 flags.kSaveAllPalettes = options.save_all_palettes;
2231 flags.kSaveGfxGroups = options.save_gfx_groups;
2233 if (auto* session = session_coordinator_->GetActiveRomSession()) {
2234 session->feature_flags = flags;
2235 }
2236 }
2237
2238 // Create project if requested
2239 if (options.create_project && !options.project_name.empty()) {
2240 auto status =
2242 if (!status.ok()) {
2243 toast_manager_.Show(absl::StrFormat("Failed to create project: %s",
2244 status.message()),
2246 } else {
2247 toast_manager_.Show("Project created: " + options.project_name,
2249 }
2250 }
2251
2252 // Close dialog and show editor selection
2253 show_rom_load_options_ = false;
2254 if (ui_coordinator_) {
2255 ui_coordinator_->SetEditorSelectionVisible(true);
2256 }
2257
2258 LOG_INFO("EditorManager", "ROM load options applied: preset=%s",
2259 options.selected_preset.c_str());
2260 });
2261}
2262
2264 // Initialize welcome screen callbacks
2266
2268 [this]() { status_ = CreateNewProject(); });
2269
2271 [this](const std::string& template_name) {
2272 status_ = CreateNewProject(template_name);
2273 });
2274
2275 welcome_screen_.SetOpenProjectCallback([this](const std::string& filepath) {
2276 status_ = OpenRomOrProject(filepath);
2277 if (status_.ok() && ui_coordinator_) {
2278 ui_coordinator_->SetWelcomeScreenVisible(false);
2279 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2280 }
2281 });
2282
2284#ifdef YAZE_BUILD_AGENT_UI
2285 ShowAIAgent();
2286#endif
2287 });
2288
2291 window_manager_.OpenWindow("graphics.prototype_viewer");
2292 if (ui_coordinator_) {
2293 ui_coordinator_->SetWelcomeScreenVisible(false);
2294 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2295 }
2296 });
2297
2300 window_manager_.OpenWindow("assembly.code_editor");
2301 if (ui_coordinator_) {
2302 ui_coordinator_->SetWelcomeScreenVisible(false);
2303 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2304 }
2305 });
2306
2308 status_ = OpenProject();
2309 if (status_.ok() && ui_coordinator_) {
2310 ui_coordinator_->SetWelcomeScreenVisible(false);
2311 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2312 } else if (!status_.ok()) {
2314 absl::StrFormat("Failed to open project: %s", status_.message()),
2316 }
2317 });
2318
2320 [this]() { ShowProjectManagement(); });
2321
2323 if (current_project_.filepath.empty()) {
2324 toast_manager_.Show("No project file to edit", ToastType::kInfo);
2325 return;
2326 }
2328 });
2329
2330 // Apply welcome screen preference
2332 ui_coordinator_->SetWelcomeScreenVisible(false);
2333 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2334 }
2335}
2336
2338 // Utility callbacks removed - now handled via EventBus
2339
2341 [this](bool visible, bool expanded) {
2342 user_settings_.prefs().sidebar_visible = visible;
2343 user_settings_.prefs().sidebar_panel_expanded = expanded;
2344 settings_dirty_ = true;
2346 });
2349 settings_dirty_ = true;
2351 });
2353 [this](float width) {
2355 settings_dirty_ = true;
2357 });
2358
2360 const std::string& category) {
2361 if (category.empty() ||
2363 return;
2364 }
2367
2368 const auto& prefs = user_settings_.prefs();
2369 auto it = prefs.panel_visibility_state.find(category);
2370 if (it != prefs.panel_visibility_state.end()) {
2372 window_manager_.GetActiveSessionId(), it->second);
2373 } else {
2374 // No saved visibility state for this category yet.
2375 //
2376 // Apply LayoutPresets defaults only when this category has *no*
2377 // visible panels (editors may have already shown their own defaults,
2378 // e.g. Dungeon Workbench).
2379 const size_t session_id = window_manager_.GetActiveSessionId();
2380 bool any_visible = false;
2381 for (const auto& desc :
2382 window_manager_.GetWindowsInCategory(session_id, category)) {
2383 if (desc.visibility_flag && *desc.visibility_flag) {
2384 any_visible = true;
2385 break;
2386 }
2387 }
2388
2389 if (!any_visible) {
2390 const EditorType type =
2392 for (const auto& window_id : LayoutPresets::GetDefaultWindows(type)) {
2393 window_manager_.OpenWindow(session_id, window_id);
2394 }
2395 }
2396 }
2397
2398 settings_dirty_ = true;
2400 });
2401
2403 [this](const std::string& category) -> Editor* {
2404 Editor* editor = ResolveEditorForCategory(category);
2406 return editor;
2407 });
2408
2410 [this](const std::string& category) {
2412 if (type != EditorType::kSettings && type != EditorType::kUnknown) {
2413 SwitchToEditor(type, true);
2414 }
2415 });
2416
2418 [this](const std::string& category) {
2419 if (ui_coordinator_) {
2420 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
2421 }
2422
2423 if (category == "Agent") {
2424#ifdef YAZE_BUILD_AGENT_UI
2425 ShowAIAgent();
2426#endif
2427 return;
2428 }
2429
2431 if (type != EditorType::kSettings) {
2432 SwitchToEditor(type, true);
2433 }
2434 });
2435
2437
2439 [this](const std::string& category, const std::string& path) {
2440 if (category == "Assembly") {
2441 if (auto* editor_set = GetCurrentEditorSet()) {
2442 editor_set->ChangeActiveAssemblyFile(path);
2444 }
2445 }
2446 });
2447}
2448
2450 ShortcutDependencies shortcut_deps;
2451 shortcut_deps.editor_manager = this;
2452 shortcut_deps.editor_registry = &editor_registry_;
2453 shortcut_deps.menu_orchestrator = menu_orchestrator_.get();
2454 shortcut_deps.rom_file_manager = &rom_file_manager_;
2455 shortcut_deps.project_manager = &project_manager_;
2456 shortcut_deps.session_coordinator = session_coordinator_.get();
2457 shortcut_deps.ui_coordinator = ui_coordinator_.get();
2458 shortcut_deps.workspace_manager = &workspace_manager_;
2459 shortcut_deps.popup_manager = popup_manager_.get();
2460 shortcut_deps.toast_manager = &toast_manager_;
2461 shortcut_deps.window_manager = &window_manager_;
2462 shortcut_deps.user_settings = &user_settings_;
2463
2467}
2468
2470 const std::string& editor_name, const std::string& panels_str) {
2471 const bool has_editor = !editor_name.empty();
2472 const bool has_panels = !panels_str.empty();
2473
2474 if (!has_editor && !has_panels) {
2475 return;
2476 }
2477
2478 LOG_INFO("EditorManager",
2479 "Processing startup flags: editor='%s', panels='%s'",
2480 editor_name.c_str(), panels_str.c_str());
2481
2482 std::optional<EditorType> editor_type_to_open =
2483 has_editor ? ParseEditorTypeFromString(editor_name) : std::nullopt;
2484 if (has_editor && !editor_type_to_open.has_value()) {
2485 LOG_WARN("EditorManager", "Unknown editor specified via flag: %s",
2486 editor_name.c_str());
2487 } else if (editor_type_to_open.has_value()) {
2488 // Use EditorActivator to ensure layouts and default panels are initialized
2489 SwitchToEditor(*editor_type_to_open, true, /*from_dialog=*/true);
2490 }
2491
2492 // Open windows via WorkspaceWindowManager - works for any editor type
2493 if (!has_panels) {
2494 return;
2495 }
2496
2497 const size_t session_id = GetCurrentSessionId();
2498 std::string last_known_category = window_manager_.GetActiveCategory();
2499 bool applied_category_from_panel = false;
2500
2501 for (absl::string_view token :
2502 absl::StrSplit(panels_str, ',', absl::SkipWhitespace())) {
2503 if (token.empty()) {
2504 continue;
2505 }
2506 std::string panel_name = std::string(absl::StripAsciiWhitespace(token));
2507 LOG_DEBUG("EditorManager", "Attempting to open panel: '%s'",
2508 panel_name.c_str());
2509
2510 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2511 if (lower_name == "welcome" || lower_name == "welcome_screen") {
2512 if (ui_coordinator_) {
2513 ui_coordinator_->SetWelcomeScreenBehavior(StartupVisibility::kShow);
2514 }
2515 continue;
2516 }
2517 if (lower_name == "dashboard" || lower_name == "dashboard.main" ||
2518 lower_name == "editor_selection") {
2519 if (dashboard_panel_) {
2520 dashboard_panel_->Show();
2521 }
2522 if (ui_coordinator_) {
2523 ui_coordinator_->SetDashboardBehavior(StartupVisibility::kShow);
2524 }
2527 /*notify=*/false);
2528 continue;
2529 }
2530
2531 // Special case: "Room <id>" opens a dungeon room
2532 if (absl::StartsWith(panel_name, "Room ")) {
2533 if (auto* editor_set = GetCurrentEditorSet()) {
2534 try {
2535 int room_id = std::stoi(panel_name.substr(5));
2537 JumpToRoomRequestEvent::Create(room_id, session_id));
2538 } catch (const std::exception& e) {
2539 LOG_WARN("EditorManager", "Invalid room ID format: %s",
2540 panel_name.c_str());
2541 }
2542 }
2543 continue;
2544 }
2545
2546 std::optional<std::string> resolved_panel;
2547 if (window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2548 resolved_panel = panel_name;
2549 } else {
2550 for (const auto& [prefixed_id, descriptor] :
2552 const std::string base_id = StripSessionPrefix(prefixed_id);
2553 const std::string card_lower = absl::AsciiStrToLower(base_id);
2554 const std::string display_lower =
2555 absl::AsciiStrToLower(descriptor.display_name);
2556
2557 if (card_lower == lower_name || display_lower == lower_name) {
2558 resolved_panel = base_id;
2559 break;
2560 }
2561 }
2562 }
2563
2564 if (!resolved_panel.has_value()) {
2565 LOG_WARN("EditorManager",
2566 "Unknown panel '%s' from --open_panels (known count: %zu)",
2567 panel_name.c_str(),
2569 continue;
2570 }
2571
2572 if (window_manager_.OpenWindow(session_id, *resolved_panel)) {
2573 const auto* descriptor =
2574 window_manager_.GetWindowDescriptor(session_id, *resolved_panel);
2575 if (descriptor != nullptr) {
2576 const EditorType type =
2577 EditorRegistry::GetEditorTypeFromCategory(descriptor->category);
2578 const auto ensure_status = EnsureEditorAssetsLoaded(type);
2579 if (!ensure_status.ok()) {
2580 LOG_WARN("EditorManager", "Failed to load assets for panel '%s': %s",
2581 resolved_panel->c_str(), ensure_status.message().data());
2582 }
2583 }
2584 if (descriptor && !applied_category_from_panel &&
2585 descriptor->category != WorkspaceWindowManager::kDashboardCategory) {
2586 window_manager_.SetActiveCategory(descriptor->category);
2587 applied_category_from_panel = true;
2588 } else if (!applied_category_from_panel && descriptor &&
2589 descriptor->category.empty() && !last_known_category.empty()) {
2590 window_manager_.SetActiveCategory(last_known_category);
2591 }
2592 } else {
2593 LOG_WARN("EditorManager", "Failed to show panel '%s'",
2594 resolved_panel->c_str());
2595 }
2596 }
2597}
2598
2605
2613
2615 constexpr int kTargetRevision =
2617 if (!user_settings_.ApplyPanelLayoutDefaultsRevision(kTargetRevision)) {
2618 return;
2619 }
2620
2622 settings_dirty_ = true;
2624
2625 LOG_INFO("EditorManager",
2626 "Applied panel layout defaults migration revision %d",
2627 kTargetRevision);
2628}
2629
2631 const std::string& saved_category,
2632 const std::vector<std::string>& available_categories) const {
2633 // If saved category is valid and not Emulator, use it directly
2634 if (!saved_category.empty() && saved_category != "Emulator") {
2635 // Validate it exists in available_categories if the list is provided
2636 if (available_categories.empty()) {
2637 return saved_category;
2638 }
2639 for (const auto& cat : available_categories) {
2640 if (cat == saved_category)
2641 return saved_category;
2642 }
2643 }
2644 // Pick first non-Emulator category from available list
2645 for (const auto& cat : available_categories) {
2646 if (cat != "Emulator")
2647 return cat;
2648 }
2649 return {};
2650}
2651
2655
2657 if (ui_coordinator_) {
2658 ui_coordinator_->SetWelcomeScreenBehavior(welcome_mode_override_);
2659 ui_coordinator_->SetDashboardBehavior(dashboard_mode_override_);
2660 }
2661
2663 const bool sidebar_visible =
2665 window_manager_.SetSidebarVisible(sidebar_visible, /*notify=*/false);
2666 if (ui_coordinator_) {
2667 ui_coordinator_->SetPanelSidebarVisible(sidebar_visible);
2668 }
2669 }
2670
2671 // Force sidebar panel to collapse if Welcome Screen or Dashboard is explicitly shown
2672 // This prevents visual overlap/clutter on startup
2675 window_manager_.SetSidebarExpanded(false, /*notify=*/false);
2676 }
2677
2678 if (dashboard_panel_) {
2680 dashboard_panel_->Hide();
2682 dashboard_panel_->Show();
2683 }
2684 }
2685}
2686
2688 ApplyStartupVisibility(config);
2689 // Handle startup editor and panels
2690 std::string panels_str;
2691 for (size_t i = 0; i < config.open_panels.size(); ++i) {
2692 if (i > 0)
2693 panels_str += ",";
2694 panels_str += config.open_panels[i];
2695 }
2696 OpenEditorAndPanelsFromFlags(config.startup_editor, panels_str);
2697
2698 // Handle jump targets
2699 if (config.jump_to_room >= 0) {
2702 }
2703 if (config.jump_to_map >= 0) {
2707 } else {
2708 LOG_WARN("EditorManager",
2709 "Ignoring invalid startup overworld map target %d",
2710 config.jump_to_map);
2711 toast_manager_.Show("Ignored invalid startup overworld map: " +
2712 std::to_string(config.jump_to_map),
2714 }
2715 }
2716}
2717
2718absl::Status EditorManager::LoadAssetsForMode(uint64_t loading_handle) {
2719 switch (asset_load_mode_) {
2721 return LoadAssetsLazy(loading_handle);
2724 default:
2725 return LoadAssets(loading_handle);
2726 }
2727}
2728
2730 if (!session) {
2731 return;
2732 }
2733 session->game_data_loaded = false;
2734 session->editor_initialized.fill(false);
2735 session->editor_assets_loaded.fill(false);
2736}
2737
2739 EditorType type) {
2740 if (!session) {
2741 return;
2742 }
2743 const size_t index = EditorTypeIndex(type);
2744 if (index < session->editor_initialized.size()) {
2745 session->editor_initialized[index] = true;
2746 }
2747}
2748
2750 if (!session) {
2751 return;
2752 }
2753 const size_t index = EditorTypeIndex(type);
2754 if (index < session->editor_assets_loaded.size()) {
2755 session->editor_assets_loaded[index] = true;
2756 }
2757}
2758
2760 switch (type) {
2768 return true;
2769 default:
2770 return false;
2771 }
2772}
2773
2777
2779 EditorSet* editor_set) const {
2780 std::unordered_set<EditorType> types;
2781
2782 auto add_type = [&types](EditorType type) {
2783 switch (type) {
2787 case EditorType::kAgent:
2788 return;
2789 default:
2790 types.insert(type);
2791 return;
2792 }
2793 };
2794
2795 auto add_category = [&](const std::string& category) {
2796 if (category.empty() ||
2798 return;
2799 }
2801 };
2802
2803 const size_t session_id = GetCurrentSessionId();
2804 bool used_startup_hints = false;
2805
2806 if (!startup_editor_hint_.empty()) {
2807 if (auto startup_type = ParseEditorTypeFromString(startup_editor_hint_)) {
2808 add_type(*startup_type);
2809 used_startup_hints = true;
2810 }
2811 }
2812
2813 for (const auto& panel_name : startup_panel_hints_) {
2814 if (panel_name.empty()) {
2815 continue;
2816 }
2817
2818 if (const auto* descriptor =
2819 window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2820 add_category(descriptor->category);
2821 used_startup_hints = true;
2822 continue;
2823 }
2824
2825 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2826 for (const auto& [prefixed_id, descriptor] :
2828 const std::string base_id = StripSessionPrefix(prefixed_id);
2829 const std::string card_lower = absl::AsciiStrToLower(base_id);
2830 const std::string display_lower =
2831 absl::AsciiStrToLower(descriptor.display_name);
2832 if (card_lower == lower_name || display_lower == lower_name) {
2833 add_category(descriptor.category);
2834 used_startup_hints = true;
2835 break;
2836 }
2837 }
2838 }
2839
2840 if (used_startup_hints) {
2841 return std::vector<EditorType>(types.begin(), types.end());
2842 }
2843
2845 return {};
2846 }
2847
2848 if (editor_set) {
2849 for (auto* editor : editor_set->active_editors_) {
2850 if (editor != nullptr && *editor->active()) {
2851 add_type(editor->type());
2852 }
2853 }
2854 }
2855
2856 if (current_editor_ != nullptr) {
2857 add_type(current_editor_->type());
2858 }
2859
2860 add_category(window_manager_.GetActiveCategory());
2861
2862 for (const auto& window_id :
2864 if (const auto* descriptor =
2865 window_manager_.GetWindowDescriptor(session_id, window_id)) {
2866 add_category(descriptor->category);
2867 }
2868 }
2869
2870 return std::vector<EditorType>(types.begin(), types.end());
2871}
2872
2874 EditorSet* editor_set) const {
2875 return editor_set ? editor_set->GetEditor(type) : nullptr;
2876}
2877
2879 if (category.empty() ||
2881 return nullptr;
2882 }
2883
2884 auto* editor_set = GetCurrentEditorSet();
2885 if (!editor_set) {
2886 return nullptr;
2887 }
2888
2890 switch (type) {
2891 case EditorType::kAgent:
2892#ifdef YAZE_BUILD_AGENT_UI
2893 return agent_ui_.GetAgentEditor();
2894#else
2895 return nullptr;
2896#endif
2900 return nullptr;
2901 default:
2902 return GetEditorByType(type, editor_set);
2903 }
2904}
2905
2906void EditorManager::SyncEditorContextForCategory(const std::string& category) {
2907 if (Editor* resolved = ResolveEditorForCategory(category)) {
2908 SetCurrentEditor(resolved);
2909 } else if (!category.empty() &&
2911 LOG_DEBUG("EditorManager", "No editor context available for category '%s'",
2912 category.c_str());
2913 }
2914}
2915
2917 EditorSet* editor_set,
2918 Rom* rom) {
2919 if (!editor_set) {
2920 return absl::FailedPreconditionError("No editor set available");
2921 }
2922
2923 auto* editor = GetEditorByType(type, editor_set);
2924 if (!editor) {
2925 return absl::OkStatus();
2926 }
2927 editor->Initialize();
2928 return absl::OkStatus();
2929}
2930
2932 auto* session = session_coordinator_
2933 ? session_coordinator_->GetActiveRomSession()
2934 : nullptr;
2935 if (!session) {
2936 return absl::FailedPreconditionError("No active session");
2937 }
2938 if (session->game_data_loaded) {
2939 return absl::OkStatus();
2940 }
2941 if (!session->rom.is_loaded()) {
2942 return absl::FailedPreconditionError("ROM not loaded");
2943 }
2944
2945 RETURN_IF_ERROR(zelda3::LoadGameData(session->rom, session->game_data));
2946 *gfx::Arena::Get().mutable_gfx_sheets() = session->game_data.gfx_bitmaps;
2947
2948 auto* game_data = &session->game_data;
2949 auto* editor_set = &session->editors;
2950 editor_set->SetGameData(game_data);
2952
2954 session->game_data_loaded = true;
2955
2956 return absl::OkStatus();
2957}
2958
2960 if (type == EditorType::kUnknown) {
2961 return absl::OkStatus();
2962 }
2963
2964 auto* session = session_coordinator_
2965 ? session_coordinator_->GetActiveRomSession()
2966 : nullptr;
2967 if (!session) {
2968 return absl::OkStatus();
2969 }
2970
2971 if (!session->rom.is_loaded()) {
2973 return absl::OkStatus();
2974 }
2975 const size_t index = EditorTypeIndex(type);
2976 if (index >= session->editor_initialized.size()) {
2977 return absl::InvalidArgumentError("Invalid editor type");
2978 }
2979 if (EditorInitRequiresGameData(type)) {
2980 return absl::OkStatus();
2981 }
2982 if (!session->editor_initialized[index]) {
2984 InitializeEditorForType(type, &session->editors, &session->rom));
2985 MarkEditorInitialized(session, type);
2986 }
2987 return absl::OkStatus();
2988 }
2989
2990 const size_t index = EditorTypeIndex(type);
2991 if (index >= session->editor_initialized.size()) {
2992 return absl::InvalidArgumentError("Invalid editor type");
2993 }
2994
2995 if (EditorInitRequiresGameData(type)) {
2997 }
2998
2999 if (!session->editor_initialized[index]) {
3001 InitializeEditorForType(type, &session->editors, &session->rom));
3002 MarkEditorInitialized(session, type);
3003 }
3004
3005 if (EditorRequiresGameData(type)) {
3007 }
3008
3009 if (!session->editor_assets_loaded[index]) {
3010 auto* editor = GetEditorByType(type, &session->editors);
3011 if (editor) {
3012 RETURN_IF_ERROR(editor->Load());
3013 }
3014 MarkEditorLoaded(session, type);
3015 }
3016
3017 return absl::OkStatus();
3018}
3019
3037 ProcessInput();
3039 DrawInterface();
3040
3041 return status_;
3042}
3043
3045 // Space/focus transitions can leave mid-animation surfaces ghosted.
3046 // Reset transient animation state to a stable endpoint.
3049 right_drawer_manager_->OnHostVisibilityChanged(visible);
3050 }
3051}
3052
3054 // Update timing manager for accurate delta time across the application
3056
3057 // Execute keyboard shortcuts (registered via ShortcutConfigurator)
3059}
3060
3062 status_ = absl::OkStatus();
3064 if (ui_coordinator_) {
3065 ui_coordinator_->RefreshWorkflowActions();
3066 }
3067 // Check for layout rebuild requests and execute if needed (delegated to LayoutCoordinator)
3068 bool is_emulator_visible =
3069 ui_coordinator_ && ui_coordinator_->IsEmulatorVisible();
3070 EditorType current_type =
3072 layout_coordinator_.ProcessLayoutRebuild(current_type, is_emulator_visible);
3073
3074 // Periodic user settings auto-save
3075 if (settings_dirty_) {
3076 const float elapsed = TimingManager::Get().GetElapsedTime();
3077 if (elapsed - settings_dirty_timestamp_ >= 1.0f) {
3078 auto save_status = user_settings_.Save();
3079 if (!save_status.ok()) {
3080 LOG_WARN("EditorManager", "Failed to save user settings: %s",
3081 save_status.ToString().c_str());
3082 }
3083 settings_dirty_ = false;
3084 }
3085 }
3086
3087 // Update agent editor dashboard
3089
3090 // Ensure TestManager always has the current ROM
3091 static Rom* last_test_rom = nullptr;
3092 auto* current_rom = GetCurrentRom();
3093 if (last_test_rom != current_rom) {
3094 LOG_DEBUG(
3095 "EditorManager",
3096 "EditorManager::Update - ROM changed, updating TestManager: %p -> %p",
3097 (void*)last_test_rom, (void*)current_rom);
3099 last_test_rom = current_rom;
3100 }
3101
3102 // Autosave uses the same guarded serialization, validation, backup, and
3103 // atomic commit pipeline as an explicit save. Writing Rom::SaveToFile here
3104 // would skip editor model serialization and could persist a partially
3105 // failed buffer while clearing its dirty bit.
3106 const bool current_session_has_unsaved_work =
3108 const auto* current_session =
3109 session_coordinator_ ? session_coordinator_->GetActiveRomSession()
3110 : nullptr;
3111 const bool backup_restore_requires_explicit_save =
3112 current_session != nullptr && current_session->backup_restore_pending;
3114 current_session_has_unsaved_work &&
3115 !backup_restore_requires_explicit_save) {
3116 autosave_timer_ += ImGui::GetIO().DeltaTime;
3118 autosave_timer_ = 0.0f;
3119 auto st = AutosaveActiveSession();
3120 if (st.ok()) {
3121 toast_manager_.Show("Autosave completed", editor::ToastType::kSuccess);
3122 } else if (absl::IsCancelled(st)) {
3123 toast_manager_.Show("Autosave paused: confirmation required",
3125 } else {
3127 absl::StrFormat("Autosave failed: %s", st.message()),
3129 }
3130 }
3131 } else {
3132 autosave_timer_ = 0.0f;
3133 }
3134
3135 // Update ROM context for agent UI
3136 if (current_rom && current_rom->is_loaded()) {
3137 agent_ui_.SetRomContext(current_rom);
3139 if (auto* editor_set = GetCurrentEditorSet()) {
3140 agent_ui_.SetAsarWrapperContext(editor_set->GetAsarWrapper());
3141 // Backend-agnostic symbol feed. Works for ASAR and z3dk alike; tools
3142 // prefer this pointer over reaching through the wrapper.
3144 &editor_set->GetAssemblySymbols());
3145 }
3146 }
3147
3148 // Delegate session updates to SessionCoordinator
3150 session_coordinator_->UpdateSessions();
3151 }
3152}
3153
3155 if (!active_project_build_) {
3156 return;
3157 }
3158
3159 const auto snapshot = active_project_build_->GetSnapshot();
3160 UpdateBuildWorkflowStatus(
3163 snapshot.running
3164 ? "Build running"
3165 : (snapshot.status.ok() ? "Build succeeded" : "Build failed"),
3166 snapshot.output_tail.empty()
3167 ? (snapshot.running
3168 ? std::string(
3169 "Running the configured project build command")
3170 : std::string(snapshot.status.message()))
3171 : snapshot.output_tail,
3172 snapshot.running
3174 : (snapshot.status.ok() ? ProjectWorkflowState::kSuccess
3176 snapshot.output_tail, snapshot.running));
3178 project_management_panel_->SetBuildLogOutput(snapshot.output);
3179 }
3181
3182 if (snapshot.running || active_project_build_reported_) {
3183 return;
3184 }
3185
3187 if (!snapshot.status.ok()) {
3188 AppendWorkflowHistoryEntry(
3189 "Build",
3190 MakeBuildStatus("Build failed", std::string(snapshot.status.message()),
3191 ProjectWorkflowState::kFailure, snapshot.output_tail,
3192 false),
3193 snapshot.output);
3195 absl::StrFormat("Build failed: %s", snapshot.status.message()),
3196 snapshot.status.code() == absl::StatusCode::kCancelled
3199 return;
3200 }
3201
3202 AppendWorkflowHistoryEntry(
3203 "Build",
3205 "Build succeeded",
3206 snapshot.output_tail.empty() ? std::string("Project build completed")
3207 : snapshot.output_tail,
3208 ProjectWorkflowState::kSuccess, snapshot.output_tail, false),
3209 snapshot.output);
3210 toast_manager_.Show(snapshot.output_tail.empty()
3211 ? "Project build completed"
3212 : absl::StrFormat("Project build completed: %s",
3213 snapshot.output_tail),
3215}
3216
3218
3219 // Draw editor selection dialog (managed by UICoordinator)
3220 if (ui_coordinator_ && ui_coordinator_->IsEditorSelectionVisible()) {
3221 dashboard_panel_->Show();
3222 dashboard_panel_->Draw();
3223 if (!dashboard_panel_->IsVisible()) {
3224 ui_coordinator_->SetEditorSelectionVisible(false);
3225 }
3226 }
3227
3228 // Draw ROM load options dialog (ZSCustomOverworld, feature flags, project)
3231 }
3232
3233 // Draw window browser (managed by UICoordinator)
3234 if (ui_coordinator_ && ui_coordinator_->IsWindowBrowserVisible()) {
3235 bool show = true;
3236 if (activity_bar_) {
3237 activity_bar_->DrawWindowBrowser(GetCurrentSessionId(), &show);
3238 }
3239 if (!show) {
3240 ui_coordinator_->SetWindowBrowserVisible(false);
3241 }
3242 }
3243
3244 // Draw background grid effects
3245 if (ui_coordinator_) {
3246 ui_coordinator_->DrawBackground();
3247 }
3248
3249 // Draw UICoordinator UI components (Welcome Screen, Command Palette, etc.)
3250 if (ui_coordinator_) {
3251 ui_coordinator_->DrawAllUI();
3252 }
3253
3254 // Handle Welcome screen early-exit for rendering
3255 if (ui_coordinator_ && ui_coordinator_->ShouldShowWelcome()) {
3257 right_drawer_manager_->CloseDrawer();
3258 }
3259 return;
3260 }
3261
3264 RunEmulator();
3265
3266 // Draw sidebar
3267 if (ui_coordinator_ && ui_coordinator_->IsPanelSidebarVisible()) {
3268 auto all_categories = EditorRegistry::GetAllEditorCategories();
3269 std::unordered_set<std::string> active_editor_categories;
3270
3271 if (auto* current_editor_set = GetCurrentEditorSet()) {
3273 for (size_t session_idx = 0;
3274 session_idx < session_coordinator_->GetTotalSessionCount();
3275 ++session_idx) {
3276 auto* session = static_cast<RomSession*>(
3277 session_coordinator_->GetSession(session_idx));
3278 if (!session || !session->rom.is_loaded()) {
3279 continue;
3280 }
3281
3282 for (auto* editor : session->editors.active_editors_) {
3283 if (*editor->active() && IsPanelBasedEditor(editor->type())) {
3284 std::string category =
3285 EditorRegistry::GetEditorCategory(editor->type());
3286 active_editor_categories.insert(category);
3287 }
3288 }
3289 }
3290
3291 if (ui_coordinator_->IsEmulatorVisible()) {
3292 active_editor_categories.insert("Emulator");
3293 }
3294 }
3295 }
3296
3297 const bool prefer_dashboard_only =
3299 startup_editor_hint_.empty() && startup_panel_hints_.empty();
3300 std::string sidebar_category = window_manager_.GetActiveCategory();
3301 if (!prefer_dashboard_only && sidebar_category.empty() &&
3302 !all_categories.empty()) {
3303 sidebar_category = GetPreferredStartupCategory("", all_categories);
3304 if (!sidebar_category.empty()) {
3305 window_manager_.SetActiveCategory(sidebar_category, /*notify=*/false);
3306 SyncEditorContextForCategory(sidebar_category);
3308 sidebar_category);
3309 if (it != user_settings_.prefs().panel_visibility_state.end()) {
3311 window_manager_.GetActiveSessionId(), it->second);
3312 }
3313 }
3314 }
3315
3316 auto has_rom_callback = [this]() -> bool {
3317 auto* rom = GetCurrentRom();
3318 return rom && rom->is_loaded();
3319 };
3320
3321 if (activity_bar_ && ui_coordinator_->ShouldShowActivityBar()) {
3322 auto is_rom_dirty_callback = [this]() -> bool {
3323 auto* rom = GetCurrentRom();
3324 return rom && rom->is_loaded() && rom->dirty();
3325 };
3326 auto pending_dungeon_rooms_callback = [this]() -> int {
3327 if (auto* editor_set = GetCurrentEditorSet()) {
3328 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
3330 return dungeon_editor->PendingRoomCount();
3331 }
3332 }
3333 return 0;
3334 };
3335 activity_bar_->Render(GetCurrentSessionId(), sidebar_category,
3336 all_categories, active_editor_categories,
3337 has_rom_callback, is_rom_dirty_callback,
3338 pending_dungeon_rooms_callback);
3339 }
3340 }
3341
3342 // Draw right panel
3345 right_drawer_manager_->Draw();
3346 }
3347
3348 // Update and draw status bar
3352 session_coordinator_->GetActiveSessionCount());
3353 }
3354
3355 bool has_agent_info = false;
3356#if defined(YAZE_BUILD_AGENT_UI)
3357 if (auto* agent_editor = agent_ui_.GetAgentEditor()) {
3358 auto* chat = agent_editor->GetAgentChat();
3359 const auto* ctx = agent_ui_.GetContext();
3360 if (ctx) {
3361 const auto& config = ctx->agent_config();
3362 bool active = chat && *chat->active();
3363 status_bar_.SetAgentInfo(config.ai_provider, config.ai_model, active);
3364 has_agent_info = true;
3365 }
3366 }
3367#endif
3368 if (!has_agent_info) {
3370 }
3373 }
3374
3375 // Editor-aware context: let the active editor push its own mode/custom
3376 // segments for this frame without wiping event-driven cursor/selection/zoom
3377 // state that older editors still publish through the event bus.
3379 if (current_editor_) {
3381 }
3382
3383 if (auto* current_editor_set = GetCurrentEditorSet()) {
3384 if (auto* dungeon_editor = current_editor_set->GetEditorAs<DungeonEditorV2>(
3386 const int pending_rooms = dungeon_editor->PendingRoomCount();
3387 if (pending_rooms > 0) {
3388 StatusBarSegmentOptions pending_opts;
3389 pending_opts.tooltip = absl::StrFormat(
3390 "%d dungeon room%s still ha%s pending editor changes. Apply them "
3391 "to the ROM buffer before File > Save ROM if needed.",
3392 pending_rooms, pending_rooms == 1 ? "" : "s",
3393 pending_rooms == 1 ? "s" : "ve");
3395 "Dungeon", absl::StrFormat("%d pending", pending_rooms),
3396 std::move(pending_opts));
3397 }
3398 }
3399 }
3400
3401 status_bar_.Draw();
3402
3403 // Check if ROM is loaded before drawing panels
3404 auto* current_editor_set = GetCurrentEditorSet();
3405 if (!current_editor_set || !GetCurrentRom()) {
3406 if (window_manager_.GetActiveCategory() == "Agent") {
3408 }
3409 return;
3410 }
3411
3412 // Central workspace window drawing
3414
3415 if (ui_coordinator_ && ui_coordinator_->IsPerformanceDashboardVisible()) {
3417 }
3418
3419 // Draw SessionCoordinator UI components
3421 session_coordinator_->DrawSessionSwitcher();
3422 session_coordinator_->DrawSessionManager();
3423 session_coordinator_->DrawSessionRenameDialog();
3424 }
3425}
3426
3428 if (ImGui::BeginMenuBar()) {
3429 // Consistent button styling for sidebar toggle
3430 {
3431 const bool sidebar_visible = window_manager_.IsSidebarVisible();
3432 gui::StyleColorGuard sidebar_btn_guard(
3433 {{ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
3434 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
3435 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
3436 {ImGuiCol_Text, sidebar_visible ? gui::GetPrimaryVec4()
3438
3439 const char* icon = sidebar_visible ? ICON_MD_MENU_OPEN : ICON_MD_MENU;
3440 if (ImGui::SmallButton(icon)) {
3442 }
3443 }
3444
3445 if (ImGui::IsItemHovered()) {
3446 const char* tooltip = window_manager_.IsSidebarVisible()
3447 ? "Hide Activity Bar (Ctrl+B)"
3448 : "Show Activity Bar (Ctrl+B)";
3449 ImGui::SetTooltip("%s", tooltip);
3450 }
3451
3452 // Delegate menu building to MenuOrchestrator
3453 if (menu_orchestrator_) {
3454 menu_orchestrator_->BuildMainMenu();
3455 }
3456
3457 // Delegate menu bar extras to UICoordinator
3458 if (ui_coordinator_) {
3459 ui_coordinator_->DrawMenuBarExtras();
3460 }
3461
3462 ImGui::EndMenuBar();
3463 }
3464}
3465
3467
3468 // ImGui debug windows
3469 if (ui_coordinator_) {
3470 if (ui_coordinator_->IsImGuiDemoVisible()) {
3471 bool visible = true;
3472 ImGui::ShowDemoWindow(&visible);
3473 if (!visible)
3474 ui_coordinator_->SetImGuiDemoVisible(false);
3475 }
3476
3477 if (ui_coordinator_->IsImGuiMetricsVisible()) {
3478 bool visible = true;
3479 ImGui::ShowMetricsWindow(&visible);
3480 if (!visible)
3481 ui_coordinator_->SetImGuiMetricsVisible(false);
3482 }
3483 }
3484
3485 // Legacy window-based editors
3486 if (auto* editor_set = GetCurrentEditorSet()) {
3487 bool* hex_visibility =
3488 window_manager_.GetWindowVisibilityFlag("memory.hex_editor");
3489 if (hex_visibility) {
3490 if (auto* editor = editor_set->GetEditor(EditorType::kHex)) {
3491 // Keep the legacy panel visibility flag in sync with the window close
3492 // button (ImGui::Begin will toggle Editor::active_).
3493 editor->set_active(*hex_visibility);
3494 editor->Update();
3495 *hex_visibility = *editor->active();
3496 }
3497 }
3498
3499 if (ui_coordinator_ && ui_coordinator_->IsAsmEditorVisible()) {
3500 if (auto* editor = editor_set->GetEditor(EditorType::kAssembly)) {
3501 editor->set_active(true);
3502 editor->Update();
3503 if (!*editor->active()) {
3504 ui_coordinator_->SetAsmEditorVisible(false);
3505 }
3506 }
3507 }
3508 }
3509
3510 // Project and performance tools
3513
3514 if (ui_coordinator_ && ui_coordinator_->IsPerformanceDashboardVisible()) {
3518 if (!gfx::PerformanceDashboard::Get().IsVisible()) {
3519 ui_coordinator_->SetPerformanceDashboardVisible(false);
3520 }
3521 }
3522
3523#ifdef YAZE_ENABLE_TESTING
3524 if (show_test_dashboard_) {
3526 test::TestManager::Get().DrawTestDashboard(&show_test_dashboard_);
3527 }
3528#endif
3529}
3530
3532 // Update proposal drawer context
3534
3535 // Agent UI popups
3537
3538 // Resource label management
3539 if (ui_coordinator_ && ui_coordinator_->IsResourceLabelManagerVisible() &&
3540 GetCurrentRom()) {
3541 bool visible = true;
3547 }
3548 if (!visible)
3549 ui_coordinator_->SetResourceLabelManagerVisible(false);
3550 }
3551
3552 // Layout presets
3553 if (ui_coordinator_) {
3554 ui_coordinator_->DrawLayoutPresets();
3555 }
3556}
3557
3559 auto* current_rom = GetCurrentRom();
3560 if (!current_rom)
3561 return;
3562
3563 // Visibility gates *rendering*, not *ticking*. Run(rom) is the lazy-init +
3564 // render path; the SNES only starts (running_=true, snes_initialized_=true)
3565 // after Run(rom) fires while the emulator panel is visible. Once running,
3566 // switching to another editor hides the panel but must NOT freeze the game —
3567 // the tick-only branch below keeps audio + frame state alive.
3568 if (ui_coordinator_ && ui_coordinator_->IsEmulatorVisible()) {
3569 emulator_.Run(current_rom);
3570 } else if (emulator_.running() && emulator_.is_snes_initialized()) {
3573 } else {
3575 }
3576 }
3577}
3578
3585
3611 GetCurrentSessionId()})) {
3612 return absl::OkStatus();
3613 }
3614
3615 return LoadRomInternal();
3616}
3617
3619 auto load_from_path = [this](const std::string& file_name) -> absl::Status {
3620 if (file_name.empty()) {
3621 return absl::OkStatus();
3622 }
3623
3624 // Check if this is a project file - route to project loading
3625 if (absl::EndsWith(file_name, ".yaze") ||
3626 absl::EndsWith(file_name, ".zsproj") ||
3627 absl::EndsWith(file_name, ".yazeproj")) {
3628 return OpenRomOrProjectInternal(file_name);
3629 }
3630
3631 auto path_status =
3632 session_coordinator_->CheckBackingFileAvailable(file_name);
3633 if (!path_status.ok()) {
3634 toast_manager_.Show(std::string(path_status.message()),
3636 return path_status;
3637 }
3638
3639 // Delegate ROM loading to RomFileManager
3640 Rom temp_rom;
3641 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, file_name));
3642
3643 const std::optional<size_t> previous_session_id =
3645 const size_t previous_session_count =
3646 session_coordinator_->GetTotalSessionCount();
3647 auto session_or = session_coordinator_->CreateSessionFromRom(
3648 std::move(temp_rom), file_name);
3649 if (!session_or.ok()) {
3650 return session_or.status();
3651 }
3652
3656
3657 // HandleSessionCreated binds the project context that governed this open.
3658 // Reapply it here rather than partially clearing global runtime state.
3661
3662 // Keep ResourceLabelProvider in sync with the newly-active ROM session
3663 // before any editors/assets query room/sprite names.
3665
3666#ifdef YAZE_ENABLE_TESTING
3668#endif
3669
3671 const auto& recent_files = manager.GetRecentFiles();
3672 const bool is_first_time_rom_path =
3673 std::find(recent_files.begin(), recent_files.end(), file_name) ==
3674 recent_files.end();
3675 auto asset_status = LoadAssetsForMode();
3676 if (!asset_status.ok()) {
3677 auto rollback_status =
3678 DiscardProvisionalSessionCreatedSince(previous_session_count);
3679 RestoreProjectContextAfterFailedOpen(previous_session_id);
3680 if (!rollback_status.ok()) {
3681 return absl::InternalError(
3682 absl::StrFormat("%s; session rollback failed: %s",
3683 asset_status.message(), rollback_status.message()));
3684 }
3685 return asset_status;
3686 }
3687
3688 manager.AddFile(file_name);
3689 manager.Save();
3690
3691 if (ui_coordinator_) {
3692 ui_coordinator_->SetWelcomeScreenVisible(false);
3693
3694 // Show ROM load options and bias new ROM paths toward project creation.
3695 rom_load_options_dialog_.Open(GetCurrentRom(), is_first_time_rom_path);
3697 }
3698
3699 return absl::OkStatus();
3700 };
3701
3702#if defined(__APPLE__) && TARGET_OS_IOS == 1
3703 // On iOS, route through the SwiftUI overlay document picker to get proper
3704 // security-scoped access to iCloud Drive and Files app locations. This
3705 // mirrors how OpenProject() works on iOS and supports cloud ROMs.
3706 // The SwiftUI picker calls YazeIOSBridge.loadRomAtPath: after importing the
3707 // ROM to a persistent sandbox directory.
3709 return absl::OkStatus();
3710#else
3713 return load_from_path(file_name);
3714#endif
3715}
3716
3717absl::Status EditorManager::LoadAssets(uint64_t passed_handle) {
3718 auto* current_rom = GetCurrentRom();
3719 auto* current_editor_set = GetCurrentEditorSet();
3720 if (!current_rom || !current_editor_set) {
3721 return absl::FailedPreconditionError("No ROM or editor set loaded");
3722 }
3723
3724 auto* current_session = session_coordinator_->GetActiveRomSession();
3725 if (!current_session) {
3726 return absl::FailedPreconditionError("No active ROM session");
3727 }
3728 ResetAssetState(current_session);
3729
3730 auto start_time = std::chrono::steady_clock::now();
3731
3732#ifdef __EMSCRIPTEN__
3733 // Use passed handle if provided, otherwise create new one
3734 auto loading_handle =
3735 passed_handle != 0
3736 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3737 passed_handle)
3738 : app::platform::WasmLoadingManager::BeginLoading(
3739 "Loading Editor Assets");
3740
3741 // Progress starts at 10% (ROM already loaded), goes to 100%
3742 constexpr float kStartProgress = 0.10f;
3743 constexpr float kEndProgress = 1.0f;
3744 constexpr int kTotalSteps = 11; // Graphics + 8 editors + profiler + finish
3745 int current_step = 0;
3746 auto update_progress = [&](const std::string& message) {
3747 current_step++;
3748 float progress =
3749 kStartProgress + (kEndProgress - kStartProgress) *
3750 (static_cast<float>(current_step) / kTotalSteps);
3751 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, progress);
3752 app::platform::WasmLoadingManager::UpdateMessage(loading_handle, message);
3753 };
3754 // RAII guard to ensure loading indicator is closed even on early return
3755 auto cleanup_loading = [&]() {
3756 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3757 };
3758 struct LoadingGuard {
3759 std::function<void()> cleanup;
3760 bool dismissed = false;
3761 ~LoadingGuard() {
3762 if (!dismissed)
3763 cleanup();
3764 }
3765 void dismiss() { dismissed = true; }
3766 } loading_guard{cleanup_loading};
3767#else
3768 (void)passed_handle; // Unused on non-WASM
3769#endif
3770
3771 // Set renderer for emulator (lazy initialization happens in Run())
3772 if (renderer_) {
3774 }
3775
3776 const auto preload_editor_list = CollectEditorsToPreload(current_editor_set);
3777 const std::unordered_set<EditorType> preload_types(
3778 preload_editor_list.begin(), preload_editor_list.end());
3779
3780 // Initialize only the editors needed for the current startup surface. This
3781 // registers their windows and sets up editor-specific resources before Load().
3782 struct InitStep {
3783 EditorType type;
3784 bool mark_loaded;
3785 };
3786 const InitStep init_steps[] = {
3788 {EditorType::kGraphics, false}, {EditorType::kScreen, false},
3789 {EditorType::kSprite, false}, {EditorType::kPalette, false},
3790 {EditorType::kAssembly, true}, {EditorType::kMusic, false},
3791 {EditorType::kDungeon, false},
3792 };
3793 for (const auto& step : init_steps) {
3794 if (!preload_types.contains(step.type)) {
3795 continue;
3796 }
3797 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3798 editor->Initialize();
3799 MarkEditorInitialized(current_session, step.type);
3800 if (step.mark_loaded) {
3801 MarkEditorLoaded(current_session, step.type);
3802 }
3803 }
3804 }
3805
3806#ifdef __EMSCRIPTEN__
3807 update_progress("Loading graphics sheets...");
3808#endif
3809 // Load all Zelda3-specific data (metadata, palettes, gfx groups, graphics)
3810 gfx::PaletteManager::Get().ReleaseSession(&current_session->game_data);
3812 zelda3::LoadGameData(*current_rom, current_session->game_data));
3813 current_session->game_data_loaded = true;
3814
3815 // Copy loaded graphics to Arena for global access
3817 current_session->game_data.gfx_bitmaps;
3818
3819 // Propagate GameData to editors that already exist; future editors inherit it
3820 // on first construction via EditorSet.
3821 auto* game_data = &current_session->game_data;
3822 current_editor_set->SetGameData(game_data);
3824
3825 struct LoadStep {
3826 EditorType type;
3827 const char* progress_message;
3828 };
3829 const LoadStep load_steps[] = {
3830 {EditorType::kOverworld, "Loading overworld..."},
3831 {EditorType::kDungeon, "Loading dungeons..."},
3832 {EditorType::kScreen, "Loading screen editor..."},
3833 {EditorType::kGraphics, "Loading graphics editor..."},
3834 {EditorType::kSprite, "Loading sprites..."},
3835 {EditorType::kMessage, "Loading messages..."},
3836 {EditorType::kMusic, "Loading music..."},
3837 {EditorType::kPalette, "Loading palettes..."},
3838 };
3839 for (const auto& step : load_steps) {
3840 if (!preload_types.contains(step.type)) {
3841 continue;
3842 }
3843#ifdef __EMSCRIPTEN__
3844 update_progress(step.progress_message);
3845#endif
3846 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3847 RETURN_IF_ERROR(editor->Load());
3848 MarkEditorLoaded(current_session, step.type);
3849 }
3850 }
3851
3852#ifdef __EMSCRIPTEN__
3853 update_progress("Finishing up...");
3854#endif
3855
3856 // Set up RightDrawerManager with session's settings editor
3858 auto* settings = current_editor_set->GetSettingsPanel();
3859 right_drawer_manager_->SetSettingsPanel(settings);
3860 }
3861
3862 // Apply user preferences to status bar
3864
3866
3867#ifdef __EMSCRIPTEN__
3868 // Dismiss the guard and manually close - we completed successfully
3869 loading_guard.dismiss();
3870 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3871#endif
3872
3873 auto end_time = std::chrono::steady_clock::now();
3874 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
3875 end_time - start_time);
3876 LOG_DEBUG("EditorManager", "ROM assets loaded in %lld ms", duration.count());
3877
3878 return absl::OkStatus();
3879}
3880
3881absl::Status EditorManager::LoadAssetsLazy(uint64_t passed_handle) {
3882 auto* current_rom = GetCurrentRom();
3883 auto* current_editor_set = GetCurrentEditorSet();
3884 if (!current_rom || !current_editor_set) {
3885 return absl::FailedPreconditionError("No ROM or editor set loaded");
3886 }
3887
3888 auto* current_session = session_coordinator_->GetActiveRomSession();
3889 if (!current_session) {
3890 return absl::FailedPreconditionError("No active ROM session");
3891 }
3892 ResetAssetState(current_session);
3893
3894#ifdef __EMSCRIPTEN__
3895 // Use passed handle if provided, otherwise create new one
3896 auto loading_handle =
3897 passed_handle != 0
3898 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3899 passed_handle)
3900 : app::platform::WasmLoadingManager::BeginLoading(
3901 "Loading ROM (lazy assets)");
3902 auto cleanup_loading = [&]() {
3903 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3904 };
3905 struct LoadingGuard {
3906 std::function<void()> cleanup;
3907 bool dismissed = false;
3908 ~LoadingGuard() {
3909 if (!dismissed) {
3910 cleanup();
3911 }
3912 }
3913 void dismiss() { dismissed = true; }
3914 } loading_guard{cleanup_loading};
3915#else
3916 (void)passed_handle; // Unused on non-WASM
3917#endif
3918
3919 // Set renderer for emulator (lazy initialization happens in Run())
3920 if (renderer_) {
3922 }
3923
3924 // Wire settings panel to right panel manager for the current session.
3926 auto* settings = current_editor_set->GetSettingsPanel();
3927 right_drawer_manager_->SetSettingsPanel(settings);
3928 }
3929
3930 // Apply user preferences to status bar
3932
3933#ifdef __EMSCRIPTEN__
3934 loading_guard.dismiss();
3935 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3936#endif
3937
3938 LOG_INFO("EditorManager", "Lazy asset mode: editor assets deferred");
3939 return absl::OkStatus();
3940}
3941
3958 const std::optional<std::string>& target_filename) {
3959 return rom_lifecycle_.CheckRomWritePolicy(GetCurrentRom(), target_filename);
3960}
3961
3965
3971
3973 if (!pending_rom_save_.has_value() || !session_coordinator_) {
3974 return false;
3975 }
3976 return pending_rom_save_->session_index == GetCurrentSessionIndex() &&
3977 pending_rom_save_->rom != nullptr &&
3979}
3980
3982 pending_rom_save_.reset();
3984
3985 if (!hide_popups || !popup_manager_) {
3986 return;
3987 }
3988 for (const char* popup :
3991 if (popup_manager_->IsVisible(popup)) {
3992 popup_manager_->Hide(popup);
3993 }
3994 }
3995}
3996
3998 const std::optional<std::string>& save_as_filename) {
3999 auto* rom = GetCurrentRom();
4000 if (!session_coordinator_ || !session_coordinator_->GetActiveRomSession() ||
4001 !rom) {
4002 return absl::FailedPreconditionError("No active ROM session");
4003 }
4004
4007 PendingRomSave{save_as_filename, GetCurrentSessionIndex(), rom};
4008 return absl::OkStatus();
4009}
4010
4011void EditorManager::FinishPendingRomSaveAttempt(const absl::Status& status) {
4012 // A Cancelled status is resumable only while one of this request's explicit
4013 // confirmation dialogs is pending. Every other outcome is terminal and must
4014 // consume all one-shot bypasses so a later Ctrl+S cannot inherit consent.
4015 if (status.ok() || !absl::IsCancelled(status) ||
4018 }
4019}
4020
4022 // Do not let an autosave or an unrelated Ctrl+S replace a Save As request
4023 // while one of its confirmation dialogs is still open.
4024 if (pending_rom_save_.has_value()) {
4025 const bool unresolved_confirmation =
4030 if (unresolved_confirmation) {
4031 return absl::CancelledError("Save pending confirmation");
4032 }
4033 // Preserve the established request after programmatic confirmation too.
4034 // Older callers use ConfirmRomWrite(); SaveRom() rather than the popup's
4035 // ResumePendingRomSave() helper.
4036 return ResumePendingRomSave();
4037 }
4038 RETURN_IF_ERROR(StartPendingRomSave(std::nullopt));
4039 auto status = SaveRomInternal(std::nullopt);
4041 return status;
4042}
4043
4045 const std::optional<std::string>& save_as_filename) {
4046 auto* current_rom = GetCurrentRom();
4047 auto* current_editor_set = GetCurrentEditorSet();
4048 if (!current_rom || !current_editor_set) {
4049 return absl::FailedPreconditionError("No ROM or editor set loaded");
4050 }
4051 if (save_as_filename.has_value() && save_as_filename->empty()) {
4052 return absl::InvalidArgumentError("No filename provided for save as");
4053 }
4054 if (save_as_filename.has_value()) {
4055 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4056 *save_as_filename, GetCurrentSessionId()));
4057 }
4058
4059 ASSIGN_OR_RETURN(const project::YazeProject* save_project,
4061 // Hash/write/safety checks must read the same immutable session snapshot as
4062 // the rest of this save, never a process-global project left by another ROM.
4063 rom_lifecycle_.SetProjectContext(save_project);
4064 struct LifecycleProjectContextGuard {
4065 RomLifecycleManager* lifecycle = nullptr;
4066 project::YazeProject* restore = nullptr;
4067 ~LifecycleProjectContextGuard() {
4068 if (lifecycle) {
4069 lifecycle->SetProjectContext(restore);
4070 }
4071 }
4072 } lifecycle_project_guard{&rom_lifecycle_, &current_project_};
4073
4074 // --- State machine checks (delegated to RomLifecycleManager) ---
4076 return absl::CancelledError("Save pending confirmation");
4077 }
4078
4079 RETURN_IF_ERROR(CheckRomWritePolicy(save_as_filename));
4080
4082 return absl::CancelledError("Save pending confirmation");
4083 }
4084
4085 const bool pot_items_enabled =
4088 !rom_lifecycle_.ShouldSuppressPotItemSave() && pot_items_enabled) {
4089 const int loaded_rooms = current_editor_set->LoadedDungeonRoomCount();
4090 const int total_rooms = current_editor_set->TotalDungeonRoomCount();
4091 if (loaded_rooms < total_rooms) {
4092 rom_lifecycle_.SetPotItemConfirmPending(total_rooms - loaded_rooms,
4093 total_rooms);
4094 if (popup_manager_) {
4096 }
4098 absl::StrFormat(
4099 "Save paused: pot items enabled with %d unloaded rooms",
4102 return absl::CancelledError("Pot item save confirmation required");
4103 }
4104 }
4105
4106 const bool bypass_confirm = rom_lifecycle_.ShouldBypassPotItemConfirm();
4107 const bool suppress_pot_items = rom_lifecycle_.ShouldSuppressPotItemSave();
4109
4110 struct PotItemFlagGuard {
4111 bool restore = false;
4112 bool previous = false;
4113 ~PotItemFlagGuard() {
4114 if (restore) {
4115 core::FeatureFlags::get().dungeon.kSavePotItems = previous;
4116 }
4117 }
4118 } pot_item_guard;
4119
4120 if (suppress_pot_items) {
4121 pot_item_guard.previous = core::FeatureFlags::get().dungeon.kSavePotItems;
4122 pot_item_guard.restore = true;
4124 } else if (bypass_confirm) {
4125 // Explicitly allow pot item save once after confirmation.
4126 }
4127
4128 // --- Backup policy setup ---
4129 if (save_project->project_opened()) {
4131 save_project->workspace_settings.backup_on_save,
4132 save_project->GetAbsolutePath(save_project->rom_backup_folder),
4136 } else {
4138 user_settings_.prefs().backup_before_save, "", 20, true, 14);
4139 }
4140
4141 // Reject an already-invalid Oracle layout before any editor serializer can
4142 // mutate the ROM buffer or clear its own dirty tracking. The post-save check
4143 // below remains mandatory because serializers may introduce a violation.
4145
4146 // Serializers clear their dirty flags after a successful in-memory write.
4147 // Capture their conservative write prediction first so conflict protection
4148 // still has a fail-closed fallback when the on-disk ROM cannot be read.
4149 const auto predicted_dungeon_write_ranges =
4150 current_editor_set->CollectDungeonWriteRanges();
4151
4152 ScopedRomTransaction rom_transaction(*current_rom);
4153 ScopedEditorSaveTransactions editor_transactions;
4154
4155 auto save_editor = [&editor_transactions](Editor* editor) -> absl::Status {
4156 if (editor == nullptr) {
4157 return absl::OkStatus();
4158 }
4159 RETURN_IF_ERROR(editor_transactions.Begin(editor));
4160 return editor->Save();
4161 };
4162
4163 // --- Save editor-specific data ---
4165 save_editor(current_editor_set->GetEditor(EditorType::kScreen)));
4167 save_editor(current_editor_set->GetEditor(EditorType::kDungeon)));
4169 save_editor(current_editor_set->GetEditor(EditorType::kOverworld)));
4170
4171 if (core::FeatureFlags::get().kSaveMessages) {
4174 save_editor(current_editor_set->GetEditor(EditorType::kMessage)));
4175 }
4176
4177 if (core::FeatureFlags::get().kSaveGraphicsSheet) {
4179 *current_rom, gfx::Arena::Get().gfx_sheets()));
4180 }
4181
4182 // Oracle guardrails: refuse to write obviously corrupted ROM layouts.
4184
4185 // --- Write conflict check (ASM-owned address protection) ---
4186 if (save_project->project_opened() && save_project->hack_manifest.loaded()) {
4188 std::vector<std::pair<uint32_t, uint32_t>> write_ranges;
4189 bool diff_computed = false;
4190
4191 if (!current_rom->filename().empty()) {
4192 std::ifstream file(current_rom->filename(), std::ios::binary);
4193 if (file.is_open()) {
4194 file.seekg(0, std::ios::end);
4195 const std::streampos end = file.tellg();
4196 if (end >= 0) {
4197 std::vector<uint8_t> disk_data(static_cast<size_t>(end));
4198 file.seekg(0, std::ios::beg);
4199 file.read(reinterpret_cast<char*>(disk_data.data()),
4200 static_cast<std::streamsize>(disk_data.size()));
4201 if (file) {
4202 diff_computed = true;
4203 auto diff = yaze::rom::ComputeDiffRanges(disk_data,
4204 current_rom->vector());
4205 if (!diff.ranges.empty()) {
4206 LOG_DEBUG("EditorManager",
4207 "ROM save diff: %zu bytes changed in %zu range(s)",
4208 diff.total_bytes_changed, diff.ranges.size());
4209 write_ranges = std::move(diff.ranges);
4210 }
4211 }
4212 }
4213 }
4214 }
4215
4216 if (write_ranges.empty() && !diff_computed) {
4217 write_ranges = predicted_dungeon_write_ranges;
4218 }
4219
4220 if (!write_ranges.empty()) {
4221 auto conflicts =
4222 save_project->hack_manifest.AnalyzePcWriteRanges(write_ranges);
4223 if (!conflicts.empty()) {
4224 rom_lifecycle_.SetPendingWriteConflicts(std::move(conflicts));
4225 if (popup_manager_) {
4227 }
4229 absl::StrFormat(
4230 "Save paused: %zu write conflict(s) with ASM hooks",
4233 return absl::CancelledError("Write conflict confirmation required");
4234 }
4235 }
4236 } else {
4237 // Bypass is single-use, set by the warning popup.
4239 }
4240 }
4241
4242 // Delegate the final atomic disk write to RomFileManager. Save As is part of
4243 // this same transaction and writes only the requested target path.
4244 auto save_status =
4245 save_as_filename.has_value()
4246 ? rom_file_manager_.SaveRomAs(current_rom, *save_as_filename)
4247 : rom_file_manager_.SaveRom(current_rom);
4248 if (save_status.ok()) {
4249 editor_transactions.Commit();
4250 rom_transaction.Commit();
4253 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4254 session->backup_restore_pending = false;
4255 }
4256 }
4257 // Write-confirm bypass is single-use. Clear it after a successful save.
4259
4260 if (save_as_filename.has_value()) {
4262 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4263 session->filepath = *save_as_filename;
4264 }
4265 }
4266
4268 manager.AddFile(*save_as_filename);
4269 manager.Save();
4270 if (popup_manager_) {
4272 }
4273 }
4274 }
4275 return save_status;
4276}
4277
4278absl::Status EditorManager::SaveRomAs(const std::string& filename) {
4279 if (filename.empty()) {
4280 return absl::InvalidArgumentError("No filename provided for save as");
4281 }
4282 if (pending_rom_save_.has_value() && HasPendingRomSaveConfirmation()) {
4283 return absl::CancelledError("Save pending confirmation");
4284 }
4286 auto status = SaveRomInternal(filename);
4288 return status;
4289}
4290
4292 if (!pending_rom_save_.has_value()) {
4294 return absl::FailedPreconditionError("No pending ROM save to resume");
4295 }
4297 CancelPendingRomSave(/*hide_popups=*/true);
4298 return absl::FailedPreconditionError(
4299 "Pending ROM save belongs to a different session");
4300 }
4301
4302 const auto save_as_filename = pending_rom_save_->save_as_filename;
4303 auto status = SaveRomInternal(save_as_filename);
4305 return status;
4306}
4307
4308absl::Status EditorManager::OpenRomOrProject(const std::string& filename) {
4311 GetCurrentSessionId(), SIZE_MAX, filename})) {
4312 return absl::OkStatus();
4313 }
4314
4315 return OpenRomOrProjectInternal(filename);
4316}
4317
4319 const std::string& filename) {
4320 LOG_INFO("EditorManager", "OpenRomOrProject called with: '%s'",
4321 filename.c_str());
4322 if (filename.empty()) {
4323 LOG_INFO("EditorManager", "Empty filename provided, skipping load.");
4324 return absl::OkStatus();
4325 }
4326
4327#ifdef __EMSCRIPTEN__
4328 // Start loading indicator early for WASM builds
4329 auto loading_handle =
4330 app::platform::WasmLoadingManager::BeginLoading("Loading ROM");
4331 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4332 "Reading ROM file...");
4333 // RAII guard to ensure loading indicator is closed even on early return
4334 struct LoadingGuard {
4335 app::platform::WasmLoadingManager::LoadingHandle handle;
4336 bool dismissed = false;
4337 ~LoadingGuard() {
4338 if (!dismissed)
4339 app::platform::WasmLoadingManager::EndLoading(handle);
4340 }
4341 void dismiss() { dismissed = true; }
4342 } loading_guard{loading_handle};
4343#endif
4344
4345 if (absl::EndsWith(filename, ".yaze") ||
4346 absl::EndsWith(filename, ".zsproj") ||
4347 absl::EndsWith(filename, ".yazeproj")) {
4348 // Parse first so a failed project open cannot destroy the active session's
4349 // working context. Detach only when the incoming project is ready.
4350 project::YazeProject incoming_project;
4351 RETURN_IF_ERROR(incoming_project.Open(filename));
4352 if (!incoming_project.rom_filename.empty()) {
4353 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4354 incoming_project.rom_filename));
4355 }
4356 const std::optional<size_t> previous_session_id =
4359 pending_project_open_previous_session_id_ = previous_session_id;
4361 current_project_ = std::move(incoming_project);
4364
4365 // Load ROM directly from project - don't prompt user
4366 auto project_status = LoadProjectWithRom();
4367 if (!project_status.ok()) {
4368 RestoreProjectContextAfterFailedOpen(previous_session_id);
4369 }
4370 return project_status;
4371 } else {
4372#ifdef __EMSCRIPTEN__
4373 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.05f);
4374 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4375 "Loading ROM data...");
4376#endif
4377 Rom temp_rom;
4378 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(filename));
4379 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, filename));
4381
4382 const std::optional<size_t> previous_session_id =
4384 const size_t previous_session_count =
4385 session_coordinator_->GetTotalSessionCount();
4386 auto session_or = session_coordinator_->CreateSessionFromRom(
4387 std::move(temp_rom), filename);
4388 if (!session_or.ok()) {
4389 return session_or.status();
4390 }
4391 RomSession* session = *session_or;
4392
4396
4397 // Apply project feature flags to both session and global singleton
4401
4402 // Keep ResourceLabelProvider in sync with the active ROM session before
4403 // editors register room/sprite labels.
4405
4406 // Update test manager with current ROM for ROM-dependent tests (only when
4407 // tests are enabled)
4408#ifdef YAZE_ENABLE_TESTING
4409 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
4410 (void*)GetCurrentRom(),
4411 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
4413#endif
4414
4415 if (auto* editor_set = GetCurrentEditorSet();
4416 editor_set && !current_project_.code_folder.empty()) {
4417 const std::string absolute_code_folder =
4419 // iOS: avoid blocking the main thread during project open / scene updates.
4420 // Large iCloud-backed projects can trigger watchdog termination if we
4421 // eagerly enumerate folders here.
4422#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
4423 editor_set->OpenAssemblyFolder(absolute_code_folder);
4424#endif
4425 // Also set the sidebar file browser path (refresh happens during UI draw).
4426 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
4427 }
4428
4429#ifdef __EMSCRIPTEN__
4430 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.10f);
4431 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4432 "Initializing editors...");
4433 // Pass the loading handle to LoadAssets and dismiss our guard
4434 // LoadAssets will manage closing the indicator when done
4435 loading_guard.dismiss();
4436 auto asset_status = LoadAssetsForMode(loading_handle);
4437#else
4438 auto asset_status = LoadAssetsForMode();
4439#endif
4440 if (!asset_status.ok()) {
4441 auto rollback_status =
4442 DiscardProvisionalSessionCreatedSince(previous_session_count);
4443 RestoreProjectContextAfterFailedOpen(previous_session_id);
4444 if (!rollback_status.ok()) {
4445 return absl::InternalError(
4446 absl::StrFormat("%s; session rollback failed: %s",
4447 asset_status.message(), rollback_status.message()));
4448 }
4449 return asset_status;
4450 }
4451
4452 // Hide welcome screen and show editor selection when ROM is loaded
4453 ui_coordinator_->SetWelcomeScreenVisible(false);
4454 // dashboard_panel_->ClearRecentEditors();
4455 ui_coordinator_->SetEditorSelectionVisible(true);
4456
4457 // Set Dashboard category to suppress panel drawing until user selects an editor
4460 /*notify=*/false);
4461 }
4462 return absl::OkStatus();
4463}
4464
4465absl::Status EditorManager::CreateNewProject(const std::string& template_name) {
4467 return absl::FailedPreconditionError(
4468 "Save or discard pending session work before creating a project");
4469 }
4470
4471 if (!ui_coordinator_) {
4472 return absl::FailedPreconditionError(
4473 "Project creation dialog is not available");
4474 }
4475 ui_coordinator_->OpenNewProjectDialog(
4476 template_name.empty() ? "Vanilla ROM Hack" : template_name);
4477 return absl::OkStatus();
4478}
4479
4481 const std::string& template_name, const std::string& rom_path,
4482 const std::string& project_name, const std::string& project_path) {
4483 if (rom_path.empty() || project_name.empty()) {
4484 return absl::InvalidArgumentError("ROM path and project name are required");
4485 }
4486 if (absl::EndsWith(rom_path, ".yaze") ||
4487 absl::EndsWith(rom_path, ".yazeproj") ||
4488 absl::EndsWith(rom_path, ".zsproj")) {
4489 return absl::InvalidArgumentError(
4490 "New projects require a ROM file, not a project descriptor");
4491 }
4493 return absl::FailedPreconditionError(
4494 "Save or discard pending session work before creating a project");
4495 }
4496
4497 auto* reusable_session = session_coordinator_
4498 ? session_coordinator_->GetActiveRomSession()
4499 : nullptr;
4500 const bool reuse_active_raw_session =
4501 reusable_session && reusable_session->rom.is_loaded() &&
4503 rom_path, reusable_session->rom.filename()) &&
4506 const std::optional<size_t> previous_session_id =
4508 const size_t previous_session_count =
4509 session_coordinator_ ? session_coordinator_->GetTotalSessionCount() : 0;
4511 auto rom_status = project_manager_.SetProjectRom(rom_path);
4512 if (!rom_status.ok()) {
4514 return rom_status;
4515 }
4517 project_name, project_path);
4518 if (!target_status.ok()) {
4520 return target_status;
4521 }
4522
4523 if (reuse_active_raw_session) {
4524 return FinalizeNewProject(project_name, project_path);
4525 }
4526
4530
4531 auto open_status = OpenRomOrProjectInternal(rom_path);
4532 if (!open_status.ok()) {
4534 RestoreProjectContextAfterFailedOpen(previous_session_id);
4535 return open_status;
4536 }
4537
4538 auto finalize_status = FinalizeNewProject(project_name, project_path);
4539 if (!finalize_status.ok()) {
4540 auto rollback_status =
4541 DiscardProvisionalSessionCreatedSince(previous_session_count);
4542 RestoreProjectContextAfterFailedOpen(previous_session_id);
4543 if (!rollback_status.ok()) {
4544 return absl::InternalError(absl::StrFormat(
4545 "%s; session rollback failed: %s", finalize_status.message(),
4546 rollback_status.message()));
4547 }
4548 }
4549 return finalize_status;
4550}
4551
4553 const std::string& project_name, const std::string& project_path) {
4554 if (!session_coordinator_) {
4555 return absl::FailedPreconditionError("No session coordinator");
4556 }
4557 auto* session = session_coordinator_->GetActiveRomSession();
4558 if (!session || !session->rom.is_loaded()) {
4559 return absl::FailedPreconditionError(
4560 "Load a ROM before finalizing the project");
4561 }
4562
4563 // The ROM-load-options dialog can create a project directly from an
4564 // already-open raw ROM. Start from a clean shell in that path rather than
4565 // reusing ProjectManager state left by an earlier project/import.
4566 const bool created_project_shell = !project_manager_.IsPendingRomSelection();
4567 if (created_project_shell) {
4569 }
4570
4571 auto project_rom_status =
4572 project_manager_.SetProjectRom(session->rom.filename());
4573 if (!project_rom_status.ok()) {
4575 return project_rom_status;
4576 }
4577 auto& pending_project = project_manager_.GetCurrentProject();
4578 if (created_project_shell) {
4579 // ROM-load options apply their selected flags directly to the raw session.
4580 // Guided creation already configured the pending template, including when
4581 // it reuses an existing raw-ROM session.
4582 pending_project.feature_flags = session->feature_flags;
4583 }
4584 pending_project.workspace_settings.font_global_scale =
4586 pending_project.workspace_settings.autosave_enabled =
4588 pending_project.workspace_settings.autosave_interval_secs =
4590 pending_project.workspace_settings.backup_on_save =
4592
4593 auto finalize_status =
4594 project_manager_.FinalizeProjectCreation(project_name, project_path);
4595 if (!finalize_status.ok()) {
4597 return finalize_status;
4598 }
4599
4602 session->project_dirty = false;
4603 active_project_context_session_id_ = session->session_id();
4604 runtime_feature_flags_session_id_ = session->session_id();
4606 if (version_manager_) {
4608 current_project_.git_repository = session->project_context->git_repository;
4610 }
4614 return absl::OkStatus();
4615}
4616
4620 GetCurrentSessionId()})) {
4621 return absl::OkStatus();
4622 }
4623
4624 return OpenProjectInternal();
4625}
4626
4628 auto open_project_from_path =
4629 [this](const std::string& file_path) -> absl::Status {
4630 if (file_path.empty()) {
4631 return absl::OkStatus();
4632 }
4633
4634 project::YazeProject new_project;
4635 RETURN_IF_ERROR(new_project.Open(file_path));
4636
4637 if (!new_project.rom_filename.empty()) {
4638 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4639 new_project.rom_filename));
4640 }
4641
4642 // Validate project
4643 auto validation_status = new_project.Validate();
4644 if (!validation_status.ok()) {
4645 toast_manager_.Show(absl::StrFormat("Project validation failed: %s",
4646 validation_status.message()),
4648
4649 // Ask user if they want to repair
4650 popup_manager_->Show("Project Repair");
4651 }
4652
4653 const std::optional<size_t> previous_session_id =
4656 pending_project_open_previous_session_id_ = previous_session_id;
4658 current_project_ = std::move(new_project);
4660
4661 auto project_status = LoadProjectWithRom();
4662 if (!project_status.ok()) {
4663 RestoreProjectContextAfterFailedOpen(previous_session_id);
4664 }
4665 return project_status;
4666 };
4667
4668#if defined(__APPLE__) && TARGET_OS_IOS == 1
4669 // On iOS, route project selection through the SwiftUI overlay document picker
4670 // so we get open-in-place + security-scoped access for iCloud Drive bundles.
4671 platform::ios::PostOverlayCommand("open_project");
4672 return absl::OkStatus();
4673#else
4675 return open_project_from_path(file_path);
4676#endif
4677}
4678
4680 const std::string& rom_path) {
4681 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4682
4683 Rom candidate_rom;
4684 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&candidate_rom, rom_path));
4686
4689 .candidate_path = rom_path,
4690 };
4691 return absl::OkStatus();
4692}
4693
4695 auto pending_selection = std::move(pending_project_rom_selection_);
4697 std::string project_rom_path = pending_selection.has_value()
4698 ? pending_selection->candidate_path
4700 const std::string previous_project_rom_path =
4701 pending_selection.has_value() ? pending_selection->previous_path
4703 bool persist_selected_rom = pending_selection.has_value();
4704 auto abandon_selected_rom = [&]() {
4705 if (!persist_selected_rom) {
4706 return;
4707 }
4708 current_project_.rom_filename = previous_project_rom_path;
4710 };
4711
4712 // Check if project has a ROM file specified
4713 if (project_rom_path.empty()) {
4714 // No ROM specified - prompt user to select one
4716 "Project has no ROM file configured. Please select a ROM.",
4718#if defined(__APPLE__) && TARGET_OS_IOS == 1
4719 // Guard: if the project lives inside a .yazeproj bundle the ROM path
4720 // defaults to bundle/rom, which may not exist yet because iCloud hasn't
4721 // finished the download. Popping the file picker here would let a
4722 // temporary path overwrite the correct bundle path in project.yaze.
4723 // Show guidance and let the user reopen the project once the download
4724 // is complete.
4725 {
4726 auto bundle_parent =
4727 std::filesystem::path(current_project_.filepath).parent_path();
4728 if (bundle_parent.extension() == ".yazeproj") {
4730 "ROM is downloading from iCloud. Reopen the project in a few "
4731 "seconds.",
4732 ToastType::kInfo, 6.0f);
4733 return absl::UnavailableError(
4734 "Project ROM is not available from iCloud yet");
4735 }
4736 }
4737 const std::string target_project_filepath = current_project_.filepath;
4738 const std::optional<size_t> target_session_id =
4740 const uint64_t selection_generation = ++project_rom_selection_generation_;
4743 [this, target_project_filepath, target_session_id,
4744 selection_generation](const std::string& rom_path) {
4745 if (selection_generation != project_rom_selection_generation_ ||
4746 current_project_.filepath != target_project_filepath ||
4747 active_project_context_session_id_ != target_session_id) {
4748 toast_manager_.Show(
4749 "ROM selection ignored because the active project changed",
4750 ToastType::kWarning);
4751 return;
4752 }
4753 const bool restore_pending_transition =
4755 const std::optional<size_t> previous_session_id =
4757 auto restore_after_failure = [&]() {
4758 if (restore_pending_transition) {
4759 RestoreProjectContextAfterFailedOpen(previous_session_id);
4760 }
4761 };
4762 if (rom_path.empty()) {
4763 restore_after_failure();
4764 return;
4765 }
4766 auto path_status =
4767 session_coordinator_->CheckBackingFileAvailable(rom_path);
4768 if (!path_status.ok()) {
4769 toast_manager_.Show(std::string(path_status.message()),
4771 restore_after_failure();
4772 return;
4773 }
4774 auto selection_status = ValidateProjectRomSelection(rom_path);
4775 if (!selection_status.ok()) {
4777 absl::StrFormat("Failed to update project ROM: %s",
4778 selection_status.message()),
4780 restore_after_failure();
4781 return;
4782 }
4783 auto status = LoadProjectWithRom();
4784 if (!status.ok()) {
4786 absl::StrFormat("Failed to load project ROM: %s",
4787 status.message()),
4789 restore_after_failure();
4790 }
4791 });
4792 return absl::OkStatus();
4793#else
4796 if (rom_path.empty()) {
4797 return absl::CancelledError("Project ROM selection cancelled");
4798 }
4799 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4800 project_rom_path = rom_path;
4801 persist_selected_rom = true;
4802#endif
4803 }
4804
4805 auto backing_status =
4806 session_coordinator_->CheckBackingFileAvailable(project_rom_path);
4807 if (!backing_status.ok()) {
4808 abandon_selected_rom();
4809 return backing_status;
4810 }
4811
4812 // Load ROM from project
4813 Rom temp_rom;
4814 auto load_status = rom_file_manager_.LoadRom(&temp_rom, project_rom_path);
4815 if (!load_status.ok()) {
4816 // ROM file not found or invalid - prompt user to select new ROM
4818 absl::StrFormat("Could not load ROM '%s': %s. Please select a new ROM.",
4819 project_rom_path, load_status.message()),
4821#if defined(__APPLE__) && TARGET_OS_IOS == 1
4822 abandon_selected_rom();
4823 // If the ROM is inside a .yazeproj bundle its path may not be readable
4824 // yet because iCloud hasn't finished downloading it. Saving any other
4825 // path here would corrupt the project file with a temporary location.
4826 // Show guidance and bail without touching current_project_.rom_filename.
4827 {
4828 auto rom_parent = std::filesystem::path(project_rom_path).parent_path();
4829 if (rom_parent.extension() == ".yazeproj") {
4831 "ROM is still downloading from iCloud. Try reopening the project "
4832 "in a moment.",
4833 ToastType::kInfo, 6.0f);
4834 return absl::UnavailableError(
4835 "Project ROM is still downloading from iCloud");
4836 }
4837 }
4838 const std::string target_project_filepath = current_project_.filepath;
4839 const std::optional<size_t> target_session_id =
4841 const uint64_t selection_generation = ++project_rom_selection_generation_;
4844 [this, target_project_filepath, target_session_id,
4845 selection_generation](const std::string& rom_path) {
4846 if (selection_generation != project_rom_selection_generation_ ||
4847 current_project_.filepath != target_project_filepath ||
4848 active_project_context_session_id_ != target_session_id) {
4849 toast_manager_.Show(
4850 "ROM selection ignored because the active project changed",
4851 ToastType::kWarning);
4852 return;
4853 }
4854 const bool restore_pending_transition =
4856 const std::optional<size_t> previous_session_id =
4858 auto restore_after_failure = [&]() {
4859 if (restore_pending_transition) {
4860 RestoreProjectContextAfterFailedOpen(previous_session_id);
4861 }
4862 };
4863 if (rom_path.empty()) {
4864 restore_after_failure();
4865 return;
4866 }
4867 auto path_status =
4868 session_coordinator_->CheckBackingFileAvailable(rom_path);
4869 if (!path_status.ok()) {
4870 toast_manager_.Show(std::string(path_status.message()),
4872 restore_after_failure();
4873 return;
4874 }
4875 auto selection_status = ValidateProjectRomSelection(rom_path);
4876 if (!selection_status.ok()) {
4878 absl::StrFormat("Failed to update project ROM: %s",
4879 selection_status.message()),
4881 restore_after_failure();
4882 return;
4883 }
4884 auto status = LoadProjectWithRom();
4885 if (!status.ok()) {
4887 absl::StrFormat("Failed to load project ROM: %s",
4888 status.message()),
4890 restore_after_failure();
4891 }
4892 });
4893 return absl::OkStatus();
4894#else
4897 if (rom_path.empty()) {
4898 return absl::CancelledError("Project ROM selection cancelled");
4899 }
4900 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4901 project_rom_path = rom_path;
4902 persist_selected_rom = true;
4903 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, rom_path));
4904#endif
4905 }
4906
4907 auto policy_status = rom_lifecycle_.CheckRomOpenPolicy(&temp_rom);
4908 if (!policy_status.ok()) {
4909 abandon_selected_rom();
4910 return policy_status;
4911 }
4912
4913 if (persist_selected_rom) {
4914 current_project_.rom_filename = project_rom_path;
4915 }
4916
4917 const size_t previous_session_count =
4918 session_coordinator_->GetTotalSessionCount();
4919 auto session_or = session_coordinator_->CreateSessionFromRom(
4920 std::move(temp_rom), project_rom_path);
4921 if (!session_or.ok()) {
4922 abandon_selected_rom();
4923 return session_or.status();
4924 }
4925 RomSession* session = *session_or;
4926
4930
4931 // Auto-enable custom object rendering when a project defines custom object
4932 // data but the stale feature flag is off.
4933 if (ProjectUsesCustomObjects(current_project_) &&
4936 LOG_WARN("EditorManager",
4937 "Project has custom object data but 'enable_custom_objects' was "
4938 "disabled. Enabling at runtime.");
4939 toast_manager_.Show("Custom object rendering auto-enabled for this project",
4941 }
4942 std::string legacy_mapping_warning;
4943 if (SeedLegacyTrackObjectMapping(&current_project_,
4944 &legacy_mapping_warning)) {
4945 LOG_WARN("EditorManager", "%s", legacy_mapping_warning.c_str());
4947 "Seeded default custom object mapping for object 0x31 (save project "
4948 "to persist)",
4950 }
4951
4952 // Apply project feature flags to both session and global singleton.
4956#if !defined(NDEBUG)
4957 LOG_INFO(
4958 "EditorManager",
4959 "Feature flags applied: kEnableCustomObjects=%s, "
4960 "custom_objects_folder='%s', custom_object_files=%zu entries",
4964#endif
4965
4968 if (version_manager_) {
4969 // Preserve the existing best-effort Git initialization behavior, now
4970 // against the stable session-owned project object.
4972 current_project_.git_repository = session->project_context->git_repository;
4973 }
4974
4975 if (auto* rom = GetCurrentRom(); rom && rom->is_loaded()) {
4976 if (IsRomHashMismatch()) {
4978 "Project ROM hash mismatch detected. Check ROM Identity settings.",
4980 }
4981 auto warnings = ValidateRomAddressOverrides(
4983 if (!warnings.empty()) {
4984 for (const auto& warning : warnings) {
4985 LOG_WARN("EditorManager", "%s", warning.c_str());
4986 }
4987 toast_manager_.Show(absl::StrFormat("ROM override warnings: %d (see log)",
4988 warnings.size()),
4990 }
4991 }
4992
4993 // Update test manager with current ROM for ROM-dependent tests (only when
4994 // tests are enabled)
4995#ifdef YAZE_ENABLE_TESTING
4996 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
4997 (void*)GetCurrentRom(),
4998 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
5000#endif
5001
5002 if (auto* editor_set = GetCurrentEditorSet();
5003 editor_set && !current_project_.code_folder.empty()) {
5004 const std::string absolute_code_folder =
5006 // iOS: avoid blocking the main thread during project open / scene updates.
5007#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
5008 editor_set->OpenAssemblyFolder(absolute_code_folder);
5009#endif
5010 // Also set the sidebar file browser path (refresh happens during UI draw).
5011 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
5012 }
5013
5014 // Initialize labels before loading editor assets so room lists / command
5015 // palette entries resolve project registry labels on first render.
5017
5018 auto asset_status = LoadAssetsForMode();
5019 if (!asset_status.ok()) {
5020 abandon_selected_rom();
5021 auto rollback_status =
5022 DiscardProvisionalSessionCreatedSince(previous_session_count);
5023 if (!rollback_status.ok()) {
5024 return absl::InternalError(
5025 absl::StrFormat("%s; session rollback failed: %s",
5026 asset_status.message(), rollback_status.message()));
5027 }
5028 return asset_status;
5029 }
5030
5031 if (persist_selected_rom) {
5032 auto save_status = current_project_.Save();
5033 if (!save_status.ok()) {
5034 abandon_selected_rom();
5035 auto rollback_status =
5036 DiscardProvisionalSessionCreatedSince(previous_session_count);
5037 if (!rollback_status.ok()) {
5038 return absl::InternalError(
5039 absl::StrFormat("%s; session rollback failed: %s",
5040 save_status.message(), rollback_status.message()));
5041 }
5042 return save_status;
5043 }
5045 }
5046
5047 // Hide welcome screen and show editor selection when project ROM is loaded
5048 if (ui_coordinator_) {
5049 ui_coordinator_->SetWelcomeScreenVisible(false);
5050 ui_coordinator_->SetEditorSelectionVisible(true);
5051 }
5052
5053 // Set Dashboard category to suppress panel drawing until user selects an editor
5055 /*notify=*/false);
5056
5057 // Apply workspace settings
5066 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
5067
5070
5078 project_management_panel_->SetBuildLogOutput("");
5079 }
5081 active_project_build_.reset();
5083
5084 // Publish project context for hack workflow panels and other consumers
5086
5087 // Add to recent files
5089 manager.AddFile(current_project_.filepath);
5090 manager.Save();
5091
5092 // Update project management panel with loaded project
5095 project_management_panel_->SetVersionManager(version_manager_);
5097 }
5098
5099 toast_manager_.Show(absl::StrFormat("Project '%s' loaded successfully",
5102
5105 return absl::OkStatus();
5106}
5107
5109 Rom&& rom, const std::string& filepath) {
5110 if (!session_coordinator_) {
5111 return absl::FailedPreconditionError("No session coordinator");
5112 }
5113 auto* session = session_coordinator_->GetActiveRomSession();
5114 if (!session) {
5115 return absl::FailedPreconditionError("No active ROM session");
5116 }
5117
5118 Rom previous_rom = session->rom;
5119 const std::string previous_filepath = session->filepath;
5120 const bool game_data_was_loaded = session->game_data_loaded;
5121 auto* dungeon_editor = static_cast<DungeonEditorV2*>(
5122 session->editors.GetExistingEditor(EditorType::kDungeon));
5123 const size_t dungeon_index = EditorTypeIndex(EditorType::kDungeon);
5124 const bool dungeon_was_initialized =
5125 session->editor_initialized[dungeon_index];
5126 const bool dungeon_assets_were_loaded =
5127 session->editor_assets_loaded[dungeon_index];
5128 auto restore_dungeon_asset_state = [&]() {
5129 if (dungeon_editor) {
5130 session->editor_initialized[dungeon_index] =
5131 session->editor_initialized[dungeon_index] || dungeon_was_initialized;
5132 session->editor_assets_loaded[dungeon_index] =
5133 session->editor_assets_loaded[dungeon_index] ||
5134 dungeon_assets_were_loaded;
5135 }
5136 };
5137
5138 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5139 session->rom = std::move(rom);
5140 session->filepath = filepath;
5141 session->game_data.Clear();
5142 session->game_data.set_rom(&session->rom);
5143 session->editors.SetGameData(&session->game_data);
5144 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5145 ResetAssetState(session);
5146 ConfigureSession(session);
5149 auto load_status = LoadAssetsForMode();
5150 if (load_status.ok() && game_data_was_loaded && !session->game_data_loaded) {
5151 load_status = EnsureGameDataLoaded();
5152 }
5153 if (load_status.ok() && dungeon_editor) {
5154 load_status = dungeon_editor->RefreshRomBackedState();
5155 if (load_status.ok()) {
5156 restore_dungeon_asset_state();
5157 }
5158 }
5159 if (!load_status.ok()) {
5160 // Loading editors is fallible. Restore the prior ROM and rebuild its
5161 // assets so a failed reload never leaves a half-initialized live session.
5162 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5163 session->rom = std::move(previous_rom);
5164 session->filepath = previous_filepath;
5165 session->game_data.Clear();
5166 session->game_data.set_rom(&session->rom);
5167 session->editors.SetGameData(&session->game_data);
5168 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5169 ResetAssetState(session);
5170 ConfigureSession(session);
5173 auto rollback_status = LoadAssetsForMode();
5174 if (game_data_was_loaded && !session->game_data_loaded) {
5175 auto game_data_status = EnsureGameDataLoaded();
5176 if (rollback_status.ok()) {
5177 rollback_status = game_data_status;
5178 }
5179 }
5180 if (dungeon_editor) {
5181 auto dungeon_status = dungeon_editor->RefreshRomBackedState();
5182 if (dungeon_status.ok()) {
5183 restore_dungeon_asset_state();
5184 }
5185 if (rollback_status.ok()) {
5186 rollback_status = dungeon_status;
5187 }
5188 }
5190 if (!rollback_status.ok()) {
5191 return absl::InternalError(absl::StrFormat(
5192 "ROM reload failed (%s) and restoring prior assets failed (%s)",
5193 load_status.message(), rollback_status.message()));
5194 }
5195 return load_status;
5196 }
5198 return absl::OkStatus();
5199}
5200
5201absl::Status EditorManager::SwapProjectRom(const std::string& rom_path) {
5202 if (rom_path.empty()) {
5203 return absl::InvalidArgumentError("ROM path cannot be empty");
5204 }
5205 if (!session_coordinator_) {
5206 return absl::FailedPreconditionError("No session coordinator");
5207 }
5208 auto* session = session_coordinator_->GetActiveRomSession();
5209 if (!session || !session->rom.is_loaded() ||
5211 return absl::FailedPreconditionError("No active project ROM session");
5212 }
5214 return absl::FailedPreconditionError(
5215 "Save or discard ROM edits before swapping the project ROM");
5216 }
5218 if (session->project_file_editor_state.initialized &&
5219 session->project_file_editor_state.modified &&
5221 return absl::FailedPreconditionError(
5222 "Preserve the raw project-file draft before swapping ROMs");
5223 }
5224
5225 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5226 rom_path, session->session_id()));
5227 Rom replacement_rom;
5228 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom, rom_path));
5230
5231 const std::string old_rom_path = current_project_.rom_filename;
5232 current_project_.rom_filename = rom_path;
5233 auto save_status = current_project_.Save();
5234 if (!save_status.ok()) {
5235 current_project_.rom_filename = old_rom_path;
5236 return save_status;
5237 }
5238
5240 auto replace_status =
5241 ReplaceActiveSessionRom(std::move(replacement_rom), rom_path);
5242 if (!replace_status.ok()) {
5243 current_project_.rom_filename = old_rom_path;
5244 auto rollback_status = current_project_.Save();
5247 if (!rollback_status.ok()) {
5248 return absl::InternalError(absl::StrFormat(
5249 "ROM swap failed (%s) and restoring the project descriptor failed "
5250 "(%s)",
5251 replace_status.message(), rollback_status.message()));
5252 }
5253 return replace_status;
5254 }
5255 session->project_dirty = false;
5257 project_management_panel_->SetProjectDirty(false);
5258 }
5261 return absl::OkStatus();
5262}
5263
5265 if (!session_coordinator_) {
5266 return absl::FailedPreconditionError("No session coordinator");
5267 }
5268 auto* session = session_coordinator_->GetActiveRomSession();
5269 if (!session || !session->rom.is_loaded() ||
5272 return absl::FailedPreconditionError("No active project ROM to reload");
5273 }
5275 return absl::FailedPreconditionError(
5276 "Save or discard ROM edits before reloading from disk");
5277 }
5278
5279 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5280 current_project_.rom_filename, session->session_id()));
5281 Rom replacement_rom;
5282 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom,
5285 return ReplaceActiveSessionRom(std::move(replacement_rom),
5287}
5288
5292 if (auto* session = session_coordinator_->GetActiveRomSession();
5293 session && session->project_file_editor_state.initialized &&
5294 session->project_file_editor_state.modified &&
5296 return absl::FailedPreconditionError(
5297 "Raw project file also has unsaved changes; use raw Save As to "
5298 "preserve that draft before saving project settings");
5299 }
5300 }
5303 return CreateNewProject();
5304 }
5305
5306 // Update project with current settings
5309 auto* session = session_coordinator_->GetActiveRomSession();
5310 if (session) {
5311 current_project_.feature_flags = session->feature_flags;
5312 }
5313 }
5314
5323
5324 // Save recent files
5327 for (const auto& file : manager.GetRecentFiles()) {
5329 }
5330 }
5331
5332 auto status = current_project_.Save();
5333 if (status.ok()) {
5335 project_management_panel_->SetProjectDirty(false);
5336 }
5338 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5339 session->project_dirty = false;
5340 }
5341 }
5343 }
5345 return status;
5346}
5347
5350 project_management_panel_->SetProjectDirty(true);
5351 }
5353 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5354 session->project_dirty = true;
5355 }
5356 }
5357}
5358
5360 if (!session_coordinator_) {
5362 project_management_panel_->IsProjectDirty();
5363 }
5364 const auto* session = session_coordinator_->GetActiveRomSession();
5365 return session != nullptr && session->project_dirty;
5366}
5367
5369 if (!session_coordinator_) {
5370 return absl::FailedPreconditionError("No session coordinator");
5371 }
5372 auto* session = session_coordinator_->GetActiveRomSession();
5373 if (!session) {
5374 return absl::FailedPreconditionError("No active ROM session");
5375 }
5376
5378 const bool project_dirty = session->project_dirty;
5379 const bool project_file_dirty =
5380 session->project_file_editor_state.initialized &&
5381 session->project_file_editor_state.modified;
5382
5383 if (project_dirty && project_file_dirty &&
5385 return absl::FailedPreconditionError(
5386 "Project settings and raw project file both have unsaved changes; "
5387 "use raw Save As to preserve the draft, then save project settings");
5388 }
5389
5390 if (project_dirty) {
5392 current_project_.filepath.empty()) {
5393 return absl::FailedPreconditionError(
5394 "Project settings have no project file to save");
5395 }
5397 }
5398
5399 if (project_file_dirty) {
5400 if (project_file_editor_.filepath().empty()) {
5401 return absl::FailedPreconditionError(
5402 "Project file draft has no destination; use Save As first");
5403 }
5405 session->project_file_editor_state = project_file_editor_.CaptureState();
5406 }
5407
5408 return absl::OkStatus();
5409}
5410
5412 if (!session_coordinator_) {
5413 return absl::FailedPreconditionError("No session coordinator");
5414 }
5415 const size_t session_index = GetCurrentSessionIndex();
5417 const auto* session = session_coordinator_->GetActiveRomSession();
5418 if (session != nullptr && session->backup_restore_pending) {
5419 return absl::CancelledError(
5420 "Staged backup restore requires an explicit Save ROM");
5421 }
5422 if (SessionHasPendingRomWork(session_index)) {
5424 }
5425 if (SessionHasPendingUnsavedWork(session_index)) {
5426 return absl::FailedPreconditionError(
5427 "Autosave left unsaved project or ROM work");
5428 }
5429 return absl::OkStatus();
5430}
5431
5433 PotItemSaveDecision decision) {
5434 // Map EditorManager enum to RomLifecycleManager enum
5435 auto lifecycle_decision =
5437 static_cast<int>(decision));
5439
5440 if (decision == PotItemSaveDecision::kCancel) {
5442 toast_manager_.Show("Save cancelled", ToastType::kInfo);
5443 return;
5444 }
5445
5446 auto status = ResumePendingRomSave();
5447 if (!absl::IsCancelled(status) && !status.ok()) {
5449 absl::StrFormat("Failed to save ROM: %s", status.message()),
5451 }
5452}
5453
5455 // Get current project name for default filename
5456 std::string default_name = current_project_.project_opened()
5458 : "untitled_project";
5459
5460 auto file_path =
5461 util::FileDialogWrapper::ShowSaveFileDialog(default_name, "yaze");
5462 if (file_path.empty()) {
5463 return absl::OkStatus();
5464 }
5465
5466 return SaveProjectAs(file_path);
5467}
5468
5469absl::Status EditorManager::SaveProjectAs(const std::string& filepath) {
5470 if (filepath.empty()) {
5471 return absl::InvalidArgumentError("Project file path cannot be empty");
5472 }
5475 std::string file_path = filepath;
5476
5477 // Ensure a project extension.
5478 if (!(absl::EndsWith(file_path, ".yaze") ||
5479 absl::EndsWith(file_path, ".yazeproj"))) {
5480 file_path += ".yaze";
5481 }
5482
5484 if (auto* session = session_coordinator_->GetActiveRomSession();
5485 session && session->project_file_editor_state.initialized &&
5486 session->project_file_editor_state.modified &&
5488 session->project_file_editor_state.filepath, file_path)) {
5489 return absl::FailedPreconditionError(
5490 "Raw project draft targets the selected file; preserve it with "
5491 "raw Save As before overwriting that destination");
5492 }
5493 }
5494
5495 // Update project filepath and save
5496 std::string old_filepath = current_project_.filepath;
5497 current_project_.filepath = file_path;
5498
5499 auto save_status = current_project_.Save();
5500 if (save_status.ok()) {
5502 project_management_panel_->SetProjectDirty(false);
5503 }
5505 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5506 session->project_dirty = false;
5507 }
5508 }
5510 RebaseCleanProjectFileDraft(file_path);
5511
5512 // Add to recent files
5514 manager.AddFile(file_path);
5515 manager.Save();
5516
5517 toast_manager_.Show(absl::StrFormat("Project saved as: %s", file_path),
5519 } else {
5520 // Restore old filepath on failure
5521 current_project_.filepath = old_filepath;
5523 absl::StrFormat("Failed to save project: %s", save_status.message()),
5525 }
5526
5528
5529 return save_status;
5530}
5531
5532absl::StatusOr<std::string> EditorManager::ResolveProjectBuildCommand() const {
5534 return absl::FailedPreconditionError("No project open");
5535 }
5536
5537 std::string command = current_project_.build_script;
5538 if (command.empty() && current_project_.hack_manifest.loaded()) {
5540 }
5541 if (command.empty()) {
5542 return absl::NotFoundError("Project does not define a build command");
5543 }
5544 return command;
5545}
5546
5547absl::StatusOr<std::string> EditorManager::ResolveProjectRunTarget() const {
5549 return absl::FailedPreconditionError("No project open");
5550 }
5551
5552 std::string target;
5555 }
5556 if (target.empty()) {
5558 }
5559 if (target.empty()) {
5560 return absl::NotFoundError("Project does not define a run target ROM");
5561 }
5562 return current_project_.GetAbsolutePath(target);
5563}
5564
5566 const std::string& summary, const std::string& detail,
5567 ProjectWorkflowState state, const std::string& output_tail,
5568 bool can_cancel) const {
5569 return {.visible = true,
5570 .can_cancel = can_cancel,
5571 .label = "Build",
5572 .summary = summary,
5573 .detail = detail,
5574 .output_tail = output_tail,
5575 .state = state};
5576}
5577
5579 const std::string& summary, const std::string& detail,
5580 ProjectWorkflowState state) const {
5581 return {.visible = true,
5582 .label = "Run",
5583 .summary = summary,
5584 .detail = detail,
5585 .state = state};
5586}
5587
5588absl::StatusOr<std::string> EditorManager::RunProjectBuildCommand() {
5589 auto command_or = ResolveProjectBuildCommand();
5590 if (!command_or.ok()) {
5591 return command_or.status();
5592 }
5593
5594 const std::string command = *command_or;
5595 const std::filesystem::path project_root =
5596 std::filesystem::path(current_project_.filepath).parent_path();
5598 auto start_status = task.Start(command, project_root.string());
5599 if (!start_status.ok()) {
5600 return start_status;
5601 }
5602 auto wait_status = task.Wait();
5603 const auto snapshot = task.GetSnapshot();
5604 if (!wait_status.ok()) {
5605 return wait_status;
5606 }
5607 const std::string summary = LastNonEmptyLine(snapshot.output);
5608 return summary.empty() ? command : summary;
5609}
5610
5612 const std::string running_detail =
5613 "Running the configured project build command";
5614 UpdateBuildWorkflowStatus(
5616 MakeBuildStatus("Build running", running_detail,
5617 ProjectWorkflowState::kRunning, "", false));
5618
5619 auto result_or = RunProjectBuildCommand();
5620 if (!result_or.ok()) {
5621 UpdateBuildWorkflowStatus(
5623 MakeBuildStatus("Build failed",
5624 std::string(result_or.status().message()),
5627 absl::StrFormat("Build unavailable: %s", result_or.status().message()),
5629 return result_or.status();
5630 }
5631
5632 const std::string summary = *result_or;
5633 UpdateBuildWorkflowStatus(&status_bar_, project_management_panel_.get(),
5634 MakeBuildStatus("Build succeeded", summary,
5637 summary.empty() ? "Project build completed"
5638 : absl::StrFormat("Project build completed: %s", summary),
5640 LOG_INFO("EditorManager", "Project build completed: %s", summary.c_str());
5641 return absl::OkStatus();
5642}
5643
5646 const auto snapshot = active_project_build_->GetSnapshot();
5647 if (snapshot.running) {
5648 toast_manager_.Show("A project build is already running",
5650 return;
5651 }
5652 }
5653
5654 auto command_or = ResolveProjectBuildCommand();
5655 if (!command_or.ok()) {
5656 UpdateBuildWorkflowStatus(
5658 MakeBuildStatus("Build unavailable",
5659 std::string(command_or.status().message()),
5662 absl::StrFormat("Build unavailable: %s", command_or.status().message()),
5664 return;
5665 }
5666
5667 const std::filesystem::path project_root =
5668 std::filesystem::path(current_project_.filepath).parent_path();
5669 active_project_build_ = std::make_unique<BackgroundCommandTask>();
5670 auto start_status =
5671 active_project_build_->Start(*command_or, project_root.string());
5672 if (!start_status.ok()) {
5673 UpdateBuildWorkflowStatus(
5675 MakeBuildStatus("Build unavailable",
5676 std::string(start_status.message()),
5679 absl::StrFormat("Build unavailable: %s", start_status.message()),
5681 active_project_build_.reset();
5682 return;
5683 }
5684
5686 UpdateBuildWorkflowStatus(
5688 MakeBuildStatus("Build running",
5689 "Running the configured project build command",
5692 project_management_panel_->SetBuildLogOutput("");
5693 }
5695}
5696
5698 if (!active_project_build_) {
5699 return;
5700 }
5701
5702 const auto snapshot = active_project_build_->GetSnapshot();
5703 if (!snapshot.running) {
5704 return;
5705 }
5706
5707 active_project_build_->Cancel();
5708 UpdateBuildWorkflowStatus(
5710 MakeBuildStatus("Cancelling build", "Stopping the active project build",
5711 ProjectWorkflowState::kRunning, snapshot.output_tail,
5712 false));
5713}
5714
5716 UpdateRunWorkflowStatus(
5718 MakeRunStatus("Run queued",
5719 "Preparing the project output for emulator reload",
5721
5722 auto run_target_or = ResolveProjectRunTarget();
5723 if (!run_target_or.ok()) {
5724 UpdateRunWorkflowStatus(
5726 MakeRunStatus("Run unavailable",
5727 std::string(run_target_or.status().message()),
5729 toast_manager_.Show(absl::StrFormat("Run unavailable: %s",
5730 run_target_or.status().message()),
5732 return run_target_or.status();
5733 }
5734 const std::string run_target = *run_target_or;
5735 if (!std::filesystem::exists(run_target)) {
5736 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5737 MakeRunStatus("Run target missing", run_target,
5739 toast_manager_.Show("Run target ROM not found. Build the project first.",
5741 return absl::NotFoundError("Run target ROM not found");
5742 }
5743
5744#ifdef YAZE_WITH_GRPC
5745 if (auto* emulator_backend = Application::Instance().GetEmulatorBackend()) {
5746 auto load_status = emulator_backend->LoadRom(run_target);
5747 if (load_status.ok()) {
5748 AppendWorkflowHistoryEntry(
5749 "Run",
5750 MakeRunStatus("Reloaded in backend", run_target,
5752 "");
5753 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5754 MakeRunStatus("Reloaded in backend", run_target,
5756 if (ui_coordinator_) {
5757 ui_coordinator_->SetEmulatorVisible(true);
5758 }
5760 absl::StrFormat("Reloaded project output in emulator backend: %s",
5761 run_target),
5763 return absl::OkStatus();
5764 }
5765 }
5766#endif
5767
5768 Rom temp_rom;
5769 auto load_rom_status = rom_file_manager_.LoadRom(&temp_rom, run_target);
5770 if (!load_rom_status.ok()) {
5771 UpdateRunWorkflowStatus(
5773 MakeRunStatus("Run load failed", std::string(load_rom_status.message()),
5775 toast_manager_.Show(absl::StrFormat("Failed to load run target ROM: %s",
5776 load_rom_status.message()),
5778 return load_rom_status;
5779 }
5780
5781 auto reload_status = emulator_.ReloadRuntimeRom(temp_rom.vector());
5782 if (!reload_status.ok()) {
5783 UpdateRunWorkflowStatus(
5785 MakeRunStatus("Reload failed", std::string(reload_status.message()),
5787 toast_manager_.Show(absl::StrFormat("Failed to reload emulator runtime: %s",
5788 reload_status.message()),
5790 return reload_status;
5791 }
5792
5793 if (ui_coordinator_) {
5794 ui_coordinator_->SetEmulatorVisible(true);
5795 }
5796 AppendWorkflowHistoryEntry("Run",
5797 MakeRunStatus("Reloaded in emulator", run_target,
5799 "");
5800 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5801 MakeRunStatus("Reloaded in emulator", run_target,
5804 absl::StrFormat("Reloaded project output in emulator: %s", run_target),
5806 return absl::OkStatus();
5807}
5808
5809absl::Status EditorManager::ImportProject(const std::string& project_path) {
5810 // Delegate to ProjectManager for import logic
5812 // Sync local project reference
5816 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5819 }
5820 }
5823 return absl::OkStatus();
5824}
5825
5828 return absl::FailedPreconditionError("No project is currently open");
5829 }
5830
5833 toast_manager_.Show("Project repaired successfully",
5835
5836 return absl::OkStatus();
5837}
5838
5840 if (auto* editor_set = GetCurrentEditorSet()) {
5841 return editor_set->GetOverworldData();
5842 }
5843 return nullptr;
5844}
5845
5847 if (!rom) {
5848 return absl::InvalidArgumentError("Invalid ROM pointer");
5849 }
5850
5851 // We need to find the session that owns this ROM.
5852 // This is inefficient but SetCurrentRom is rare.
5854 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
5855 auto* session =
5856 static_cast<RomSession*>(session_coordinator_->GetSession(i));
5857 if (session && &session->rom == rom) {
5858 session_coordinator_->SwitchToSession(i);
5859 // Update test manager with current ROM for ROM-dependent tests
5863 return absl::OkStatus();
5864 }
5865 }
5866 }
5867 // If ROM wasn't found in existing sessions, treat as new session.
5868 // Copying an external ROM object is avoided; instead, fail.
5869 return absl::NotFoundError("ROM not found in existing sessions");
5870}
5871
5876
5880
5881// IsRomHashMismatch() is now inline in editor_manager.h delegating to
5882// rom_lifecycle_.
5883
5884std::vector<RomFileManager::BackupEntry> EditorManager::GetRomBackups() const {
5886}
5887
5889 if (!session_coordinator_) {
5890 return false;
5891 }
5892 const auto* session = session_coordinator_->GetActiveRomSession();
5893 return session != nullptr && session->backup_restore_pending;
5894}
5895
5896absl::Status EditorManager::RestoreRomBackup(const std::string& backup_path) {
5897 auto* rom = GetCurrentRom();
5898 if (!rom) {
5899 return absl::FailedPreconditionError("No ROM loaded");
5900 }
5902 return absl::FailedPreconditionError(
5903 "Save or discard ROM edits before restoring a backup");
5904 }
5905
5906 const std::string original_filename = rom->filename();
5907 const project::ResourceLabelManager original_resource_labels =
5908 *rom->resource_label();
5909 const auto backups = rom_file_manager_.ListBackups(original_filename);
5910 const bool is_managed_backup =
5911 std::any_of(backups.begin(), backups.end(), [&](const auto& backup) {
5912 return SessionCoordinator::PathsReferToSameBackingFile(backup.path,
5913 backup_path);
5914 });
5915 if (!is_managed_backup) {
5916 return absl::InvalidArgumentError(
5917 "Selected file is not a managed backup for the active ROM");
5918 }
5919
5920 // Load the backup away from the live session. Backups may legitimately have
5921 // a different size when they predate a ROM expansion or shrink. Reusing the
5922 // ROM-replacement path restores the prior ROM bytes and backing identity if
5923 // rebuilding assets fails partway through.
5924 Rom restored_rom;
5925 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&restored_rom, backup_path));
5926
5927 if (!original_filename.empty()) {
5928 restored_rom.set_filename(original_filename);
5929 }
5931 // Normal ROM backups do not copy the active `.labels` sidecar. Preserve the
5932 // session's loaded and in-memory resource labels instead of replacing them
5933 // with the usually-empty `<backup>.labels` lookup from LoadFromFile().
5934 *restored_rom.resource_label() = original_resource_labels;
5935 // Restore is intentionally staged in memory. Mark it dirty so closing or
5936 // autosave cannot silently treat the on-disk source as already restored.
5937 restored_rom.set_dirty(true);
5939 ReplaceActiveSessionRom(std::move(restored_rom), original_filename));
5940 auto* restored_session = session_coordinator_->GetActiveRomSession();
5941 if (!restored_session) {
5942 return absl::InternalError(
5943 "ROM backup restored without an active session to stage it");
5944 }
5945 restored_session->backup_restore_pending = true;
5946 return absl::OkStatus();
5947}
5948
5950 if (!session_coordinator_) {
5951 return absl::FailedPreconditionError("No session coordinator");
5952 }
5953 auto* session = session_coordinator_->GetActiveRomSession();
5954 if (!session || !session->rom.is_loaded()) {
5955 return absl::FailedPreconditionError("No ROM loaded");
5956 }
5957 if (!session->backup_restore_pending) {
5958 return absl::FailedPreconditionError("No restored backup is staged");
5959 }
5960
5961 const size_t session_index = GetCurrentSessionIndex();
5962 if (HasPendingDungeonChangesForSession(session_index) ||
5963 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
5964 return absl::FailedPreconditionError(
5965 "Resolve pending dungeon or palette edits before discarding the "
5966 "restored backup");
5967 }
5968
5969 const std::string backing_path = session->rom.filename();
5970 if (backing_path.empty()) {
5971 return absl::FailedPreconditionError("ROM has no backing file to reload");
5972 }
5973
5974 // Load into scratch storage before touching the live session so file I/O
5975 // failures preserve the staged ROM. Resource labels are session work, not
5976 // part of normal ROM backups, so preserve them across the byte-level discard.
5977 const project::ResourceLabelManager resource_labels =
5978 *session->rom.resource_label();
5979 Rom backing_rom;
5980 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&backing_rom, backing_path));
5981 *backing_rom.resource_label() = resource_labels;
5983 ReplaceActiveSessionRom(std::move(backing_rom), backing_path));
5984
5985 session->backup_restore_pending = false;
5986 session->rom.ClearDirty();
5987 return absl::OkStatus();
5988}
5989
5993
6001
6005
6009
6012 session_coordinator_->CreateNewSession();
6013 // Toast messages are now shown by SessionCoordinator
6014 }
6015}
6016
6019 session_coordinator_->DuplicateCurrentSession();
6020 }
6021}
6022
6024 if (!session_coordinator_) {
6025 return;
6026 }
6027
6028 if (!session_coordinator_->HasMultipleSessions()) {
6029 // Preserve the coordinator's existing warning without offering a
6030 // "Close Without Saving" action that cannot close the final session.
6031 session_coordinator_->CloseCurrentSession();
6032 return;
6033 }
6034
6035 const size_t current_session_id = GetCurrentSessionId();
6038 current_session_id})) {
6039 return;
6040 }
6041
6042 session_coordinator_->CloseCurrentSession();
6044}
6045
6047 if (!session_coordinator_ ||
6048 !session_coordinator_->IsValidSessionIndex(index)) {
6049 return;
6050 }
6051
6052 const size_t session_id = session_coordinator_->GetSessionId(index);
6055 session_id})) {
6056 return;
6057 }
6058
6059 session_coordinator_->RemoveSession(index);
6061}
6062
6064 if (!session_coordinator_) {
6065 return;
6066 }
6067
6068 const size_t current_index = GetCurrentSessionIndex();
6069 if (index == current_index ||
6070 !session_coordinator_->IsValidSessionIndex(index)) {
6071 return;
6072 }
6073
6074 const size_t current_session_id = GetCurrentSessionId();
6075 const size_t target_session_id = session_coordinator_->GetSessionId(index);
6078 current_session_id, target_session_id})) {
6079 return;
6080 }
6081
6082 session_coordinator_->SwitchToSession(index);
6083}
6084
6088 return;
6089 }
6090
6091 quit_ = true;
6092}
6093
6097
6099 size_t session_id) const {
6100 if (!session_coordinator_ || session_id == SIZE_MAX) {
6101 return std::nullopt;
6102 }
6103
6104 for (size_t index = 0; index < session_coordinator_->GetTotalSessionCount();
6105 ++index) {
6106 if (session_coordinator_->GetSessionId(index) == session_id) {
6107 return index;
6108 }
6109 }
6110 return std::nullopt;
6111}
6112
6115 return "";
6116 }
6117
6118 const auto& action = *pending_unsaved_session_action_;
6119 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6120 return absl::StrFormat("%s\n\nQuitting now will discard them.",
6122 }
6123
6124 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6125 const std::string session_name =
6126 source_index.has_value()
6127 ? session_coordinator_->GetSessionDisplayName(*source_index)
6128 : "Closed Session";
6129 const std::string work = source_index.has_value()
6130 ? DescribePendingUnsavedWork(*source_index)
6131 : "unsaved work";
6132
6133 switch (action.type) {
6136 return absl::StrFormat(
6137 "Session '%s' has %s. Opening another ROM or project will not save "
6138 "them automatically.",
6139 session_name, work);
6141 return absl::StrFormat(
6142 "Session '%s' has %s. Opening another project will not save them "
6143 "automatically.",
6144 session_name, work);
6146 return absl::StrFormat(
6147 "Session '%s' has %s. Switching sessions will leave them behind in "
6148 "this session.",
6149 session_name, work);
6151 return absl::StrFormat(
6152 "Session '%s' has %s. Closing it now will discard them.",
6153 session_name, work);
6155 break;
6156 }
6157
6158 return "";
6159}
6160
6163 return "Save Work";
6164 }
6165
6166 switch (pending_unsaved_session_action_->type) {
6169 return "Save Work & Open";
6171 return "Save Work & Open Project";
6173 return "Save Work & Switch";
6175 return "Save Work & Close";
6177 return ModifiedSessionCount() == 1 ? "Save Work & Quit"
6178 : "Save Modified Work & Quit";
6179 }
6180
6181 return "Save Work";
6182}
6183
6186 return "Continue";
6187 }
6188
6189 switch (pending_unsaved_session_action_->type) {
6192 return "Open Without Saving";
6194 return "Open Project Without Saving";
6196 return "Switch Without Saving";
6198 return "Close Without Saving";
6200 return "Quit Without Saving";
6201 }
6202
6203 return "Continue";
6204}
6205
6208 return;
6209 }
6210
6211 const auto action = *pending_unsaved_session_action_;
6213
6214 if (popup_manager_) {
6216 }
6217
6218 const size_t original_session_id = GetCurrentSessionId();
6219 auto save_modified_session = [this](size_t session_id) -> absl::Status {
6220 const auto session_index = ResolveSessionIndexById(session_id);
6221 if (!session_index.has_value()) {
6222 return absl::FailedPreconditionError(
6223 "The ROM session is no longer available");
6224 }
6225 if (!SessionHasPendingUnsavedWork(*session_index)) {
6226 return absl::OkStatus();
6227 }
6228
6229 session_coordinator_->SwitchToSession(*session_index);
6231 if (SessionHasPendingRomWork(*session_index)) {
6233 }
6234
6235 // A successful file write does not necessarily mean every pending editor
6236 // domain participated in the save. For example, dungeon palette saving is
6237 // user-configurable. Never execute a destructive follow-up while work that
6238 // the confirmation dialog promised to save is still pending.
6239 if (SessionHasPendingUnsavedWork(*session_index)) {
6240 return absl::FailedPreconditionError(
6241 absl::StrFormat("Save completed, but session still has %s",
6242 DescribePendingUnsavedWork(*session_index)));
6243 }
6244 return absl::OkStatus();
6245 };
6246
6247 absl::Status save_status = absl::OkStatus();
6248 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6250 std::vector<size_t> session_ids;
6251 session_ids.reserve(session_coordinator_->GetTotalSessionCount());
6252 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount();
6253 ++i) {
6254 session_ids.push_back(session_coordinator_->GetSessionId(i));
6255 }
6256 for (const size_t session_id : session_ids) {
6257 save_status = save_modified_session(session_id);
6258 if (!save_status.ok()) {
6259 break;
6260 }
6261 }
6262 }
6263 } else {
6264 save_status = save_modified_session(action.source_session_id);
6265 }
6266
6267 if (!save_status.ok()) {
6268 const bool resumable_confirmation = absl::IsCancelled(save_status) &&
6271 const auto original_session_index =
6272 ResolveSessionIndexById(original_session_id);
6273 if (!resumable_confirmation && original_session_index.has_value()) {
6274 session_coordinator_->SwitchToSession(*original_session_index);
6275 }
6276
6277 if (absl::IsCancelled(save_status)) {
6278 toast_manager_.Show("Save paused. Finish saving, then retry your action.",
6280 } else {
6282 absl::StrFormat("Failed to save before continuing: %s",
6283 save_status.message()),
6285 }
6286 return;
6287 }
6288
6289 // Saving an inactive close target temporarily activates that session. Put
6290 // the original session back before compacting the target index so the same
6291 // stable session remains active after the close.
6293 original_session_id != action.target_session_id) {
6294 const auto original_session_index =
6295 ResolveSessionIndexById(original_session_id);
6296 if (original_session_index.has_value()) {
6297 session_coordinator_->SwitchToSession(*original_session_index);
6298 }
6299 }
6300
6302}
6303
6316
6323
6327 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6328 const bool has_pending_work =
6331 : source_index.has_value() &&
6332 SessionHasPendingUnsavedWork(*source_index);
6333 if (!has_pending_work) {
6334 return true;
6335 }
6336
6337 pending_unsaved_session_action_ = std::move(action);
6338 if (popup_manager_) {
6340 }
6341 return false;
6342}
6343
6345 const PendingUnsavedSessionAction& action) {
6346 switch (action.type) {
6348 auto status = LoadRomInternal();
6349 if (!status.ok()) {
6351 absl::StrFormat("Failed to load ROM: %s", status.message()),
6353 }
6354 break;
6355 }
6357 auto status = OpenRomOrProjectInternal(action.path);
6358 if (!status.ok()) {
6359 toast_manager_.Show(absl::StrFormat("Failed to open ROM / project: %s",
6360 status.message()),
6362 }
6363 break;
6364 }
6366 auto status = OpenProjectInternal();
6367 if (!status.ok()) {
6369 absl::StrFormat("Failed to open project: %s", status.message()),
6371 }
6372 break;
6373 }
6376 const auto target_index =
6378 if (target_index.has_value()) {
6379 session_coordinator_->SwitchToSession(*target_index);
6380 }
6381 }
6382 break;
6385 const auto target_index =
6387 if (target_index.has_value()) {
6388 session_coordinator_->RemoveSession(*target_index);
6390 }
6391 }
6392 break;
6394 quit_ = true;
6395 break;
6396 }
6397}
6398
6399bool EditorManager::SessionHasPendingUnsavedWork(size_t session_index) const {
6400 if (!session_coordinator_ ||
6401 !session_coordinator_->IsValidSessionIndex(session_index)) {
6402 return false;
6403 }
6404
6405 auto* session =
6406 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6407 return session != nullptr &&
6408 (SessionHasPendingRomWork(session_index) || session->project_dirty ||
6409 (session->project_file_editor_state.initialized &&
6410 session->project_file_editor_state.modified));
6411}
6412
6413bool EditorManager::SessionHasPendingRomWork(size_t session_index) const {
6414 if (!session_coordinator_ ||
6415 !session_coordinator_->IsValidSessionIndex(session_index)) {
6416 return false;
6417 }
6418
6419 auto* session =
6420 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6421 return session != nullptr &&
6422 ((session->rom.is_loaded() && session->rom.dirty()) ||
6423 HasPendingDungeonChangesForSession(session_index) ||
6424 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data));
6425}
6426
6430
6432 size_t session_index) const {
6433 if (!session_coordinator_ ||
6434 !session_coordinator_->IsValidSessionIndex(session_index)) {
6435 return false;
6436 }
6437
6438 auto* session =
6439 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6440 if (!session) {
6441 return false;
6442 }
6443
6444 if (auto* dungeon_editor =
6445 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6446 return dungeon_editor->HasPendingDungeonChanges();
6447 }
6448 return false;
6449}
6450
6452 size_t session_index) const {
6453 if (!session_coordinator_ ||
6454 !session_coordinator_->IsValidSessionIndex(session_index)) {
6455 return 0;
6456 }
6457
6458 auto* session =
6459 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6460 if (!session) {
6461 return 0;
6462 }
6463
6464 if (auto* dungeon_editor =
6465 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6466 return dungeon_editor->PendingRoomCount();
6467 }
6468 return 0;
6469}
6470
6472 size_t session_index) const {
6473 if (!session_coordinator_ ||
6474 !session_coordinator_->IsValidSessionIndex(session_index)) {
6475 return 0;
6476 }
6477
6478 auto* session =
6479 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6480 return session != nullptr ? gfx::PaletteManager::Get().GetModifiedColorCount(
6481 &session->game_data)
6482 : 0;
6483}
6484
6486 if (!session_coordinator_) {
6487 return 0;
6488 }
6489
6490 int count = 0;
6491 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6493 ++count;
6494 }
6495 }
6496 return count;
6497}
6498
6500 size_t session_index) const {
6501 if (!session_coordinator_ ||
6502 !session_coordinator_->IsValidSessionIndex(session_index)) {
6503 return "unsaved work";
6504 }
6505
6506 auto* session =
6507 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6508 const bool rom_dirty =
6509 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
6510 const bool pending_dungeon_changes =
6512 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
6513 const size_t pending_palette_colors =
6515 const bool project_dirty = session != nullptr && session->project_dirty;
6516 const bool project_file_dirty =
6517 session != nullptr && session->project_file_editor_state.initialized &&
6518 session->project_file_editor_state.modified;
6519
6520 std::vector<std::string> work;
6521 if (pending_rooms > 0) {
6522 work.push_back(absl::StrFormat("%d unapplied dungeon room%s", pending_rooms,
6523 pending_rooms == 1 ? "" : "s"));
6524 } else if (pending_dungeon_changes) {
6525 work.emplace_back("unapplied dungeon metadata");
6526 }
6527 if (pending_palette_colors > 0) {
6528 work.push_back(absl::StrFormat("%zu unapplied palette color%s",
6529 pending_palette_colors,
6530 pending_palette_colors == 1 ? "" : "s"));
6531 }
6532 if (rom_dirty) {
6533 work.emplace_back("unsaved ROM-buffer changes");
6534 }
6535 if (project_dirty) {
6536 work.emplace_back("unsaved project settings");
6537 }
6538 if (project_file_dirty) {
6539 work.emplace_back("an unsaved project-file draft");
6540 }
6541 return work.empty() ? "unsaved work" : absl::StrJoin(work, " and ");
6542}
6543
6545 const int modified_sessions = ModifiedSessionCount();
6546 if (modified_sessions <= 0) {
6547 return "No sessions have unsaved work.";
6548 }
6549
6550 if (modified_sessions == 1 && session_coordinator_) {
6551 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6553 return absl::StrFormat("Session '%s' has %s.",
6554 session_coordinator_->GetSessionDisplayName(i),
6556 }
6557 }
6558 }
6559
6560 return absl::StrFormat(
6561 "%d sessions have unsaved ROM, dungeon, palette, or project work.",
6562 modified_sessions);
6563}
6564
6566 return session_coordinator_ ? session_coordinator_->GetActiveSessionIndex()
6567 : 0;
6568}
6569
6571 UiSyncState state;
6572 state.frame_id = ui_sync_frame_id_.load(std::memory_order_relaxed);
6573
6574 int pending_editor =
6575 pending_editor_deferred_actions_.load(std::memory_order_relaxed);
6576 if (pending_editor < 0) {
6577 pending_editor = 0;
6578 }
6579 state.pending_editor_actions = pending_editor;
6580
6581 int pending_layout = layout_coordinator_.PendingDeferredActionCount();
6582 if (pending_layout < 0) {
6583 pending_layout = 0;
6584 }
6585 state.pending_layout_actions = pending_layout;
6587 layout_manager_ ? layout_manager_->IsRebuildRequested() : false;
6588 return state;
6589}
6590
6592 return session_coordinator_ ? session_coordinator_->GetActiveSessionCount()
6593 : 0;
6594}
6595
6597 EditorType type, size_t session_index) const {
6598 const char* base_name = kEditorNames[static_cast<int>(type)];
6599 return session_coordinator_ ? session_coordinator_->GenerateUniqueEditorTitle(
6600 base_name, session_index)
6601 : std::string(base_name);
6602}
6603
6604void EditorManager::SwitchToEditor(EditorType editor_type, bool force_visible,
6605 bool from_dialog) {
6606 // Special case: Agent editor requires EditorManager-specific handling
6607#ifdef YAZE_BUILD_AGENT_UI
6608 if (editor_type == EditorType::kAgent) {
6609 ShowAIAgent();
6610 return;
6611 }
6612#endif
6613
6614 // Fresh launch has no ROM session, so GetCurrentEditorSet() is null and
6615 // EditorActivator::SwitchToEditor would silently no-op (BUG-020) — that was
6616 // why File->Settings, the "no ROM" welcome buttons, and Tools editors did
6617 // nothing. Create an empty session on demand so ROM-less editors (Settings,
6618 // Assembly, ...) have an editor set to live in. Normal ROM loads create their
6619 // own session, so this only fires when the user reaches an editor first.
6620 if (session_coordinator_ && GetCurrentEditorSet() == nullptr) {
6621 session_coordinator_->CreateNewSession();
6622 }
6623
6624 auto status = EnsureEditorAssetsLoaded(editor_type);
6625 if (!status.ok()) {
6627 absl::StrFormat("Failed to prepare %s: %s",
6628 kEditorNames[static_cast<int>(editor_type)],
6629 status.message()),
6631 }
6632
6633 // Delegate all other editor switching to EditorActivator
6634 editor_activator_.SwitchToEditor(editor_type, force_visible, from_dialog);
6635}
6636
6638 if (!ui_coordinator_) {
6639 return;
6640 }
6641 ui_coordinator_->SetEditorSelectionVisible(false);
6642 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
6643}
6644
6646 if (!session)
6647 return;
6649 ConfigureEditorDependencies(&session->editors, &session->rom,
6650 session->editors.session_id());
6651}
6652
6653// SessionScope implementation
6655 size_t session_index)
6656 : manager_(manager),
6657 prev_rom_(manager->GetCurrentRom()),
6658 prev_editor_set_(manager->GetCurrentEditorSet()),
6659 prev_session_index_(manager->GetCurrentSessionIndex()) {
6660 // Set new session context
6661 manager_->session_coordinator_->SwitchToSession(session_index);
6662}
6663
6665 // Restore previous context
6666 manager_->session_coordinator_->SwitchToSession(prev_session_index_);
6667}
6668
6669bool EditorManager::HasDuplicateSession(const std::string& filepath) {
6670 return session_coordinator_ &&
6671 session_coordinator_->HasDuplicateSession(filepath);
6672}
6673
6699 // Menu actions can run before the next Update() frame captures drawer
6700 // edits. Preserve the panel's current dirty bit before rebinding it.
6703 // Update project panel context before showing
6707 project_management_panel_->SetVersionManager(version_manager_);
6709 }
6710 right_drawer_manager_->ToggleDrawer(
6712 }
6713}
6714
6717 auto* session = session_coordinator_
6718 ? session_coordinator_->GetActiveRomSession()
6719 : nullptr;
6720
6721 // Preserve an existing draft for this session. Only load from disk the first
6722 // time this session opens the project-file editor.
6723 if (session && session->project_file_editor_state.initialized &&
6724 !session->project_file_editor_state.modified &&
6725 !current_project_.filepath.empty() &&
6727 session->project_file_editor_state.filepath,
6730 }
6731
6732 if (session && session->project_file_editor_state.initialized) {
6733 project_file_editor_.RestoreState(session->project_file_editor_state,
6735 } else if (!current_project_.filepath.empty()) {
6737 if (!status.ok()) {
6739 absl::StrFormat("Failed to load project file: %s", status.message()),
6741 return;
6742 }
6743 } else {
6745 }
6746 // Set the project pointer for label import functionality
6748 // Activate the editor window
6750 if (session) {
6751 session->project_file_editor_state = project_file_editor_.CaptureState();
6752 }
6753}
6754
6756 size_t session_id) {
6757 if (!editor_set) {
6758 return;
6759 }
6760
6761 EditorDependencies deps;
6762 deps.rom = rom;
6763 deps.session_id = session_id;
6766 deps.popup_manager = popup_manager_.get();
6771 const auto session_index = ResolveSessionIndexById(session_id);
6772 if (session_index.has_value()) {
6773 auto* session = static_cast<RomSession*>(
6774 session_coordinator_->GetSession(*session_index));
6775 if (session && session->project_context.has_value()) {
6776 deps.project = &*session->project_context;
6777 deps.version_manager = session->version_manager.get();
6778 }
6779 }
6780 }
6781 deps.global_context = editor_context_.get();
6782 deps.status_bar = &status_bar_;
6783 deps.renderer = renderer_;
6784 deps.emulator = &emulator_;
6785 deps.custom_data = this;
6786 deps.gfx_group_workspace = editor_set->gfx_group_workspace();
6787
6788 editor_set->ApplyDependencies(deps);
6789
6790 // If configuring the active session, update the properties panel
6791 if (session_id == GetCurrentSessionId()) {
6793 }
6794}
6795
6796} // 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:162
auto filename() const
Definition rom.h:157
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:518
void set_dirty(bool dirty)
Definition rom.h:146
const auto & vector() const
Definition rom.h:155
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool dirty() const
Definition rom.h:145
auto set_filename(std::string_view name)
Definition rom.h:158
bool is_loaded() const
Definition rom.h:144
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
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_
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)
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
absl::Status SaveAllGraphicsData(Rom &rom, const std::array< gfx::Bitmap, kNumGfxSheets > &sheets)
Saves all graphics sheets back to ROM.
Definition game_data.cc:633
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
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:1425
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:1459
WorkspaceSettings workspace_settings
Definition project.h:201
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1486
absl::Status Open(const std::string &project_path)
Definition project.cc:429
absl::Status Validate() const
Definition project.cc:1355
core::FeatureFlags::Flags feature_flags
Definition project.h:200
core::RomAddressOverrides rom_address_overrides
Definition project.h:203