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"
69#include "app/emu/emulator.h"
75#include "app/gui/core/icons.h"
80#include "app/platform/timing.h"
82#include "core/features.h"
83#include "core/project.h"
84#include "core/rom_settings.h"
93#include "rom/rom.h"
94#include "rom/rom_diff.h"
95#include "rom/transaction.h"
96#include "startup_flags.h"
97#include "util/file_util.h"
99#include "util/log.h"
100#include "util/macro.h"
101#include "util/rom_hash.h"
102#include "yaze_config.h"
108#include "zelda3/game_data.h"
114#include "zelda3/sprite/sprite.h"
115
116// Conditional platform headers
117#ifdef __EMSCRIPTEN__
121#endif
122
123// Conditional test headers
124#ifdef YAZE_ENABLE_TESTING
130#endif
131#ifdef YAZE_ENABLE_GTEST
133#endif
134#ifdef YAZE_WITH_GRPC
136#endif
137
138// Conditional agent UI headers
139#ifdef YAZE_BUILD_AGENT_UI
141#endif
142
143namespace yaze::editor {
144
145namespace {
147 public:
151 delete;
152
154 if (!committed_) {
155 for (auto it = editors_.rbegin(); it != editors_.rend(); ++it) {
156 (*it)->RollbackSaveTransaction();
157 }
158 }
159 }
160
161 absl::Status Begin(Editor* editor) {
162 if (editor == nullptr) {
163 return absl::OkStatus();
164 }
166 editors_.push_back(editor);
167 return absl::OkStatus();
168 }
169
170 void Commit() {
171 for (Editor* editor : editors_) {
172 editor->CommitSaveTransaction();
173 }
174 committed_ = true;
175 }
176
177 private:
178 std::vector<Editor*> editors_;
179 bool committed_ = false;
180};
181
183 std::initializer_list<const char*> keys) {
184 for (const char* key : keys) {
185 if (overrides.addresses.find(key) != overrides.addresses.end()) {
186 return true;
187 }
188 }
189 return false;
190}
191
192bool IsTransientPanelVisibilityId(absl::string_view panel_id) {
193 constexpr absl::string_view kRoomPanelPrefix = "dungeon.room_";
194 if (!absl::StartsWith(panel_id, kRoomPanelPrefix) ||
195 panel_id.size() <= kRoomPanelPrefix.size()) {
196 return false;
197 }
198 for (char ch : panel_id.substr(kRoomPanelPrefix.size())) {
199 if (ch < '0' || ch > '9') {
200 return false;
201 }
202 }
203 return true;
204}
205
206std::string LastNonEmptyLine(const std::string& text) {
207 std::vector<std::string> lines = absl::StrSplit(text, '\n');
208 for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
209 std::string line = std::string(*it);
210 absl::StripAsciiWhitespace(&line);
211 if (!line.empty()) {
212 return line;
213 }
214 }
215 return "";
216}
217
219 ProjectManagementPanel* project_panel,
220 const ProjectWorkflowStatus& status) {
222 if (status_bar != nullptr) {
223 status_bar->SetBuildStatus(status);
224 }
225 if (project_panel != nullptr) {
226 project_panel->SetBuildStatus(status);
227 }
228}
229
231 ProjectManagementPanel* project_panel,
232 const ProjectWorkflowStatus& status) {
234 if (status_bar != nullptr) {
235 status_bar->SetRunStatus(status);
236 }
237 if (project_panel != nullptr) {
238 project_panel->SetRunStatus(status);
239 }
240}
241
242void AppendWorkflowHistoryEntry(const std::string& kind,
243 const ProjectWorkflowStatus& status,
244 const std::string& output_log) {
246 {.kind = kind,
247 .status = status,
248 .output_log = output_log,
249 .timestamp = std::chrono::system_clock::now()});
250}
251
253 return !project.custom_objects_folder.empty() ||
254 !project.custom_object_files.empty();
255}
256
258 const project::YazeProject& project) {
259 return {
260 .base_path = project.custom_objects_folder.empty()
261 ? ""
262 : project.GetAbsolutePath(project.custom_objects_folder),
263 .custom_file_map = project.custom_object_files,
264 };
265}
266
267constexpr int kTrackCustomObjectId = 0x31;
268
270 std::string* warning) {
271 if (project == nullptr) {
272 return false;
273 }
274 if (project->custom_objects_folder.empty()) {
275 return false;
276 }
277 if (project->custom_object_files.find(kTrackCustomObjectId) !=
278 project->custom_object_files.end()) {
279 return false;
280 }
281
282 const auto& defaults =
285 if (defaults.empty()) {
286 return false;
287 }
288
289 project->custom_object_files[kTrackCustomObjectId] = defaults;
290 if (warning != nullptr) {
291 *warning = absl::StrFormat(
292 "Project defines custom_objects_folder but is missing "
293 "custom_object_files[0x%02X]. Seeded default mapping (%d entries).",
294 kTrackCustomObjectId, static_cast<int>(defaults.size()));
295 }
296 return true;
297}
298
299std::vector<std::string> ValidateRomAddressOverrides(
300 const core::RomAddressOverrides& overrides, const Rom& rom) {
301 std::vector<std::string> warnings;
302 if (overrides.addresses.empty()) {
303 return warnings;
304 }
305
306 const auto rom_size = rom.size();
307 auto warn = [&](const std::string& message) {
308 warnings.push_back(message);
309 };
310
311 auto check_range = [&](const std::string& label, uint32_t addr, size_t span) {
312 const size_t addr_size = static_cast<size_t>(addr);
313 if (addr_size >= rom_size || addr_size + span > rom_size) {
314 warn(absl::StrFormat("ROM override '%s' out of range: 0x%X (size 0x%X)",
315 label, addr, rom_size));
316 }
317 };
318
319 for (const auto& [key, value] : overrides.addresses) {
320 check_range(key, value, 1);
321 }
322
325 // Defaults match the Oracle expanded message bank ($2F8000-$2FFFFF).
326 // Kept local to avoid pulling message editor constants into core validation.
327 constexpr uint32_t kExpandedMessageStartDefault = 0x178000;
328 constexpr uint32_t kExpandedMessageEndDefault = 0x17FFFF;
329 const uint32_t start =
331 .value_or(kExpandedMessageStartDefault);
332 const uint32_t end =
334 .value_or(kExpandedMessageEndDefault);
335 if (start >= rom_size || end >= rom_size) {
336 warn(absl::StrFormat(
337 "Expanded message range out of ROM bounds: 0x%X-0x%X", start, end));
338 } else if (end < start) {
339 warn(absl::StrFormat("Expanded message range invalid: 0x%X-0x%X", start,
340 end));
341 }
342 }
343
344 if (auto hook =
346 check_range(core::RomAddressKey::kExpandedMusicHook, *hook, 4);
347 if (*hook < rom_size) {
348 auto opcode = rom.ReadByte(*hook);
349 if (opcode.ok() && opcode.value() != 0x22) {
350 warn(absl::StrFormat(
351 "Expanded music hook at 0x%X is not a JSL opcode (0x%02X)", *hook,
352 opcode.value()));
353 }
354 }
355 }
356
357 if (auto main =
360 }
361
362 if (auto aux = overrides.GetAddress(core::RomAddressKey::kExpandedMusicAux)) {
363 check_range(core::RomAddressKey::kExpandedMusicAux, *aux, 4);
364 }
365
366 if (HasAnyOverride(overrides,
371 const uint32_t marker =
374 const uint32_t magic =
377 check_range("overworld_ptr_marker", marker, 1);
378 if (marker < rom_size) {
379 if (rom.data()[marker] != static_cast<uint8_t>(magic)) {
380 warn(absl::StrFormat(
381 "Overworld expanded pointer marker mismatch at 0x%X: expected "
382 "0x%02X, found 0x%02X",
383 marker, static_cast<uint8_t>(magic), rom.data()[marker]));
384 }
385 }
386 }
387
388 if (HasAnyOverride(overrides,
393 const uint32_t flag_addr =
394 overrides
397 check_range("overworld_entrance_flag_expanded", flag_addr, 1);
398 if (flag_addr < rom_size && rom.data()[flag_addr] == 0xB8) {
399 warn(
400 absl::StrFormat("Overworld entrance flag at 0x%X is still 0xB8 "
401 "(vanilla); expanded entrance tables may be ignored",
402 flag_addr));
403 }
404 }
405
406 return warnings;
407}
408
409std::string StripSessionPrefix(absl::string_view panel_id) {
410 if (panel_id.size() > 2 && panel_id[0] == 's' &&
411 absl::ascii_isdigit(panel_id[1])) {
412 const size_t dot = panel_id.find('.');
413 if (dot != absl::string_view::npos) {
414 return std::string(panel_id.substr(dot + 1));
415 }
416 }
417 return std::string(panel_id);
418}
419
421 namespace fs = std::filesystem;
422
423 std::vector<fs::path> roots;
424 if (const char* home = std::getenv("HOME");
425 home != nullptr && std::strlen(home) > 0) {
426 roots.push_back(fs::path(home) / "src" / "hobby" / "oracle-of-secrets");
427 }
428
429 std::vector<fs::path> candidates;
430 std::unordered_set<std::string> seen;
431 auto add_candidate = [&](const fs::path& path) {
432 std::error_code ec;
433 if (!fs::exists(path, ec) || ec) {
434 return;
435 }
436 std::string ext = path.extension().string();
437 absl::AsciiStrToLower(&ext);
438 if (ext != ".yaze" && ext != ".yazeproj") {
439 return;
440 }
441 const std::string normalized = fs::weakly_canonical(path, ec).string();
442 const std::string key = normalized.empty() ? path.string() : normalized;
443 if (key.empty() || seen.count(key) > 0) {
444 return;
445 }
446 seen.insert(key);
447 candidates.push_back(path);
448 };
449
450 for (const auto& root : roots) {
451 std::error_code ec;
452 if (!fs::exists(root, ec) || ec) {
453 continue;
454 }
455
456 // Priority candidates first.
457 add_candidate(root / "Oracle-of-Secrets.yaze");
458 add_candidate(root / "Oracle of Secrets.yaze");
459 add_candidate(root / "Oracle-of-Secrets.yazeproj");
460 add_candidate(root / "Oracle of Secrets.yazeproj");
461 add_candidate(root / "Roms" / "Oracle of Secrets.yaze");
462
463 // Also detect additional local project files nearby.
464 fs::recursive_directory_iterator it(
465 root, fs::directory_options::skip_permission_denied, ec);
466 fs::recursive_directory_iterator end;
467 if (ec) {
468 continue;
469 }
470 for (; it != end; it.increment(ec)) {
471 if (ec) {
472 ec.clear();
473 continue;
474 }
475 if (it.depth() > 2) {
476 it.disable_recursion_pending();
477 continue;
478 }
479 add_candidate(it->path());
480 }
481 }
482
483 if (candidates.empty()) {
484 return;
485 }
486
488 // Add in reverse so the first candidate remains most recent.
489 for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
490 manager.AddFile(it->string());
491 }
492 manager.Save();
493}
494
495} // namespace
496
497std::optional<EditorType> ParseEditorTypeFromString(absl::string_view name) {
498 std::string normalized = std::string(name);
499 absl::StripAsciiWhitespace(&normalized);
500 const std::string lower = absl::AsciiStrToLower(normalized);
501 for (int i = 0; i < static_cast<int>(EditorType::kSettings) + 1; ++i) {
502 const auto type = static_cast<EditorType>(i);
503 const std::string name_candidate =
504 absl::AsciiStrToLower(EditorRegistry::GetEditorName(type));
505 const std::string category_candidate =
506 absl::AsciiStrToLower(EditorRegistry::GetEditorCategory(type));
507 if (name_candidate == lower || category_candidate == lower) {
508 return type;
509 }
510 }
511 return std::nullopt;
512}
513
514// Static registry of editors that use the card-based layout system
515// These editors register their cards with WorkspaceWindowManager and manage their
516// own windows They do NOT need the traditional ImGui::Begin/End wrapper - they
517// create cards internally
521
525
526void EditorManager::ApplyLayoutPreset(const std::string& preset_name) {
528}
529
530bool EditorManager::ApplyLayoutProfile(const std::string& profile_id) {
531 if (!layout_manager_) {
532 return false;
533 }
534
535 const size_t session_id = GetCurrentSessionId();
536 const EditorType current_type =
538
539 LayoutProfile applied_profile;
540 if (!layout_manager_->ApplyBuiltInProfile(profile_id, session_id,
541 current_type, &applied_profile)) {
542 return false;
543 }
544
545 if (applied_profile.open_agent_chat && right_drawer_manager_) {
546 right_drawer_manager_->OpenDrawer(
548 const float default_width = RightDrawerManager::GetDefaultDrawerWidth(
550 right_drawer_manager_->SetDrawerWidth(
552 std::max(default_width, 480.0f));
553 }
554
556 absl::StrFormat("Layout Profile: %s", applied_profile.label),
558 return true;
559}
560
562 if (!layout_manager_) {
563 return;
564 }
565 layout_manager_->CaptureTemporarySessionLayout(GetCurrentSessionId());
566 toast_manager_.Show("Captured temporary layout snapshot", ToastType::kInfo);
567}
568
569void EditorManager::RestoreTemporaryLayoutSnapshot(bool clear_after_restore) {
570 if (!layout_manager_) {
571 return;
572 }
573
574 if (layout_manager_->RestoreTemporarySessionLayout(GetCurrentSessionId(),
575 clear_after_restore)) {
576 toast_manager_.Show("Restored temporary layout snapshot",
578 } else {
579 toast_manager_.Show("No temporary layout snapshot available",
581 }
582}
583
585 if (!layout_manager_) {
586 return;
587 }
588 layout_manager_->ClearTemporarySessionLayout();
589 toast_manager_.Show("Cleared temporary layout snapshot", ToastType::kInfo);
590}
591
592bool EditorManager::SaveLayoutSnapshotAs(const std::string& name) {
593 if (!layout_manager_)
594 return false;
595 if (name.empty()) {
596 toast_manager_.Show("Snapshot name cannot be empty", ToastType::kWarning);
597 return false;
598 }
599 const bool ok =
600 layout_manager_->SaveNamedSnapshot(name, GetCurrentSessionId());
601 if (ok) {
602 toast_manager_.Show(absl::StrFormat("Saved snapshot '%s'", name),
604 } else {
605 toast_manager_.Show(absl::StrFormat("Failed to save snapshot '%s'", name),
607 }
608 return ok;
609}
610
611bool EditorManager::RestoreLayoutSnapshot(const std::string& name,
612 bool remove_after_restore) {
613 if (!layout_manager_)
614 return false;
615 const bool ok = layout_manager_->RestoreNamedSnapshot(
616 name, GetCurrentSessionId(), remove_after_restore);
617 if (ok) {
618 toast_manager_.Show(absl::StrFormat("Restored snapshot '%s'", name),
620 } else {
621 toast_manager_.Show(absl::StrFormat("Snapshot '%s' not available", name),
623 }
624 return ok;
625}
626
627bool EditorManager::DeleteLayoutSnapshot(const std::string& name) {
628 if (!layout_manager_)
629 return false;
630 const bool ok = layout_manager_->DeleteNamedSnapshot(name);
631 if (ok) {
632 toast_manager_.Show(absl::StrFormat("Deleted snapshot '%s'", name),
634 }
635 return ok;
636}
637
638std::vector<std::string> EditorManager::ListLayoutSnapshots() const {
639 if (!layout_manager_)
640 return {};
641 return layout_manager_->ListNamedSnapshots(GetCurrentSessionId());
642}
643
645 if (!layout_manager_) {
646 return;
647 }
648
650 layout_manager_->SetProjectLayoutKey(
652 } else {
653 layout_manager_->UseGlobalLayouts();
654 }
655}
656
665
666#ifdef YAZE_BUILD_AGENT_UI
667void EditorManager::ShowAIAgent() {
670 "AI Agent is experimental — enable Experimental Editors in Settings",
672 return;
673 }
674 // Apply saved agent settings from the current project when opening the Agent
675 // UI to respect the user's preferred provider/model.
676 // TODO: Implement LoadAgentSettingsFromProject in AgentChat or AgentEditor
680 for (const auto& window_id :
681 LayoutPresets::GetDefaultWindows(EditorType::kAgent)) {
682 window_manager_.OpenWindow(window_id);
683 }
684}
685
686void EditorManager::ShowChatHistory() {
688}
689#endif
690
692 : project_manager_(&toast_manager_), rom_file_manager_(&toast_manager_) {
693 std::stringstream ss;
694 ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "."
695 << YAZE_VERSION_PATCH;
696 ss >> version_;
697
698 // Initialize Core Context
699 editor_context_ = std::make_unique<GlobalEditorContext>(event_bus_);
702
706
707 // STEP 5: ShortcutConfigurator created later in Initialize() method
708 // It depends on all above coordinators being available
709}
710
713
714 // STEP 1: Initialize PopupManager FIRST
715 popup_manager_ = std::make_unique<PopupManager>(this);
716 popup_manager_->Initialize(); // Registers all popups with PopupID constants
717
718 // STEP 2: Initialize SessionCoordinator (independent of popups)
719 session_coordinator_ = std::make_unique<SessionCoordinator>(
721 session_coordinator_->SetEditorRegistry(&editor_registry_);
722
723 // STEP 3: Initialize MenuOrchestrator (depends on popup_manager_,
724 // session_coordinator_)
725 menu_orchestrator_ = std::make_unique<MenuOrchestrator>(
728
729 // Wire up the window manager for the View menu window listing
730 menu_orchestrator_->SetWindowManager(&window_manager_);
731 menu_orchestrator_->SetStatusBar(&status_bar_);
732 menu_orchestrator_->SetUserSettings(&user_settings_);
733
734 session_coordinator_->SetEditorManager(this);
735 session_coordinator_->SetEventBus(&event_bus_); // Enable event publishing
737 &event_bus_); // Global event bus access
738
739 // Expose UserSettings to cross-editor panels that don't get constructor
740 // injection (e.g. Layout Designer, which reads named_layouts). The
741 // LayoutManager setter happens below once the unique_ptr is constructed.
743
744 // STEP 3.5: Initialize RomLifecycleManager (depends on popup_manager_,
745 // session_coordinator_, rom_file_manager_, toast_manager_)
747 .rom_file_manager = &rom_file_manager_,
748 .session_coordinator = session_coordinator_.get(),
751 .project = &current_project_,
752 });
753
754 // STEP 4: Initialize UICoordinator (depends on popup_manager_,
755 // session_coordinator_, window_manager_)
756 ui_coordinator_ = std::make_unique<UICoordinator>(
760
761 // STEP 4.5: Initialize LayoutManager (DockBuilder layouts for editors)
762 layout_manager_ = std::make_unique<LayoutManager>();
763 layout_manager_->SetWindowManager(&window_manager_);
764 layout_manager_->UseGlobalLayouts();
765 // Expose to cross-editor panels that drive the live dockspace (Layout
766 // Designer). Must land after the unique_ptr is constructed.
770 [this](const std::string& preset_name) {
771 ApplyLayoutPreset(preset_name);
772 });
774 [this](const std::string& preset_name) {
775 ApplyLayoutPreset(preset_name);
776 });
777
778 // STEP 4.6: Initialize RightDrawerManager (right-side sliding drawers)
779 right_drawer_manager_ = std::make_unique<RightDrawerManager>();
780 right_drawer_manager_->SetToastManager(&toast_manager_);
781 right_drawer_manager_->SetProposalDrawer(&proposal_drawer_);
783 right_drawer_manager_->SetShortcutManager(&shortcut_manager_);
785 [this](const std::string& prompt) {
786#if defined(YAZE_BUILD_AGENT_UI)
787 auto* agent_editor = agent_ui_.GetAgentEditor();
788 if (!agent_editor) {
789 return;
790 }
791 auto* chat = agent_editor->GetAgentChat();
792 if (!chat) {
793 return;
794 }
795 chat->set_active(true);
796 chat->SendMessage(prompt);
797#else
798 (void)prompt;
799#endif
800 },
801 [this]() {
802#if defined(YAZE_BUILD_AGENT_UI)
804 right_drawer_manager_->OpenDrawer(
806 }
807#endif
808 });
811 right_drawer_manager_->ToggleDrawer(
813 } else {
815 }
816 });
817
818 // Initialize ProjectManagementPanel for project/version management
819 project_management_panel_ = std::make_unique<ProjectManagementPanel>();
820 project_management_panel_->SetToastManager(&toast_manager_);
822 [this](const std::string& filepath, const std::string& contents) {
823 return PrepareRawProjectFileSave(filepath, contents);
824 });
826 [this](const std::string& filepath, const std::string& contents) {
827 return CommitRawProjectFileSave(filepath, contents);
828 });
830 std::make_unique<workflow::ProjectWorkflowOutputPanel>());
831 project_management_panel_->SetSwapRomCallback([this]() {
832 // Prompt user to select a new ROM for the project
835 if (!rom_path.empty()) {
836 auto status = SwapProjectRom(rom_path);
837 if (status.ok()) {
838 toast_manager_.Show("Project ROM updated and reloaded.",
840 } else {
841 toast_manager_.Show(absl::StrFormat("Failed to update project ROM: %s",
842 status.message()),
844 }
845 }
846 });
847 project_management_panel_->SetReloadRomCallback([this]() {
850 auto status = ReloadProjectRom();
851 if (!status.ok()) {
853 absl::StrFormat("Failed to reload ROM: %s", status.message()),
855 }
856 }
857 });
858 project_management_panel_->SetSaveProjectCallback([this]() {
859 auto status = SaveProject();
860 if (status.ok()) {
861 toast_manager_.Show("Project saved", ToastType::kSuccess);
862 } else {
864 absl::StrFormat("Failed to save project: %s", status.message()),
866 }
867 return status;
868 });
869 project_management_panel_->SetBrowseFolderCallback(
870 [this](const std::string& type) {
872 if (!folder_path.empty()) {
873 if (type == "code") {
874 current_project_.code_folder = folder_path;
875 // Update assembly editor path
876 if (auto* editor_set = GetCurrentEditorSet()) {
877// iOS: avoid blocking folder enumeration on the UI thread.
878#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
879 editor_set->OpenAssemblyFolder(folder_path);
880#endif
881 window_manager_.SetFileBrowserPath("Assembly", folder_path);
882 }
883 } else if (type == "assets") {
884 current_project_.assets_folder = folder_path;
885 }
889 }
890 toast_manager_.Show(absl::StrFormat("%s folder set: %s", type.c_str(),
891 folder_path.c_str()),
893 }
894 });
895 project_management_panel_->SetBuildProjectCallback(
896 [this]() { QueueBuildCurrentProject(); });
897 project_management_panel_->SetCancelBuildCallback(
898 [this]() { CancelQueuedProjectBuild(); });
899 project_management_panel_->SetRunProjectCallback(
900 [this]() { (void)RunCurrentProject(); });
902 [this]() { QueueBuildCurrentProject(); });
904 [this]() { (void)RunCurrentProject(); });
906 window_manager_.OpenWindow("workflow.output");
907 window_manager_.MarkWindowRecentlyUsed("workflow.output");
908 });
910 [this]() { CancelQueuedProjectBuild(); });
911 right_drawer_manager_->SetProjectManagementPanel(
913
914 // STEP 4.6.1: Initialize LayoutCoordinator (facade for layout operations)
916 layout_deps.layout_manager = layout_manager_.get();
917 layout_deps.window_manager = &window_manager_;
918 layout_deps.ui_coordinator = ui_coordinator_.get();
919 layout_deps.toast_manager = &toast_manager_;
920 layout_deps.status_bar = &status_bar_;
922 layout_coordinator_.Initialize(layout_deps);
923
924 // STEP 4.6.2: Initialize EditorActivator (editor switching and jump navigation)
925 EditorActivator::Dependencies activator_deps;
926 activator_deps.window_manager = &window_manager_;
927 activator_deps.layout_manager = layout_manager_.get();
928 activator_deps.ui_coordinator = ui_coordinator_.get();
929 activator_deps.right_drawer_manager = right_drawer_manager_.get();
930 activator_deps.toast_manager = &toast_manager_;
931 activator_deps.event_bus = &event_bus_;
932 activator_deps.ensure_editor_assets_loaded = [this](EditorType type) {
933 return EnsureEditorAssetsLoaded(type);
934 };
935 activator_deps.get_current_editor_set = [this]() {
936 return GetCurrentEditorSet();
937 };
938 activator_deps.get_current_session_id = [this]() {
939 return GetCurrentSessionId();
940 };
941 activator_deps.queue_deferred_action = [this](std::function<void()> action) {
942 QueueDeferredAction(std::move(action));
943 };
944 editor_activator_.Initialize(activator_deps);
945
946 // STEP 4.7: Initialize ActivityBar
947 activity_bar_ = std::make_unique<ActivityBar>(
949 [this]() -> bool {
950 if (auto* editor_set = GetCurrentEditorSet()) {
951 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
953 return dungeon_editor->IsWorkbenchWorkflowEnabled();
954 }
955 }
956 return false;
957 },
958 [this](bool enabled) {
959 if (auto* editor_set = GetCurrentEditorSet()) {
960 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
962 dungeon_editor->QueueWorkbenchWorkflowMode(enabled);
963 }
964 }
965 });
966
967 // Wire per-user sidebar prefs so right-click / drag mutate persisted state.
968 activity_bar_->SetUserSettings(&user_settings_);
969
970 // Populate the MoreActions registry with the default rail entries. External
971 // callers can Register/Unregister further entries at runtime.
972 auto& registry = activity_bar_->actions_registry();
973 registry.Register({"command_palette",
974 "Command Palette",
977 {}});
978 registry.Register({"keyboard_shortcuts",
979 "Keyboard Shortcuts",
982 {}});
983 registry.Register({"open_rom_project",
984 "Open ROM / Project",
986 [this]() { window_manager_.TriggerOpenRom(); },
987 {}});
988 registry.Register({"settings",
989 "Settings",
991 [this]() { window_manager_.TriggerShowSettings(); },
992 {}});
993
994 // WindowHost is the declarative window registration surface used by
995 // editor/runtime systems.
996 window_host_ = std::make_unique<WindowHost>(&window_manager_);
997
998 // Wire up EventBus to WorkspaceWindowManager for action event publishing
1000}
1001
1003 // Auto-register panels from ContentRegistry (Unified Panel System)
1004 auto registry_panels = ContentRegistry::Panels::CreateAll();
1005 for (auto& panel : registry_panels) {
1007 }
1008
1009 // STEP 4.8: Initialize DashboardPanel
1010 dashboard_panel_ = std::make_unique<DashboardPanel>(this);
1011
1012 if (window_host_) {
1013 WindowDefinition dashboard_definition;
1014 dashboard_definition.id = "dashboard.main";
1015 dashboard_definition.display_name = "Dashboard";
1016 dashboard_definition.icon = ICON_MD_DASHBOARD;
1017 dashboard_definition.category = "Dashboard";
1018 dashboard_definition.window_title = " Dashboard";
1019 dashboard_definition.shortcut_hint = "Ctrl+E";
1020 dashboard_definition.priority = 0;
1021 dashboard_definition.visibility_flag = dashboard_panel_->visibility_flag();
1022 window_host_->RegisterWindow(dashboard_definition);
1023 } else {
1025 {.card_id = "dashboard.main",
1026 .display_name = "Dashboard",
1027 .window_title = " Dashboard",
1028 .icon = ICON_MD_DASHBOARD,
1029 .category = "Dashboard",
1030 .shortcut_hint = "Ctrl+E",
1031 .visibility_flag = dashboard_panel_->visibility_flag(),
1032 .priority = 0});
1033 }
1034}
1035
1037 // Subscribe to session lifecycle events via EventBus
1038 // (replaces SessionObserver pattern)
1040 [this](const SessionSwitchedEvent& e) {
1041 HandleSessionSwitched(e.new_index, e.session, e.transient);
1042 });
1043
1045 [this](const SessionCreatedEvent& e) {
1046 HandleSessionCreated(e.index, e.session);
1047 });
1048
1050 [this](const SessionClosedEvent& e) { HandleSessionClosed(e.index); });
1051
1053 HandleSessionRomLoaded(e.session_id, e.rom);
1054 });
1055
1056 // Subscribe to FrameGuiBeginEvent for ImGui-safe deferred action processing
1057 // This replaces scattered manual processing calls with event-driven execution
1059 ui_sync_frame_id_.fetch_add(1, std::memory_order_relaxed);
1060
1061 // Process LayoutCoordinator's deferred actions
1063
1064 // Process EditorManager's deferred actions
1065 if (!deferred_actions_.empty()) {
1066 std::vector<std::function<void()>> actions_to_execute;
1067 actions_to_execute.swap(deferred_actions_);
1068 const int processed_count = static_cast<int>(actions_to_execute.size());
1069 for (auto& action : actions_to_execute) {
1070 action();
1071 }
1072 if (processed_count > 0) {
1073 const int remaining = pending_editor_deferred_actions_.fetch_sub(
1074 processed_count, std::memory_order_relaxed) -
1075 processed_count;
1076 if (remaining < 0) {
1077 pending_editor_deferred_actions_.store(0, std::memory_order_relaxed);
1078 }
1079 }
1080 }
1081 });
1082
1083 // Subscribe to UIActionRequestEvent for activity bar actions
1084 // This replaces direct callbacks from WorkspaceWindowManager
1086 [this](const UIActionRequestEvent& e) {
1087 HandleUIActionRequest(e.action);
1088 });
1089
1091 [this](const PanelVisibilityChangedEvent& e) {
1092 if (e.category.empty() ||
1094 return;
1095 }
1096 if (IsTransientPanelVisibilityId(e.base_panel_id)) {
1097 return;
1098 }
1099 if (e.visible && layout_manager_) {
1100 layout_manager_->DockPresetPositionOnPanelOpen(e.session_id,
1101 e.base_panel_id);
1102 }
1103 auto& prefs = user_settings_.prefs();
1104 prefs.panel_visibility_state[e.category][e.base_panel_id] = e.visible;
1105 settings_dirty_ = true;
1107 });
1108}
1109
1111 // ContentRegistry::Context holds non-owning pointers into our members
1112 // (event_bus_, editor_context_, user_settings_, layout_manager_, callbacks).
1113 // Clear them before our members destruct so widgets that read the singleton
1114 // (e.g. Canvas::set_global_scale via Context::event_bus()) don't see
1115 // dangling pointers in tests that instantiate multiple EditorManagers.
1116 // Clear() resets fallback state but intentionally keeps global_context for
1117 // session-close semantics, so null it explicitly here.
1120
1121 // GetResourceLabels() is a process-wide singleton. RefreshResourceLabelProvider
1122 // hands it a pointer to current_project_.hack_manifest, which dangles after
1123 // we destruct. Clear before our members go out of scope.
1126
1127 // ThemeManager is a singleton that outlives us. Clear the theme-changed
1128 // callback so it stops calling back into a destroyed EditorManager via the
1129 // captured `this` pointer. Matters for unit tests that construct and destroy
1130 // multiple EditorManager instances in the same process.
1133
1134 // EventBus subscriptions are automatically cleaned up when event_bus_ is
1135 // destroyed (owned by this class). No manual unsubscription needed.
1136}
1137
1138// ============================================================================
1139// Session Event Handlers (EventBus subscribers)
1140// ============================================================================
1141
1143 auto& provider = zelda3::GetResourceLabels();
1146
1147 auto merge_labels =
1150 for (const auto& [type, labels] : src) {
1151 auto& out = (*dst)[type];
1152 for (const auto& [key, value] : labels) {
1153 out[key] = value;
1154 }
1155 }
1156 };
1157
1158 merged_labels.clear();
1159 std::vector<std::string> source_parts;
1160
1161 // 1) ROM-local labels as baseline for active session.
1162 if (auto* rom = GetCurrentRom();
1163 rom && rom->resource_label() && !rom->resource_label()->labels_.empty()) {
1164 merge_labels(&merged_labels, rom->resource_label()->labels_);
1165 source_parts.push_back("rom");
1166 }
1167
1168 // 2) Project registry labels from the active hack manifest.
1172 merge_labels(
1173 &merged_labels,
1175 source_parts.push_back("registry");
1176 }
1177
1178 // 3) Explicit project labels override all other sources.
1181 merge_labels(&merged_labels, current_project_.resource_labels);
1182 source_parts.push_back("project");
1183 }
1184
1185 auto* label_source = merged_labels.empty() ? &empty_labels : &merged_labels;
1186 std::string source =
1187 source_parts.empty() ? "empty" : absl::StrJoin(source_parts, "+");
1188
1189 provider.SetProjectLabels(label_source);
1190 provider.SetHackManifest(current_project_.project_opened() &&
1193 : nullptr);
1194
1195 const bool prefer_hmagic =
1199 provider.SetPreferHMagicNames(prefer_hmagic);
1200
1201 LOG_DEBUG("EditorManager",
1202 "ResourceLabelProvider refreshed (source=%s, project_open=%s)",
1203 source.c_str(),
1204 current_project_.project_opened() ? "true" : "false");
1205}
1206
1209 return;
1210 }
1211
1212 const auto index =
1214 if (!index.has_value()) {
1216 return;
1217 }
1218
1219 auto* session =
1220 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1221 if (!session) {
1223 return;
1224 }
1225
1227 if (session->project_context.has_value()) {
1228 session->project_context->feature_flags = session->feature_flags;
1229 }
1231 current_project_.feature_flags = session->feature_flags;
1232 }
1233}
1234
1236 if (!active_project_context_session_id_.has_value() ||
1238 return;
1239 }
1240
1243 const auto index =
1245 if (!index.has_value()) {
1247 return;
1248 }
1249
1250 auto* session =
1251 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1252 if (!session) {
1254 return;
1255 }
1256
1257 // The singleton can temporarily belong to another session while all editor
1258 // windows are ticked. Only use that value when it still belongs to this
1259 // project owner; otherwise its session-owned copy is authoritative.
1262 }
1263 // These preferences are exposed through global runtime settings, but they
1264 // are project workspace state. Capture them before leaving the session so a
1265 // later SaveProject cannot copy another session's values into this project.
1274 current_project_.feature_flags = session->feature_flags;
1276}
1277
1279 if (!active_project_context_session_id_.has_value() ||
1281 return;
1282 }
1283
1284 const auto index =
1286 if (!index.has_value()) {
1287 return;
1288 }
1289 auto* session =
1290 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1291 if (!session) {
1292 return;
1293 }
1294
1296 session->project_dirty = project_management_panel_->IsProjectDirty();
1297 }
1298 session->project_file_editor_state = project_file_editor_.CaptureState();
1299}
1300
1302 if (!session_coordinator_) {
1303 return absl::OkStatus();
1304 }
1305 auto* session = session_coordinator_->GetActiveRomSession();
1306 if (session == nullptr || !session->editors.HasPendingProjectDraftChanges()) {
1307 return absl::OkStatus();
1308 }
1309 if (!IsCurrentProjectContextOwnedBySession(session->session_id())) {
1310 return absl::FailedPreconditionError(
1311 "Project editor drafts require their project context to be active");
1312 }
1313 return session->editors.PrepareProjectSave();
1314}
1315
1325
1327 std::optional<size_t> previous_session_id) {
1330 if (previous_session_id.has_value() && session_coordinator_) {
1331 const auto index = ResolveSessionIndexById(*previous_session_id);
1332 if (index.has_value()) {
1333 auto* previous_session =
1334 static_cast<RomSession*>(session_coordinator_->GetSession(*index));
1335 if (previous_session) {
1336 if (session_coordinator_->GetActiveSessionIndex() != *index) {
1337 session_coordinator_->SwitchToSession(*index);
1338 } else {
1339 RestoreProjectContextForSession(previous_session);
1340 }
1341 return;
1342 }
1343 }
1344 }
1345
1349 version_manager_ = nullptr;
1352}
1353
1355 size_t previous_session_count) {
1356 if (!session_coordinator_ ||
1357 session_coordinator_->GetTotalSessionCount() <= previous_session_count) {
1358 return absl::OkStatus();
1359 }
1360 auto* provisional_session = session_coordinator_->GetActiveRomSession();
1361 if (!provisional_session) {
1362 return absl::InternalError(
1363 "A provisional session was created but is not active");
1364 }
1365 return session_coordinator_->DiscardProvisionalSession(
1366 provisional_session->session_id());
1367}
1368
1377
1379 const std::string& filepath, const std::string& contents) {
1381 const bool targets_current = !current_project_.filepath.empty() &&
1383 filepath, current_project_.filepath);
1384 if (!targets_current) {
1385 // Save As to a separate descriptor is the deterministic escape hatch when
1386 // both the structured panel and raw document have edits.
1387 return absl::OkStatus();
1388 }
1389 if (IsCurrentProjectDirty()) {
1390 return absl::FailedPreconditionError(
1391 "Project settings also have unsaved changes; use raw Save As to "
1392 "preserve this draft, then save the project settings");
1393 }
1394
1395 auto* session = session_coordinator_
1396 ? session_coordinator_->GetActiveRomSession()
1397 : nullptr;
1398 if (!session || !session->rom.is_loaded()) {
1399 return absl::FailedPreconditionError(
1400 "No active ROM session for this project file");
1401 }
1402
1403 project::YazeProject parsed_project;
1404 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1405 if (!parsed_project.project_opened()) {
1406 return absl::InvalidArgumentError(
1407 "Raw project file must contain a non-empty project name");
1408 }
1409 if (parsed_project.rom_filename.empty() ||
1411 parsed_project.rom_filename, session->filepath) ||
1413 parsed_project.rom_filename, session->rom.filename()))) {
1414 return absl::FailedPreconditionError(
1415 "Raw project file cannot change the ROM backing file of a loaded "
1416 "session; use Project Management > Swap ROM");
1417 }
1418 return session_coordinator_->CheckBackingFileAvailable(
1419 parsed_project.rom_filename, session->session_id());
1420}
1421
1423 const std::string& filepath, const std::string& contents) {
1424 if (current_project_.filepath.empty() ||
1426 filepath, current_project_.filepath)) {
1427 return absl::OkStatus();
1428 }
1429
1430 auto* session = session_coordinator_
1431 ? session_coordinator_->GetActiveRomSession()
1432 : nullptr;
1433 if (!session) {
1434 return absl::FailedPreconditionError("No active ROM session");
1435 }
1436
1437 project::YazeProject parsed_project;
1438 RETURN_IF_ERROR(parsed_project.LoadFromString(contents, filepath));
1439 current_project_ = std::move(parsed_project);
1441 session->project_dirty = false;
1442 active_project_context_session_id_ = session->session_id();
1443 runtime_feature_flags_session_id_ = session->session_id();
1444 version_manager_ = session->version_manager.get();
1447 project_management_panel_->SetProject(&current_project_, false);
1448 }
1449 return absl::OkStatus();
1450}
1451
1452void EditorManager::RebaseCleanProjectFileDraft(const std::string& filepath) {
1453 if (!session_coordinator_ || filepath.empty()) {
1454 return;
1455 }
1456 auto* session = session_coordinator_->GetActiveRomSession();
1457 if (!session || !session->project_file_editor_state.initialized ||
1458 session->project_file_editor_state.modified) {
1459 return;
1460 }
1461
1462 const bool was_active = project_file_editor_.is_active();
1463 auto status = project_file_editor_.LoadFile(filepath);
1464 if (!status.ok()) {
1465 LOG_WARN("EditorManager", "Failed to rebase raw project editor: %s",
1466 status.message());
1467 return;
1468 }
1470 project_file_editor_.set_active(was_active);
1471 session->project_file_editor_state = project_file_editor_.CaptureState();
1472}
1473
1475 RomSession* session, const project::YazeProject& project) {
1476 if (!session) {
1477 return;
1478 }
1479 const bool needs_version_manager =
1480 !session->project_context.has_value() || !session->version_manager;
1481 if (session->project_context.has_value()) {
1482 *session->project_context = project;
1483 } else {
1484 session->project_context.emplace(project);
1485 }
1486 if (needs_version_manager) {
1487 session->version_manager =
1488 std::make_unique<core::VersionManager>(&*session->project_context);
1489 }
1490 session->feature_flags = project.feature_flags;
1491 // Configure after the stable context and VersionManager exist. EditorSet
1492 // instances are created before SessionCreatedEvent, so their first
1493 // dependency pass can legitimately have no project yet.
1494 ConfigureSession(session);
1495}
1496
1500
1509 if (ImGui::GetCurrentContext() != nullptr) {
1510 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
1511 }
1512
1515 auto& custom_object_manager = zelda3::CustomObjectManager::Get();
1516 const auto custom_object_state =
1517 BuildCustomObjectRuntimeState(current_project_);
1518 if (session_coordinator_ != nullptr &&
1519 session_coordinator_->GetActiveRomSession() != nullptr) {
1520 custom_object_manager.ActivateRuntimeContext(
1521 session_coordinator_->GetActiveRomSession()->session_id(),
1522 custom_object_state);
1523 } else {
1524 custom_object_manager.RestoreState(custom_object_state);
1525 }
1526
1534 } else {
1536 }
1537
1543 const auto* session = session_coordinator_
1544 ? session_coordinator_->GetActiveRomSession()
1545 : nullptr;
1546 project_management_panel_->SetProject(
1547 &current_project_, session != nullptr && session->project_dirty);
1548 project_management_panel_->SetVersionManager(version_manager_);
1550 }
1551}
1552
1554 if (!session || !session->project_context.has_value()) {
1558 version_manager_ = nullptr;
1560 project_management_panel_->SetVersionManager(nullptr);
1561 }
1562 return false;
1563 }
1564
1565 if (!session->version_manager) {
1566 session->version_manager =
1567 std::make_unique<core::VersionManager>(&*session->project_context);
1568 ConfigureSession(session);
1569 }
1571 version_manager_ = session->version_manager.get();
1577 return true;
1578}
1579
1581 if (!session) {
1583 project_management_panel_->SetProject(nullptr);
1584 }
1586 return;
1587 }
1588
1591 session->project_dirty);
1592 }
1596 } else {
1598 }
1599}
1600
1601absl::StatusOr<const project::YazeProject*>
1603 if (!session_coordinator_) {
1604 return absl::FailedPreconditionError("No session coordinator");
1605 }
1606 auto* session = session_coordinator_->GetActiveRomSession();
1607 if (!session || !session->rom.is_loaded()) {
1608 return absl::FailedPreconditionError("No active ROM session");
1609 }
1610
1612 if (!active_project_context_session_id_.has_value() ||
1613 *active_project_context_session_id_ != session->session_id()) {
1614 return absl::FailedPreconditionError(
1615 "Active ROM session project context is not restored");
1616 }
1618 if (!session->project_context.has_value()) {
1619 return absl::FailedPreconditionError(
1620 "Active ROM session has no project/save context");
1621 }
1622 return &*session->project_context;
1623}
1624
1625void EditorManager::HandleSessionSwitched(size_t new_index, RomSession* session,
1626 bool transient) {
1627 // Confirmation consent and Save As targets belong to the ROM that started
1628 // the request. Never carry them across a user session switch. Frame-level
1629 // context switches are temporary and must not consume the active request.
1630 if (!transient) {
1635 CancelPendingRomSave(/*hide_popups=*/true);
1638 if (!RestoreProjectContextForSession(session) && session != nullptr) {
1639 LOG_ERROR("EditorManager",
1640 "Session %zu has no project context; saves are disabled",
1641 new_index);
1642 }
1643 } else {
1644 // Frame iteration must not copy full projects or reset project-wide caches.
1645 // Feature flags are cheap and directly affect per-editor draw/save paths.
1647 if (session) {
1650 const auto custom_object_state =
1651 session->project_context.has_value()
1652 ? BuildCustomObjectRuntimeState(*session->project_context)
1655 session->session_id(), custom_object_state);
1656 } else {
1659 }
1661 }
1662
1663 // Palette edit history and dirty tracking are session-owned. Select the
1664 // matching state before any session-specific editor or save action runs.
1666 : nullptr);
1667
1668 // Update RightDrawerManager with the new session's settings editor
1670 right_drawer_manager_->SetSettingsPanel(
1671 session ? session->editors.GetSettingsPanel() : nullptr);
1672 }
1673
1674 // Update properties panel with new ROM
1675 if (session) {
1676 if (!transient) {
1678 }
1680 } else {
1683 }
1684
1685 // Update ContentRegistry context with current session's ROM and GameData
1686 ContentRegistry::Context::SetRom(session ? &session->rom : nullptr);
1688 : nullptr);
1689
1690 // current_editor_ is also session-owned. Resolve it from the newly active
1691 // EditorSet, or clear it when the current category has no editor backing.
1692 const std::string active_category = window_manager_.GetActiveCategory();
1693 Editor* active_editor = ResolveEditorForCategory(active_category);
1694 SetCurrentEditor(active_editor);
1696 active_editor);
1697
1698 // Keep room/sprite labels in sync with active session context.
1700
1701 const std::string category = window_manager_.GetActiveCategory();
1702 if (session != nullptr && !category.empty() &&
1704 auto it = user_settings_.prefs().panel_visibility_state.find(category);
1705 if (it != user_settings_.prefs().panel_visibility_state.end()) {
1706 const size_t session_id = session ? session->session_id() : new_index;
1707 window_manager_.RestoreVisibilityState(session_id, it->second);
1708 }
1709 }
1710
1711 // A user-facing ROM-session switch does not reactivate the editor through
1712 // EditorActivator::SwitchToEditor. Refresh an existing layout context so
1713 // optional panels opened afterward dock against this session's live tree.
1714 // First-time layout construction must remain on the normal activation path,
1715 // after EnsureEditorAssetsLoaded has initialized and registered the panels.
1716 // Transient switches are frame-iteration implementation details and must not
1717 // replace the user-facing session context.
1718 const EditorType active_editor_type =
1719 active_editor ? active_editor->type() : EditorType::kUnknown;
1720 const size_t active_editor_index = EditorTypeIndex(active_editor_type);
1721 const bool active_session_editor_initialized =
1722 session != nullptr &&
1723 active_editor_index < session->editor_initialized.size() &&
1724 session->editor_initialized[active_editor_index];
1725 if (!transient && active_editor != nullptr &&
1726 EditorRegistry::IsPanelBasedEditor(active_editor_type) &&
1727 active_session_editor_initialized &&
1728 layout_manager_->IsLayoutInitialized(active_editor_type)) {
1729 editor_activator_.InitializeEditorLayout(active_editor_type);
1730 }
1731
1732#ifdef YAZE_ENABLE_TESTING
1733 test::TestManager::Get().SetCurrentRom(session ? &session->rom : nullptr);
1734#endif
1735
1736 // RomLifecycleManager caches both identity hash and path; refresh them for
1737 // the newly active session before its next save-policy check.
1739
1740 LOG_DEBUG("EditorManager", "Session switched to %zu via EventBus", new_index);
1741}
1742
1745 CancelPendingRomSave(/*hide_popups=*/true);
1746 }
1748 if (session && !session->project_context.has_value()) {
1749 // A parsed project deliberately detaches the prior owner before creating
1750 // its ROM session. Normal raw-ROM and empty-session opens must instead get
1751 // a neutral context, not a copy of whichever project happened to be active.
1752 if (!active_project_context_session_id_.has_value() &&
1755 } else {
1756 project::YazeProject neutral_project;
1757 BindProjectContextToSession(session, neutral_project);
1758 }
1759 }
1760
1761 const size_t session_id = session ? session->session_id() : index;
1765 index == session_coordinator_->GetActiveSessionIndex()) {
1768 : nullptr);
1770 }
1771 LOG_INFO("EditorManager", "Session %zu created via EventBus", index);
1772}
1773
1775 CancelPendingRomSave(/*hide_popups=*/true);
1776
1777 // SessionClosedEvent is emitted before the owning RomSession is erased and
1778 // before the active index is adjusted. Bind the surviving session now so the
1779 // closing session's destructor cannot leave PaletteManager unbound.
1780 RomSession* palette_session = nullptr;
1782 const size_t session_count = session_coordinator_->GetTotalSessionCount();
1783 auto* active_session = session_coordinator_->GetActiveRomSession();
1784 auto* closing_session =
1785 session_coordinator_->IsValidSessionIndex(index)
1786 ? static_cast<RomSession*>(session_coordinator_->GetSession(index))
1787 : nullptr;
1788 if (closing_session != nullptr) {
1790 closing_session->session_id());
1791 }
1792 if (closing_session != nullptr &&
1793 active_project_context_session_id_ == closing_session->session_id()) {
1796 if (runtime_feature_flags_session_id_ == closing_session->session_id()) {
1798 }
1799 version_manager_ = nullptr;
1801 project_management_panel_->SetVersionManager(nullptr);
1802 }
1803 }
1804 palette_session = active_session;
1805 if (closing_session != nullptr && active_session == closing_session &&
1806 session_count > 1) {
1807 const size_t replacement_index = index > 0 ? index - 1 : 1;
1808 palette_session = static_cast<RomSession*>(
1809 session_coordinator_->GetSession(replacement_index));
1810 }
1811 }
1813 palette_session ? &palette_session->game_data : nullptr);
1814
1815 // Update ContentRegistry - it will be set to new active ROM on next switch
1816 // If no sessions remain, clear the context
1818 session_coordinator_->GetTotalSessionCount() == 0) {
1820 }
1821
1822 // Avoid dangling/stale label map pointers after session close.
1824
1825#ifdef YAZE_ENABLE_TESTING
1826 // Update test manager - it will get the new current ROM on next switch
1828#endif
1829
1830 LOG_INFO("EditorManager", "Session %zu closed via EventBus", index);
1831}
1832
1834 auto* session =
1835 static_cast<RomSession*>(session_coordinator_->GetSession(index));
1836 ResetAssetState(session);
1837
1838 // Update ContentRegistry when ROM is loaded (if this is the active session)
1839 if (rom && session_coordinator_ &&
1840 index == session_coordinator_->GetActiveSessionIndex()) {
1842 // Also update GameData from the session
1843 if (session) {
1844 ContentRegistry::Context::SetGameData(&session->game_data);
1845 }
1847 }
1848
1849#ifdef YAZE_ENABLE_TESTING
1850 if (rom) {
1852 }
1853#endif
1854
1855 LOG_INFO("EditorManager", "ROM loaded in session %zu via EventBus", index);
1856}
1857
1859 using Action = UIActionRequestEvent::Action;
1860 switch (action) {
1861 case Action::kShowEmulator:
1862 if (ui_coordinator_) {
1863 ui_coordinator_->SetEmulatorVisible(true);
1864 }
1865 break;
1866
1867 case Action::kShowSettings:
1868 // Toggle Settings panel in sidebar
1870 right_drawer_manager_->ToggleDrawer(
1872 } else {
1874 }
1875 break;
1876
1877 case Action::kShowPanelBrowser:
1878 if (ui_coordinator_) {
1879 ui_coordinator_->ShowWindowBrowser();
1880 }
1881 break;
1882
1883 case Action::kShowSearch:
1884 if (ui_coordinator_) {
1885 ui_coordinator_->ShowGlobalSearch();
1886 }
1887 break;
1888
1889 case Action::kShowShortcuts:
1890 if (ui_coordinator_) {
1891 ui_coordinator_->ShowShortcutsBrowser();
1892 }
1893 break;
1894
1895 case Action::kShowCommandPalette:
1896 if (ui_coordinator_) {
1897 ui_coordinator_->ShowCommandPalette();
1898 }
1899 break;
1900
1901 case Action::kShowHelp:
1903 // Toggle Help panel in sidebar
1904 right_drawer_manager_->ToggleDrawer(
1906 } else if (popup_manager_) {
1907 // Fallback to "About" dialog if sidebar not available
1909 }
1910 break;
1911
1912 case Action::kShowAgentChatSidebar:
1914 right_drawer_manager_->OpenDrawer(
1916 } else {
1918 }
1919 break;
1920
1921 case Action::kShowAgentProposalsSidebar:
1923 right_drawer_manager_->OpenDrawer(
1925 }
1926 break;
1927
1928 case Action::kOpenRom: {
1929 auto status = LoadRom();
1930 if (!status.ok()) {
1932 std::string("Open failed: ") + std::string(status.message()),
1934 }
1935 } break;
1936
1937 case Action::kSaveRom:
1938 if (GetCurrentRom() && GetCurrentRom()->is_loaded()) {
1939 auto status = SaveRom();
1940 if (!absl::IsCancelled(status) && !status.ok()) {
1942 std::string("Save failed: ") + std::string(status.message()),
1944 }
1945 }
1946 break;
1947
1948 case Action::kUndo:
1949 if (auto* current_editor = GetCurrentEditor()) {
1950 auto status = current_editor->Undo();
1951 if (!status.ok()) {
1953 std::string("Undo failed: ") + std::string(status.message()),
1955 }
1956 }
1957 break;
1958
1959 case Action::kRedo:
1960 if (auto* current_editor = GetCurrentEditor()) {
1961 auto status = current_editor->Redo();
1962 if (!status.ok()) {
1964 std::string("Redo failed: ") + std::string(status.message()),
1966 }
1967 }
1968 break;
1969
1970 case Action::kResetLayout:
1972 break;
1973 }
1974}
1975
1977 auto& test_manager = test::TestManager::Get();
1978
1979#ifdef YAZE_ENABLE_TESTING
1980 // Register comprehensive test suites
1981 test_manager.RegisterTestSuite(
1982 std::make_unique<test::CoreSystemsTestSuite>());
1983 test_manager.RegisterTestSuite(std::make_unique<test::IntegratedTestSuite>());
1984 test_manager.RegisterTestSuite(
1985 std::make_unique<test::PerformanceTestSuite>());
1986 test_manager.RegisterTestSuite(std::make_unique<test::UITestSuite>());
1987 test_manager.RegisterTestSuite(
1988 std::make_unique<test::RomDependentTestSuite>());
1989
1990 // Register new E2E and ZSCustomOverworld test suites
1991 test_manager.RegisterTestSuite(std::make_unique<test::E2ETestSuite>());
1992 test_manager.RegisterTestSuite(
1993 std::make_unique<test::ZSCustomOverworldTestSuite>());
1994#endif
1995
1996 // Register Google Test suite if available
1997#ifdef YAZE_ENABLE_GTEST
1998 test_manager.RegisterTestSuite(std::make_unique<test::UnitTestSuite>());
1999#endif
2000
2001 // Register z3ed AI Agent test suites (requires gRPC)
2002#ifdef YAZE_WITH_GRPC
2004#endif
2005
2006 // Update resource monitoring to track Arena state
2007 test_manager.UpdateResourceStats();
2008}
2009
2011 const std::string& filename) {
2012 renderer_ = renderer;
2014 SeedOracleProjectInRecents();
2015
2016 // Inject the window manager into emulator and workspace_manager
2019
2020 // Point to a blank editor set when no ROM is loaded
2021 // current_editor_set_ = &blank_editor_set_;
2022
2023 if (!filename.empty()) {
2025 }
2026
2027 // Note: PopupManager is now initialized in constructor before
2028 // MenuOrchestrator This ensures all menu callbacks can safely call
2029 // popup_manager_.Show()
2030
2034
2035 // Apply sidebar state from settings AFTER registering callbacks
2036 // This triggers the callbacks but they should be safe now
2038 /*notify=*/false);
2041 /*notify=*/false);
2043 user_settings_.prefs().sidebar_panel_width, /*notify=*/false);
2046 /*notify=*/false);
2047 {
2048 const bool prefer_dashboard_only =
2050 startup_editor_hint_.empty() && startup_panel_hints_.empty();
2051 if (!prefer_dashboard_only) {
2052 const std::string category = GetPreferredStartupCategory(
2054 if (!category.empty()) {
2055 window_manager_.SetActiveCategory(category, /*notify=*/false);
2057 auto it = user_settings_.prefs().panel_visibility_state.find(category);
2058 if (it != user_settings_.prefs().panel_visibility_state.end()) {
2060 window_manager_.GetActiveSessionId(), it->second);
2061 }
2062 }
2063 }
2064 }
2065
2069 }
2070
2071 // Initialize testing system only when tests are enabled
2072#ifdef YAZE_ENABLE_TESTING
2074#endif
2075
2076 // TestManager will be updated when ROMs are loaded via SetCurrentRom calls
2077
2079}
2080
2082 // Register emulator panels early (emulator Initialize might not be called).
2083 const std::vector<WindowDefinition> panel_definitions = {
2084 {.id = "emulator.cpu_debugger",
2085 .display_name = "CPU Debugger",
2086 .icon = ICON_MD_BUG_REPORT,
2087 .category = "Emulator",
2088 .priority = 10},
2089 {.id = "emulator.ppu_viewer",
2090 .display_name = "PPU Viewer",
2092 .category = "Emulator",
2093 .priority = 20},
2094 {.id = "emulator.memory_viewer",
2095 .display_name = "Memory Viewer",
2096 .icon = ICON_MD_MEMORY,
2097 .category = "Emulator",
2098 .priority = 30},
2099 {.id = "emulator.breakpoints",
2100 .display_name = "Breakpoints",
2101 .icon = ICON_MD_STOP,
2102 .category = "Emulator",
2103 .priority = 40},
2104 {.id = "emulator.performance",
2105 .display_name = "Performance",
2106 .icon = ICON_MD_SPEED,
2107 .category = "Emulator",
2108 .priority = 50},
2109 {.id = "emulator.ai_agent",
2110 .display_name = "AI Agent",
2111 .icon = ICON_MD_SMART_TOY,
2112 .category = "Emulator",
2113 .priority = 60},
2114 {.id = "emulator.save_states",
2115 .display_name = "Save States",
2116 .icon = ICON_MD_SAVE,
2117 .category = "Emulator",
2118 .priority = 70},
2119 {.id = "emulator.keyboard_config",
2120 .display_name = "Keyboard Config",
2121 .icon = ICON_MD_KEYBOARD,
2122 .category = "Emulator",
2123 .priority = 80},
2124 {.id = "emulator.virtual_controller",
2125 .display_name = "Virtual Controller",
2126 .icon = ICON_MD_SPORTS_ESPORTS,
2127 .category = "Emulator",
2128 .priority = 85},
2129 {.id = "emulator.apu_debugger",
2130 .display_name = "APU Debugger",
2131 .icon = ICON_MD_AUDIOTRACK,
2132 .category = "Emulator",
2133 .priority = 90},
2134 {.id = "emulator.audio_mixer",
2135 .display_name = "Audio Mixer",
2136 .icon = ICON_MD_AUDIO_FILE,
2137 .category = "Emulator",
2138 .priority = 100},
2139 {.id = "memory.hex_editor",
2140 .display_name = "Hex Editor",
2141 .icon = ICON_MD_MEMORY,
2142 .category = "Memory",
2143 .window_title = ICON_MD_MEMORY " Hex Editor",
2144 .priority = 10,
2145 .legacy_ids = {"Memory Editor"}},
2146 };
2147
2148 if (window_host_) {
2149 window_host_->RegisterPanels(panel_definitions);
2150 return;
2151 }
2152
2153 for (const auto& definition : panel_definitions) {
2154 WindowDescriptor descriptor;
2155 descriptor.card_id = definition.id;
2156 descriptor.display_name = definition.display_name;
2157 descriptor.window_title = definition.window_title;
2158 descriptor.icon = definition.icon;
2159 descriptor.category = definition.category;
2160 descriptor.shortcut_hint = definition.shortcut_hint;
2161 descriptor.priority = definition.priority;
2162 descriptor.scope = definition.scope;
2163 descriptor.window_lifecycle = definition.window_lifecycle;
2164 descriptor.context_scope = definition.context_scope;
2165 descriptor.visibility_flag = definition.visibility_flag;
2166 descriptor.on_show = definition.on_show;
2167 descriptor.on_hide = definition.on_hide;
2168
2169 for (const auto& legacy_id : definition.legacy_ids) {
2170 window_manager_.RegisterPanelAlias(legacy_id, definition.id);
2171 }
2172
2173 window_manager_.RegisterWindow(descriptor);
2174 if (definition.visible_by_default) {
2175 window_manager_.OpenWindow(definition.id);
2176 }
2177 }
2178}
2179
2181 // Initialize project file editor
2183
2184 // Initialize agent UI (no-op when agent UI is disabled)
2188
2189 // Note: Unified gRPC Server is started from Application::Initialize()
2190 // after gRPC infrastructure is properly set up
2191
2192 // Load critical user settings first
2194 if (!status_.ok()) {
2195 LOG_WARN("EditorManager", "Failed to load user settings: %s",
2196 status_.ToString().c_str());
2197 }
2198
2199 // Wire theme persistence. Any successful theme application (selector click,
2200 // command-palette switch, programmatic call) stamps the name into prefs and
2201 // rides the existing debounced-save path. Decouples ThemeManager (singleton
2202 // in app/gui) from UserSettings (editor-layer) — ThemeManager holds only a
2203 // std::function.
2205 [this](const std::string& theme_name) {
2206 user_settings_.prefs().last_theme_name = theme_name;
2207 settings_dirty_ = true;
2209 });
2211 [this](const std::string& locale) {
2212 user_settings_.prefs().language_locale = locale;
2213 settings_dirty_ = true;
2215 });
2216
2217 auto& prefs = user_settings_.prefs();
2218 i18n::LanguageManager::Get().SetLanguage(prefs.language_locale);
2219
2220 // Apply the persisted font selection (defaults to index 0 = Karla).
2221 // Fonts were loaded by the window backend before EditorManager init, so
2222 // ImGui::GetIO().Fonts->Fonts is fully populated here.
2223 ::yaze::SetActiveFontIndex(prefs.font_family_index);
2224
2225 prefs.switch_motion_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
2227 prefs.reduced_motion,
2228 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
2229
2231
2234 right_drawer_manager_->ResetDrawerWidths();
2236 right_drawer_manager_->SerializeDrawerWidths();
2237 } else {
2238 right_drawer_manager_->RestoreDrawerWidths(
2240 }
2241 right_drawer_manager_->SetDrawerWidthChangedCallback(
2242 [this](RightDrawerManager::DrawerType, float) {
2243 if (!right_drawer_manager_) {
2244 return;
2245 }
2246 user_settings_.prefs().right_panel_widths =
2247 right_drawer_manager_->SerializeDrawerWidths();
2248 settings_dirty_ = true;
2250 });
2251 }
2253 // Apply sprite naming preference globally.
2256
2258
2259 // Apply font scale after loading (only if ImGui context exists)
2260 if (ImGui::GetCurrentContext() != nullptr) {
2261 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
2262 } else {
2263 LOG_WARN("EditorManager",
2264 "ImGui context not available; skipping FontGlobalScale update");
2265 }
2266
2267 // Restore the user's last theme. Empty (first run) or "Custom Accent"
2268 // (transient generated themes without a matching discovered preset) both
2269 // skip restoration and keep the built-in default (Classic YAZE / YAZE Tre).
2270 const auto& last_theme = user_settings_.prefs().last_theme_name;
2271 if (!last_theme.empty() && last_theme != "Custom Accent") {
2272 gui::ThemeManager::Get().ApplyTheme(last_theme);
2273 }
2274
2275 // Initialize WASM control and session APIs for browser/agent integration
2276#ifdef __EMSCRIPTEN__
2279 LOG_INFO("EditorManager", "WASM Control and Session APIs initialized");
2280#endif
2281}
2282
2286
2288 ui_coordinator_->SetWelcomeScreenVisible(false);
2289 ui_coordinator_->SetWelcomeScreenManuallyClosed(true);
2290 }
2291}
2292
2294 // Initialize ROM load options dialog callbacks
2296 [this](const RomLoadOptionsDialog::LoadOptions& options) {
2297 // Apply feature flags from dialog
2298 auto& flags = core::FeatureFlags::get();
2299 flags.overworld.kSaveOverworldMaps = options.save_overworld_maps;
2300 flags.overworld.kSaveOverworldEntrances =
2302 flags.overworld.kSaveOverworldExits = options.save_overworld_exits;
2303 flags.overworld.kSaveOverworldItems = options.save_overworld_items;
2304 flags.overworld.kLoadCustomOverworld = options.enable_custom_overworld;
2305 flags.kSaveDungeonMaps = options.save_dungeon_maps;
2306 flags.kSaveAllPalettes = options.save_all_palettes;
2307 flags.kSaveGfxGroups = options.save_gfx_groups;
2309 if (auto* session = session_coordinator_->GetActiveRomSession()) {
2310 session->feature_flags = flags;
2311 }
2312 }
2313
2314 // Create project if requested
2315 if (options.create_project && !options.project_name.empty()) {
2316 auto status =
2318 if (!status.ok()) {
2319 toast_manager_.Show(absl::StrFormat("Failed to create project: %s",
2320 status.message()),
2322 } else {
2323 toast_manager_.Show("Project created: " + options.project_name,
2325 }
2326 }
2327
2328 // Close dialog and show editor selection
2329 show_rom_load_options_ = false;
2330 if (ui_coordinator_) {
2331 ui_coordinator_->SetEditorSelectionVisible(true);
2332 }
2333
2334 LOG_INFO("EditorManager", "ROM load options applied: preset=%s",
2335 options.selected_preset.c_str());
2336 });
2337}
2338
2340 // Utility callbacks removed - now handled via EventBus
2341
2343 [this](bool visible, bool expanded) {
2344 user_settings_.prefs().sidebar_visible = visible;
2345 user_settings_.prefs().sidebar_panel_expanded = expanded;
2346 settings_dirty_ = true;
2348 });
2351 settings_dirty_ = true;
2353 });
2355 [this](float width) {
2357 settings_dirty_ = true;
2359 });
2360
2362 const std::string& category) {
2363 if (category.empty() ||
2365 return;
2366 }
2369
2370 const auto& prefs = user_settings_.prefs();
2371 auto it = prefs.panel_visibility_state.find(category);
2372 if (it != prefs.panel_visibility_state.end()) {
2374 window_manager_.GetActiveSessionId(), it->second);
2375 } else {
2376 // No saved visibility state for this category yet.
2377 //
2378 // Apply LayoutPresets defaults only when this category has *no*
2379 // visible panels (editors may have already shown their own defaults,
2380 // e.g. Dungeon Workbench).
2381 const size_t session_id = window_manager_.GetActiveSessionId();
2382 bool any_visible = false;
2383 for (const auto& desc :
2384 window_manager_.GetWindowsInCategory(session_id, category)) {
2385 if (desc.visibility_flag && *desc.visibility_flag) {
2386 any_visible = true;
2387 break;
2388 }
2389 }
2390
2391 if (!any_visible) {
2392 const EditorType type =
2394 for (const auto& window_id : LayoutPresets::GetDefaultWindows(type)) {
2395 window_manager_.OpenWindow(session_id, window_id);
2396 }
2397 }
2398 }
2399
2400 settings_dirty_ = true;
2402 });
2403
2405 [this](const std::string& category) -> Editor* {
2406 Editor* editor = ResolveEditorForCategory(category);
2408 return editor;
2409 });
2410
2412 [this](const std::string& category) {
2414 if (type != EditorType::kSettings && type != EditorType::kUnknown) {
2415 SwitchToEditor(type, true);
2416 }
2417 });
2418
2420 [this](const std::string& category) {
2421 if (ui_coordinator_) {
2422 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
2423 }
2424
2425 if (category == "Agent") {
2426#ifdef YAZE_BUILD_AGENT_UI
2427 ShowAIAgent();
2428#endif
2429 return;
2430 }
2431
2433 if (type != EditorType::kSettings) {
2434 SwitchToEditor(type, true);
2435 }
2436 });
2437
2439
2441 [this](const std::string& category, const std::string& path) {
2442 if (category == "Assembly") {
2443 if (auto* editor_set = GetCurrentEditorSet()) {
2444 editor_set->ChangeActiveAssemblyFile(path);
2446 }
2447 }
2448 });
2449}
2450
2452 ShortcutDependencies shortcut_deps;
2453 shortcut_deps.editor_manager = this;
2454 shortcut_deps.editor_registry = &editor_registry_;
2455 shortcut_deps.menu_orchestrator = menu_orchestrator_.get();
2456 shortcut_deps.rom_file_manager = &rom_file_manager_;
2457 shortcut_deps.project_manager = &project_manager_;
2458 shortcut_deps.session_coordinator = session_coordinator_.get();
2459 shortcut_deps.ui_coordinator = ui_coordinator_.get();
2460 shortcut_deps.workspace_manager = &workspace_manager_;
2461 shortcut_deps.popup_manager = popup_manager_.get();
2462 shortcut_deps.toast_manager = &toast_manager_;
2463 shortcut_deps.window_manager = &window_manager_;
2464 shortcut_deps.user_settings = &user_settings_;
2465
2469}
2470
2472 const std::string& editor_name, const std::string& panels_str) {
2473 const bool has_editor = !editor_name.empty();
2474 const bool has_panels = !panels_str.empty();
2475
2476 if (!has_editor && !has_panels) {
2477 return;
2478 }
2479
2480 LOG_INFO("EditorManager",
2481 "Processing startup flags: editor='%s', panels='%s'",
2482 editor_name.c_str(), panels_str.c_str());
2483
2484 std::optional<EditorType> editor_type_to_open =
2485 has_editor ? ParseEditorTypeFromString(editor_name) : std::nullopt;
2486 if (has_editor && !editor_type_to_open.has_value()) {
2487 LOG_WARN("EditorManager", "Unknown editor specified via flag: %s",
2488 editor_name.c_str());
2489 } else if (editor_type_to_open.has_value()) {
2490 // Use EditorActivator to ensure layouts and default panels are initialized
2491 SwitchToEditor(*editor_type_to_open, true, /*from_dialog=*/true);
2492 }
2493
2494 // Open windows via WorkspaceWindowManager - works for any editor type
2495 if (!has_panels) {
2496 return;
2497 }
2498
2499 const size_t session_id = GetCurrentSessionId();
2500 std::string last_known_category = window_manager_.GetActiveCategory();
2501 bool applied_category_from_panel = false;
2502
2503 for (absl::string_view token :
2504 absl::StrSplit(panels_str, ',', absl::SkipWhitespace())) {
2505 if (token.empty()) {
2506 continue;
2507 }
2508 std::string panel_name = std::string(absl::StripAsciiWhitespace(token));
2509 LOG_DEBUG("EditorManager", "Attempting to open panel: '%s'",
2510 panel_name.c_str());
2511
2512 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2513 if (lower_name == "welcome" || lower_name == "welcome_screen") {
2514 if (ui_coordinator_) {
2515 ui_coordinator_->SetWelcomeScreenBehavior(StartupVisibility::kShow);
2516 }
2517 continue;
2518 }
2519 if (lower_name == "dashboard" || lower_name == "dashboard.main" ||
2520 lower_name == "editor_selection") {
2521 if (dashboard_panel_) {
2522 dashboard_panel_->Show();
2523 }
2524 if (ui_coordinator_) {
2525 ui_coordinator_->SetDashboardBehavior(StartupVisibility::kShow);
2526 }
2529 /*notify=*/false);
2530 continue;
2531 }
2532
2533 // Special case: "Room <id>" opens a dungeon room
2534 if (absl::StartsWith(panel_name, "Room ")) {
2535 if (auto* editor_set = GetCurrentEditorSet()) {
2536 try {
2537 int room_id = std::stoi(panel_name.substr(5));
2539 JumpToRoomRequestEvent::Create(room_id, session_id));
2540 } catch (const std::exception& e) {
2541 LOG_WARN("EditorManager", "Invalid room ID format: %s",
2542 panel_name.c_str());
2543 }
2544 }
2545 continue;
2546 }
2547
2548 std::optional<std::string> resolved_panel;
2549 if (window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2550 resolved_panel = panel_name;
2551 } else {
2552 for (const auto& [prefixed_id, descriptor] :
2554 const std::string base_id = StripSessionPrefix(prefixed_id);
2555 const std::string card_lower = absl::AsciiStrToLower(base_id);
2556 const std::string display_lower =
2557 absl::AsciiStrToLower(descriptor.display_name);
2558
2559 if (card_lower == lower_name || display_lower == lower_name) {
2560 resolved_panel = base_id;
2561 break;
2562 }
2563 }
2564 }
2565
2566 if (!resolved_panel.has_value()) {
2567 LOG_WARN("EditorManager",
2568 "Unknown panel '%s' from --open_panels (known count: %zu)",
2569 panel_name.c_str(),
2571 continue;
2572 }
2573
2574 if (window_manager_.OpenWindow(session_id, *resolved_panel)) {
2575 const auto* descriptor =
2576 window_manager_.GetWindowDescriptor(session_id, *resolved_panel);
2577 if (descriptor != nullptr) {
2578 const EditorType type =
2579 EditorRegistry::GetEditorTypeFromCategory(descriptor->category);
2580 const auto ensure_status = EnsureEditorAssetsLoaded(type);
2581 if (!ensure_status.ok()) {
2582 LOG_WARN("EditorManager", "Failed to load assets for panel '%s': %s",
2583 resolved_panel->c_str(), ensure_status.message().data());
2584 }
2585 }
2586 if (descriptor && !applied_category_from_panel &&
2587 descriptor->category != WorkspaceWindowManager::kDashboardCategory) {
2588 window_manager_.SetActiveCategory(descriptor->category);
2589 applied_category_from_panel = true;
2590 } else if (!applied_category_from_panel && descriptor &&
2591 descriptor->category.empty() && !last_known_category.empty()) {
2592 window_manager_.SetActiveCategory(last_known_category);
2593 }
2594 } else {
2595 LOG_WARN("EditorManager", "Failed to show panel '%s'",
2596 resolved_panel->c_str());
2597 }
2598 }
2599}
2600
2607
2615
2617 constexpr int kTargetRevision =
2619 const int previous_revision =
2621 if (!user_settings_.ApplyPanelLayoutDefaultsRevision(kTargetRevision)) {
2622 return;
2623 }
2624
2625 const bool includes_workspace_reset_revision =
2626 previous_revision <
2628 kTargetRevision >=
2631 pending_layout_defaults_reset_ || includes_workspace_reset_revision;
2632 settings_dirty_ = true;
2634
2635 LOG_INFO("EditorManager",
2636 "Applied panel layout defaults migration %d -> %d (%s)",
2637 previous_revision, kTargetRevision,
2638 includes_workspace_reset_revision ? "workspace reset"
2639 : "targeted preferences");
2640}
2641
2643 const std::string& saved_category,
2644 const std::vector<std::string>& available_categories) const {
2645 const std::string preferred =
2646 PreferStartupCategory(saved_category, available_categories);
2647 if (preferred.empty()) {
2648 return preferred;
2649 }
2650
2654 return PreferStartupCategory("", available_categories);
2655 }
2656 return preferred;
2657}
2658
2662
2664 if (ui_coordinator_) {
2665 ui_coordinator_->SetWelcomeScreenBehavior(welcome_mode_override_);
2666 ui_coordinator_->SetDashboardBehavior(dashboard_mode_override_);
2667 }
2668
2670 const bool sidebar_visible =
2672 window_manager_.SetSidebarVisible(sidebar_visible, /*notify=*/false);
2673 if (ui_coordinator_) {
2674 ui_coordinator_->SetPanelSidebarVisible(sidebar_visible);
2675 }
2676 }
2677
2678 // Force sidebar panel to collapse if Welcome Screen or Dashboard is explicitly shown
2679 // This prevents visual overlap/clutter on startup
2682 window_manager_.SetSidebarExpanded(false, /*notify=*/false);
2683 }
2684
2685 if (dashboard_panel_) {
2687 dashboard_panel_->Hide();
2689 dashboard_panel_->Show();
2690 }
2691 }
2692}
2693
2695 ApplyStartupVisibility(config);
2696 // Handle startup editor and panels
2697 std::string panels_str;
2698 for (size_t i = 0; i < config.open_panels.size(); ++i) {
2699 if (i > 0)
2700 panels_str += ",";
2701 panels_str += config.open_panels[i];
2702 }
2703 OpenEditorAndPanelsFromFlags(config.startup_editor, panels_str);
2704
2705 // Handle jump targets
2706 if (config.jump_to_room >= 0) {
2709 }
2710 if (config.jump_to_map >= 0) {
2714 } else {
2715 LOG_WARN("EditorManager",
2716 "Ignoring invalid startup overworld map target %d",
2717 config.jump_to_map);
2718 toast_manager_.Show("Ignored invalid startup overworld map: " +
2719 std::to_string(config.jump_to_map),
2721 }
2722 }
2723}
2724
2725absl::Status EditorManager::LoadAssetsForMode(uint64_t loading_handle) {
2726 switch (asset_load_mode_) {
2728 return LoadAssetsLazy(loading_handle);
2731 default:
2732 return LoadAssets(loading_handle);
2733 }
2734}
2735
2737 if (!session) {
2738 return;
2739 }
2740 session->game_data_loaded = false;
2741 session->editor_initialized.fill(false);
2742 session->editor_assets_loaded.fill(false);
2743}
2744
2746 EditorType type) {
2747 if (!session) {
2748 return;
2749 }
2750 const size_t index = EditorTypeIndex(type);
2751 if (index < session->editor_initialized.size()) {
2752 session->editor_initialized[index] = true;
2753 }
2754}
2755
2757 if (!session) {
2758 return;
2759 }
2760 const size_t index = EditorTypeIndex(type);
2761 if (index < session->editor_assets_loaded.size()) {
2762 session->editor_assets_loaded[index] = true;
2763 }
2764}
2765
2767 EditorType type) {
2768 if (!session || !layout_manager_) {
2769 return;
2770 }
2771
2772 const size_t index = EditorTypeIndex(type);
2774 index >= session->editor_initialized.size() ||
2775 !session->editor_initialized[index] ||
2777 return;
2778 }
2779
2780 const std::string category = EditorRegistry::GetEditorCategory(type);
2781 // Session switching can happen before a lazily loaded editor registers its
2782 // panels. Repeat the persisted visibility pass once those descriptors exist.
2783 const auto visibility_it =
2785 if (visibility_it != user_settings_.prefs().panel_visibility_state.end()) {
2787 visibility_it->second);
2788 }
2789
2790 // Only the active category owns the shared dock tree. A cross-category
2791 // activation will refresh its layout immediately after the category switch.
2792 if (window_manager_.GetActiveCategory() == category &&
2793 layout_manager_->IsLayoutInitialized(type)) {
2795 }
2796}
2797
2803
2805 switch (type) {
2813 return true;
2814 default:
2815 return false;
2816 }
2817}
2818
2822
2824 EditorSet* editor_set) const {
2825 std::unordered_set<EditorType> types;
2826
2827 auto add_type = [&types](EditorType type) {
2828 switch (type) {
2832 case EditorType::kAgent:
2833 return;
2834 default:
2835 types.insert(type);
2836 return;
2837 }
2838 };
2839
2840 auto add_category = [&](const std::string& category) {
2841 if (category.empty() ||
2843 return;
2844 }
2846 };
2847
2848 const size_t session_id = GetCurrentSessionId();
2849 bool used_startup_hints = false;
2850
2851 if (!startup_editor_hint_.empty()) {
2852 if (auto startup_type = ParseEditorTypeFromString(startup_editor_hint_)) {
2853 add_type(*startup_type);
2854 used_startup_hints = true;
2855 }
2856 }
2857
2858 for (const auto& panel_name : startup_panel_hints_) {
2859 if (panel_name.empty()) {
2860 continue;
2861 }
2862
2863 if (const auto* descriptor =
2864 window_manager_.GetWindowDescriptor(session_id, panel_name)) {
2865 add_category(descriptor->category);
2866 used_startup_hints = true;
2867 continue;
2868 }
2869
2870 const std::string lower_name = absl::AsciiStrToLower(panel_name);
2871 for (const auto& [prefixed_id, descriptor] :
2873 const std::string base_id = StripSessionPrefix(prefixed_id);
2874 const std::string card_lower = absl::AsciiStrToLower(base_id);
2875 const std::string display_lower =
2876 absl::AsciiStrToLower(descriptor.display_name);
2877 if (card_lower == lower_name || display_lower == lower_name) {
2878 add_category(descriptor.category);
2879 used_startup_hints = true;
2880 break;
2881 }
2882 }
2883 }
2884
2885 if (used_startup_hints) {
2886 return std::vector<EditorType>(types.begin(), types.end());
2887 }
2888
2890 return {};
2891 }
2892
2893 if (editor_set) {
2894 for (auto* editor : editor_set->active_editors_) {
2895 if (editor != nullptr && *editor->active()) {
2896 add_type(editor->type());
2897 }
2898 }
2899 }
2900
2901 if (current_editor_ != nullptr) {
2902 add_type(current_editor_->type());
2903 }
2904
2905 add_category(window_manager_.GetActiveCategory());
2906
2907 for (const auto& window_id :
2909 if (const auto* descriptor =
2910 window_manager_.GetWindowDescriptor(session_id, window_id)) {
2911 add_category(descriptor->category);
2912 }
2913 }
2914
2915 return std::vector<EditorType>(types.begin(), types.end());
2916}
2917
2919 EditorSet* editor_set) const {
2920 return editor_set ? editor_set->GetEditor(type) : nullptr;
2921}
2922
2924 if (category.empty() ||
2926 return nullptr;
2927 }
2928
2929 auto* editor_set = GetCurrentEditorSet();
2930 if (!editor_set) {
2931 return nullptr;
2932 }
2933
2935 switch (type) {
2936 case EditorType::kAgent:
2937#ifdef YAZE_BUILD_AGENT_UI
2938 return agent_ui_.GetAgentEditor();
2939#else
2940 return nullptr;
2941#endif
2945 return nullptr;
2946 default:
2947 return GetEditorByType(type, editor_set);
2948 }
2949}
2950
2951void EditorManager::SyncEditorContextForCategory(const std::string& category) {
2952 if (Editor* resolved = ResolveEditorForCategory(category)) {
2953 SetCurrentEditor(resolved);
2954 } else if (!category.empty() &&
2956 LOG_DEBUG("EditorManager", "No editor context available for category '%s'",
2957 category.c_str());
2958 }
2959}
2960
2962 EditorSet* editor_set,
2963 Rom* rom) {
2964 if (!editor_set) {
2965 return absl::FailedPreconditionError("No editor set available");
2966 }
2967
2968 auto* editor = GetEditorByType(type, editor_set);
2969 if (!editor) {
2970 return absl::OkStatus();
2971 }
2972 editor->Initialize();
2973 return absl::OkStatus();
2974}
2975
2977 auto* session = session_coordinator_
2978 ? session_coordinator_->GetActiveRomSession()
2979 : nullptr;
2980 if (!session) {
2981 return absl::FailedPreconditionError("No active session");
2982 }
2983 if (session->game_data_loaded) {
2984 return absl::OkStatus();
2985 }
2986 if (!session->rom.is_loaded()) {
2987 return absl::FailedPreconditionError("ROM not loaded");
2988 }
2989
2990 RETURN_IF_ERROR(zelda3::LoadGameData(session->rom, session->game_data));
2991 *gfx::Arena::Get().mutable_gfx_sheets() = session->game_data.gfx_bitmaps;
2992
2993 auto* game_data = &session->game_data;
2994 auto* editor_set = &session->editors;
2995 editor_set->SetGameData(game_data);
2997
2999 session->game_data_loaded = true;
3000
3001 return absl::OkStatus();
3002}
3003
3005 if (type == EditorType::kUnknown) {
3006 return absl::OkStatus();
3007 }
3008
3009 auto* session = session_coordinator_
3010 ? session_coordinator_->GetActiveRomSession()
3011 : nullptr;
3012 if (!session) {
3013 return absl::OkStatus();
3014 }
3015
3016 if (!session->rom.is_loaded()) {
3018 return absl::OkStatus();
3019 }
3020 const size_t index = EditorTypeIndex(type);
3021 if (index >= session->editor_initialized.size()) {
3022 return absl::InvalidArgumentError("Invalid editor type");
3023 }
3024 if (EditorInitRequiresGameData(type)) {
3025 return absl::OkStatus();
3026 }
3027 bool initialized_now = false;
3028 if (!session->editor_initialized[index]) {
3030 InitializeEditorForType(type, &session->editors, &session->rom));
3031 MarkEditorInitialized(session, type);
3032 initialized_now = true;
3033 }
3034 if (initialized_now) {
3035 RestoreEditorLayoutAfterAssets(session, type);
3036 }
3037 return absl::OkStatus();
3038 }
3039
3040 const size_t index = EditorTypeIndex(type);
3041 if (index >= session->editor_initialized.size()) {
3042 return absl::InvalidArgumentError("Invalid editor type");
3043 }
3044
3045 if (EditorInitRequiresGameData(type)) {
3047 }
3048
3049 bool initialized_now = false;
3050 if (!session->editor_initialized[index]) {
3052 InitializeEditorForType(type, &session->editors, &session->rom));
3053 MarkEditorInitialized(session, type);
3054 initialized_now = true;
3055 }
3056
3057 if (EditorRequiresGameData(type)) {
3059 }
3060
3061 bool loaded_now = false;
3062 if (!session->editor_assets_loaded[index]) {
3063 auto* editor = GetEditorByType(type, &session->editors);
3064 if (editor) {
3065 RETURN_IF_ERROR(editor->Load());
3066 }
3067 MarkEditorLoaded(session, type);
3068 loaded_now = true;
3069 }
3070
3071 if (initialized_now || loaded_now) {
3072 RestoreEditorLayoutAfterAssets(session, type);
3073 }
3074
3075 return absl::OkStatus();
3076}
3077
3095 ProcessInput();
3097 DrawInterface();
3098
3099 return status_;
3100}
3101
3103 // Space/focus transitions can leave mid-animation surfaces ghosted.
3104 // Reset transient animation state to a stable endpoint.
3107 right_drawer_manager_->OnHostVisibilityChanged(visible);
3108 }
3109}
3110
3112 // Update timing manager for accurate delta time across the application
3114
3115 // Execute keyboard shortcuts (registered via ShortcutConfigurator)
3117}
3118
3120 status_ = absl::OkStatus();
3122 if (ui_coordinator_) {
3123 ui_coordinator_->RefreshWorkflowActions();
3124 }
3125 // Check for layout rebuild requests and execute if needed (delegated to LayoutCoordinator)
3126 bool is_emulator_visible =
3127 ui_coordinator_ && ui_coordinator_->IsEmulatorVisible();
3128 EditorType current_type =
3130 layout_coordinator_.ProcessLayoutRebuild(current_type, is_emulator_visible);
3131
3132 // Periodic user settings auto-save
3133 if (settings_dirty_) {
3134 const float elapsed = TimingManager::Get().GetElapsedTime();
3135 if (elapsed - settings_dirty_timestamp_ >= 1.0f) {
3136 auto save_status = user_settings_.Save();
3137 if (!save_status.ok()) {
3138 LOG_WARN("EditorManager", "Failed to save user settings: %s",
3139 save_status.ToString().c_str());
3140 }
3141 settings_dirty_ = false;
3142 }
3143 }
3144
3145 // Update agent editor dashboard
3147
3148 // Ensure TestManager always has the current ROM
3149 static Rom* last_test_rom = nullptr;
3150 auto* current_rom = GetCurrentRom();
3151 if (last_test_rom != current_rom) {
3152 LOG_DEBUG(
3153 "EditorManager",
3154 "EditorManager::Update - ROM changed, updating TestManager: %p -> %p",
3155 (void*)last_test_rom, (void*)current_rom);
3157 last_test_rom = current_rom;
3158 }
3159
3160 // Autosave uses the same guarded serialization, validation, backup, and
3161 // atomic commit pipeline as an explicit save. Writing Rom::SaveToFile here
3162 // would skip editor model serialization and could persist a partially
3163 // failed buffer while clearing its dirty bit.
3164 const bool current_session_has_unsaved_work =
3166 const auto* current_session =
3167 session_coordinator_ ? session_coordinator_->GetActiveRomSession()
3168 : nullptr;
3169 const bool backup_restore_requires_explicit_save =
3170 current_session != nullptr && current_session->backup_restore_pending;
3172 current_session_has_unsaved_work &&
3173 !backup_restore_requires_explicit_save) {
3174 autosave_timer_ += ImGui::GetIO().DeltaTime;
3176 autosave_timer_ = 0.0f;
3177 auto st = AutosaveActiveSession();
3178 if (st.ok()) {
3179 toast_manager_.Show("Autosave completed", editor::ToastType::kSuccess);
3180 } else if (absl::IsCancelled(st)) {
3181 toast_manager_.Show("Autosave paused: confirmation required",
3183 } else {
3185 absl::StrFormat("Autosave failed: %s", st.message()),
3187 }
3188 }
3189 } else {
3190 autosave_timer_ = 0.0f;
3191 }
3192
3193 // Update ROM context for agent UI
3194 if (current_rom && current_rom->is_loaded()) {
3195 agent_ui_.SetRomContext(current_rom);
3197 if (auto* editor_set = GetCurrentEditorSet()) {
3198 agent_ui_.SetAsarWrapperContext(editor_set->GetAsarWrapper());
3199 // Backend-agnostic symbol feed. Works for ASAR and z3dk alike; tools
3200 // prefer this pointer over reaching through the wrapper.
3202 &editor_set->GetAssemblySymbols());
3203 }
3204 }
3205
3206 // Delegate session updates to SessionCoordinator
3208 session_coordinator_->UpdateSessions();
3209 }
3210}
3211
3213 if (!active_project_build_) {
3214 return;
3215 }
3216
3217 const auto snapshot = active_project_build_->GetSnapshot();
3218 UpdateBuildWorkflowStatus(
3221 snapshot.running
3222 ? "Build running"
3223 : (snapshot.status.ok() ? "Build succeeded" : "Build failed"),
3224 snapshot.output_tail.empty()
3225 ? (snapshot.running
3226 ? std::string(
3227 "Running the configured project build command")
3228 : std::string(snapshot.status.message()))
3229 : snapshot.output_tail,
3230 snapshot.running
3232 : (snapshot.status.ok() ? ProjectWorkflowState::kSuccess
3234 snapshot.output_tail, snapshot.running));
3236 project_management_panel_->SetBuildLogOutput(snapshot.output);
3237 }
3239
3240 if (snapshot.running || active_project_build_reported_) {
3241 return;
3242 }
3243
3245 if (!snapshot.status.ok()) {
3246 AppendWorkflowHistoryEntry(
3247 "Build",
3248 MakeBuildStatus("Build failed", std::string(snapshot.status.message()),
3249 ProjectWorkflowState::kFailure, snapshot.output_tail,
3250 false),
3251 snapshot.output);
3253 absl::StrFormat("Build failed: %s", snapshot.status.message()),
3254 snapshot.status.code() == absl::StatusCode::kCancelled
3257 return;
3258 }
3259
3260 AppendWorkflowHistoryEntry(
3261 "Build",
3263 "Build succeeded",
3264 snapshot.output_tail.empty() ? std::string("Project build completed")
3265 : snapshot.output_tail,
3266 ProjectWorkflowState::kSuccess, snapshot.output_tail, false),
3267 snapshot.output);
3268 toast_manager_.Show(snapshot.output_tail.empty()
3269 ? "Project build completed"
3270 : absl::StrFormat("Project build completed: %s",
3271 snapshot.output_tail),
3273}
3274
3276
3277 // Draw editor selection dialog (managed by UICoordinator)
3278 if (ui_coordinator_ && ui_coordinator_->IsEditorSelectionVisible()) {
3279 dashboard_panel_->Show();
3280 dashboard_panel_->Draw();
3281 if (!dashboard_panel_->IsVisible()) {
3282 ui_coordinator_->SetEditorSelectionVisible(false);
3283 }
3284 }
3285
3286 // Draw ROM load options dialog (ZSCustomOverworld, feature flags, project)
3289 }
3290
3291 // Draw window browser (managed by UICoordinator)
3292 if (ui_coordinator_ && ui_coordinator_->IsWindowBrowserVisible()) {
3293 bool show = true;
3294 if (activity_bar_) {
3295 activity_bar_->DrawWindowBrowser(GetCurrentSessionId(), &show);
3296 }
3297 if (!show) {
3298 ui_coordinator_->SetWindowBrowserVisible(false);
3299 }
3300 }
3301
3302 // Draw background grid effects
3303 if (ui_coordinator_) {
3304 ui_coordinator_->DrawBackground();
3305 }
3306
3307 // Draw UICoordinator UI components (Welcome Screen, Command Palette, etc.)
3308 if (ui_coordinator_) {
3309 ui_coordinator_->DrawAllUI();
3310 }
3311
3312 // Handle Welcome screen early-exit for rendering
3313 if (ui_coordinator_ && ui_coordinator_->ShouldShowWelcome()) {
3315 right_drawer_manager_->CloseDrawer();
3316 }
3317 return;
3318 }
3319
3322 RunEmulator();
3323
3324 // Draw sidebar
3325 if (ui_coordinator_ && ui_coordinator_->IsPanelSidebarVisible()) {
3326 auto all_categories = EditorRegistry::GetAllEditorCategories();
3327 std::unordered_set<std::string> active_editor_categories;
3328
3329 if (auto* current_editor_set = GetCurrentEditorSet()) {
3331 for (size_t session_idx = 0;
3332 session_idx < session_coordinator_->GetTotalSessionCount();
3333 ++session_idx) {
3334 auto* session = static_cast<RomSession*>(
3335 session_coordinator_->GetSession(session_idx));
3336 if (!session || !session->rom.is_loaded()) {
3337 continue;
3338 }
3339
3340 for (auto* editor : session->editors.active_editors_) {
3341 if (*editor->active() && IsPanelBasedEditor(editor->type())) {
3342 std::string category =
3343 EditorRegistry::GetEditorCategory(editor->type());
3344 active_editor_categories.insert(category);
3345 }
3346 }
3347 }
3348
3349 if (ui_coordinator_->IsEmulatorVisible()) {
3350 active_editor_categories.insert("Emulator");
3351 }
3352 }
3353 }
3354
3355 const bool prefer_dashboard_only =
3357 startup_editor_hint_.empty() && startup_panel_hints_.empty();
3358 std::string sidebar_category = window_manager_.GetActiveCategory();
3359 if (!prefer_dashboard_only && sidebar_category.empty() &&
3360 !all_categories.empty()) {
3361 sidebar_category = GetPreferredStartupCategory("", all_categories);
3362 if (!sidebar_category.empty()) {
3363 window_manager_.SetActiveCategory(sidebar_category, /*notify=*/false);
3364 SyncEditorContextForCategory(sidebar_category);
3366 sidebar_category);
3367 if (it != user_settings_.prefs().panel_visibility_state.end()) {
3369 window_manager_.GetActiveSessionId(), it->second);
3370 }
3371 }
3372 }
3373
3374 auto has_rom_callback = [this]() -> bool {
3375 auto* rom = GetCurrentRom();
3376 return rom && rom->is_loaded();
3377 };
3378
3379 if (activity_bar_ && ui_coordinator_->ShouldShowActivityBar()) {
3380 auto is_rom_dirty_callback = [this]() -> bool {
3381 auto* rom = GetCurrentRom();
3382 return rom && rom->is_loaded() && rom->dirty();
3383 };
3384 auto pending_dungeon_rooms_callback = [this]() -> int {
3385 if (auto* editor_set = GetCurrentEditorSet()) {
3386 if (auto* dungeon_editor = editor_set->GetEditorAs<DungeonEditorV2>(
3388 return dungeon_editor->PendingRoomCount();
3389 }
3390 }
3391 return 0;
3392 };
3393 activity_bar_->Render(GetCurrentSessionId(), sidebar_category,
3394 all_categories, active_editor_categories,
3395 has_rom_callback, is_rom_dirty_callback,
3396 pending_dungeon_rooms_callback);
3397 }
3398 }
3399
3400 // Draw right panel
3403 right_drawer_manager_->Draw();
3404 }
3405
3406 // Update and draw status bar
3409 const size_t session_index = GetCurrentSessionIndex();
3410 const size_t session_count = session_coordinator_->GetActiveSessionCount();
3412 session_index, session_count,
3413 session_count > 1 ? session_coordinator_->GetActiveSessionDisplayName()
3414 : std::string{});
3415 }
3416
3417 bool has_agent_info = false;
3418#if defined(YAZE_BUILD_AGENT_UI)
3419 if (auto* agent_editor = agent_ui_.GetAgentEditor()) {
3420 auto* chat = agent_editor->GetAgentChat();
3421 const auto* ctx = agent_ui_.GetContext();
3422 if (ctx) {
3423 const auto& config = ctx->agent_config();
3424 bool active = chat && *chat->active();
3425 status_bar_.SetAgentInfo(config.ai_provider, config.ai_model, active);
3426 has_agent_info = true;
3427 }
3428 }
3429#endif
3430 if (!has_agent_info) {
3432 }
3435 }
3436
3437 // Editor-aware context: let the active editor push its own mode/custom
3438 // segments for this frame without wiping event-driven cursor/selection/zoom
3439 // state that older editors still publish through the event bus.
3441 if (current_editor_) {
3443
3444 StatusBarSegmentOptions editor_opts;
3445 editor_opts.tooltip = "Click to switch editor (Ctrl+E)";
3446 editor_opts.on_click = [this]() {
3447 if (ui_coordinator_) {
3448 ui_coordinator_->ShowEditorSelection();
3449 }
3450 };
3453 std::move(editor_opts));
3454 } else {
3456 }
3457
3458 const size_t session_index = GetCurrentSessionIndex();
3459 if (SessionHasPendingUnsavedWork(session_index)) {
3460 StatusBarSegmentOptions dirty_opts;
3461 dirty_opts.tooltip = absl::StrFormat(
3462 "%s. Click to save ROM when ROM-buffer work is pending, otherwise open "
3463 "the Project drawer.",
3464 DescribePendingUnsavedWork(session_index));
3465 dirty_opts.on_click = [this, session_index]() {
3466 if (SessionHasPendingRomWork(session_index)) {
3467 auto status = SaveRom();
3468 if (!status.ok()) {
3469 toast_manager_.Show(std::string(status.message()), ToastType::kError);
3470 }
3471 } else if (right_drawer_manager_) {
3472 right_drawer_manager_->OpenDrawer(
3474 }
3475 };
3477 std::move(dirty_opts));
3478 } else {
3480 }
3481
3482 if (right_drawer_manager_ && right_drawer_manager_->IsDrawerExpanded()) {
3483 const auto active = right_drawer_manager_->GetActiveDrawer();
3485 StatusBarSegmentOptions drawer_opts;
3486 drawer_opts.tooltip =
3487 "Right drawer open — Esc closes, View > Drawers switches";
3489 std::move(drawer_opts));
3490 }
3491 }
3492
3493 status_bar_.Draw();
3494
3495 // Check if ROM is loaded before drawing panels
3496 auto* current_editor_set = GetCurrentEditorSet();
3497 if (!current_editor_set || !GetCurrentRom()) {
3498 if (window_manager_.GetActiveCategory() == "Agent") {
3500 }
3501 return;
3502 }
3503
3504 // Central workspace window drawing
3506
3507 // The Performance Dashboard is rendered by DrawSecondaryWindows below, which
3508 // also drives Update() and syncs the visibility flag back. Rendering here as
3509 // well gave a second Begin/End on the same window name every frame, stacking
3510 // duplicate metrics, memory blocks and frame plots with colliding plot IDs.
3511
3512 // Draw SessionCoordinator UI components
3514 session_coordinator_->DrawSessionSwitcher();
3515 session_coordinator_->DrawSessionManager();
3516 session_coordinator_->DrawSessionRenameDialog();
3517 }
3518}
3519
3521 if (ImGui::BeginMenuBar()) {
3522 const bool sidebar_visible = window_manager_.IsSidebarVisible();
3523 const char* icon = sidebar_visible ? ICON_MD_MENU_OPEN : ICON_MD_MENU;
3524 const char* tooltip = sidebar_visible ? "Hide Activity Bar (Ctrl+B)"
3525 : "Show Activity Bar (Ctrl+B)";
3526 if (ui_coordinator_ && ui_coordinator_->DrawMenuBarIconButton(
3527 icon, tooltip, sidebar_visible)) {
3529 }
3530
3531 // Delegate menu building to MenuOrchestrator
3532 if (menu_orchestrator_) {
3533 menu_orchestrator_->BuildMainMenu();
3534 }
3535
3536 // Delegate menu bar extras to UICoordinator
3537 if (ui_coordinator_) {
3538 ui_coordinator_->DrawMenuBarExtras();
3539 }
3540
3541 ImGui::EndMenuBar();
3542 }
3543}
3544
3546
3547 // ImGui debug windows
3548 if (ui_coordinator_) {
3549 if (ui_coordinator_->IsImGuiDemoVisible()) {
3550 bool visible = true;
3551 ImGui::ShowDemoWindow(&visible);
3552 if (!visible)
3553 ui_coordinator_->SetImGuiDemoVisible(false);
3554 }
3555
3556 if (ui_coordinator_->IsImGuiMetricsVisible()) {
3557 bool visible = true;
3558 ImGui::ShowMetricsWindow(&visible);
3559 if (!visible)
3560 ui_coordinator_->SetImGuiMetricsVisible(false);
3561 }
3562 }
3563
3564 // Legacy window-based editors
3565 if (auto* editor_set = GetCurrentEditorSet()) {
3566 bool* hex_visibility =
3567 window_manager_.GetWindowVisibilityFlag("memory.hex_editor");
3568 if (hex_visibility) {
3569 if (auto* editor = editor_set->GetEditor(EditorType::kHex)) {
3570 // Keep the legacy panel visibility flag in sync with the window close
3571 // button (ImGui::Begin will toggle Editor::active_).
3572 editor->set_active(*hex_visibility);
3573 editor->Update();
3574 *hex_visibility = *editor->active();
3575 }
3576 }
3577
3578 if (ui_coordinator_ && ui_coordinator_->IsAsmEditorVisible()) {
3579 if (auto* editor = editor_set->GetEditor(EditorType::kAssembly)) {
3580 editor->set_active(true);
3581 editor->Update();
3582 if (!*editor->active()) {
3583 ui_coordinator_->SetAsmEditorVisible(false);
3584 }
3585 }
3586 }
3587 }
3588
3589 // Project and performance tools
3592
3593 if (ui_coordinator_ && ui_coordinator_->IsPerformanceDashboardVisible()) {
3597 if (!gfx::PerformanceDashboard::Get().IsVisible()) {
3598 ui_coordinator_->SetPerformanceDashboardVisible(false);
3599 }
3600 }
3601
3602#ifdef YAZE_ENABLE_TESTING
3603 if (show_test_dashboard_) {
3605 test::TestManager::Get().DrawTestDashboard(&show_test_dashboard_);
3606 }
3607#endif
3608}
3609
3611 // Update proposal drawer context
3613
3614 // Agent UI popups
3616
3617 // Resource label management
3618 if (ui_coordinator_ && ui_coordinator_->IsResourceLabelManagerVisible() &&
3619 GetCurrentRom()) {
3620 bool visible = true;
3626 }
3627 if (!visible)
3628 ui_coordinator_->SetResourceLabelManagerVisible(false);
3629 }
3630
3631 // Layout presets
3632 if (ui_coordinator_) {
3633 ui_coordinator_->DrawLayoutPresets();
3634 }
3635}
3636
3638 auto* current_rom = GetCurrentRom();
3639 if (!current_rom)
3640 return;
3641
3642 // Visible emulator: lazy-init + render + tick via Run(rom).
3643 // Hidden emulator: pause by default so switching editors does not keep game
3644 // audio/frame state advancing. Opt-in background keep-alive is a user pref.
3645 // MusicPlayer drives its own RunAudioFrame path when needed.
3646 if (ui_coordinator_ && ui_coordinator_->IsEmulatorVisible()) {
3647 emulator_.Run(current_rom);
3648 return;
3649 }
3650
3652 return;
3653 }
3656 return;
3657 }
3658
3661 } else {
3663 }
3664}
3665
3672
3698 GetCurrentSessionId()})) {
3699 return absl::OkStatus();
3700 }
3701
3702 return LoadRomInternal();
3703}
3704
3706 auto load_from_path = [this](const std::string& file_name) -> absl::Status {
3707 if (file_name.empty()) {
3708 return absl::OkStatus();
3709 }
3710
3711 // Check if this is a project file - route to project loading
3712 if (absl::EndsWith(file_name, ".yaze") ||
3713 absl::EndsWith(file_name, ".zsproj") ||
3714 absl::EndsWith(file_name, ".yazeproj")) {
3715 return OpenRomOrProjectInternal(file_name);
3716 }
3717
3718 auto path_status =
3719 session_coordinator_->CheckBackingFileAvailable(file_name);
3720 if (!path_status.ok()) {
3721 toast_manager_.Show(std::string(path_status.message()),
3723 return path_status;
3724 }
3725
3726 // Delegate ROM loading to RomFileManager
3727 Rom temp_rom;
3728 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, file_name));
3729
3730 const std::optional<size_t> previous_session_id =
3732 const size_t previous_session_count =
3733 session_coordinator_->GetTotalSessionCount();
3734 auto session_or = session_coordinator_->CreateSessionFromRom(
3735 std::move(temp_rom), file_name);
3736 if (!session_or.ok()) {
3737 return session_or.status();
3738 }
3739
3743
3744 // HandleSessionCreated binds the project context that governed this open.
3745 // Reapply it here rather than partially clearing global runtime state.
3748
3749 // Keep ResourceLabelProvider in sync with the newly-active ROM session
3750 // before any editors/assets query room/sprite names.
3752
3753#ifdef YAZE_ENABLE_TESTING
3755#endif
3756
3758 const auto& recent_files = manager.GetRecentFiles();
3759 const bool is_first_time_rom_path =
3760 std::find(recent_files.begin(), recent_files.end(), file_name) ==
3761 recent_files.end();
3762 auto asset_status = LoadAssetsForMode();
3763 if (!asset_status.ok()) {
3764 auto rollback_status =
3765 DiscardProvisionalSessionCreatedSince(previous_session_count);
3766 RestoreProjectContextAfterFailedOpen(previous_session_id);
3767 if (!rollback_status.ok()) {
3768 return absl::InternalError(
3769 absl::StrFormat("%s; session rollback failed: %s",
3770 asset_status.message(), rollback_status.message()));
3771 }
3772 return asset_status;
3773 }
3774
3775 manager.AddFile(file_name);
3776 manager.Save();
3777
3778 if (ui_coordinator_) {
3779 ui_coordinator_->SetWelcomeScreenVisible(false);
3780
3781 // Show ROM load options and bias new ROM paths toward project creation.
3782 rom_load_options_dialog_.Open(GetCurrentRom(), is_first_time_rom_path);
3784 }
3785
3786 return absl::OkStatus();
3787 };
3788
3789#if defined(__APPLE__) && TARGET_OS_IOS == 1
3790 // On iOS, route through the SwiftUI overlay document picker to get proper
3791 // security-scoped access to iCloud Drive and Files app locations. This
3792 // mirrors how OpenProject() works on iOS and supports cloud ROMs.
3793 // The SwiftUI picker calls YazeIOSBridge.loadRomAtPath: after importing the
3794 // ROM to a persistent sandbox directory.
3796 return absl::OkStatus();
3797#else
3800 return load_from_path(file_name);
3801#endif
3802}
3803
3804absl::Status EditorManager::LoadAssets(uint64_t passed_handle) {
3805 auto* current_rom = GetCurrentRom();
3806 auto* current_editor_set = GetCurrentEditorSet();
3807 if (!current_rom || !current_editor_set) {
3808 return absl::FailedPreconditionError("No ROM or editor set loaded");
3809 }
3810
3811 auto* current_session = session_coordinator_->GetActiveRomSession();
3812 if (!current_session) {
3813 return absl::FailedPreconditionError("No active ROM session");
3814 }
3815 ResetAssetState(current_session);
3816
3817 auto start_time = std::chrono::steady_clock::now();
3818
3819#ifdef __EMSCRIPTEN__
3820 // Use passed handle if provided, otherwise create new one
3821 auto loading_handle =
3822 passed_handle != 0
3823 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3824 passed_handle)
3825 : app::platform::WasmLoadingManager::BeginLoading(
3826 "Loading Editor Assets");
3827
3828 // Progress starts at 10% (ROM already loaded), goes to 100%
3829 constexpr float kStartProgress = 0.10f;
3830 constexpr float kEndProgress = 1.0f;
3831 constexpr int kTotalSteps = 11; // Graphics + 8 editors + profiler + finish
3832 int current_step = 0;
3833 auto update_progress = [&](const std::string& message) {
3834 current_step++;
3835 float progress =
3836 kStartProgress + (kEndProgress - kStartProgress) *
3837 (static_cast<float>(current_step) / kTotalSteps);
3838 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, progress);
3839 app::platform::WasmLoadingManager::UpdateMessage(loading_handle, message);
3840 };
3841 // RAII guard to ensure loading indicator is closed even on early return
3842 auto cleanup_loading = [&]() {
3843 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3844 };
3845 struct LoadingGuard {
3846 std::function<void()> cleanup;
3847 bool dismissed = false;
3848 ~LoadingGuard() {
3849 if (!dismissed)
3850 cleanup();
3851 }
3852 void dismiss() { dismissed = true; }
3853 } loading_guard{cleanup_loading};
3854#else
3855 (void)passed_handle; // Unused on non-WASM
3856#endif
3857
3858 // Set renderer for emulator (lazy initialization happens in Run())
3859 if (renderer_) {
3861 }
3862
3863 const auto preload_editor_list = CollectEditorsToPreload(current_editor_set);
3864 const std::unordered_set<EditorType> preload_types(
3865 preload_editor_list.begin(), preload_editor_list.end());
3866
3867 // Initialize only the editors needed for the current startup surface. This
3868 // registers their windows and sets up editor-specific resources before Load().
3869 struct InitStep {
3870 EditorType type;
3871 bool mark_loaded;
3872 };
3873 const InitStep init_steps[] = {
3875 {EditorType::kGraphics, false}, {EditorType::kScreen, false},
3876 {EditorType::kSprite, false}, {EditorType::kPalette, false},
3877 {EditorType::kAssembly, true}, {EditorType::kMusic, false},
3878 {EditorType::kDungeon, false},
3879 };
3880 for (const auto& step : init_steps) {
3881 if (!preload_types.contains(step.type)) {
3882 continue;
3883 }
3884 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3885 editor->Initialize();
3886 MarkEditorInitialized(current_session, step.type);
3887 if (step.mark_loaded) {
3888 MarkEditorLoaded(current_session, step.type);
3889 }
3890 }
3891 }
3892
3893#ifdef __EMSCRIPTEN__
3894 update_progress("Loading graphics sheets...");
3895#endif
3896 // Load all Zelda3-specific data (metadata, palettes, gfx groups, graphics)
3897 gfx::PaletteManager::Get().ReleaseSession(&current_session->game_data);
3899 zelda3::LoadGameData(*current_rom, current_session->game_data));
3900 current_session->game_data_loaded = true;
3901
3902 // Copy loaded graphics to Arena for global access
3904 current_session->game_data.gfx_bitmaps;
3905
3906 // Propagate GameData to editors that already exist; future editors inherit it
3907 // on first construction via EditorSet.
3908 auto* game_data = &current_session->game_data;
3909 current_editor_set->SetGameData(game_data);
3911
3912 struct LoadStep {
3913 EditorType type;
3914 const char* progress_message;
3915 };
3916 const LoadStep load_steps[] = {
3917 {EditorType::kOverworld, "Loading overworld..."},
3918 {EditorType::kDungeon, "Loading dungeons..."},
3919 {EditorType::kScreen, "Loading screen editor..."},
3920 {EditorType::kGraphics, "Loading graphics editor..."},
3921 {EditorType::kSprite, "Loading sprites..."},
3922 {EditorType::kMessage, "Loading messages..."},
3923 {EditorType::kMusic, "Loading music..."},
3924 {EditorType::kPalette, "Loading palettes..."},
3925 };
3926 for (const auto& step : load_steps) {
3927 if (!preload_types.contains(step.type)) {
3928 continue;
3929 }
3930#ifdef __EMSCRIPTEN__
3931 update_progress(step.progress_message);
3932#endif
3933 if (auto* editor = current_editor_set->GetEditor(step.type)) {
3934 RETURN_IF_ERROR(editor->Load());
3935 MarkEditorLoaded(current_session, step.type);
3936 }
3937 }
3938
3939#ifdef __EMSCRIPTEN__
3940 update_progress("Finishing up...");
3941#endif
3942
3943 // Set up RightDrawerManager with session's settings editor
3945 auto* settings = current_editor_set->GetSettingsPanel();
3946 right_drawer_manager_->SetSettingsPanel(settings);
3947 }
3948
3949 // Apply user preferences to status bar
3951
3952 RestoreActiveEditorLayoutAfterAssets(current_session);
3953
3955
3956#ifdef __EMSCRIPTEN__
3957 // Dismiss the guard and manually close - we completed successfully
3958 loading_guard.dismiss();
3959 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3960#endif
3961
3962 auto end_time = std::chrono::steady_clock::now();
3963 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
3964 end_time - start_time);
3965 LOG_DEBUG("EditorManager", "ROM assets loaded in %lld ms", duration.count());
3966
3967 return absl::OkStatus();
3968}
3969
3970absl::Status EditorManager::LoadAssetsLazy(uint64_t passed_handle) {
3971 auto* current_rom = GetCurrentRom();
3972 auto* current_editor_set = GetCurrentEditorSet();
3973 if (!current_rom || !current_editor_set) {
3974 return absl::FailedPreconditionError("No ROM or editor set loaded");
3975 }
3976
3977 auto* current_session = session_coordinator_->GetActiveRomSession();
3978 if (!current_session) {
3979 return absl::FailedPreconditionError("No active ROM session");
3980 }
3981 ResetAssetState(current_session);
3982
3983#ifdef __EMSCRIPTEN__
3984 // Use passed handle if provided, otherwise create new one
3985 auto loading_handle =
3986 passed_handle != 0
3987 ? static_cast<app::platform::WasmLoadingManager::LoadingHandle>(
3988 passed_handle)
3989 : app::platform::WasmLoadingManager::BeginLoading(
3990 "Loading ROM (lazy assets)");
3991 auto cleanup_loading = [&]() {
3992 app::platform::WasmLoadingManager::EndLoading(loading_handle);
3993 };
3994 struct LoadingGuard {
3995 std::function<void()> cleanup;
3996 bool dismissed = false;
3997 ~LoadingGuard() {
3998 if (!dismissed) {
3999 cleanup();
4000 }
4001 }
4002 void dismiss() { dismissed = true; }
4003 } loading_guard{cleanup_loading};
4004#else
4005 (void)passed_handle; // Unused on non-WASM
4006#endif
4007
4008 // Set renderer for emulator (lazy initialization happens in Run())
4009 if (renderer_) {
4011 }
4012
4013 // Wire settings panel to right panel manager for the current session.
4015 auto* settings = current_editor_set->GetSettingsPanel();
4016 right_drawer_manager_->SetSettingsPanel(settings);
4017 }
4018
4019 // Apply user preferences to status bar
4021
4022#ifdef __EMSCRIPTEN__
4023 loading_guard.dismiss();
4024 app::platform::WasmLoadingManager::EndLoading(loading_handle);
4025#endif
4026
4027 LOG_INFO("EditorManager", "Lazy asset mode: editor assets deferred");
4028 return absl::OkStatus();
4029}
4030
4047 const std::optional<std::string>& target_filename) {
4048 return rom_lifecycle_.CheckRomWritePolicy(GetCurrentRom(), target_filename);
4049}
4050
4054
4060
4062 if (!pending_rom_save_.has_value() || !session_coordinator_) {
4063 return false;
4064 }
4065 return pending_rom_save_->session_index == GetCurrentSessionIndex() &&
4066 pending_rom_save_->rom != nullptr &&
4068}
4069
4071 pending_rom_save_.reset();
4073
4074 if (!hide_popups || !popup_manager_) {
4075 return;
4076 }
4077 for (const char* popup :
4080 if (popup_manager_->IsVisible(popup)) {
4081 popup_manager_->Hide(popup);
4082 }
4083 }
4084}
4085
4087 const std::optional<std::string>& save_as_filename) {
4088 auto* rom = GetCurrentRom();
4089 if (!session_coordinator_ || !session_coordinator_->GetActiveRomSession() ||
4090 !rom) {
4091 return absl::FailedPreconditionError("No active ROM session");
4092 }
4093
4096 PendingRomSave{save_as_filename, GetCurrentSessionIndex(), rom};
4097 return absl::OkStatus();
4098}
4099
4100void EditorManager::FinishPendingRomSaveAttempt(const absl::Status& status) {
4101 // A Cancelled status is resumable only while one of this request's explicit
4102 // confirmation dialogs is pending. Every other outcome is terminal and must
4103 // consume all one-shot bypasses so a later Ctrl+S cannot inherit consent.
4104 if (status.ok() || !absl::IsCancelled(status) ||
4107 }
4108}
4109
4111 // Do not let an autosave or an unrelated Ctrl+S replace a Save As request
4112 // while one of its confirmation dialogs is still open.
4113 if (pending_rom_save_.has_value()) {
4114 const bool unresolved_confirmation =
4119 if (unresolved_confirmation) {
4120 return absl::CancelledError("Save pending confirmation");
4121 }
4122 // Preserve the established request after programmatic confirmation too.
4123 // Older callers use ConfirmRomWrite(); SaveRom() rather than the popup's
4124 // ResumePendingRomSave() helper.
4125 return ResumePendingRomSave();
4126 }
4127 RETURN_IF_ERROR(StartPendingRomSave(std::nullopt));
4128 auto status = SaveRomInternal(std::nullopt);
4130 return status;
4131}
4132
4134 const std::optional<std::string>& save_as_filename) {
4135 auto* current_rom = GetCurrentRom();
4136 auto* current_editor_set = GetCurrentEditorSet();
4137 if (!current_rom || !current_editor_set) {
4138 return absl::FailedPreconditionError("No ROM or editor set loaded");
4139 }
4140 if (save_as_filename.has_value() && save_as_filename->empty()) {
4141 return absl::InvalidArgumentError("No filename provided for save as");
4142 }
4143 if (save_as_filename.has_value()) {
4144 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4145 *save_as_filename, GetCurrentSessionId()));
4146 }
4147
4148 ASSIGN_OR_RETURN(const project::YazeProject* save_project,
4150 // Hash/write/safety checks must read the same immutable session snapshot as
4151 // the rest of this save, never a process-global project left by another ROM.
4152 rom_lifecycle_.SetProjectContext(save_project);
4153 struct LifecycleProjectContextGuard {
4154 RomLifecycleManager* lifecycle = nullptr;
4155 project::YazeProject* restore = nullptr;
4156 ~LifecycleProjectContextGuard() {
4157 if (lifecycle) {
4158 lifecycle->SetProjectContext(restore);
4159 }
4160 }
4161 } lifecycle_project_guard{&rom_lifecycle_, &current_project_};
4162
4163 // GraphicsEditor tracks pixel edits in its model, not in the ROM buffer.
4164 // The previous global graphics writer was a success-returning stub, and
4165 // the editor-specific writer is not yet safe to join this coordinated save.
4166 // Block before any serializer can mutate the ROM rather than report a save
4167 // that silently omitted the pending sheets.
4168 if (current_editor_set->HasPendingGraphicsChanges()) {
4169 return absl::FailedPreconditionError(absl::StrFormat(
4170 "Save blocked: graphics sheet edits are pending, but graphics ROM "
4171 "persistence is not safely available (Save Graphics Sheets is %s). "
4172 "Discard the graphics sheet edits before saving the ROM.",
4173 core::FeatureFlags::get().kSaveGraphicsSheet ? "enabled" : "disabled"));
4174 }
4175
4176 // ScreenEditor keeps dungeon-map, Tile16, title-screen, and pause-map edits
4177 // in editor-owned models. Not every domain can safely participate in the
4178 // coordinated ROM transaction yet, so fail before any serializer mutates
4179 // the ROM instead of reporting a partial save as successful.
4180 if (current_editor_set->HasPendingScreenChanges()) {
4181 return absl::FailedPreconditionError(absl::StrFormat(
4182 "Save blocked: Screen Editor edits are pending, but they cannot all "
4183 "participate safely in the coordinated ROM save (Save Dungeon Maps is "
4184 "%s). Title-screen and pause-map ROM writes remain disabled until "
4185 "write/reopen/readback verification exists; discard the pending Screen "
4186 "Editor edits before saving the ROM.",
4187 core::FeatureFlags::get().kSaveDungeonMaps ? "enabled" : "disabled"));
4188 }
4189
4190 // --- State machine checks (delegated to RomLifecycleManager) ---
4192 return absl::CancelledError("Save pending confirmation");
4193 }
4194
4195 RETURN_IF_ERROR(CheckRomWritePolicy(save_as_filename));
4196
4198 return absl::CancelledError("Save pending confirmation");
4199 }
4200
4201 const bool pot_items_enabled =
4204 !rom_lifecycle_.ShouldSuppressPotItemSave() && pot_items_enabled) {
4205 const int loaded_rooms = current_editor_set->LoadedDungeonRoomCount();
4206 const int total_rooms = current_editor_set->TotalDungeonRoomCount();
4207 if (loaded_rooms < total_rooms) {
4208 rom_lifecycle_.SetPotItemConfirmPending(total_rooms - loaded_rooms,
4209 total_rooms);
4210 if (popup_manager_) {
4212 }
4214 absl::StrFormat(
4215 "Save paused: pot items enabled with %d unloaded rooms",
4218 return absl::CancelledError("Pot item save confirmation required");
4219 }
4220 }
4221
4222 const bool bypass_confirm = rom_lifecycle_.ShouldBypassPotItemConfirm();
4223 const bool suppress_pot_items = rom_lifecycle_.ShouldSuppressPotItemSave();
4225
4226 struct PotItemFlagGuard {
4227 bool restore = false;
4228 bool previous = false;
4229 ~PotItemFlagGuard() {
4230 if (restore) {
4231 core::FeatureFlags::get().dungeon.kSavePotItems = previous;
4232 }
4233 }
4234 } pot_item_guard;
4235
4236 if (suppress_pot_items) {
4237 pot_item_guard.previous = core::FeatureFlags::get().dungeon.kSavePotItems;
4238 pot_item_guard.restore = true;
4240 } else if (bypass_confirm) {
4241 // Explicitly allow pot item save once after confirmation.
4242 }
4243
4244 // --- Backup policy setup ---
4245 if (save_project->project_opened()) {
4247 save_project->workspace_settings.backup_on_save,
4248 save_project->GetAbsolutePath(save_project->rom_backup_folder),
4252 } else {
4254 user_settings_.prefs().backup_before_save, "", 20, true, 14);
4255 }
4256
4257 // Reject an already-invalid Oracle layout before any editor serializer can
4258 // mutate the ROM buffer or clear its own dirty tracking. The post-save check
4259 // below remains mandatory because serializers may introduce a violation.
4261
4262 // Serializers clear their dirty flags after a successful in-memory write.
4263 // Capture their conservative write prediction first so conflict protection
4264 // still has a fail-closed fallback when the on-disk ROM cannot be read.
4265 const auto predicted_dungeon_write_ranges =
4266 current_editor_set->CollectDungeonWriteRanges();
4267
4268 ScopedRomTransaction rom_transaction(*current_rom);
4269 ScopedEditorSaveTransactions editor_transactions;
4270
4271 auto save_editor = [&editor_transactions](Editor* editor) -> absl::Status {
4272 if (editor == nullptr) {
4273 return absl::OkStatus();
4274 }
4275 RETURN_IF_ERROR(editor_transactions.Begin(editor));
4276 return editor->Save();
4277 };
4278
4279 // --- Save editor-specific data ---
4280 auto* screen_editor = static_cast<ScreenEditor*>(
4281 current_editor_set->GetExistingEditor(EditorType::kScreen));
4282 // A failed lazy Screen load leaves an invalid, clean editor behind. It has
4283 // nothing to serialize and must not poison unrelated ROM saves.
4284 if (screen_editor != nullptr && (screen_editor->IsRomBackedStateValid() ||
4285 screen_editor->HasPendingScreenChanges())) {
4286 RETURN_IF_ERROR(save_editor(screen_editor));
4287 }
4288 // A coordinated save must not materialize an unrelated lazy editor. Its
4289 // serializer can project broad writes (and correctly trip manifest guards)
4290 // even though the user never opened or edited that domain.
4292 save_editor(current_editor_set->GetExistingEditor(EditorType::kDungeon)));
4293 RETURN_IF_ERROR(save_editor(
4294 current_editor_set->GetExistingEditor(EditorType::kOverworld)));
4295
4296 if (core::FeatureFlags::get().kSaveMessages) {
4299 save_editor(current_editor_set->GetEditor(EditorType::kMessage)));
4300 }
4301
4302 // Oracle guardrails: refuse to write obviously corrupted ROM layouts.
4304
4305 // --- Write conflict check (ASM-owned address protection) ---
4306 if (save_project->project_opened() && save_project->hack_manifest.loaded()) {
4308 std::vector<std::pair<uint32_t, uint32_t>> write_ranges;
4309 bool diff_computed = false;
4310
4311 if (!current_rom->filename().empty()) {
4312 std::ifstream file(current_rom->filename(), std::ios::binary);
4313 if (file.is_open()) {
4314 file.seekg(0, std::ios::end);
4315 const std::streampos end = file.tellg();
4316 if (end >= 0) {
4317 std::vector<uint8_t> disk_data(static_cast<size_t>(end));
4318 file.seekg(0, std::ios::beg);
4319 file.read(reinterpret_cast<char*>(disk_data.data()),
4320 static_cast<std::streamsize>(disk_data.size()));
4321 if (file) {
4322 diff_computed = true;
4323 auto diff = yaze::rom::ComputeDiffRanges(disk_data,
4324 current_rom->vector());
4325 if (!diff.ranges.empty()) {
4326 LOG_DEBUG("EditorManager",
4327 "ROM save diff: %zu bytes changed in %zu range(s)",
4328 diff.total_bytes_changed, diff.ranges.size());
4329 write_ranges = std::move(diff.ranges);
4330 }
4331 }
4332 }
4333 }
4334 }
4335
4336 if (write_ranges.empty() && !diff_computed) {
4337 write_ranges = predicted_dungeon_write_ranges;
4338 }
4339
4340 if (!write_ranges.empty()) {
4341 auto conflicts =
4342 save_project->hack_manifest.AnalyzePcWriteRanges(write_ranges);
4343 if (!conflicts.empty()) {
4344 rom_lifecycle_.SetPendingWriteConflicts(std::move(conflicts));
4345 if (popup_manager_) {
4347 }
4349 absl::StrFormat(
4350 "Save paused: %zu write conflict(s) with ASM hooks",
4353 return absl::CancelledError("Write conflict confirmation required");
4354 }
4355 }
4356 } else {
4357 // Bypass is single-use, set by the warning popup.
4359 }
4360 }
4361
4362 // Delegate the final atomic disk write to RomFileManager. Save As is part of
4363 // this same transaction and writes only the requested target path.
4364 auto save_status =
4365 save_as_filename.has_value()
4366 ? rom_file_manager_.SaveRomAs(current_rom, *save_as_filename)
4367 : rom_file_manager_.SaveRom(current_rom);
4368 if (save_status.ok()) {
4369 editor_transactions.Commit();
4370 rom_transaction.Commit();
4373 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4374 session->backup_restore_pending = false;
4375 }
4376 }
4377 // Write-confirm bypass is single-use. Clear it after a successful save.
4379
4380 if (save_as_filename.has_value()) {
4382 if (auto* session = session_coordinator_->GetActiveRomSession()) {
4383 session->filepath = *save_as_filename;
4384 }
4385 }
4386
4388 manager.AddFile(*save_as_filename);
4389 manager.Save();
4390 if (popup_manager_) {
4392 }
4393 }
4394 }
4395 return save_status;
4396}
4397
4398absl::Status EditorManager::SaveRomAs(const std::string& filename) {
4399 if (filename.empty()) {
4400 return absl::InvalidArgumentError("No filename provided for save as");
4401 }
4402 if (pending_rom_save_.has_value() && HasPendingRomSaveConfirmation()) {
4403 return absl::CancelledError("Save pending confirmation");
4404 }
4406 auto status = SaveRomInternal(filename);
4408 return status;
4409}
4410
4412 if (!pending_rom_save_.has_value()) {
4414 return absl::FailedPreconditionError("No pending ROM save to resume");
4415 }
4417 CancelPendingRomSave(/*hide_popups=*/true);
4418 return absl::FailedPreconditionError(
4419 "Pending ROM save belongs to a different session");
4420 }
4421
4422 const auto save_as_filename = pending_rom_save_->save_as_filename;
4423 auto status = SaveRomInternal(save_as_filename);
4425 return status;
4426}
4427
4428absl::Status EditorManager::OpenRomOrProject(const std::string& filename) {
4431 GetCurrentSessionId(), SIZE_MAX, filename})) {
4432 return absl::OkStatus();
4433 }
4434
4435 return OpenRomOrProjectInternal(filename);
4436}
4437
4439 const std::string& filename) {
4440 LOG_INFO("EditorManager", "OpenRomOrProject called with: '%s'",
4441 filename.c_str());
4442 if (filename.empty()) {
4443 LOG_INFO("EditorManager", "Empty filename provided, skipping load.");
4444 return absl::OkStatus();
4445 }
4446
4447#ifdef __EMSCRIPTEN__
4448 // Start loading indicator early for WASM builds
4449 auto loading_handle =
4450 app::platform::WasmLoadingManager::BeginLoading("Loading ROM");
4451 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4452 "Reading ROM file...");
4453 // RAII guard to ensure loading indicator is closed even on early return
4454 struct LoadingGuard {
4455 app::platform::WasmLoadingManager::LoadingHandle handle;
4456 bool dismissed = false;
4457 ~LoadingGuard() {
4458 if (!dismissed)
4459 app::platform::WasmLoadingManager::EndLoading(handle);
4460 }
4461 void dismiss() { dismissed = true; }
4462 } loading_guard{loading_handle};
4463#endif
4464
4465 if (absl::EndsWith(filename, ".yaze") ||
4466 absl::EndsWith(filename, ".zsproj") ||
4467 absl::EndsWith(filename, ".yazeproj")) {
4468 // Parse first so a failed project open cannot destroy the active session's
4469 // working context. Detach only when the incoming project is ready.
4470 project::YazeProject incoming_project;
4471 RETURN_IF_ERROR(incoming_project.Open(filename));
4472 if (!incoming_project.rom_filename.empty()) {
4473 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4474 incoming_project.rom_filename));
4475 }
4476 const std::optional<size_t> previous_session_id =
4479 pending_project_open_previous_session_id_ = previous_session_id;
4481 current_project_ = std::move(incoming_project);
4484
4485 // Load ROM directly from project - don't prompt user
4486 auto project_status = LoadProjectWithRom();
4487 if (!project_status.ok()) {
4488 RestoreProjectContextAfterFailedOpen(previous_session_id);
4489 }
4490 return project_status;
4491 } else {
4492#ifdef __EMSCRIPTEN__
4493 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.05f);
4494 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4495 "Loading ROM data...");
4496#endif
4497 Rom temp_rom;
4498 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(filename));
4499 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, filename));
4501
4502 const std::optional<size_t> previous_session_id =
4504 const size_t previous_session_count =
4505 session_coordinator_->GetTotalSessionCount();
4506 auto session_or = session_coordinator_->CreateSessionFromRom(
4507 std::move(temp_rom), filename);
4508 if (!session_or.ok()) {
4509 return session_or.status();
4510 }
4511 RomSession* session = *session_or;
4512
4516
4517 // Apply project feature flags to both session and global singleton
4521
4522 // Keep ResourceLabelProvider in sync with the active ROM session before
4523 // editors register room/sprite labels.
4525
4526 // Update test manager with current ROM for ROM-dependent tests (only when
4527 // tests are enabled)
4528#ifdef YAZE_ENABLE_TESTING
4529 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
4530 (void*)GetCurrentRom(),
4531 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
4533#endif
4534
4535 if (auto* editor_set = GetCurrentEditorSet();
4536 editor_set && !current_project_.code_folder.empty()) {
4537 const std::string absolute_code_folder =
4539 // iOS: avoid blocking the main thread during project open / scene updates.
4540 // Large iCloud-backed projects can trigger watchdog termination if we
4541 // eagerly enumerate folders here.
4542#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
4543 editor_set->OpenAssemblyFolder(absolute_code_folder);
4544#endif
4545 // Also set the sidebar file browser path (refresh happens during UI draw).
4546 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
4547 }
4548
4549#ifdef __EMSCRIPTEN__
4550 app::platform::WasmLoadingManager::UpdateProgress(loading_handle, 0.10f);
4551 app::platform::WasmLoadingManager::UpdateMessage(loading_handle,
4552 "Initializing editors...");
4553 // Pass the loading handle to LoadAssets and dismiss our guard
4554 // LoadAssets will manage closing the indicator when done
4555 loading_guard.dismiss();
4556 auto asset_status = LoadAssetsForMode(loading_handle);
4557#else
4558 auto asset_status = LoadAssetsForMode();
4559#endif
4560 if (!asset_status.ok()) {
4561 auto rollback_status =
4562 DiscardProvisionalSessionCreatedSince(previous_session_count);
4563 RestoreProjectContextAfterFailedOpen(previous_session_id);
4564 if (!rollback_status.ok()) {
4565 return absl::InternalError(
4566 absl::StrFormat("%s; session rollback failed: %s",
4567 asset_status.message(), rollback_status.message()));
4568 }
4569 return asset_status;
4570 }
4571
4572 // Hide welcome screen and show editor selection when ROM is loaded
4573 ui_coordinator_->SetWelcomeScreenVisible(false);
4574 // dashboard_panel_->ClearRecentEditors();
4575 ui_coordinator_->SetEditorSelectionVisible(true);
4576
4577 // Set Dashboard category to suppress panel drawing until user selects an editor
4580 /*notify=*/false);
4581 }
4582 return absl::OkStatus();
4583}
4584
4585absl::Status EditorManager::CreateNewProject(const std::string& template_name) {
4587 return absl::FailedPreconditionError(
4588 "Save or discard pending session work before creating a project");
4589 }
4590
4591 if (!ui_coordinator_) {
4592 return absl::FailedPreconditionError(
4593 "Project creation dialog is not available");
4594 }
4595 ui_coordinator_->OpenNewProjectDialog(
4596 template_name.empty() ? "Vanilla ROM Hack" : template_name);
4597 return absl::OkStatus();
4598}
4599
4601 const std::string& template_name, const std::string& rom_path,
4602 const std::string& project_name, const std::string& project_path) {
4603 if (rom_path.empty() || project_name.empty()) {
4604 return absl::InvalidArgumentError("ROM path and project name are required");
4605 }
4606 if (absl::EndsWith(rom_path, ".yaze") ||
4607 absl::EndsWith(rom_path, ".yazeproj") ||
4608 absl::EndsWith(rom_path, ".zsproj")) {
4609 return absl::InvalidArgumentError(
4610 "New projects require a ROM file, not a project descriptor");
4611 }
4613 return absl::FailedPreconditionError(
4614 "Save or discard pending session work before creating a project");
4615 }
4616
4617 auto* reusable_session = session_coordinator_
4618 ? session_coordinator_->GetActiveRomSession()
4619 : nullptr;
4620 const bool reuse_active_raw_session =
4621 reusable_session && reusable_session->rom.is_loaded() &&
4623 rom_path, reusable_session->rom.filename()) &&
4626 const std::optional<size_t> previous_session_id =
4628 const size_t previous_session_count =
4629 session_coordinator_ ? session_coordinator_->GetTotalSessionCount() : 0;
4631 auto rom_status = project_manager_.SetProjectRom(rom_path);
4632 if (!rom_status.ok()) {
4634 return rom_status;
4635 }
4637 project_name, project_path);
4638 if (!target_status.ok()) {
4640 return target_status;
4641 }
4642
4643 if (reuse_active_raw_session) {
4644 return FinalizeNewProject(project_name, project_path);
4645 }
4646
4650
4651 auto open_status = OpenRomOrProjectInternal(rom_path);
4652 if (!open_status.ok()) {
4654 RestoreProjectContextAfterFailedOpen(previous_session_id);
4655 return open_status;
4656 }
4657
4658 auto finalize_status = FinalizeNewProject(project_name, project_path);
4659 if (!finalize_status.ok()) {
4660 auto rollback_status =
4661 DiscardProvisionalSessionCreatedSince(previous_session_count);
4662 RestoreProjectContextAfterFailedOpen(previous_session_id);
4663 if (!rollback_status.ok()) {
4664 return absl::InternalError(absl::StrFormat(
4665 "%s; session rollback failed: %s", finalize_status.message(),
4666 rollback_status.message()));
4667 }
4668 }
4669 return finalize_status;
4670}
4671
4673 const std::string& project_name, const std::string& project_path) {
4674 if (!session_coordinator_) {
4675 return absl::FailedPreconditionError("No session coordinator");
4676 }
4677 auto* session = session_coordinator_->GetActiveRomSession();
4678 if (!session || !session->rom.is_loaded()) {
4679 return absl::FailedPreconditionError(
4680 "Load a ROM before finalizing the project");
4681 }
4682
4683 // The ROM-load-options dialog can create a project directly from an
4684 // already-open raw ROM. Start from a clean shell in that path rather than
4685 // reusing ProjectManager state left by an earlier project/import.
4686 const bool created_project_shell = !project_manager_.IsPendingRomSelection();
4687 if (created_project_shell) {
4689 }
4690
4691 auto project_rom_status =
4692 project_manager_.SetProjectRom(session->rom.filename());
4693 if (!project_rom_status.ok()) {
4695 return project_rom_status;
4696 }
4697 auto& pending_project = project_manager_.GetCurrentProject();
4698 if (created_project_shell) {
4699 // ROM-load options apply their selected flags directly to the raw session.
4700 // Guided creation already configured the pending template, including when
4701 // it reuses an existing raw-ROM session.
4702 pending_project.feature_flags = session->feature_flags;
4703 }
4704 pending_project.workspace_settings.font_global_scale =
4706 pending_project.workspace_settings.autosave_enabled =
4708 pending_project.workspace_settings.autosave_interval_secs =
4710 pending_project.workspace_settings.backup_on_save =
4712
4713 auto finalize_status =
4714 project_manager_.FinalizeProjectCreation(project_name, project_path);
4715 if (!finalize_status.ok()) {
4717 return finalize_status;
4718 }
4719
4722 session->project_dirty = false;
4723 active_project_context_session_id_ = session->session_id();
4724 runtime_feature_flags_session_id_ = session->session_id();
4726 if (version_manager_) {
4728 current_project_.git_repository = session->project_context->git_repository;
4730 }
4734 return absl::OkStatus();
4735}
4736
4740 GetCurrentSessionId()})) {
4741 return absl::OkStatus();
4742 }
4743
4744 return OpenProjectInternal();
4745}
4746
4748 auto open_project_from_path =
4749 [this](const std::string& file_path) -> absl::Status {
4750 if (file_path.empty()) {
4751 return absl::OkStatus();
4752 }
4753
4754 project::YazeProject new_project;
4755 RETURN_IF_ERROR(new_project.Open(file_path));
4756
4757 if (!new_project.rom_filename.empty()) {
4758 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
4759 new_project.rom_filename));
4760 }
4761
4762 // Validate project
4763 auto validation_status = new_project.Validate();
4764 if (!validation_status.ok()) {
4765 toast_manager_.Show(absl::StrFormat("Project validation failed: %s",
4766 validation_status.message()),
4768
4769 // Ask user if they want to repair
4770 popup_manager_->Show("Project Repair");
4771 }
4772
4773 const std::optional<size_t> previous_session_id =
4776 pending_project_open_previous_session_id_ = previous_session_id;
4778 current_project_ = std::move(new_project);
4780
4781 auto project_status = LoadProjectWithRom();
4782 if (!project_status.ok()) {
4783 RestoreProjectContextAfterFailedOpen(previous_session_id);
4784 }
4785 return project_status;
4786 };
4787
4788#if defined(__APPLE__) && TARGET_OS_IOS == 1
4789 // On iOS, route project selection through the SwiftUI overlay document picker
4790 // so we get open-in-place + security-scoped access for iCloud Drive bundles.
4791 platform::ios::PostOverlayCommand("open_project");
4792 return absl::OkStatus();
4793#else
4795 return open_project_from_path(file_path);
4796#endif
4797}
4798
4800 const std::string& rom_path) {
4801 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4802
4803 Rom candidate_rom;
4804 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&candidate_rom, rom_path));
4806
4809 .candidate_path = rom_path,
4810 };
4811 return absl::OkStatus();
4812}
4813
4815 auto pending_selection = std::move(pending_project_rom_selection_);
4817 std::string project_rom_path = pending_selection.has_value()
4818 ? pending_selection->candidate_path
4820 const std::string previous_project_rom_path =
4821 pending_selection.has_value() ? pending_selection->previous_path
4823 bool persist_selected_rom = pending_selection.has_value();
4824 auto abandon_selected_rom = [&]() {
4825 if (!persist_selected_rom) {
4826 return;
4827 }
4828 current_project_.rom_filename = previous_project_rom_path;
4830 };
4831
4832 // Check if project has a ROM file specified
4833 if (project_rom_path.empty()) {
4834 // No ROM specified - prompt user to select one
4836 "Project has no ROM file configured. Please select a ROM.",
4838#if defined(__APPLE__) && TARGET_OS_IOS == 1
4839 // Guard: if the project lives inside a .yazeproj bundle the ROM path
4840 // defaults to bundle/rom, which may not exist yet because iCloud hasn't
4841 // finished the download. Popping the file picker here would let a
4842 // temporary path overwrite the correct bundle path in project.yaze.
4843 // Show guidance and let the user reopen the project once the download
4844 // is complete.
4845 {
4846 auto bundle_parent =
4847 std::filesystem::path(current_project_.filepath).parent_path();
4848 if (bundle_parent.extension() == ".yazeproj") {
4850 "ROM is downloading from iCloud. Reopen the project in a few "
4851 "seconds.",
4852 ToastType::kInfo, 6.0f);
4853 return absl::UnavailableError(
4854 "Project ROM is not available from iCloud yet");
4855 }
4856 }
4857 const std::string target_project_filepath = current_project_.filepath;
4858 const std::optional<size_t> target_session_id =
4860 const uint64_t selection_generation = ++project_rom_selection_generation_;
4863 [this, target_project_filepath, target_session_id,
4864 selection_generation](const std::string& rom_path) {
4865 if (selection_generation != project_rom_selection_generation_ ||
4866 current_project_.filepath != target_project_filepath ||
4867 active_project_context_session_id_ != target_session_id) {
4868 toast_manager_.Show(
4869 "ROM selection ignored because the active project changed",
4870 ToastType::kWarning);
4871 return;
4872 }
4873 const bool restore_pending_transition =
4875 const std::optional<size_t> previous_session_id =
4877 auto restore_after_failure = [&]() {
4878 if (restore_pending_transition) {
4879 RestoreProjectContextAfterFailedOpen(previous_session_id);
4880 }
4881 };
4882 if (rom_path.empty()) {
4883 restore_after_failure();
4884 return;
4885 }
4886 auto path_status =
4887 session_coordinator_->CheckBackingFileAvailable(rom_path);
4888 if (!path_status.ok()) {
4889 toast_manager_.Show(std::string(path_status.message()),
4891 restore_after_failure();
4892 return;
4893 }
4894 auto selection_status = ValidateProjectRomSelection(rom_path);
4895 if (!selection_status.ok()) {
4897 absl::StrFormat("Failed to update project ROM: %s",
4898 selection_status.message()),
4900 restore_after_failure();
4901 return;
4902 }
4903 auto status = LoadProjectWithRom();
4904 if (!status.ok()) {
4906 absl::StrFormat("Failed to load project ROM: %s",
4907 status.message()),
4909 restore_after_failure();
4910 }
4911 });
4912 return absl::OkStatus();
4913#else
4916 if (rom_path.empty()) {
4917 return absl::CancelledError("Project ROM selection cancelled");
4918 }
4919 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
4920 project_rom_path = rom_path;
4921 persist_selected_rom = true;
4922#endif
4923 }
4924
4925 auto backing_status =
4926 session_coordinator_->CheckBackingFileAvailable(project_rom_path);
4927 if (!backing_status.ok()) {
4928 abandon_selected_rom();
4929 return backing_status;
4930 }
4931
4932 // Load ROM from project
4933 Rom temp_rom;
4934 auto load_status = rom_file_manager_.LoadRom(&temp_rom, project_rom_path);
4935 if (!load_status.ok()) {
4936 // ROM file not found or invalid - prompt user to select new ROM
4938 absl::StrFormat("Could not load ROM '%s': %s. Please select a new ROM.",
4939 project_rom_path, load_status.message()),
4941#if defined(__APPLE__) && TARGET_OS_IOS == 1
4942 abandon_selected_rom();
4943 // If the ROM is inside a .yazeproj bundle its path may not be readable
4944 // yet because iCloud hasn't finished downloading it. Saving any other
4945 // path here would corrupt the project file with a temporary location.
4946 // Show guidance and bail without touching current_project_.rom_filename.
4947 {
4948 auto rom_parent = std::filesystem::path(project_rom_path).parent_path();
4949 if (rom_parent.extension() == ".yazeproj") {
4951 "ROM is still downloading from iCloud. Try reopening the project "
4952 "in a moment.",
4953 ToastType::kInfo, 6.0f);
4954 return absl::UnavailableError(
4955 "Project ROM is still downloading from iCloud");
4956 }
4957 }
4958 const std::string target_project_filepath = current_project_.filepath;
4959 const std::optional<size_t> target_session_id =
4961 const uint64_t selection_generation = ++project_rom_selection_generation_;
4964 [this, target_project_filepath, target_session_id,
4965 selection_generation](const std::string& rom_path) {
4966 if (selection_generation != project_rom_selection_generation_ ||
4967 current_project_.filepath != target_project_filepath ||
4968 active_project_context_session_id_ != target_session_id) {
4969 toast_manager_.Show(
4970 "ROM selection ignored because the active project changed",
4971 ToastType::kWarning);
4972 return;
4973 }
4974 const bool restore_pending_transition =
4976 const std::optional<size_t> previous_session_id =
4978 auto restore_after_failure = [&]() {
4979 if (restore_pending_transition) {
4980 RestoreProjectContextAfterFailedOpen(previous_session_id);
4981 }
4982 };
4983 if (rom_path.empty()) {
4984 restore_after_failure();
4985 return;
4986 }
4987 auto path_status =
4988 session_coordinator_->CheckBackingFileAvailable(rom_path);
4989 if (!path_status.ok()) {
4990 toast_manager_.Show(std::string(path_status.message()),
4992 restore_after_failure();
4993 return;
4994 }
4995 auto selection_status = ValidateProjectRomSelection(rom_path);
4996 if (!selection_status.ok()) {
4998 absl::StrFormat("Failed to update project ROM: %s",
4999 selection_status.message()),
5001 restore_after_failure();
5002 return;
5003 }
5004 auto status = LoadProjectWithRom();
5005 if (!status.ok()) {
5007 absl::StrFormat("Failed to load project ROM: %s",
5008 status.message()),
5010 restore_after_failure();
5011 }
5012 });
5013 return absl::OkStatus();
5014#else
5017 if (rom_path.empty()) {
5018 return absl::CancelledError("Project ROM selection cancelled");
5019 }
5020 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
5021 project_rom_path = rom_path;
5022 persist_selected_rom = true;
5023 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, rom_path));
5024#endif
5025 }
5026
5027 auto policy_status = rom_lifecycle_.CheckRomOpenPolicy(&temp_rom);
5028 if (!policy_status.ok()) {
5029 abandon_selected_rom();
5030 return policy_status;
5031 }
5032
5033 if (persist_selected_rom) {
5034 current_project_.rom_filename = project_rom_path;
5035 }
5036
5037 const size_t previous_session_count =
5038 session_coordinator_->GetTotalSessionCount();
5039 auto session_or = session_coordinator_->CreateSessionFromRom(
5040 std::move(temp_rom), project_rom_path);
5041 if (!session_or.ok()) {
5042 abandon_selected_rom();
5043 return session_or.status();
5044 }
5045 RomSession* session = *session_or;
5046
5050
5051 // Auto-enable custom object rendering when a project defines custom object
5052 // data but the stale feature flag is off.
5053 if (ProjectUsesCustomObjects(current_project_) &&
5056 LOG_WARN("EditorManager",
5057 "Project has custom object data but 'enable_custom_objects' was "
5058 "disabled. Enabling at runtime.");
5059 toast_manager_.Show("Custom object rendering auto-enabled for this project",
5061 }
5062 std::string legacy_mapping_warning;
5063 if (SeedLegacyTrackObjectMapping(&current_project_,
5064 &legacy_mapping_warning)) {
5065 LOG_WARN("EditorManager", "%s", legacy_mapping_warning.c_str());
5067 "Seeded default custom object mapping for object 0x31 (save project "
5068 "to persist)",
5070 }
5071
5072 // Apply project feature flags to both session and global singleton.
5076#if !defined(NDEBUG)
5077 LOG_INFO(
5078 "EditorManager",
5079 "Feature flags applied: kEnableCustomObjects=%s, "
5080 "custom_objects_folder='%s', custom_object_files=%zu entries",
5084#endif
5085
5088 if (version_manager_) {
5089 // Preserve the existing best-effort Git initialization behavior, now
5090 // against the stable session-owned project object.
5092 current_project_.git_repository = session->project_context->git_repository;
5093 }
5094
5095 if (auto* rom = GetCurrentRom(); rom && rom->is_loaded()) {
5096 if (IsRomHashMismatch()) {
5098 "Project ROM hash mismatch detected. Check ROM Identity settings.",
5100 }
5101 auto warnings = ValidateRomAddressOverrides(
5103 if (!warnings.empty()) {
5104 for (const auto& warning : warnings) {
5105 LOG_WARN("EditorManager", "%s", warning.c_str());
5106 }
5107 toast_manager_.Show(absl::StrFormat("ROM override warnings: %d (see log)",
5108 warnings.size()),
5110 }
5111 }
5112
5113 // Update test manager with current ROM for ROM-dependent tests (only when
5114 // tests are enabled)
5115#ifdef YAZE_ENABLE_TESTING
5116 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
5117 (void*)GetCurrentRom(),
5118 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
5120#endif
5121
5122 if (auto* editor_set = GetCurrentEditorSet();
5123 editor_set && !current_project_.code_folder.empty()) {
5124 const std::string absolute_code_folder =
5126 // iOS: avoid blocking the main thread during project open / scene updates.
5127#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
5128 editor_set->OpenAssemblyFolder(absolute_code_folder);
5129#endif
5130 // Also set the sidebar file browser path (refresh happens during UI draw).
5131 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
5132 }
5133
5134 // Initialize labels before loading editor assets so room lists / command
5135 // palette entries resolve project registry labels on first render.
5137
5138 auto asset_status = LoadAssetsForMode();
5139 if (!asset_status.ok()) {
5140 abandon_selected_rom();
5141 auto rollback_status =
5142 DiscardProvisionalSessionCreatedSince(previous_session_count);
5143 if (!rollback_status.ok()) {
5144 return absl::InternalError(
5145 absl::StrFormat("%s; session rollback failed: %s",
5146 asset_status.message(), rollback_status.message()));
5147 }
5148 return asset_status;
5149 }
5150
5151 if (persist_selected_rom) {
5152 auto save_status = current_project_.Save();
5153 if (!save_status.ok()) {
5154 abandon_selected_rom();
5155 auto rollback_status =
5156 DiscardProvisionalSessionCreatedSince(previous_session_count);
5157 if (!rollback_status.ok()) {
5158 return absl::InternalError(
5159 absl::StrFormat("%s; session rollback failed: %s",
5160 save_status.message(), rollback_status.message()));
5161 }
5162 return save_status;
5163 }
5165 }
5166
5167 // Hide welcome screen and show editor selection when project ROM is loaded
5168 if (ui_coordinator_) {
5169 ui_coordinator_->SetWelcomeScreenVisible(false);
5170 ui_coordinator_->SetEditorSelectionVisible(true);
5171 }
5172
5173 // Set Dashboard category to suppress panel drawing until user selects an editor
5175 /*notify=*/false);
5176
5177 // Apply workspace settings
5186 ImGui::GetIO().FontGlobalScale = user_settings_.prefs().font_global_scale;
5187
5190
5198 project_management_panel_->SetBuildLogOutput("");
5199 }
5201 active_project_build_.reset();
5203
5204 // Publish project context for hack workflow panels and other consumers
5206
5207 // Add to recent files
5209 manager.AddFile(current_project_.filepath);
5210 manager.Save();
5211
5212 // Update project management panel with loaded project
5215 project_management_panel_->SetVersionManager(version_manager_);
5217 }
5218
5219 toast_manager_.Show(absl::StrFormat("Project '%s' loaded successfully",
5222
5225 return absl::OkStatus();
5226}
5227
5229 Rom&& rom, const std::string& filepath) {
5230 if (!session_coordinator_) {
5231 return absl::FailedPreconditionError("No session coordinator");
5232 }
5233 auto* session = session_coordinator_->GetActiveRomSession();
5234 if (!session) {
5235 return absl::FailedPreconditionError("No active ROM session");
5236 }
5237
5238 Rom previous_rom = session->rom;
5239 const std::string previous_filepath = session->filepath;
5240 const bool game_data_was_loaded = session->game_data_loaded;
5241 auto* dungeon_editor = static_cast<DungeonEditorV2*>(
5242 session->editors.GetExistingEditor(EditorType::kDungeon));
5243 const size_t dungeon_index = EditorTypeIndex(EditorType::kDungeon);
5244 const bool dungeon_was_initialized =
5245 session->editor_initialized[dungeon_index];
5246 const bool dungeon_assets_were_loaded =
5247 session->editor_assets_loaded[dungeon_index];
5248 auto* screen_editor = static_cast<ScreenEditor*>(
5249 session->editors.GetExistingEditor(EditorType::kScreen));
5250 const size_t screen_index = EditorTypeIndex(EditorType::kScreen);
5251 const bool screen_was_initialized = session->editor_initialized[screen_index];
5252 const bool screen_assets_were_loaded =
5253 session->editor_assets_loaded[screen_index];
5254 auto restore_dungeon_asset_state = [&]() {
5255 if (dungeon_editor) {
5256 session->editor_initialized[dungeon_index] =
5257 session->editor_initialized[dungeon_index] || dungeon_was_initialized;
5258 session->editor_assets_loaded[dungeon_index] =
5259 session->editor_assets_loaded[dungeon_index] ||
5260 dungeon_assets_were_loaded;
5261 }
5262 };
5263 auto restore_screen_asset_state = [&]() {
5264 if (screen_editor) {
5265 session->editor_initialized[screen_index] =
5266 session->editor_initialized[screen_index] || screen_was_initialized;
5267 session->editor_assets_loaded[screen_index] =
5268 session->editor_assets_loaded[screen_index] ||
5269 screen_assets_were_loaded;
5270 }
5271 };
5272
5273 session->editors.InvalidateScreenRomBackedState();
5274 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5275 session->rom = std::move(rom);
5276 session->filepath = filepath;
5277 session->game_data.Clear();
5278 session->game_data.set_rom(&session->rom);
5279 session->editors.SetGameData(&session->game_data);
5280 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5281 ResetAssetState(session);
5282 ConfigureSession(session);
5285 auto load_status = LoadAssetsForMode();
5286 if (load_status.ok() && game_data_was_loaded && !session->game_data_loaded) {
5287 load_status = EnsureGameDataLoaded();
5288 }
5289 if (load_status.ok() && screen_editor && screen_assets_were_loaded) {
5290 load_status = screen_editor->RefreshRomBackedState();
5291 }
5292 if (load_status.ok()) {
5293 restore_screen_asset_state();
5294 }
5295 if (load_status.ok() && dungeon_editor) {
5296 load_status = dungeon_editor->RefreshRomBackedState();
5297 if (load_status.ok()) {
5298 restore_dungeon_asset_state();
5299 }
5300 }
5301 if (!load_status.ok()) {
5302 // Loading editors is fallible. Restore the prior ROM and rebuild its
5303 // assets so a failed reload never leaves a half-initialized live session.
5304 session->editors.InvalidateScreenRomBackedState();
5305 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
5306 session->rom = std::move(previous_rom);
5307 session->filepath = previous_filepath;
5308 session->game_data.Clear();
5309 session->game_data.set_rom(&session->rom);
5310 session->editors.SetGameData(&session->game_data);
5311 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
5312 ResetAssetState(session);
5313 ConfigureSession(session);
5316 auto rollback_status = LoadAssetsForMode();
5317 if (game_data_was_loaded && !session->game_data_loaded) {
5318 auto game_data_status = EnsureGameDataLoaded();
5319 if (rollback_status.ok()) {
5320 rollback_status = game_data_status;
5321 }
5322 }
5323 if (screen_editor && screen_assets_were_loaded) {
5324 auto screen_status = screen_editor->RefreshRomBackedState();
5325 if (screen_status.ok()) {
5326 restore_screen_asset_state();
5327 }
5328 if (rollback_status.ok()) {
5329 rollback_status = screen_status;
5330 }
5331 } else {
5332 restore_screen_asset_state();
5333 }
5334 if (dungeon_editor) {
5335 auto dungeon_status = dungeon_editor->RefreshRomBackedState();
5336 if (dungeon_status.ok()) {
5337 restore_dungeon_asset_state();
5338 }
5339 if (rollback_status.ok()) {
5340 rollback_status = dungeon_status;
5341 }
5342 }
5344 if (!rollback_status.ok()) {
5345 return absl::InternalError(absl::StrFormat(
5346 "ROM reload failed (%s) and restoring prior assets failed (%s)",
5347 load_status.message(), rollback_status.message()));
5348 }
5349 return load_status;
5350 }
5352 return absl::OkStatus();
5353}
5354
5355absl::Status EditorManager::SwapProjectRom(const std::string& rom_path) {
5356 if (rom_path.empty()) {
5357 return absl::InvalidArgumentError("ROM path cannot be empty");
5358 }
5359 if (!session_coordinator_) {
5360 return absl::FailedPreconditionError("No session coordinator");
5361 }
5362 auto* session = session_coordinator_->GetActiveRomSession();
5363 if (!session || !session->rom.is_loaded() ||
5365 return absl::FailedPreconditionError("No active project ROM session");
5366 }
5368 return absl::FailedPreconditionError(
5369 "Save or discard ROM edits before swapping the project ROM");
5370 }
5372 if (session->project_file_editor_state.initialized &&
5373 session->project_file_editor_state.modified &&
5375 return absl::FailedPreconditionError(
5376 "Preserve the raw project-file draft before swapping ROMs");
5377 }
5378
5379 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5380 rom_path, session->session_id()));
5381 Rom replacement_rom;
5382 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom, rom_path));
5384
5385 const std::string old_rom_path = current_project_.rom_filename;
5386 current_project_.rom_filename = rom_path;
5387 auto save_status = current_project_.Save();
5388 if (!save_status.ok()) {
5389 current_project_.rom_filename = old_rom_path;
5390 return save_status;
5391 }
5392
5394 auto replace_status =
5395 ReplaceActiveSessionRom(std::move(replacement_rom), rom_path);
5396 if (!replace_status.ok()) {
5397 current_project_.rom_filename = old_rom_path;
5398 auto rollback_status = current_project_.Save();
5401 if (!rollback_status.ok()) {
5402 return absl::InternalError(absl::StrFormat(
5403 "ROM swap failed (%s) and restoring the project descriptor failed "
5404 "(%s)",
5405 replace_status.message(), rollback_status.message()));
5406 }
5407 return replace_status;
5408 }
5409 session->project_dirty = false;
5411 project_management_panel_->SetProjectDirty(false);
5412 }
5415 return absl::OkStatus();
5416}
5417
5419 if (!session_coordinator_) {
5420 return absl::FailedPreconditionError("No session coordinator");
5421 }
5422 auto* session = session_coordinator_->GetActiveRomSession();
5423 if (!session || !session->rom.is_loaded() ||
5426 return absl::FailedPreconditionError("No active project ROM to reload");
5427 }
5429 return absl::FailedPreconditionError(
5430 "Save or discard ROM edits before reloading from disk");
5431 }
5432
5433 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
5434 current_project_.rom_filename, session->session_id()));
5435 Rom replacement_rom;
5436 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&replacement_rom,
5439 return ReplaceActiveSessionRom(std::move(replacement_rom),
5441}
5442
5446 if (auto* session = session_coordinator_->GetActiveRomSession();
5447 session && session->project_file_editor_state.initialized &&
5448 session->project_file_editor_state.modified &&
5450 return absl::FailedPreconditionError(
5451 "Raw project file also has unsaved changes; use raw Save As to "
5452 "preserve that draft before saving project settings");
5453 }
5454 }
5458 return CreateNewProject();
5459 }
5460
5461 // Update project with current settings
5464 auto* session = session_coordinator_->GetActiveRomSession();
5465 if (session) {
5466 current_project_.feature_flags = session->feature_flags;
5467 }
5468 }
5469
5478
5479 // Save recent files
5482 for (const auto& file : manager.GetRecentFiles()) {
5484 }
5485 }
5486
5487 auto status = current_project_.Save();
5488 if (status.ok()) {
5490 project_management_panel_->SetProjectDirty(false);
5491 }
5493 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5494 session->project_dirty = false;
5495 }
5496 }
5498 }
5500 return status;
5501}
5502
5505 project_management_panel_->SetProjectDirty(true);
5506 }
5508 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5509 session->project_dirty = true;
5510 }
5511 }
5512}
5513
5515 if (!session_coordinator_) {
5517 project_management_panel_->IsProjectDirty();
5518 }
5519 const auto* session = session_coordinator_->GetActiveRomSession();
5520 return session != nullptr &&
5521 (session->project_dirty ||
5522 session->editors.HasPendingProjectDraftChanges());
5523}
5524
5526 if (!session_coordinator_) {
5527 return absl::FailedPreconditionError("No session coordinator");
5528 }
5529 auto* session = session_coordinator_->GetActiveRomSession();
5530 if (!session) {
5531 return absl::FailedPreconditionError("No active ROM session");
5532 }
5533
5535 const bool project_dirty = session->project_dirty ||
5536 session->editors.HasPendingProjectDraftChanges();
5537 const bool project_file_dirty =
5538 session->project_file_editor_state.initialized &&
5539 session->project_file_editor_state.modified;
5540
5541 if (project_dirty && project_file_dirty &&
5543 return absl::FailedPreconditionError(
5544 "Project settings and raw project file both have unsaved changes; "
5545 "use raw Save As to preserve the draft, then save project settings");
5546 }
5547
5548 if (project_dirty) {
5550 current_project_.filepath.empty()) {
5551 return absl::FailedPreconditionError(
5552 "Project settings have no project file to save");
5553 }
5555 }
5556
5557 if (project_file_dirty) {
5558 if (project_file_editor_.filepath().empty()) {
5559 return absl::FailedPreconditionError(
5560 "Project file draft has no destination; use Save As first");
5561 }
5563 session->project_file_editor_state = project_file_editor_.CaptureState();
5564 }
5565
5566 return absl::OkStatus();
5567}
5568
5570 if (!session_coordinator_) {
5571 return absl::FailedPreconditionError("No session coordinator");
5572 }
5573 const size_t session_index = GetCurrentSessionIndex();
5575 const auto* session = session_coordinator_->GetActiveRomSession();
5576 if (session != nullptr && session->backup_restore_pending) {
5577 return absl::CancelledError(
5578 "Staged backup restore requires an explicit Save ROM");
5579 }
5580 if (SessionHasPendingRomWork(session_index)) {
5582 }
5583 if (SessionHasPendingUnsavedWork(session_index)) {
5584 return absl::FailedPreconditionError(
5585 "Autosave left unsaved project or ROM work");
5586 }
5587 return absl::OkStatus();
5588}
5589
5591 PotItemSaveDecision decision) {
5592 // Map EditorManager enum to RomLifecycleManager enum
5593 auto lifecycle_decision =
5595 static_cast<int>(decision));
5597
5598 if (decision == PotItemSaveDecision::kCancel) {
5600 toast_manager_.Show("Save cancelled", ToastType::kInfo);
5601 return;
5602 }
5603
5604 auto status = ResumePendingRomSave();
5605 if (!absl::IsCancelled(status) && !status.ok()) {
5607 absl::StrFormat("Failed to save ROM: %s", status.message()),
5609 }
5610}
5611
5613 // Get current project name for default filename
5614 std::string default_name = current_project_.project_opened()
5616 : "untitled_project";
5617
5618 auto file_path =
5619 util::FileDialogWrapper::ShowSaveFileDialog(default_name, "yaze");
5620 if (file_path.empty()) {
5621 return absl::OkStatus();
5622 }
5623
5624 return SaveProjectAs(file_path);
5625}
5626
5627absl::Status EditorManager::SaveProjectAs(const std::string& filepath) {
5628 if (filepath.empty()) {
5629 return absl::InvalidArgumentError("Project file path cannot be empty");
5630 }
5632 std::string file_path = filepath;
5633
5634 // Ensure a project extension.
5635 if (!(absl::EndsWith(file_path, ".yaze") ||
5636 absl::EndsWith(file_path, ".yazeproj"))) {
5637 file_path += ".yaze";
5638 }
5639
5641 if (auto* session = session_coordinator_->GetActiveRomSession();
5642 session && session->project_file_editor_state.initialized &&
5643 session->project_file_editor_state.modified &&
5645 session->project_file_editor_state.filepath, file_path)) {
5646 return absl::FailedPreconditionError(
5647 "Raw project draft targets the selected file; preserve it with "
5648 "raw Save As before overwriting that destination");
5649 }
5650 }
5651
5654
5655 // Update project filepath and save
5656 std::string old_filepath = current_project_.filepath;
5657 current_project_.filepath = file_path;
5658
5659 auto save_status = current_project_.Save();
5660 if (save_status.ok()) {
5662 project_management_panel_->SetProjectDirty(false);
5663 }
5665 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5666 session->project_dirty = false;
5667 }
5668 }
5670 RebaseCleanProjectFileDraft(file_path);
5671
5672 // Add to recent files
5674 manager.AddFile(file_path);
5675 manager.Save();
5676
5677 toast_manager_.Show(absl::StrFormat("Project saved as: %s", file_path),
5679 } else {
5680 // Restore old filepath on failure
5681 current_project_.filepath = old_filepath;
5683 absl::StrFormat("Failed to save project: %s", save_status.message()),
5685 }
5686
5688
5689 return save_status;
5690}
5691
5692absl::StatusOr<std::string> EditorManager::ResolveProjectBuildCommand() const {
5694 return absl::FailedPreconditionError("No project open");
5695 }
5696
5697 std::string command = current_project_.build_script;
5698 if (command.empty() && current_project_.hack_manifest.loaded()) {
5700 }
5701 if (command.empty()) {
5702 return absl::NotFoundError("Project does not define a build command");
5703 }
5704 return command;
5705}
5706
5707absl::StatusOr<std::string> EditorManager::ResolveProjectRunTarget() const {
5709 return absl::FailedPreconditionError("No project open");
5710 }
5711
5712 std::string target;
5715 }
5716 if (target.empty()) {
5718 }
5719 if (target.empty()) {
5720 return absl::NotFoundError("Project does not define a run target ROM");
5721 }
5722 return current_project_.GetAbsolutePath(target);
5723}
5724
5726 const std::string& summary, const std::string& detail,
5727 ProjectWorkflowState state, const std::string& output_tail,
5728 bool can_cancel) const {
5729 return {.visible = true,
5730 .can_cancel = can_cancel,
5731 .label = "Build",
5732 .summary = summary,
5733 .detail = detail,
5734 .output_tail = output_tail,
5735 .state = state};
5736}
5737
5739 const std::string& summary, const std::string& detail,
5740 ProjectWorkflowState state) const {
5741 return {.visible = true,
5742 .label = "Run",
5743 .summary = summary,
5744 .detail = detail,
5745 .state = state};
5746}
5747
5748absl::StatusOr<std::string> EditorManager::RunProjectBuildCommand() {
5749 auto command_or = ResolveProjectBuildCommand();
5750 if (!command_or.ok()) {
5751 return command_or.status();
5752 }
5753
5754 const std::string command = *command_or;
5755 const std::filesystem::path project_root =
5756 std::filesystem::path(current_project_.filepath).parent_path();
5758 auto start_status = task.Start(command, project_root.string());
5759 if (!start_status.ok()) {
5760 return start_status;
5761 }
5762 auto wait_status = task.Wait();
5763 const auto snapshot = task.GetSnapshot();
5764 if (!wait_status.ok()) {
5765 return wait_status;
5766 }
5767 const std::string summary = LastNonEmptyLine(snapshot.output);
5768 return summary.empty() ? command : summary;
5769}
5770
5772 const std::string running_detail =
5773 "Running the configured project build command";
5774 UpdateBuildWorkflowStatus(
5776 MakeBuildStatus("Build running", running_detail,
5777 ProjectWorkflowState::kRunning, "", false));
5778
5779 auto result_or = RunProjectBuildCommand();
5780 if (!result_or.ok()) {
5781 UpdateBuildWorkflowStatus(
5783 MakeBuildStatus("Build failed",
5784 std::string(result_or.status().message()),
5787 absl::StrFormat("Build unavailable: %s", result_or.status().message()),
5789 return result_or.status();
5790 }
5791
5792 const std::string summary = *result_or;
5793 UpdateBuildWorkflowStatus(&status_bar_, project_management_panel_.get(),
5794 MakeBuildStatus("Build succeeded", summary,
5797 summary.empty() ? "Project build completed"
5798 : absl::StrFormat("Project build completed: %s", summary),
5800 LOG_INFO("EditorManager", "Project build completed: %s", summary.c_str());
5801 return absl::OkStatus();
5802}
5803
5806 const auto snapshot = active_project_build_->GetSnapshot();
5807 if (snapshot.running) {
5808 toast_manager_.Show("A project build is already running",
5810 return;
5811 }
5812 }
5813
5814 auto command_or = ResolveProjectBuildCommand();
5815 if (!command_or.ok()) {
5816 UpdateBuildWorkflowStatus(
5818 MakeBuildStatus("Build unavailable",
5819 std::string(command_or.status().message()),
5822 absl::StrFormat("Build unavailable: %s", command_or.status().message()),
5824 return;
5825 }
5826
5827 const std::filesystem::path project_root =
5828 std::filesystem::path(current_project_.filepath).parent_path();
5829 active_project_build_ = std::make_unique<BackgroundCommandTask>();
5830 auto start_status =
5831 active_project_build_->Start(*command_or, project_root.string());
5832 if (!start_status.ok()) {
5833 UpdateBuildWorkflowStatus(
5835 MakeBuildStatus("Build unavailable",
5836 std::string(start_status.message()),
5839 absl::StrFormat("Build unavailable: %s", start_status.message()),
5841 active_project_build_.reset();
5842 return;
5843 }
5844
5846 UpdateBuildWorkflowStatus(
5848 MakeBuildStatus("Build running",
5849 "Running the configured project build command",
5852 project_management_panel_->SetBuildLogOutput("");
5853 }
5855}
5856
5858 if (!active_project_build_) {
5859 return;
5860 }
5861
5862 const auto snapshot = active_project_build_->GetSnapshot();
5863 if (!snapshot.running) {
5864 return;
5865 }
5866
5867 active_project_build_->Cancel();
5868 UpdateBuildWorkflowStatus(
5870 MakeBuildStatus("Cancelling build", "Stopping the active project build",
5871 ProjectWorkflowState::kRunning, snapshot.output_tail,
5872 false));
5873}
5874
5876 UpdateRunWorkflowStatus(
5878 MakeRunStatus("Run queued",
5879 "Preparing the project output for emulator reload",
5881
5882 auto run_target_or = ResolveProjectRunTarget();
5883 if (!run_target_or.ok()) {
5884 UpdateRunWorkflowStatus(
5886 MakeRunStatus("Run unavailable",
5887 std::string(run_target_or.status().message()),
5889 toast_manager_.Show(absl::StrFormat("Run unavailable: %s",
5890 run_target_or.status().message()),
5892 return run_target_or.status();
5893 }
5894 const std::string run_target = *run_target_or;
5895 if (!std::filesystem::exists(run_target)) {
5896 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5897 MakeRunStatus("Run target missing", run_target,
5899 toast_manager_.Show("Run target ROM not found. Build the project first.",
5901 return absl::NotFoundError("Run target ROM not found");
5902 }
5903
5904#ifdef YAZE_WITH_GRPC
5905 if (auto* emulator_backend = Application::Instance().GetEmulatorBackend()) {
5906 auto load_status = emulator_backend->LoadRom(run_target);
5907 if (load_status.ok()) {
5908 AppendWorkflowHistoryEntry(
5909 "Run",
5910 MakeRunStatus("Reloaded in backend", run_target,
5912 "");
5913 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5914 MakeRunStatus("Reloaded in backend", run_target,
5916 if (ui_coordinator_) {
5917 ui_coordinator_->SetEmulatorVisible(true);
5918 }
5920 absl::StrFormat("Reloaded project output in emulator backend: %s",
5921 run_target),
5923 return absl::OkStatus();
5924 }
5925 }
5926#endif
5927
5928 Rom temp_rom;
5929 auto load_rom_status = rom_file_manager_.LoadRom(&temp_rom, run_target);
5930 if (!load_rom_status.ok()) {
5931 UpdateRunWorkflowStatus(
5933 MakeRunStatus("Run load failed", std::string(load_rom_status.message()),
5935 toast_manager_.Show(absl::StrFormat("Failed to load run target ROM: %s",
5936 load_rom_status.message()),
5938 return load_rom_status;
5939 }
5940
5941 auto reload_status = emulator_.ReloadRuntimeRom(temp_rom.vector());
5942 if (!reload_status.ok()) {
5943 UpdateRunWorkflowStatus(
5945 MakeRunStatus("Reload failed", std::string(reload_status.message()),
5947 toast_manager_.Show(absl::StrFormat("Failed to reload emulator runtime: %s",
5948 reload_status.message()),
5950 return reload_status;
5951 }
5952
5953 if (ui_coordinator_) {
5954 ui_coordinator_->SetEmulatorVisible(true);
5955 }
5956 AppendWorkflowHistoryEntry("Run",
5957 MakeRunStatus("Reloaded in emulator", run_target,
5959 "");
5960 UpdateRunWorkflowStatus(&status_bar_, project_management_panel_.get(),
5961 MakeRunStatus("Reloaded in emulator", run_target,
5964 absl::StrFormat("Reloaded project output in emulator: %s", run_target),
5966 return absl::OkStatus();
5967}
5968
5969absl::Status EditorManager::ImportProject(const std::string& project_path) {
5970 // Delegate to ProjectManager for import logic
5972 // Sync local project reference
5976 if (auto* session = session_coordinator_->GetActiveRomSession()) {
5979 }
5980 }
5983 return absl::OkStatus();
5984}
5985
5988 return absl::FailedPreconditionError("No project is currently open");
5989 }
5990
5993 toast_manager_.Show("Project repaired successfully",
5995
5996 return absl::OkStatus();
5997}
5998
6000 if (auto* editor_set = GetCurrentEditorSet()) {
6001 return editor_set->GetOverworldData();
6002 }
6003 return nullptr;
6004}
6005
6007 if (!rom) {
6008 return absl::InvalidArgumentError("Invalid ROM pointer");
6009 }
6010
6011 // We need to find the session that owns this ROM.
6012 // This is inefficient but SetCurrentRom is rare.
6014 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6015 auto* session =
6016 static_cast<RomSession*>(session_coordinator_->GetSession(i));
6017 if (session && &session->rom == rom) {
6018 session_coordinator_->SwitchToSession(i);
6019 // Update test manager with current ROM for ROM-dependent tests
6023 return absl::OkStatus();
6024 }
6025 }
6026 }
6027 // If ROM wasn't found in existing sessions, treat as new session.
6028 // Copying an external ROM object is avoided; instead, fail.
6029 return absl::NotFoundError("ROM not found in existing sessions");
6030}
6031
6036
6040
6041// IsRomHashMismatch() is now inline in editor_manager.h delegating to
6042// rom_lifecycle_.
6043
6044std::vector<RomFileManager::BackupEntry> EditorManager::GetRomBackups() const {
6046}
6047
6049 if (!session_coordinator_) {
6050 return false;
6051 }
6052 const auto* session = session_coordinator_->GetActiveRomSession();
6053 return session != nullptr && session->backup_restore_pending;
6054}
6055
6056absl::Status EditorManager::RestoreRomBackup(const std::string& backup_path) {
6057 auto* rom = GetCurrentRom();
6058 if (!rom) {
6059 return absl::FailedPreconditionError("No ROM loaded");
6060 }
6062 return absl::FailedPreconditionError(
6063 "Save or discard ROM edits before restoring a backup");
6064 }
6065
6066 const std::string original_filename = rom->filename();
6067 const project::ResourceLabelManager original_resource_labels =
6068 *rom->resource_label();
6069 const auto backups = rom_file_manager_.ListBackups(original_filename);
6070 const bool is_managed_backup =
6071 std::any_of(backups.begin(), backups.end(), [&](const auto& backup) {
6072 return SessionCoordinator::PathsReferToSameBackingFile(backup.path,
6073 backup_path);
6074 });
6075 if (!is_managed_backup) {
6076 return absl::InvalidArgumentError(
6077 "Selected file is not a managed backup for the active ROM");
6078 }
6079
6080 // Load the backup away from the live session. Backups may legitimately have
6081 // a different size when they predate a ROM expansion or shrink. Reusing the
6082 // ROM-replacement path restores the prior ROM bytes and backing identity if
6083 // rebuilding assets fails partway through.
6084 Rom restored_rom;
6085 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&restored_rom, backup_path));
6086
6087 if (!original_filename.empty()) {
6088 restored_rom.set_filename(original_filename);
6089 }
6091 // Normal ROM backups do not copy the active `.labels` sidecar. Preserve the
6092 // session's loaded and in-memory resource labels instead of replacing them
6093 // with the usually-empty `<backup>.labels` lookup from LoadFromFile().
6094 *restored_rom.resource_label() = original_resource_labels;
6095 // Restore is intentionally staged in memory. Mark it dirty so closing or
6096 // autosave cannot silently treat the on-disk source as already restored.
6097 restored_rom.set_dirty(true);
6099 ReplaceActiveSessionRom(std::move(restored_rom), original_filename));
6100 auto* restored_session = session_coordinator_->GetActiveRomSession();
6101 if (!restored_session) {
6102 return absl::InternalError(
6103 "ROM backup restored without an active session to stage it");
6104 }
6105 restored_session->backup_restore_pending = true;
6106 return absl::OkStatus();
6107}
6108
6110 if (!session_coordinator_) {
6111 return absl::FailedPreconditionError("No session coordinator");
6112 }
6113 auto* session = session_coordinator_->GetActiveRomSession();
6114 if (!session || !session->rom.is_loaded()) {
6115 return absl::FailedPreconditionError("No ROM loaded");
6116 }
6117 if (!session->backup_restore_pending) {
6118 return absl::FailedPreconditionError("No restored backup is staged");
6119 }
6120
6121 const size_t session_index = GetCurrentSessionIndex();
6122 if (session->editors.HasPendingGraphicsChanges() ||
6123 session->editors.HasPendingScreenChanges() ||
6124 HasPendingDungeonChangesForSession(session_index) ||
6125 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data)) {
6126 return absl::FailedPreconditionError(
6127 "Resolve pending graphics, screen, dungeon, or palette edits before "
6128 "discarding the restored backup");
6129 }
6130
6131 const std::string backing_path = session->rom.filename();
6132 if (backing_path.empty()) {
6133 return absl::FailedPreconditionError("ROM has no backing file to reload");
6134 }
6135
6136 // Load into scratch storage before touching the live session so file I/O
6137 // failures preserve the staged ROM. Resource labels are session work, not
6138 // part of normal ROM backups, so preserve them across the byte-level discard.
6139 const project::ResourceLabelManager resource_labels =
6140 *session->rom.resource_label();
6141 Rom backing_rom;
6142 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&backing_rom, backing_path));
6143 *backing_rom.resource_label() = resource_labels;
6145 ReplaceActiveSessionRom(std::move(backing_rom), backing_path));
6146
6147 session->backup_restore_pending = false;
6148 session->rom.ClearDirty();
6149 return absl::OkStatus();
6150}
6151
6155
6163
6167
6171
6174 session_coordinator_->CreateNewSession();
6175 // Toast messages are now shown by SessionCoordinator
6176 }
6177}
6178
6181 session_coordinator_->DuplicateCurrentSession();
6182 }
6183}
6184
6186 if (!session_coordinator_) {
6187 return;
6188 }
6189
6190 if (!session_coordinator_->HasMultipleSessions()) {
6191 // Preserve the coordinator's existing warning without offering a
6192 // "Close Without Saving" action that cannot close the final session.
6193 session_coordinator_->CloseCurrentSession();
6194 return;
6195 }
6196
6197 const size_t current_session_id = GetCurrentSessionId();
6200 current_session_id})) {
6201 return;
6202 }
6203
6204 session_coordinator_->CloseCurrentSession();
6206}
6207
6209 if (!session_coordinator_ ||
6210 !session_coordinator_->IsValidSessionIndex(index)) {
6211 return;
6212 }
6213
6214 const size_t session_id = session_coordinator_->GetSessionId(index);
6217 session_id})) {
6218 return;
6219 }
6220
6221 session_coordinator_->RemoveSession(index);
6223}
6224
6226 if (!session_coordinator_) {
6227 return;
6228 }
6229
6230 const size_t current_index = GetCurrentSessionIndex();
6231 if (index == current_index ||
6232 !session_coordinator_->IsValidSessionIndex(index)) {
6233 return;
6234 }
6235
6236 const size_t current_session_id = GetCurrentSessionId();
6237 const size_t target_session_id = session_coordinator_->GetSessionId(index);
6240 current_session_id, target_session_id})) {
6241 return;
6242 }
6243
6244 session_coordinator_->SwitchToSession(index);
6245}
6246
6250 return;
6251 }
6252
6253 quit_ = true;
6254}
6255
6259
6261 size_t session_id) const {
6262 if (!session_coordinator_ || session_id == SIZE_MAX) {
6263 return std::nullopt;
6264 }
6265
6266 for (size_t index = 0; index < session_coordinator_->GetTotalSessionCount();
6267 ++index) {
6268 if (session_coordinator_->GetSessionId(index) == session_id) {
6269 return index;
6270 }
6271 }
6272 return std::nullopt;
6273}
6274
6277 return "";
6278 }
6279
6280 const auto& action = *pending_unsaved_session_action_;
6281 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6282 return absl::StrFormat("%s\n\nQuitting now will discard them.",
6284 }
6285
6286 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6287 const std::string session_name =
6288 source_index.has_value()
6289 ? session_coordinator_->GetSessionDisplayName(*source_index)
6290 : "Closed Session";
6291 const std::string work = source_index.has_value()
6292 ? DescribePendingUnsavedWork(*source_index)
6293 : "unsaved work";
6294
6295 switch (action.type) {
6298 return absl::StrFormat(
6299 "Session '%s' has %s. Opening another ROM or project will not save "
6300 "them automatically.",
6301 session_name, work);
6303 return absl::StrFormat(
6304 "Session '%s' has %s. Opening another project will not save them "
6305 "automatically.",
6306 session_name, work);
6308 return absl::StrFormat(
6309 "Session '%s' has %s. Switching sessions will leave them behind in "
6310 "this session.",
6311 session_name, work);
6313 return absl::StrFormat(
6314 "Session '%s' has %s. Closing it now will discard them.",
6315 session_name, work);
6317 break;
6318 }
6319
6320 return "";
6321}
6322
6325 return "Save Work";
6326 }
6327
6328 switch (pending_unsaved_session_action_->type) {
6331 return "Save Work & Open";
6333 return "Save Work & Open Project";
6335 return "Save Work & Switch";
6337 return "Save Work & Close";
6339 return ModifiedSessionCount() == 1 ? "Save Work & Quit"
6340 : "Save Modified Work & Quit";
6341 }
6342
6343 return "Save Work";
6344}
6345
6348 return "Continue";
6349 }
6350
6351 switch (pending_unsaved_session_action_->type) {
6354 return "Open Without Saving";
6356 return "Open Project Without Saving";
6358 return "Switch Without Saving";
6360 return "Close Without Saving";
6362 return "Quit Without Saving";
6363 }
6364
6365 return "Continue";
6366}
6367
6370 return;
6371 }
6372
6373 const auto action = *pending_unsaved_session_action_;
6375
6376 if (popup_manager_) {
6378 }
6379
6380 const size_t original_session_id = GetCurrentSessionId();
6381 auto save_modified_session = [this](size_t session_id) -> absl::Status {
6382 const auto session_index = ResolveSessionIndexById(session_id);
6383 if (!session_index.has_value()) {
6384 return absl::FailedPreconditionError(
6385 "The ROM session is no longer available");
6386 }
6387 if (!SessionHasPendingUnsavedWork(*session_index)) {
6388 return absl::OkStatus();
6389 }
6390
6391 session_coordinator_->SwitchToSession(*session_index);
6393 if (SessionHasPendingRomWork(*session_index)) {
6395 }
6396
6397 // A successful file write does not necessarily mean every pending editor
6398 // domain participated in the save. For example, dungeon palette saving is
6399 // user-configurable. Never execute a destructive follow-up while work that
6400 // the confirmation dialog promised to save is still pending.
6401 if (SessionHasPendingUnsavedWork(*session_index)) {
6402 return absl::FailedPreconditionError(
6403 absl::StrFormat("Save completed, but session still has %s",
6404 DescribePendingUnsavedWork(*session_index)));
6405 }
6406 return absl::OkStatus();
6407 };
6408
6409 absl::Status save_status = absl::OkStatus();
6410 if (action.type == PendingUnsavedSessionAction::Type::kQuit) {
6412 std::vector<size_t> session_ids;
6413 session_ids.reserve(session_coordinator_->GetTotalSessionCount());
6414 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount();
6415 ++i) {
6416 session_ids.push_back(session_coordinator_->GetSessionId(i));
6417 }
6418 for (const size_t session_id : session_ids) {
6419 save_status = save_modified_session(session_id);
6420 if (!save_status.ok()) {
6421 break;
6422 }
6423 }
6424 }
6425 } else {
6426 save_status = save_modified_session(action.source_session_id);
6427 }
6428
6429 if (!save_status.ok()) {
6430 const bool resumable_confirmation = absl::IsCancelled(save_status) &&
6433 const auto original_session_index =
6434 ResolveSessionIndexById(original_session_id);
6435 if (!resumable_confirmation && original_session_index.has_value()) {
6436 session_coordinator_->SwitchToSession(*original_session_index);
6437 }
6438
6439 if (absl::IsCancelled(save_status)) {
6440 toast_manager_.Show("Save paused. Finish saving, then retry your action.",
6442 } else {
6444 absl::StrFormat("Failed to save before continuing: %s",
6445 save_status.message()),
6447 }
6448 return;
6449 }
6450
6451 // Saving an inactive close target temporarily activates that session. Put
6452 // the original session back before compacting the target index so the same
6453 // stable session remains active after the close.
6455 original_session_id != action.target_session_id) {
6456 const auto original_session_index =
6457 ResolveSessionIndexById(original_session_id);
6458 if (original_session_index.has_value()) {
6459 session_coordinator_->SwitchToSession(*original_session_index);
6460 }
6461 }
6462
6464}
6465
6478
6485
6489 const auto source_index = ResolveSessionIndexById(action.source_session_id);
6490 const bool has_pending_work =
6493 : source_index.has_value() &&
6494 SessionHasPendingUnsavedWork(*source_index);
6495 if (!has_pending_work) {
6496 return true;
6497 }
6498
6499 pending_unsaved_session_action_ = std::move(action);
6500 if (popup_manager_) {
6502 }
6503 return false;
6504}
6505
6507 const PendingUnsavedSessionAction& action) {
6508 switch (action.type) {
6510 auto status = LoadRomInternal();
6511 if (!status.ok()) {
6513 absl::StrFormat("Failed to load ROM: %s", status.message()),
6515 }
6516 break;
6517 }
6519 auto status = OpenRomOrProjectInternal(action.path);
6520 if (!status.ok()) {
6521 toast_manager_.Show(absl::StrFormat("Failed to open ROM / project: %s",
6522 status.message()),
6524 }
6525 break;
6526 }
6528 auto status = OpenProjectInternal();
6529 if (!status.ok()) {
6531 absl::StrFormat("Failed to open project: %s", status.message()),
6533 }
6534 break;
6535 }
6538 const auto target_index =
6540 if (target_index.has_value()) {
6541 session_coordinator_->SwitchToSession(*target_index);
6542 }
6543 }
6544 break;
6547 const auto target_index =
6549 if (target_index.has_value()) {
6550 session_coordinator_->RemoveSession(*target_index);
6552 }
6553 }
6554 break;
6556 quit_ = true;
6557 break;
6558 }
6559}
6560
6561bool EditorManager::SessionHasPendingUnsavedWork(size_t session_index) const {
6562 if (!session_coordinator_ ||
6563 !session_coordinator_->IsValidSessionIndex(session_index)) {
6564 return false;
6565 }
6566
6567 auto* session =
6568 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6569 return session != nullptr &&
6570 (SessionHasPendingRomWork(session_index) || session->project_dirty ||
6571 session->editors.HasPendingProjectDraftChanges() ||
6572 (session->project_file_editor_state.initialized &&
6573 session->project_file_editor_state.modified));
6574}
6575
6576bool EditorManager::SessionHasPendingRomWork(size_t session_index) const {
6577 if (!session_coordinator_ ||
6578 !session_coordinator_->IsValidSessionIndex(session_index)) {
6579 return false;
6580 }
6581
6582 auto* session =
6583 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6584 return session != nullptr &&
6585 ((session->rom.is_loaded() && session->rom.dirty()) ||
6586 session->editors.HasPendingGraphicsChanges() ||
6587 session->editors.HasPendingScreenChanges() ||
6588 HasPendingDungeonChangesForSession(session_index) ||
6589 gfx::PaletteManager::Get().HasUnsavedChanges(&session->game_data));
6590}
6591
6595
6597 size_t session_index) const {
6598 if (!session_coordinator_ ||
6599 !session_coordinator_->IsValidSessionIndex(session_index)) {
6600 return false;
6601 }
6602
6603 auto* session =
6604 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6605 if (!session) {
6606 return false;
6607 }
6608
6609 if (auto* dungeon_editor =
6610 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6611 return dungeon_editor->HasPendingDungeonChanges();
6612 }
6613 return false;
6614}
6615
6617 size_t session_index) const {
6618 if (!session_coordinator_ ||
6619 !session_coordinator_->IsValidSessionIndex(session_index)) {
6620 return 0;
6621 }
6622
6623 auto* session =
6624 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6625 if (!session) {
6626 return 0;
6627 }
6628
6629 if (auto* dungeon_editor =
6630 session->editors.GetEditorAs<DungeonEditorV2>(EditorType::kDungeon)) {
6631 return dungeon_editor->PendingRoomCount();
6632 }
6633 return 0;
6634}
6635
6637 size_t session_index) const {
6638 if (!session_coordinator_ ||
6639 !session_coordinator_->IsValidSessionIndex(session_index)) {
6640 return 0;
6641 }
6642
6643 auto* session =
6644 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6645 return session != nullptr ? gfx::PaletteManager::Get().GetModifiedColorCount(
6646 &session->game_data)
6647 : 0;
6648}
6649
6651 if (!session_coordinator_) {
6652 return 0;
6653 }
6654
6655 int count = 0;
6656 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6658 ++count;
6659 }
6660 }
6661 return count;
6662}
6663
6665 size_t session_index) const {
6666 if (!session_coordinator_ ||
6667 !session_coordinator_->IsValidSessionIndex(session_index)) {
6668 return "unsaved work";
6669 }
6670
6671 auto* session =
6672 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6673 const bool rom_dirty =
6674 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
6675 const bool pending_dungeon_changes =
6677 const bool pending_graphics_changes =
6678 session != nullptr && session->editors.HasPendingGraphicsChanges();
6679 const bool pending_screen_changes =
6680 session != nullptr && session->editors.HasPendingScreenChanges();
6681 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
6682 const size_t pending_palette_colors =
6684 const bool project_dirty = session != nullptr && session->project_dirty;
6685 const bool project_editor_draft =
6686 session != nullptr && session->editors.HasPendingProjectDraftChanges();
6687 const bool project_file_dirty =
6688 session != nullptr && session->project_file_editor_state.initialized &&
6689 session->project_file_editor_state.modified;
6690
6691 std::vector<std::string> work;
6692 if (pending_rooms > 0) {
6693 work.push_back(absl::StrFormat("%d unapplied dungeon room%s", pending_rooms,
6694 pending_rooms == 1 ? "" : "s"));
6695 } else if (pending_dungeon_changes) {
6696 work.emplace_back("unapplied dungeon metadata");
6697 }
6698 if (pending_palette_colors > 0) {
6699 work.push_back(absl::StrFormat("%zu unapplied palette color%s",
6700 pending_palette_colors,
6701 pending_palette_colors == 1 ? "" : "s"));
6702 }
6703 if (pending_graphics_changes) {
6704 work.emplace_back("unapplied graphics sheet edits");
6705 }
6706 if (pending_screen_changes) {
6707 work.emplace_back("unapplied Screen Editor edits");
6708 }
6709 if (rom_dirty) {
6710 work.emplace_back("unsaved ROM-buffer changes");
6711 }
6712 if (project_dirty) {
6713 work.emplace_back("unsaved project settings");
6714 }
6715 if (project_editor_draft) {
6716 work.emplace_back("an uncommitted project editor draft");
6717 }
6718 if (project_file_dirty) {
6719 work.emplace_back("an unsaved project-file draft");
6720 }
6721 return work.empty() ? "unsaved work" : absl::StrJoin(work, " and ");
6722}
6723
6725 size_t session_index) const {
6726 if (!session_coordinator_ ||
6727 !session_coordinator_->IsValidSessionIndex(session_index)) {
6728 return {};
6729 }
6730
6731 auto* session =
6732 static_cast<RomSession*>(session_coordinator_->GetSession(session_index));
6733 const bool rom_dirty =
6734 session != nullptr && session->rom.is_loaded() && session->rom.dirty();
6735 const bool pending_dungeon_changes =
6737 const bool pending_graphics_changes =
6738 session != nullptr && session->editors.HasPendingGraphicsChanges();
6739 const bool pending_screen_changes =
6740 session != nullptr && session->editors.HasPendingScreenChanges();
6741 const int pending_rooms = PendingDungeonRoomCountForSession(session_index);
6742 const size_t pending_palette_colors =
6744 const bool project_dirty = session != nullptr && session->project_dirty;
6745 const bool project_editor_draft =
6746 session != nullptr && session->editors.HasPendingProjectDraftChanges();
6747 const bool project_file_dirty =
6748 session != nullptr && session->project_file_editor_state.initialized &&
6749 session->project_file_editor_state.modified;
6750
6751 std::vector<std::string> tags;
6752 if (pending_rooms > 0) {
6753 tags.push_back("Rooms");
6754 } else if (pending_dungeon_changes) {
6755 tags.push_back("Dungeon");
6756 }
6757 if (pending_palette_colors > 0) {
6758 tags.push_back("Palette");
6759 }
6760 if (pending_graphics_changes) {
6761 tags.push_back("Gfx");
6762 }
6763 if (pending_screen_changes) {
6764 tags.push_back("Screen");
6765 }
6766 if (rom_dirty) {
6767 tags.push_back("ROM");
6768 }
6769 if (project_dirty || project_editor_draft || project_file_dirty) {
6770 tags.push_back("Project");
6771 }
6772 return absl::StrJoin(tags, "+");
6773}
6774
6776 const int modified_sessions = ModifiedSessionCount();
6777 if (modified_sessions <= 0) {
6778 return "No sessions have unsaved work.";
6779 }
6780
6781 if (modified_sessions == 1 && session_coordinator_) {
6782 for (size_t i = 0; i < session_coordinator_->GetTotalSessionCount(); ++i) {
6784 return absl::StrFormat("Session '%s' has %s.",
6785 session_coordinator_->GetSessionDisplayName(i),
6787 }
6788 }
6789 }
6790
6791 return absl::StrFormat(
6792 "%d sessions have unsaved ROM, graphics, screen, dungeon, palette, or "
6793 "project work.",
6794 modified_sessions);
6795}
6796
6798 return session_coordinator_ ? session_coordinator_->GetActiveSessionIndex()
6799 : 0;
6800}
6801
6803 UiSyncState state;
6804 state.frame_id = ui_sync_frame_id_.load(std::memory_order_relaxed);
6805
6806 int pending_editor =
6807 pending_editor_deferred_actions_.load(std::memory_order_relaxed);
6808 if (pending_editor < 0) {
6809 pending_editor = 0;
6810 }
6811 state.pending_editor_actions = pending_editor;
6812
6813 int pending_layout = layout_coordinator_.PendingDeferredActionCount();
6814 if (pending_layout < 0) {
6815 pending_layout = 0;
6816 }
6817 state.pending_layout_actions = pending_layout;
6819 layout_manager_ ? layout_manager_->IsRebuildRequested() : false;
6820 return state;
6821}
6822
6824 return session_coordinator_ ? session_coordinator_->GetActiveSessionCount()
6825 : 0;
6826}
6827
6829 EditorType type, size_t session_index) const {
6830 const char* base_name = kEditorNames[static_cast<int>(type)];
6831 return session_coordinator_ ? session_coordinator_->GenerateUniqueEditorTitle(
6832 base_name, session_index)
6833 : std::string(base_name);
6834}
6835
6836void EditorManager::SwitchToEditor(EditorType editor_type, bool force_visible,
6837 bool from_dialog) {
6838 if (EditorRegistry::IsExperimentalEditor(editor_type) &&
6841 absl::StrFormat(
6842 "%s is experimental — enable Experimental Editors in Settings",
6843 kEditorNames[static_cast<int>(editor_type)]),
6845 return;
6846 }
6847
6848 // Special case: Agent editor requires EditorManager-specific handling
6849#ifdef YAZE_BUILD_AGENT_UI
6850 if (editor_type == EditorType::kAgent) {
6851 ShowAIAgent();
6852 return;
6853 }
6854#endif
6855
6856 // Fresh launch has no ROM session, so GetCurrentEditorSet() is null and
6857 // EditorActivator::SwitchToEditor would silently no-op (BUG-020) — that was
6858 // why File->Settings, the "no ROM" welcome buttons, and Tools editors did
6859 // nothing. Create an empty session on demand so ROM-less editors (Settings,
6860 // Assembly, ...) have an editor set to live in. Normal ROM loads create their
6861 // own session, so this only fires when the user reaches an editor first.
6862 if (session_coordinator_ && GetCurrentEditorSet() == nullptr) {
6863 session_coordinator_->CreateNewSession();
6864 }
6865
6866 auto status = EnsureEditorAssetsLoaded(editor_type);
6867 if (!status.ok()) {
6869 absl::StrFormat("Failed to prepare %s: %s",
6870 kEditorNames[static_cast<int>(editor_type)],
6871 status.message()),
6873 }
6874
6875 // Delegate all other editor switching to EditorActivator
6876 editor_activator_.SwitchToEditor(editor_type, force_visible, from_dialog);
6877}
6878
6880 if (!ui_coordinator_) {
6881 return;
6882 }
6883 ui_coordinator_->SetEditorSelectionVisible(false);
6884 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
6885}
6886
6888 if (!session)
6889 return;
6891 ConfigureEditorDependencies(&session->editors, &session->rom,
6892 session->editors.session_id());
6893}
6894
6895// SessionScope implementation
6897 size_t session_index)
6898 : manager_(manager),
6899 prev_rom_(manager->GetCurrentRom()),
6900 prev_editor_set_(manager->GetCurrentEditorSet()),
6901 prev_session_index_(manager->GetCurrentSessionIndex()) {
6902 // Set new session context
6903 manager_->session_coordinator_->SwitchToSession(session_index);
6904}
6905
6907 // Restore previous context
6908 manager_->session_coordinator_->SwitchToSession(prev_session_index_);
6909}
6910
6911bool EditorManager::HasDuplicateSession(const std::string& filepath) {
6912 return session_coordinator_ &&
6913 session_coordinator_->HasDuplicateSession(filepath);
6914}
6915
6941 // Menu actions can run before the next Update() frame captures drawer
6942 // edits. Preserve the panel's current dirty bit before rebinding it.
6945 // Update project panel context before showing
6949 project_management_panel_->SetVersionManager(version_manager_);
6951 }
6952 right_drawer_manager_->ToggleDrawer(
6954 }
6955}
6956
6959 auto* session = session_coordinator_
6960 ? session_coordinator_->GetActiveRomSession()
6961 : nullptr;
6962
6963 // Preserve an existing draft for this session. Only load from disk the first
6964 // time this session opens the project-file editor.
6965 if (session && session->project_file_editor_state.initialized &&
6966 !session->project_file_editor_state.modified &&
6967 !current_project_.filepath.empty() &&
6969 session->project_file_editor_state.filepath,
6972 }
6973
6974 if (session && session->project_file_editor_state.initialized) {
6975 project_file_editor_.RestoreState(session->project_file_editor_state,
6977 } else if (!current_project_.filepath.empty()) {
6979 if (!status.ok()) {
6981 absl::StrFormat("Failed to load project file: %s", status.message()),
6983 return;
6984 }
6985 } else {
6987 }
6988 // Set the project pointer for label import functionality
6990 // Activate the editor window
6992 if (session) {
6993 session->project_file_editor_state = project_file_editor_.CaptureState();
6994 }
6995}
6996
6998 size_t session_id) {
6999 if (!editor_set) {
7000 return;
7001 }
7002
7003 EditorDependencies deps;
7004 deps.rom = rom;
7005 deps.session_id = session_id;
7008 deps.popup_manager = popup_manager_.get();
7013 const auto session_index = ResolveSessionIndexById(session_id);
7014 if (session_index.has_value()) {
7015 auto* session = static_cast<RomSession*>(
7016 session_coordinator_->GetSession(*session_index));
7017 if (session) {
7018 deps.game_data = &session->game_data;
7019 if (session->project_context.has_value()) {
7020 deps.project = &*session->project_context;
7021 deps.version_manager = session->version_manager.get();
7022 }
7023 }
7024 }
7025 }
7026 deps.global_context = editor_context_.get();
7027 deps.status_bar = &status_bar_;
7028 deps.renderer = renderer_;
7029 deps.emulator = &emulator_;
7030 deps.custom_data = this;
7031 deps.gfx_group_workspace = editor_set->gfx_group_workspace();
7032
7033 editor_set->ApplyDependencies(deps);
7034
7035 if (auto* settings_panel = editor_set->GetSettingsPanel()) {
7036 settings_panel->SetOpenMinecartTracksCallback(
7037 [this, session_id]() -> absl::Status {
7038 if (!IsCurrentProjectContextOwnedBySession(session_id)) {
7039 return absl::FailedPreconditionError(
7040 "Minecart Tracks requires its project session to be active.");
7041 }
7042 if (!core::FeatureFlags::get().kEnableCustomObjects) {
7043 return absl::FailedPreconditionError(
7044 "Enable Custom Dungeon Objects before opening Minecart "
7045 "Tracks.");
7046 }
7047
7049 auto* dungeon = GetCurrentEditorSet()->GetEditorAs<DungeonEditorV2>(
7051 if (dungeon == nullptr) {
7052 return absl::NotFoundError(
7053 "Dungeon editor is unavailable in the active project "
7054 "session.");
7055 }
7056 RETURN_IF_ERROR(dungeon->EnsureMinecartTrackEditorPanel());
7057
7061 return absl::NotFoundError(
7062 "Minecart Tracks could not be opened in the active Dungeon "
7063 "editor.");
7064 }
7065 return absl::OkStatus();
7066 });
7067 }
7068
7069 // If configuring the active session, update the properties panel
7070 if (session_id == GetCurrentSessionId()) {
7072 }
7073}
7074
7075} // namespace yaze::editor
static Application & Instance()
void Publish(const T &event)
Definition event_bus.h:35
HandlerId Subscribe(std::function< void(const T &)> handler)
Definition event_bus.h:22
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
project::ResourceLabelManager * resource_label()
Definition rom.h:180
auto filename() const
Definition rom.h:175
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:563
void set_dirty(bool dirty)
Definition rom.h:157
const auto & vector() const
Definition rom.h:173
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool dirty() const
Definition rom.h:156
auto set_filename(std::string_view name)
Definition rom.h:176
bool is_loaded() const
Definition rom.h:155
static TimingManager & Get()
Definition timing.h:20
float Update()
Update the timing manager (call once per frame)
Definition timing.h:29
float GetElapsedTime() const
Get total elapsed time since first update.
Definition timing.h:70
static void Initialize(editor::EditorManager *)
static void Initialize(editor::EditorManager *)
static Flags & get()
Definition features.h:119
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
bool loaded() const
Check if the manifest has been loaded.
const BuildPipeline & build_pipeline() const
static RomSettings & Get()
void SetAddressOverrides(const RomAddressOverrides &overrides)
void set_active(bool active)
Definition agent_chat.h:71
AgentConfigState & agent_config()
void SetProjectContext(project::YazeProject *project)
void ApplyUserSettingsDefaults(bool force=false)
void SetAssemblySymbolTableContext(const std::map< std::string, core::AsarSymbol > *table)
void Initialize(ToastManager *toast_manager, ProposalDrawer *proposal_drawer, RightDrawerManager *right_drawer_manager, WorkspaceWindowManager *window_manager, UserSettings *user_settings)
void SetAsarWrapperContext(core::AsarWrapper *asar_wrapper)
absl::Status Start(const std::string &command, const std::string &directory)
DungeonEditorV2 - Simplified dungeon editor using component delegation.
static constexpr const char * kMinecartTrackEditorId
void SwitchToEditor(EditorType type, bool force_visible=false, bool from_dialog=false)
Switch to an editor, optionally forcing visibility.
void Initialize(const Dependencies &deps)
void InitializeEditorLayout(EditorType type)
Initialize the DockBuilder layout for an editor.
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::string CompactPendingUnsavedWorkLabel(size_t session_index) const
std::vector< std::string > ListLayoutSnapshots() const
SharedClipboard shared_clipboard_
bool EditorInitRequiresGameData(EditorType type) const
void SyncEditorContextForCategory(const std::string &category)
void ShowProjectManagement()
Injects dependencies into all editors within an EditorSet.
Editor * ResolveEditorForCategory(const std::string &category)
void Initialize(gfx::IRenderer *renderer, const std::string &filename="")
std::optional< size_t > runtime_feature_flags_session_id_
absl::Status FinalizeNewProject(const std::string &project_name, const std::string &project_path=std::string())
std::string DescribeAllPendingUnsavedWork() const
void MarkEditorLoaded(RomSession *session, EditorType type)
absl::Status StartPendingRomSave(const std::optional< std::string > &save_as_filename)
absl::Status CreateNewProjectFromRom(const std::string &template_name, const std::string &rom_path, const std::string &project_name, const std::string &project_path=std::string())
absl::Status CreateNewProject(const std::string &template_name="Basic ROM Hack")
void ConfirmPendingUnsavedSessionActionDiscardAndContinue()
absl::Status InitializeEditorForType(EditorType type, EditorSet *editor_set, Rom *rom)
LayoutCoordinator layout_coordinator_
absl::Status LoadAssets(uint64_t loading_handle=0)
bool HasPendingUnsavedSessionAction() const
void RestoreActiveEditorLayoutAfterAssets(RomSession *session)
absl::Status OpenRomOrProjectInternal(const std::string &filename)
auto GetCurrentEditorSet() const -> EditorSet *
std::string DescribePendingUnsavedWork(size_t session_index) const
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
void RestoreEditorLayoutAfterAssets(RomSession *session, EditorType type)
absl::Status SaveRomInternal(const std::optional< std::string > &save_as_filename)
void SetStartupLoadHints(const AppConfig &config)
void RebaseCleanProjectFileDraft(const std::string &filepath)
std::optional< size_t > pending_project_open_previous_session_id_
absl::Status LoadAssetsLazy(uint64_t loading_handle=0)
bool PendingRomSaveMatchesActiveSession() const
absl::StatusOr< std::string > ResolveProjectBuildCommand() const
void SetAssetLoadMode(AssetLoadMode mode)
void DismissEditorSelection() override
absl::Status DiscardPendingRomBackupRestore()
std::atomic< uint64_t > ui_sync_frame_id_
std::optional< size_t > active_project_context_session_id_
std::string GetPendingUnsavedSessionActionContinueLabel() const
std::string GetPendingUnsavedSessionActionPrompt() const
bool ApplyLayoutProfile(const std::string &profile_id)
Editor * GetEditorByType(EditorType type, EditorSet *editor_set) const
StartupVisibility sidebar_mode_override_
RomLoadOptionsDialog rom_load_options_dialog_
absl::Status LoadRom()
Load a ROM file into a new or existing session.
std::vector< std::string > startup_panel_hints_
std::vector< std::function< void()> > deferred_actions_
void OpenEditorAndPanelsFromFlags(const std::string &editor_name, const std::string &panels_str)
StartupVisibility dashboard_mode_override_
void BindProjectContextToSession(RomSession *session, const project::YazeProject &project)
std::optional< size_t > ResolveSessionIndexById(size_t session_id) const
static bool IsPanelBasedEditor(EditorType type)
absl::Status Update()
Main update loop for the editor application.
absl::StatusOr< std::string > ResolveProjectRunTarget() const
absl::Status ImportProject(const std::string &project_path)
size_t GetCurrentSessionId() const
Stable workspace identity; unlike the UI index, it does not compact.
bool ProjectFileDraftTargetsCurrentProject() const
absl::Status PrepareActiveProjectEditorDraftsForSave()
ProjectWorkflowStatus MakeRunStatus(const std::string &summary, const std::string &detail, ProjectWorkflowState state) const
void QueueDeferredAction(std::function< void()> action)
absl::Status ValidateProjectRomSelection(const std::string &rom_path)
SelectionPropertiesPanel selection_properties_panel_
bool DeleteLayoutSnapshot(const std::string &name)
void ConfigureSession(RomSession *session) override
std::unique_ptr< ActivityBar > activity_bar_
ShortcutManager shortcut_manager_
bool RestoreLayoutSnapshot(const std::string &name, bool remove_after_restore=false)
bool HasPendingDungeonChangesForSession(size_t session_index) const
std::optional< PendingUnsavedSessionAction > pending_unsaved_session_action_
void MarkEditorInitialized(RomSession *session, EditorType type)
bool SessionHasPendingUnsavedWork(size_t session_index) const
std::unique_ptr< BackgroundCommandTask > active_project_build_
bool IsCurrentProjectContextOwnedBySession(size_t session_id) const
WorkspaceManager workspace_manager_
yaze::zelda3::Overworld * overworld() const
void ConfigureEditorDependencies(EditorSet *editor_set, Rom *rom, size_t session_id)
absl::StatusOr< const project::YazeProject * > PrepareActiveProjectContextForSave()
std::unique_ptr< ProjectManagementPanel > project_management_panel_
std::unique_ptr< workflow::HackWorkflowBackend > hack_workflow_backend_
absl::Status OpenRomOrProject(const std::string &filename)
void RestoreProjectContextAfterFailedOpen(std::optional< size_t > previous_session_id)
void RemoveSession(size_t index)
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
UiSyncState GetUiSyncStateSnapshot() const
ProjectWorkflowStatus MakeBuildStatus(const std::string &summary, const std::string &detail, ProjectWorkflowState state, const std::string &output_tail="", bool can_cancel=false) const
absl::Status DiscardProvisionalSessionCreatedSince(size_t previous_session_count)
absl::Status EnsureEditorAssetsLoaded(EditorType type)
std::unique_ptr< PopupManager > popup_manager_
bool RestoreProjectContextForSession(RomSession *session)
std::atomic< int > pending_editor_deferred_actions_
std::unique_ptr< UICoordinator > ui_coordinator_
EditorActivator editor_activator_
absl::Status SetCurrentRom(Rom *rom)
std::string GetPendingUnsavedSessionActionSaveLabel() const
std::optional< PendingProjectRomSelection > pending_project_rom_selection_
std::unique_ptr< WindowHost > window_host_
void HandleSessionCreated(size_t index, RomSession *session)
bool EditorRequiresGameData(EditorType type) const
absl::Status ReplaceActiveSessionRom(Rom &&rom, const std::string &filepath)
static bool UpdateAllowedWithoutLoadedRom(EditorType type)
static EditorType GetEditorTypeFromCategory(const std::string &category)
static std::vector< std::string > GetAllEditorCategories()
Get all editor categories in display order for sidebar.
static bool IsPanelBasedEditor(EditorType type)
static bool IsExperimentalEditor(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:309
virtual absl::Status Redo()=0
EditorType type() const
Definition editor.h:311
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.
static std::vector< std::string > GetDefaultWindows(EditorType type)
absl::Status LoadFile(const std::string &filepath)
Load a project file into the editor.
void SetProject(project::YazeProject *project)
Set the project pointer for label import operations.
ProjectFileEditorState CaptureState() const
void RestoreState(const ProjectFileEditorState &state, project::YazeProject *project)
void set_active(bool active)
Set whether the editor window is active.
absl::Status SaveFile()
Save the current editor contents to disk.
void ResetForProject(project::YazeProject *project)
void SetSaveCompleteCallback(SaveCompleteCallback callback)
void SetToastManager(ToastManager *toast_manager)
Set toast manager for notifications.
const std::string & filepath() const
Get the current filepath.
void SetSaveGuardCallback(SaveGuardCallback callback)
Panel for managing project settings, ROM versions, and snapshots.
void SetBuildStatus(const ProjectWorkflowStatus &status)
void SetRunStatus(const ProjectWorkflowStatus &status)
absl::Status FinalizeProjectCreation(const std::string &project_name, const std::string &project_path)
Complete project creation after ROM is loaded.
absl::Status ValidateProjectCreationTarget(const std::string &project_name, const std::string &project_path=std::string()) const
absl::Status SetProjectRom(const std::string &rom_path)
Set the ROM for the current project.
absl::Status CreateNewProject(const std::string &template_name="")
void CancelPendingProject()
Cancel pending project creation.
bool IsPendingRomSelection() const
Check if project is waiting for ROM selection.
absl::Status ImportProject(const std::string &project_path)
project::YazeProject & GetCurrentProject()
static float GetDefaultDrawerWidth(DrawerType type, EditorType editor=EditorType::kUnknown)
Get the default width for a specific drawer type.
std::vector< BackupEntry > ListBackups(const std::string &rom_filename) const
absl::Status LoadRom(Rom *rom, const std::string &filename)
absl::Status SaveRom(Rom *rom)
absl::Status SaveRomAs(Rom *rom, const std::string &filename)
Manages ROM and project persistence state.
void SetProjectContext(const project::YazeProject *project)
void UpdateCurrentRomHash(Rom *rom)
Recompute the hash of the current ROM.
std::vector< RomFileManager::BackupEntry > GetRomBackups(Rom *rom) const
void SetPotItemConfirmPending(int unloaded_rooms, int total_rooms)
Set pot-item confirmation pending (called by SaveRom when needed).
absl::Status CheckRomWritePolicy(Rom *rom, const std::optional< std::string > &target_filename=std::nullopt)
Enforce project write policy; may set pending_rom_write_confirm.
absl::Status CheckRomOpenPolicy(Rom *rom)
Validate that the loaded ROM is a safe project target to open/edit.
PotItemSaveDecision ResolvePotItemSaveConfirmation(PotItemSaveDecision decision)
const std::vector< core::WriteConflict > & pending_write_conflicts() const
void SetPendingWriteConflicts(std::vector< core::WriteConflict > conflicts)
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
Run Oracle-specific ROM safety preflight before saving.
void ApplyDefaultBackupPolicy(bool enabled, const std::string &folder, int retention_count, bool keep_daily, int keep_daily_days)
Apply default backup policy from user settings.
void Open(Rom *rom, const std::string &rom_filename)
Open the dialog after ROM detection.
void SetConfirmCallback(std::function< void(const LoadOptions &)> callback)
Set callback for when options are confirmed.
void Draw(bool *p_open)
Draw the dialog (wrapper around Show)
The ScreenEditor class allows the user to edit a variety of screens in the game or create a custom me...
void SetAgentCallbacks(std::function< void(const std::string &)> send_callback, std::function< void()> focus_callback)
void ClearSelection()
Clear the current selection.
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:57
void SetBuildStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:209
void SetSessionInfo(size_t session_id, size_t total_sessions, const std::string &display_name={})
Set session information.
void SetRunStatus(const ProjectWorkflowStatus &status)
Definition status_bar.h:212
void SetRom(Rom *rom)
Set the current ROM for dirty status and filename display.
Definition status_bar.h:77
void SetActiveEditor(const std::string &name)
Set the active editor category/name for the context strip.
void SetDirtyScope(const std::string &short_label)
Compact dirty-scope chip (ROM / Project / Rooms / …).
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:71
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:219
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
static constexpr int kLastWorkspaceResetPanelLayoutDefaultsRevision
void set_apply_preset_callback(std::function< void(const std::string &)> callback)
void set_apply_preset_callback(std::function< void(const std::string &)> callback)
void set_window_manager(WorkspaceWindowManager *manager)
void set_layout_manager(LayoutManager *manager)
void 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:205
static Arena & Get()
Definition arena.cc:24
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
bool HasUnsavedChanges() const
Check if there are ANY unsaved changes.
void 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
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...
void RemoveRuntimeContext(uint64_t context_id)
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
static CustomObjectManager & Get()
void ActivateRuntimeContext(uint64_t context_id, const State &state)
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:110
#define LOG_WARN(category, format,...)
Definition log.h:108
#define LOG_INFO(category, format,...)
Definition log.h:106
#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)
zelda3::CustomObjectManager::State BuildCustomObjectRuntimeState(const project::YazeProject &project)
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.
std::string PreferStartupCategory(const std::string &saved_category, const std::vector< std::string > &available_categories)
const char * GetDrawerTypeName(RightDrawerManager::DrawerType type)
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)
bool ShouldTickEmulatorWhenHidden(bool keep_running_in_background)
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)
Animator & GetAnimator()
Definition animator.cc:318
void PostOverlayCommand(const char *command)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
Definition rom_diff.cc:11
void RegisterZ3edTestSuites()
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
Definition file_util.cc:87
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
constexpr int kExpandedPtrTableMarker
Definition overworld.h:184
constexpr int kNumOverworldMaps
Definition common.h:85
void SetPreferHmagicSpriteNames(bool prefer)
Definition sprite.cc:276
constexpr uint8_t kExpandedPtrTableMagic
Definition overworld.h:185
constexpr int kOverworldEntranceExpandedFlagPos
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
AssetLoadMode
Asset loading mode for editor resources.
void SetActiveFontIndex(int index)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Configuration options for the application startup.
Definition application.h:26
std::string startup_editor
Definition application.h:38
StartupVisibility welcome_mode
Definition application.h:32
std::vector< std::string > open_panels
Definition application.h:40
StartupVisibility sidebar_mode
Definition application.h:34
StartupVisibility dashboard_mode
Definition application.h:33
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
std::unordered_map< std::string, uint32_t > addresses
std::optional< uint32_t > GetAddress(const std::string &key) const
std::function< EditorSet *()> get_current_editor_set
std::function< void(std::function< void()>)> queue_deferred_action
std::function< absl::Status(EditorType)> ensure_editor_assets_loaded
Unified dependency container for all editor types.
Definition editor.h:169
project::YazeProject * project
Definition editor.h:173
GlobalEditorContext * global_context
Definition editor.h:175
SharedClipboard * shared_clipboard
Definition editor.h:186
gfx::IRenderer * renderer
Definition editor.h:191
ShortcutManager * shortcut_manager
Definition editor.h:185
core::VersionManager * version_manager
Definition editor.h:174
zelda3::GameData * game_data
Definition editor.h:172
WorkspaceWindowManager * window_manager
Definition editor.h:181
GfxGroupWorkspaceState * gfx_group_workspace
Definition editor.h:178
Published after ImGui::NewFrame and dockspace creation.
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
All dependencies required by LayoutCoordinator.
Built-in workflow-oriented layout profiles.
Published when panel visibility changes.
Published when a ROM is successfully loaded into a session.
Definition core_events.h:27
Represents a single session, containing a ROM and its associated editors.
core::FeatureFlags::Flags feature_flags
ProjectFileEditorState project_file_editor_state
std::array< bool, kEditorTypeCount > editor_initialized
zelda3::GameData game_data
std::unique_ptr< core::VersionManager > version_manager
std::array< bool, kEditorTypeCount > editor_assets_loaded
std::optional< project::YazeProject > project_context
Published when a session is closed.
Published when a new session is created.
Published when the active session changes.
Definition core_events.h:89
Optional behavior for an interactive status bar segment.
Definition status_bar.h:27
std::function< void()> on_click
Definition status_bar.h:28
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:1487
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:654
bool project_opened() const
Definition project.h:348
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
Definition project.cc:611
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:1521
WorkspaceSettings workspace_settings
Definition project.h:201
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1548
absl::Status Open(const std::string &project_path)
Definition project.cc:445
absl::Status Validate() const
Definition project.cc:1417
core::FeatureFlags::Flags feature_flags
Definition project.h:200
core::RomAddressOverrides rom_address_overrides
Definition project.h:203