yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
layout_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <string>
7#include <unordered_set>
8#include <utility>
9
13#include "imgui/imgui.h"
14#include "imgui/imgui_internal.h"
15
16#if defined(__APPLE__)
17#include <TargetConditionals.h>
18#endif
19#include "util/json.h"
20#include "util/log.h"
21#include "util/platform_paths.h"
22
23namespace yaze {
24namespace editor {
25
26namespace {
27
28constexpr char kLegacyPanelsKey[] = "panels";
29constexpr char kWindowsKey[] = "windows";
30
31// Helper function to show default windows from LayoutPresets
33 EditorType type) {
34 if (!registry)
35 return;
36
37 auto default_windows = LayoutPresets::GetDefaultWindows(type);
38 for (const auto& window_id : default_windows) {
39 registry->OpenWindow(window_id);
40 }
41
42 LOG_INFO("LayoutManager", "Showing %zu default windows for editor type %d",
43 default_windows.size(), static_cast<int>(type));
44}
45
46yaze::Json BoolMapToJson(const std::unordered_map<std::string, bool>& map) {
48 for (const auto& [key, value] : map) {
49 obj[key] = value;
50 }
51 return obj;
52}
53
54void JsonToBoolMap(const yaze::Json& obj,
55 std::unordered_map<std::string, bool>* map) {
56 if (!map || !obj.is_object()) {
57 return;
58 }
59 map->clear();
60 for (const auto& [key, value] : obj.items()) {
61 if (value.is_boolean()) {
62 (*map)[key] = value.get<bool>();
63 }
64 }
65}
66
67void JsonToWindowMap(const yaze::Json& entry,
68 std::unordered_map<std::string, bool>* windows) {
69 if (!windows || !entry.is_object()) {
70 return;
71 }
72 if (entry.contains(kWindowsKey)) {
73 JsonToBoolMap(entry[kWindowsKey], windows);
74 return;
75 }
76 if (entry.contains(kLegacyPanelsKey)) {
77 JsonToBoolMap(entry[kLegacyPanelsKey], windows);
78 return;
79 }
80 windows->clear();
81}
82
83std::filesystem::path GetLayoutsFilePath(LayoutScope scope,
84 const std::string& project_key) {
85 auto layouts_dir = util::PlatformPaths::GetAppDataSubdirectory("layouts");
86 if (!layouts_dir.ok()) {
87 return {};
88 }
89
90 if (scope == LayoutScope::kProject && !project_key.empty()) {
91 std::filesystem::path projects_dir = *layouts_dir / "projects";
93 return projects_dir / (project_key + ".json");
94 }
95
96 if (scope == LayoutScope::kProject) {
97 return {};
98 }
99
100 return *layouts_dir / "layouts.json";
101}
102
103bool TryGetNamedPreset(const std::string& preset_name,
104 PanelLayoutPreset* preset_out) {
105 if (!preset_out) {
106 return false;
107 }
108
109 if (preset_name == "Minimal") {
110 *preset_out = LayoutPresets::GetMinimalPreset();
111 return true;
112 }
113 if (preset_name == "Developer") {
114 *preset_out = LayoutPresets::GetDeveloperPreset();
115 return true;
116 }
117 if (preset_name == "Designer") {
118 *preset_out = LayoutPresets::GetDesignerPreset();
119 return true;
120 }
121 if (preset_name == "Modder") {
122 *preset_out = LayoutPresets::GetModderPreset();
123 return true;
124 }
125 if (preset_name == "Overworld Expert") {
127 return true;
128 }
129 if (preset_name == "Dungeon Expert") {
131 return true;
132 }
133 if (preset_name == "Testing") {
135 return true;
136 }
137 if (preset_name == "Audio") {
139 return true;
140 }
141
142 return false;
143}
144
145std::string ResolveProfilePresetName(const std::string& profile_id,
146 EditorType editor_type) {
147 if (profile_id == "mapping") {
148 if (editor_type == EditorType::kDungeon) {
149 return "Dungeon Expert";
150 }
151 return "Overworld Expert";
152 }
153 if (profile_id == "code") {
154 return "Minimal";
155 }
156 if (profile_id == "debug") {
157 return "Developer";
158 }
159 if (profile_id == "chat") {
160 return "Modder";
161 }
162 return "";
163}
164
165} // namespace
166
168 ImGuiID dockspace_id) {
169 // Don't reinitialize if already set up
170 if (IsLayoutInitialized(type)) {
171 LOG_INFO("LayoutManager",
172 "Layout for editor type %d already initialized, skipping",
173 static_cast<int>(type));
174 return;
175 }
176
177 // Store dockspace ID and current editor type for potential rebuilds
178 last_dockspace_id_ = dockspace_id;
180
181 LOG_INFO("LayoutManager", "Initializing layout for editor type %d",
182 static_cast<int>(type));
183
184 // Clear existing layout for this dockspace
185 ImGui::DockBuilderRemoveNode(dockspace_id);
186 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
187
188 ImVec2 dockspace_size = ImVec2(1280, 720); // Safe default
189 if (auto* viewport = ImGui::GetMainViewport()) {
190 dockspace_size = viewport->WorkSize;
191 }
192
193 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
194 if (last_size.x > 0.0f && last_size.y > 0.0f) {
195 dockspace_size = last_size;
196 }
197 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
198
199 // Build layout based on editor type using generic builder
200 BuildLayoutFromPreset(type, dockspace_id);
201
202 // Show default windows from LayoutPresets (single source of truth)
203 ShowDefaultWindowsForEditor(window_manager_, type);
204
205 // Finalize the layout
206 ImGui::DockBuilderFinish(dockspace_id);
207
208 // Mark as initialized
210}
211
212void LayoutManager::RebuildLayout(EditorType type, ImGuiID dockspace_id) {
213 // Validate dockspace exists
214 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
215 if (!node) {
216 LOG_ERROR("LayoutManager",
217 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
218 return;
219 }
220
221 LOG_INFO("LayoutManager", "Forcing rebuild of layout for editor type %d",
222 static_cast<int>(type));
223
224 // Store dockspace ID and current editor type
225 last_dockspace_id_ = dockspace_id;
227
228 // Clear the layout initialization flag to force rebuild
229 layouts_initialized_[type] = false;
230
231 // Clear existing layout for this dockspace
232 ImGui::DockBuilderRemoveNode(dockspace_id);
233 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
234 ImVec2 dockspace_size = ImGui::GetMainViewport()->WorkSize;
235 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
236 if (last_size.x > 0.0f && last_size.y > 0.0f) {
237 dockspace_size = last_size;
238 }
239 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
240
241 // Build layout based on editor type using generic builder
242 BuildLayoutFromPreset(type, dockspace_id);
243
244 // Show default cards from LayoutPresets (single source of truth)
245 ShowDefaultWindowsForEditor(window_manager_, type);
246
247 // Finalize the layout
248 ImGui::DockBuilderFinish(dockspace_id);
249
250 // Mark as initialized
252
253 LOG_INFO("LayoutManager", "Layout rebuild complete for editor type %d",
254 static_cast<int>(type));
255}
256
257namespace {
258
260 float left = 0.17f;
261 float right = 0.24f;
262 float bottom = 0.22f;
263 float top = 0.12f;
264 float vertical_split = 0.52f;
265
266 // Per-editor type configuration
268 DockSplitConfig cfg;
269 switch (type) {
270 case EditorType::kDungeon:
271 // Dungeon: reserve more right-side space for object/property workflows.
272 cfg.left = 0.16f;
273 cfg.right = 0.26f;
274 cfg.bottom = 0.20f;
275 cfg.vertical_split = 0.50f;
276 break;
277 case EditorType::kOverworld:
278 cfg.left = 0.22f;
279 cfg.right = 0.26f;
280 cfg.bottom = 0.23f;
281 cfg.vertical_split = 0.42f;
282 break;
283 case EditorType::kGraphics:
284 cfg.left = 0.16f;
285 cfg.right = 0.24f;
286 cfg.bottom = 0.20f;
287 break;
288 case EditorType::kPalette:
289 cfg.left = 0.16f;
290 cfg.right = 0.22f;
291 cfg.bottom = 0.20f;
292 break;
293 case EditorType::kSprite:
294 cfg.left = 0.18f;
295 cfg.right = 0.24f;
296 cfg.bottom = 0.20f;
297 break;
298 case EditorType::kScreen:
299 cfg.left = 0.16f;
300 cfg.right = 0.22f;
301 cfg.bottom = 0.20f;
302 break;
303 case EditorType::kMessage:
304 cfg.left = 0.20f;
305 cfg.right = 0.26f;
306 cfg.bottom = 0.18f;
307 break;
308 case EditorType::kAssembly:
309 cfg.left = 0.24f;
310 cfg.right = 0.16f;
311 cfg.bottom = 0.20f;
312 break;
313 case EditorType::kEmulator:
314 cfg.left = 0.14f;
315 cfg.right = 0.28f;
316 cfg.bottom = 0.22f;
317 break;
318 case EditorType::kAgent:
319 cfg.left = 0.16f;
320 cfg.right = 0.30f;
321 cfg.bottom = 0.24f;
322 break;
323 default:
324 // Use defaults
325 break;
326 }
327 return cfg;
328 }
329};
330
332 ImGuiID center = 0;
333 ImGuiID left = 0;
334 ImGuiID right = 0;
335 ImGuiID bottom = 0;
336 ImGuiID top = 0;
337 ImGuiID left_top = 0;
338 ImGuiID left_bottom = 0;
339 ImGuiID right_top = 0;
340 ImGuiID right_bottom = 0;
341};
342
344 bool left = false;
345 bool right = false;
346 bool bottom = false;
347 bool top = false;
348 bool left_top = false;
349 bool left_bottom = false;
350 bool right_top = false;
351 bool right_bottom = false;
352};
353
355 const std::string& panel_id) {
357 return true;
358 }
359 return std::find(preset.default_visible_panels.begin(),
360 preset.default_visible_panels.end(),
361 panel_id) != preset.default_visible_panels.end();
362}
363
364std::vector<std::pair<std::string, DockPosition>> CollectDockedPanels(
365 const PanelLayoutPreset& preset) {
366 std::vector<std::pair<std::string, DockPosition>> docked_panels;
367 docked_panels.reserve(preset.panel_positions.size());
368
369 std::unordered_set<std::string> seen_panels;
370 seen_panels.reserve(preset.panel_positions.size());
371
372 auto append_ordered = [&](const std::vector<std::string>& ordered_ids) {
373 for (const auto& panel_id : ordered_ids) {
374 if (seen_panels.contains(panel_id) ||
375 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
376 continue;
377 }
378 auto it = preset.panel_positions.find(panel_id);
379 if (it == preset.panel_positions.end()) {
380 continue;
381 }
382 docked_panels.emplace_back(it->first, it->second);
383 seen_panels.insert(panel_id);
384 }
385 };
386
387 append_ordered(preset.default_visible_panels);
388 append_ordered(preset.optional_panels);
389
390 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
391 remaining_panels.reserve(preset.panel_positions.size());
392 for (const auto& [panel_id, position] : preset.panel_positions) {
393 if (seen_panels.contains(panel_id) ||
394 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
395 continue;
396 }
397 remaining_panels.emplace_back(panel_id, position);
398 }
399
400 std::sort(remaining_panels.begin(), remaining_panels.end(),
401 [](const auto& lhs, const auto& rhs) {
402 if (lhs.second != rhs.second) {
403 return static_cast<int>(lhs.second) <
404 static_cast<int>(rhs.second);
405 }
406 return lhs.first < rhs.first;
407 });
408 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
409 remaining_panels.end());
410
411 return docked_panels;
412}
413
415 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
416 DockSplitNeeds needs{};
417 for (const auto& [_, pos] : docked_panels) {
418 switch (pos) {
420 needs.left = true;
421 break;
423 needs.right = true;
424 break;
426 needs.bottom = true;
427 break;
429 needs.top = true;
430 break;
432 needs.left = true;
433 needs.left_top = true;
434 break;
436 needs.left = true;
437 needs.left_bottom = true;
438 break;
440 needs.right = true;
441 needs.right_top = true;
442 break;
444 needs.right = true;
445 needs.right_bottom = true;
446 break;
448 default:
449 break;
450 }
451 }
452 return needs;
453}
454
459
461 return pos == DockPosition::Left || pos == DockPosition::LeftTop ||
463}
464
466 WorkspaceWindowManager* window_manager,
467 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
468 bool (*matches_region)(DockPosition)) {
469 if (!window_manager) {
470 return 0.0f;
471 }
472
473 float preferred_width = 0.0f;
474 for (const auto& [panel_id, position] : docked_panels) {
475 if (!matches_region(position)) {
476 continue;
477 }
478 if (WindowContent* panel = window_manager->GetWindowContent(panel_id)) {
479 preferred_width = std::max(preferred_width, panel->GetPreferredWidth());
480 }
481 }
482 return preferred_width;
483}
484
486 DockSplitConfig* cfg, const DockSplitNeeds& needs, float viewport_width,
487 WorkspaceWindowManager* window_manager,
488 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
489 if (!cfg || !window_manager || viewport_width <= 0.0f) {
490 return;
491 }
492
493 if (needs.left) {
494 const float preferred_left = ResolvePreferredRegionWidth(
495 window_manager, docked_panels, IsLeftDockPosition);
496 if (preferred_left > 0.0f) {
497 cfg->left = std::clamp(preferred_left / viewport_width, 0.14f, 0.36f);
498 }
499 }
500
501 if (needs.right) {
502 const float preferred_right = ResolvePreferredRegionWidth(
503 window_manager, docked_panels, IsRightDockPosition);
504 if (preferred_right > 0.0f) {
505 cfg->right = std::clamp(preferred_right / viewport_width, 0.18f, 0.42f);
506 }
507 }
508}
509
510DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds& needs,
511 const DockSplitConfig& cfg) {
512 DockNodeIds ids{};
513 ids.center = dockspace_id;
514
515 // Split major regions
516 if (needs.left) {
517 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.left,
518 nullptr, &ids.center);
519 }
520 if (needs.right) {
521 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
522 cfg.right, nullptr, &ids.center);
523 }
524 if (needs.bottom) {
525 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
526 cfg.bottom, nullptr, &ids.center);
527 }
528 if (needs.top) {
529 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.top,
530 nullptr, &ids.center);
531 }
532
533 // Sub-split Left region
534 if (ids.left && (needs.left_top || needs.left_bottom)) {
535 // If we only need one, the split still happens but we use the result accordingly
536 ids.left_bottom = ImGui::DockBuilderSplitNode(
537 ids.left, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.left_top);
538
539 // If one isn't needed, we technically don't have to split, but for a stable tree,
540 // we do it and get_dock_id will map to the leaf.
541 }
542
543 // Sub-split Right region
544 if (ids.right && (needs.right_top || needs.right_bottom)) {
545 ids.right_bottom = ImGui::DockBuilderSplitNode(
546 ids.right, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.right_top);
547 }
548
549 return ids;
550}
551
552} // namespace
553
554void LayoutManager::BuildLayoutFromPreset(EditorType type,
555 ImGuiID dockspace_id) {
556 auto preset = LayoutPresets::GetDefaultPreset(type);
557
558 if (!window_manager_) {
559 LOG_WARN("LayoutManager",
560 "WorkspaceWindowManager not available, skipping dock layout for "
561 "type %d",
562 static_cast<int>(type));
563 return;
564 }
565
566 const size_t session_id =
567 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
568 const auto docked_panels = CollectDockedPanels(preset);
569
570 // On compact/touch layouts, collapse all panels into center tabs instead of
571 // splitting into left/right/bottom regions. This gives each panel full
572 // screen width and makes tab-switching more natural on touch.
573 const ImGuiViewport* viewport = ImGui::GetMainViewport();
574 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
575 const bool is_compact =
576#if defined(__APPLE__) && TARGET_OS_IOS == 1
577 [&]() {
578 static bool compact_mode = true;
579 constexpr float kEnterCompactWidth = 900.0f;
580 constexpr float kExitCompactWidth = 940.0f;
581 if (viewport_width <= 0.0f) {
582 return compact_mode;
583 }
584 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
585 : (viewport_width < kEnterCompactWidth);
586 return compact_mode;
587 }();
588#else
589 (viewport_width > 0.0f && viewport_width < 900.0f);
590#endif
591
592 DockSplitNeeds needs{};
593 DockSplitConfig cfg{};
594 if (!is_compact) {
595 needs = ComputeSplitNeeds(docked_panels);
596 cfg = DockSplitConfig::ForEditor(type);
597 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, window_manager_,
598 docked_panels);
599 }
600 // When compact, needs is all-false → BuildDockTree produces center-only.
601 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
602
603 auto get_dock_id = [&](DockPosition pos) -> ImGuiID {
604 switch (pos) {
605 case DockPosition::Left:
606 if (ids.left_top || ids.left_bottom) {
607 // If sub-nodes exist, default "Left" to Top to avoid stacking in parent
608 return ids.left_top ? ids.left_top : ids.left_bottom;
609 }
610 return ids.left ? ids.left : ids.center;
611 case DockPosition::Right:
612 if (ids.right_top || ids.right_bottom) {
613 return ids.right_top ? ids.right_top : ids.right_bottom;
614 }
615 return ids.right ? ids.right : ids.center;
616 case DockPosition::Bottom:
617 return ids.bottom ? ids.bottom : ids.center;
618 case DockPosition::Top:
619 return ids.top ? ids.top : ids.center;
620 case DockPosition::LeftTop:
621 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
622 case DockPosition::LeftBottom:
623 return ids.left_bottom ? ids.left_bottom
624 : (ids.left ? ids.left : ids.center);
625 case DockPosition::RightTop:
626 return ids.right_top ? ids.right_top
627 : (ids.right ? ids.right : ids.center);
628 case DockPosition::RightBottom:
629 return ids.right_bottom ? ids.right_bottom
630 : (ids.right ? ids.right : ids.center);
631 case DockPosition::Center:
632 default:
633 return ids.center;
634 }
635 };
636
637 std::vector<ImGuiID> auto_hide_nodes;
638
639 // Iterate through positioned windows and dock them
640 for (const auto& [panel_id, position] : docked_panels) {
641 const WindowDescriptor* desc =
642 window_manager_
643 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
644 : nullptr;
645 if (!desc) {
646 LOG_WARN("LayoutManager",
647 "Preset references window '%s' that is not registered (session "
648 "%zu)",
649 panel_id.c_str(), session_id);
650 continue;
651 }
652
653 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
654 if (window_title.empty()) {
655 LOG_WARN("LayoutManager",
656 "Cannot dock window '%s': missing window name (session %zu)",
657 panel_id.c_str(), session_id);
658 continue;
659 }
660
661 const ImGuiID dock_id = get_dock_id(position);
662 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
663
664 if (WindowContent* panel = window_manager_->GetWindowContent(panel_id);
665 panel && panel->PreferAutoHideTabBar() &&
666 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
667 auto_hide_nodes.end()) {
668 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
669 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
670 auto_hide_nodes.push_back(dock_id);
671 }
672 }
673 }
674}
675
676// Deprecated individual build methods - redirected to generic or kept empty
677void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
678 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
679}
680void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
681 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
682}
683void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
684 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
685}
686void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
687 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
688}
689void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
690 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
691}
692void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
693 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
694}
695void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
696 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
697}
698void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
699 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
700}
701void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
702 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
703}
704void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
705 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
706}
707void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
708 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
709}
710
711void LayoutManager::SaveCurrentLayout(const std::string& name, bool persist) {
712 if (!window_manager_) {
713 LOG_WARN("LayoutManager",
714 "Cannot save layout '%s': WorkspaceWindowManager not available",
715 name.c_str());
716 return;
717 }
718
719 const LayoutScope scope = GetActiveScope();
720
721 // Serialize current window visibility state
722 size_t session_id = window_manager_->GetActiveSessionId();
723 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
724
725 // Store in saved_layouts_ for later persistence
726 saved_layouts_[name] = visibility_state;
727 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
728 layout_scopes_[name] = scope;
729
730 // Also save ImGui docking layout to memory
731 size_t ini_size = 0;
732 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
733 if (ini_data && ini_size > 0) {
734 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
735 }
736
737 if (persist) {
738 SaveLayoutsToDisk(scope);
739 }
740
741 LOG_INFO("LayoutManager", "Saved layout '%s' with %zu window states%s",
742 name.c_str(), visibility_state.size(),
743 persist ? "" : " (session-only)");
744}
745
746void LayoutManager::LoadLayout(const std::string& name) {
747 if (!window_manager_) {
748 LOG_WARN("LayoutManager",
749 "Cannot load layout '%s': WorkspaceWindowManager not available",
750 name.c_str());
751 return;
752 }
753
754 // Find saved layout
755 auto layout_it = saved_layouts_.find(name);
756 if (layout_it == saved_layouts_.end()) {
757 LOG_WARN("LayoutManager", "Layout '%s' not found", name.c_str());
758 return;
759 }
760
761 // Restore window visibility
762 size_t session_id = window_manager_->GetActiveSessionId();
763 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
764 /*publish_events=*/true);
765
766 auto pinned_it = saved_pinned_layouts_.find(name);
767 if (pinned_it != saved_pinned_layouts_.end()) {
768 window_manager_->RestorePinnedState(pinned_it->second);
769 }
770
771 // Restore ImGui docking layout if available
772 auto imgui_it = saved_imgui_layouts_.find(name);
773 if (imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty()) {
774 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
775 imgui_it->second.size());
776 }
777
778 LOG_INFO("LayoutManager", "Loaded layout '%s'", name.c_str());
779}
780
781void LayoutManager::CaptureTemporarySessionLayout(size_t session_id) {
782 if (!window_manager_) {
783 return;
784 }
785
786 temp_session_id_ = session_id;
787 temp_session_visibility_ =
788 window_manager_->SerializeVisibilityState(session_id);
789 temp_session_pinned_ = window_manager_->SerializePinnedState();
790
791 size_t ini_size = 0;
792 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
793 if (ini_data && ini_size > 0) {
794 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
795 } else {
796 temp_session_imgui_layout_.clear();
797 }
798
799 has_temp_session_layout_ = true;
800 LOG_INFO(
801 "LayoutManager",
802 "Captured temporary session layout for session %zu (%zu panel states)",
803 session_id, temp_session_visibility_.size());
804}
805
806bool LayoutManager::RestoreTemporarySessionLayout(size_t session_id,
807 bool clear_after_restore) {
808 if (!window_manager_ || !has_temp_session_layout_) {
809 return false;
810 }
811
812 if (session_id != temp_session_id_) {
813 LOG_WARN("LayoutManager",
814 "Session layout snapshot belongs to session %zu, requested %zu",
815 temp_session_id_, session_id);
816 return false;
817 }
818
819 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
820 /*publish_events=*/true);
821 window_manager_->RestorePinnedState(temp_session_pinned_);
822
823 if (!temp_session_imgui_layout_.empty()) {
824 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
825 temp_session_imgui_layout_.size());
826 }
827
828 if (clear_after_restore) {
829 ClearTemporarySessionLayout();
830 }
831
832 LOG_INFO("LayoutManager", "Restored temporary session layout for session %zu",
833 session_id);
834 return true;
835}
836
837void LayoutManager::ClearTemporarySessionLayout() {
838 has_temp_session_layout_ = false;
839 temp_session_id_ = 0;
840 temp_session_visibility_.clear();
841 temp_session_pinned_.clear();
842 temp_session_imgui_layout_.clear();
843}
844
845bool LayoutManager::SaveNamedSnapshot(const std::string& name,
846 size_t session_id) {
847 if (!window_manager_ || name.empty()) {
848 return false;
849 }
850 SessionSnapshot snapshot;
851 snapshot.session_id = session_id;
852 snapshot.visibility = window_manager_->SerializeVisibilityState(session_id);
853 snapshot.pinned = window_manager_->SerializePinnedState();
854 size_t ini_size = 0;
855 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
856 if (ini_data && ini_size > 0) {
857 snapshot.imgui_layout.assign(ini_data, ini_size);
858 }
859 named_snapshots_[name] = std::move(snapshot);
860 LOG_INFO("LayoutManager", "Saved named snapshot '%s' for session %zu",
861 name.c_str(), session_id);
862 return true;
863}
864
865bool LayoutManager::RestoreNamedSnapshot(const std::string& name,
866 size_t session_id,
867 bool remove_after_restore) {
868 if (!window_manager_) {
869 return false;
870 }
871 auto it = named_snapshots_.find(name);
872 if (it == named_snapshots_.end()) {
873 return false;
874 }
875 const SessionSnapshot& snapshot = it->second;
876 if (snapshot.session_id != session_id) {
877 return false;
878 }
879 window_manager_->RestoreVisibilityState(session_id, snapshot.visibility,
880 /*publish_events=*/true);
881 window_manager_->RestorePinnedState(snapshot.pinned);
882 if (!snapshot.imgui_layout.empty()) {
883 ImGui::LoadIniSettingsFromMemory(snapshot.imgui_layout.c_str(),
884 snapshot.imgui_layout.size());
885 }
886 if (remove_after_restore) {
887 named_snapshots_.erase(it);
888 }
889 return true;
890}
891
892bool LayoutManager::DeleteNamedSnapshot(const std::string& name) {
893 auto it = named_snapshots_.find(name);
894 if (it == named_snapshots_.end()) {
895 return false;
896 }
897 named_snapshots_.erase(it);
898 return true;
899}
900
901std::vector<std::string> LayoutManager::ListNamedSnapshots(
902 size_t session_id) const {
903 std::vector<std::string> names;
904 names.reserve(named_snapshots_.size());
905 for (const auto& [name, snapshot] : named_snapshots_) {
906 if (snapshot.session_id == session_id) {
907 names.push_back(name);
908 }
909 }
910 std::sort(names.begin(), names.end());
911 return names;
912}
913
914bool LayoutManager::HasNamedSnapshot(const std::string& name) const {
915 return named_snapshots_.find(name) != named_snapshots_.end();
916}
917
918bool LayoutManager::DeleteLayout(const std::string& name) {
919 auto layout_it = saved_layouts_.find(name);
920 if (layout_it == saved_layouts_.end()) {
921 LOG_WARN("LayoutManager", "Cannot delete layout '%s': not found",
922 name.c_str());
923 return false;
924 }
925
926 LayoutScope scope = GetActiveScope();
927 auto scope_it = layout_scopes_.find(name);
928 if (scope_it != layout_scopes_.end()) {
929 scope = scope_it->second;
930 }
931
932 saved_layouts_.erase(layout_it);
933 saved_imgui_layouts_.erase(name);
934 saved_pinned_layouts_.erase(name);
935 layout_scopes_.erase(name);
936
937 SaveLayoutsToDisk(scope);
938
939 LOG_INFO("LayoutManager", "Deleted layout '%s'", name.c_str());
940 return true;
941}
942
943std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
944 return {
945 {.id = "code",
946 .label = "Code",
947 .description = "Focused editing workspace with minimal panel noise",
948 .preset_name = "Minimal",
949 .open_agent_chat = false},
950 {.id = "debug",
951 .label = "Debug",
952 .description = "Debugger-first workspace for tracing and memory tools",
953 .preset_name = "Developer",
954 .open_agent_chat = false},
955 {.id = "mapping",
956 .label = "Mapping",
957 .description = "Map-centric layout for overworld or dungeon workflows",
958 .preset_name = "Overworld Expert",
959 .open_agent_chat = false},
960 {.id = "chat",
961 .label = "Chat",
962 .description = "Collaboration-heavy layout with agent-centric tooling",
963 .preset_name = "Modder",
964 .open_agent_chat = true},
965 };
966}
967
968bool LayoutManager::ApplyBuiltInProfile(const std::string& profile_id,
969 size_t session_id,
970 EditorType editor_type,
971 LayoutProfile* out_profile) {
972 if (!window_manager_) {
973 LOG_WARN("LayoutManager",
974 "Cannot apply profile '%s': WorkspaceWindowManager not available",
975 profile_id.c_str());
976 return false;
977 }
978
979 LayoutProfile matched_profile;
980 bool found = false;
981 for (const auto& profile : GetBuiltInProfiles()) {
982 if (profile.id == profile_id) {
983 matched_profile = profile;
984 found = true;
985 break;
986 }
987 }
988
989 if (!found) {
990 LOG_WARN("LayoutManager", "Unknown layout profile id: %s",
991 profile_id.c_str());
992 return false;
993 }
994
995 const std::string resolved_preset =
996 ResolveProfilePresetName(profile_id, editor_type);
997 if (!resolved_preset.empty()) {
998 matched_profile.preset_name = resolved_preset;
999 }
1000
1001 PanelLayoutPreset preset;
1002 if (!TryGetNamedPreset(matched_profile.preset_name, &preset)) {
1003 LOG_WARN("LayoutManager", "Unable to resolve preset '%s' for profile '%s'",
1004 matched_profile.preset_name.c_str(), profile_id.c_str());
1005 return false;
1006 }
1007
1008 window_manager_->HideAllWindowsInSession(session_id);
1009 for (const auto& panel_id : preset.default_visible_panels) {
1010 window_manager_->OpenWindow(session_id, panel_id);
1011 }
1012
1013 RequestRebuild();
1014
1015 if (out_profile) {
1016 *out_profile = matched_profile;
1017 }
1018
1019 LOG_INFO("LayoutManager", "Applied profile '%s' via preset '%s'",
1020 profile_id.c_str(), matched_profile.preset_name.c_str());
1021 return true;
1022}
1023
1024std::vector<std::string> LayoutManager::GetSavedLayoutNames() const {
1025 std::vector<std::string> names;
1026 names.reserve(saved_layouts_.size());
1027 for (const auto& [name, _] : saved_layouts_) {
1028 names.push_back(name);
1029 }
1030 return names;
1031}
1032
1033bool LayoutManager::HasLayout(const std::string& name) const {
1034 return saved_layouts_.find(name) != saved_layouts_.end();
1035}
1036
1037void LayoutManager::LoadLayoutsFromDisk() {
1038 saved_layouts_.clear();
1039 saved_imgui_layouts_.clear();
1040 saved_pinned_layouts_.clear();
1041 layout_scopes_.clear();
1042
1043 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal, /*merge=*/false);
1044 if (!project_layout_key_.empty()) {
1045 LoadLayoutsFromDiskInternal(LayoutScope::kProject, /*merge=*/true);
1046 }
1047}
1048
1049void LayoutManager::SetProjectLayoutKey(const std::string& key) {
1050 if (key.empty()) {
1051 UseGlobalLayouts();
1052 return;
1053 }
1054 project_layout_key_ = key;
1055 LoadLayoutsFromDisk();
1056}
1057
1058void LayoutManager::UseGlobalLayouts() {
1059 project_layout_key_.clear();
1060 LoadLayoutsFromDisk();
1061}
1062
1063LayoutScope LayoutManager::GetActiveScope() const {
1064 return project_layout_key_.empty() ? LayoutScope::kGlobal
1065 : LayoutScope::kProject;
1066}
1067
1068void LayoutManager::LoadLayoutsFromDiskInternal(LayoutScope scope, bool merge) {
1069 std::filesystem::path layout_path =
1070 GetLayoutsFilePath(scope, project_layout_key_);
1071 if (layout_path.empty()) {
1072 return;
1073 }
1074
1075 if (!std::filesystem::exists(layout_path)) {
1076 if (!merge) {
1077 LOG_INFO("LayoutManager", "No layouts file at %s",
1078 layout_path.string().c_str());
1079 }
1080 return;
1081 }
1082
1083 try {
1084 std::ifstream file(layout_path);
1085 if (!file.is_open()) {
1086 LOG_WARN("LayoutManager", "Failed to open layouts file: %s",
1087 layout_path.string().c_str());
1088 return;
1089 }
1090
1091 yaze::Json root;
1092 file >> root;
1093
1094 if (!root.contains("layouts") || !root["layouts"].is_object()) {
1095 LOG_WARN("LayoutManager", "Layouts file missing 'layouts' object: %s",
1096 layout_path.string().c_str());
1097 return;
1098 }
1099
1100 for (auto& [name, entry] : root["layouts"].items()) {
1101 if (!entry.is_object()) {
1102 continue;
1103 }
1104
1105 std::unordered_map<std::string, bool> windows;
1106 std::unordered_map<std::string, bool> pinned;
1107
1108 JsonToWindowMap(entry, &windows);
1109 if (entry.contains("pinned")) {
1110 JsonToBoolMap(entry["pinned"], &pinned);
1111 }
1112
1113 saved_layouts_[name] = std::move(windows);
1114 saved_pinned_layouts_[name] = std::move(pinned);
1115 layout_scopes_[name] = scope;
1116
1117 if (entry.contains("imgui_ini") && entry["imgui_ini"].is_string()) {
1118 saved_imgui_layouts_[name] = entry["imgui_ini"].get<std::string>();
1119 } else {
1120 saved_imgui_layouts_.erase(name);
1121 }
1122 }
1123
1124 LOG_INFO("LayoutManager", "Loaded layouts from %s",
1125 layout_path.string().c_str());
1126 } catch (const std::exception& e) {
1127 LOG_WARN("LayoutManager", "Failed to load layouts: %s", e.what());
1128 }
1129}
1130
1131void LayoutManager::SaveLayoutsToDisk(LayoutScope scope) const {
1132 std::filesystem::path layout_path =
1133 GetLayoutsFilePath(scope, project_layout_key_);
1134 if (layout_path.empty()) {
1135 LOG_WARN("LayoutManager", "No layout path resolved for scope");
1136 return;
1137 }
1138
1139 auto status =
1140 util::PlatformPaths::EnsureDirectoryExists(layout_path.parent_path());
1141 if (!status.ok()) {
1142 LOG_WARN("LayoutManager", "Failed to create layout directory: %s",
1143 status.ToString().c_str());
1144 return;
1145 }
1146
1147 try {
1148 yaze::Json root;
1149 root["version"] = 2;
1150 root["layouts"] = yaze::Json::object();
1151
1152 for (const auto& [name, windows] : saved_layouts_) {
1153 auto scope_it = layout_scopes_.find(name);
1154 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
1155 continue;
1156 }
1157
1158 yaze::Json entry;
1159 entry[kWindowsKey] = BoolMapToJson(windows);
1160
1161 auto pinned_it = saved_pinned_layouts_.find(name);
1162 if (pinned_it != saved_pinned_layouts_.end()) {
1163 entry["pinned"] = BoolMapToJson(pinned_it->second);
1164 }
1165
1166 auto imgui_it = saved_imgui_layouts_.find(name);
1167 if (imgui_it != saved_imgui_layouts_.end()) {
1168 entry["imgui_ini"] = imgui_it->second;
1169 }
1170
1171 root["layouts"][name] = entry;
1172 }
1173
1174 std::ofstream file(layout_path);
1175 if (!file.is_open()) {
1176 LOG_WARN("LayoutManager", "Failed to open layouts file for write: %s",
1177 layout_path.string().c_str());
1178 return;
1179 }
1180 file << root.dump(2);
1181 file.close();
1182 } catch (const std::exception& e) {
1183 LOG_WARN("LayoutManager", "Failed to save layouts: %s", e.what());
1184 }
1185}
1186
1187void LayoutManager::ResetToDefaultLayout(EditorType type) {
1188 layouts_initialized_[type] = false;
1189 LOG_INFO("LayoutManager", "Reset layout for editor type %d",
1190 static_cast<int>(type));
1191}
1192
1193bool LayoutManager::IsLayoutInitialized(EditorType type) const {
1194 auto it = layouts_initialized_.find(type);
1195 return it != layouts_initialized_.end() && it->second;
1196}
1197
1198void LayoutManager::MarkLayoutInitialized(EditorType type) {
1199 layouts_initialized_[type] = true;
1200 LOG_INFO("LayoutManager", "Marked layout for editor type %d as initialized",
1201 static_cast<int>(type));
1202}
1203
1204void LayoutManager::ClearInitializationFlags() {
1205 layouts_initialized_.clear();
1206 LOG_INFO("LayoutManager", "Cleared all layout initialization flags");
1207}
1208
1209std::string LayoutManager::GetWindowTitle(const std::string& card_id) const {
1210 if (!window_manager_) {
1211 return "";
1212 }
1213
1214 const size_t session_id = window_manager_->GetActiveSessionId();
1215 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1216}
1217
1218} // namespace editor
1219} // namespace yaze
bool is_object() const
Definition json.h:57
static Json object()
Definition json.h:34
items_view items()
Definition json.h:88
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
bool contains(const std::string &) const
Definition json.h:53
WorkspaceWindowManager * window_manager_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
std::unordered_map< EditorType, bool > layouts_initialized_
bool IsLayoutInitialized(EditorType type) const
Check if a layout has been initialized for an editor.
void MarkLayoutInitialized(EditorType type)
Mark a layout as initialized.
void InitializeEditorLayout(EditorType type, ImGuiID dockspace_id)
Initialize the default layout for a specific editor type.
void BuildLayoutFromPreset(EditorType type, ImGuiID dockspace_id)
static PanelLayoutPreset GetLogicDebuggerPreset()
Get the "logic debugger" workspace preset (QA and debug focused)
static PanelLayoutPreset GetDungeonMasterPreset()
Get the "dungeon master" workspace preset.
static PanelLayoutPreset GetAudioEngineerPreset()
Get the "audio engineer" workspace preset (music focused)
static PanelLayoutPreset GetDesignerPreset()
Get the "designer" workspace preset (visual-focused)
static std::vector< std::string > GetDefaultWindows(EditorType type)
static PanelLayoutPreset GetOverworldArtistPreset()
Get the "overworld artist" workspace preset.
static PanelLayoutPreset GetModderPreset()
Get the "modder" workspace preset (full-featured)
static PanelLayoutPreset GetMinimalPreset()
Get the "minimal" workspace preset (minimal cards)
static PanelLayoutPreset GetDeveloperPreset()
Get the "developer" workspace preset (debug-focused)
Base interface for all logical window content components.
Central registry for all editor cards with session awareness and dependency injection.
bool OpenWindow(size_t session_id, const std::string &base_window_id)
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
std::vector< std::pair< std::string, DockPosition > > CollectDockedPanels(const PanelLayoutPreset &preset)
float ResolvePreferredRegionWidth(WorkspaceWindowManager *window_manager, const std::vector< std::pair< std::string, DockPosition > > &docked_panels, bool(*matches_region)(DockPosition))
std::string ResolveProfilePresetName(const std::string &profile_id, EditorType editor_type)
bool TryGetNamedPreset(const std::string &preset_name, PanelLayoutPreset *preset_out)
yaze::Json BoolMapToJson(const std::unordered_map< std::string, bool > &map)
void ApplyPreferredSplitWidths(DockSplitConfig *cfg, const DockSplitNeeds &needs, float viewport_width, WorkspaceWindowManager *window_manager, const std::vector< std::pair< std::string, DockPosition > > &docked_panels)
void JsonToWindowMap(const yaze::Json &entry, std::unordered_map< std::string, bool > *windows)
std::filesystem::path GetLayoutsFilePath(LayoutScope scope, const std::string &project_key)
void JsonToBoolMap(const yaze::Json &obj, std::unordered_map< std::string, bool > *map)
bool ShouldDockPanelInDefaultLayout(const PanelLayoutPreset &preset, const std::string &panel_id)
DockSplitNeeds ComputeSplitNeeds(const std::vector< std::pair< std::string, DockPosition > > &docked_panels)
void ShowDefaultWindowsForEditor(WorkspaceWindowManager *registry, EditorType type)
DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds &needs, const DockSplitConfig &cfg)
LayoutScope
Storage scope for saved layouts.
DockPosition
Preferred dock position for a card in a layout.
std::unordered_map< std::string, bool > visibility
std::unordered_map< std::string, bool > pinned
Built-in workflow-oriented layout profiles.
Defines default panel visibility for an editor type.
std::vector< std::string > optional_panels
std::unordered_map< std::string, DockPosition > panel_positions
std::vector< std::string > default_visible_panels
Metadata for a dockable editor window (formerly PanelInfo)