yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
workspace_window_manager.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
2
4
5#include <algorithm>
6#include <cmath>
7#include <cstdio>
8#include <fstream>
9#include <utility>
10
11#include "absl/strings/str_format.h"
19#include "app/gui/core/icons.h"
22#include "imgui/imgui.h"
23#include "imgui/imgui_internal.h" // For ImGuiWindow and FindWindowByName
24#include "util/json.h"
25#include "util/log.h"
26#include "util/platform_paths.h"
27
28namespace yaze {
29namespace editor {
30
31namespace {
32
34 const WindowContent& panel, const WindowPresentationPolicy& presentation) {
35 WindowDescriptor descriptor;
36 auto* panel_ptr = const_cast<WindowContent*>(&panel);
37 descriptor.card_id = panel.GetId();
38 descriptor.display_name = panel.GetDisplayName();
39 descriptor.icon = panel.GetIcon();
40 descriptor.category = panel.GetEditorCategory();
41 descriptor.priority = panel.GetPriority();
42 descriptor.workflow_group = panel.GetWorkflowGroup();
43 descriptor.workflow_label = panel.GetWorkflowLabel();
45 descriptor.workflow_priority = panel.GetWorkflowPriority();
46 descriptor.shortcut_hint = panel.GetShortcutHint();
47 descriptor.scope = panel.GetScope();
48 descriptor.window_lifecycle = panel.GetWindowLifecycle();
49 descriptor.context_scope = panel.GetContextScope();
50 descriptor.presentation = presentation;
51 descriptor.enabled_condition = [panel_ptr]() {
52 return panel_ptr->IsEnabled();
53 };
54 descriptor.disabled_tooltip = panel.GetDisabledTooltip();
55 descriptor.visibility_flag = nullptr; // Created by RegisterPanel
56 descriptor.window_title = panel.GetIcon() + " " + panel.GetDisplayName();
57 return descriptor;
58}
59
60} // namespace
61
62// ============================================================================
63// Category Icon Mapping
64// ============================================================================
65
67 const std::string& category) {
68 if (category == "Dungeon")
69 return ICON_MD_CASTLE;
70 if (category == "Overworld")
71 return ICON_MD_MAP;
72 if (category == "Graphics")
73 return ICON_MD_IMAGE;
74 if (category == "Palette")
75 return ICON_MD_PALETTE;
76 if (category == "Sprite")
77 return ICON_MD_PERSON;
78 if (category == "Music")
79 return ICON_MD_MUSIC_NOTE;
80 if (category == "Message")
81 return ICON_MD_MESSAGE;
82 if (category == "Screen")
83 return ICON_MD_TV;
84 if (category == "Emulator")
86 if (category == "Assembly")
87 return ICON_MD_CODE;
88 if (category == "Settings")
89 return ICON_MD_SETTINGS;
90 if (category == "Memory")
91 return ICON_MD_MEMORY;
92 if (category == "Agent")
93 return ICON_MD_SMART_TOY;
94 if (category == "Planning")
95 return ICON_MD_ROUTE;
96 return ICON_MD_FOLDER; // Default for unknown categories
97}
98
99// ============================================================================
100// Category Theme Colors (Expressive Icon Theming)
101// ============================================================================
102
104 const std::string& category) {
105 // Expressive colors for each category - vibrant when active
106 // Format: {icon_r, icon_g, icon_b, icon_a, glow_r, glow_g, glow_b}
107
108 if (category == "Dungeon") {
109 // Castle gold - warm, regal
110 return {0.95f, 0.75f, 0.20f, 1.0f, 0.95f, 0.75f, 0.20f};
111 }
112 if (category == "Overworld") {
113 // Forest green - natural, expansive
114 return {0.30f, 0.85f, 0.45f, 1.0f, 0.30f, 0.85f, 0.45f};
115 }
116 if (category == "Graphics") {
117 // Image blue - creative, visual
118 return {0.40f, 0.70f, 0.95f, 1.0f, 0.40f, 0.70f, 0.95f};
119 }
120 if (category == "Palette") {
121 // Rainbow pink/magenta - colorful, artistic
122 return {0.90f, 0.40f, 0.70f, 1.0f, 0.90f, 0.40f, 0.70f};
123 }
124 if (category == "Sprite") {
125 // Character cyan - lively, animated
126 return {0.30f, 0.85f, 0.85f, 1.0f, 0.30f, 0.85f, 0.85f};
127 }
128 if (category == "Music") {
129 // Note purple - creative, rhythmic
130 return {0.70f, 0.40f, 0.90f, 1.0f, 0.70f, 0.40f, 0.90f};
131 }
132 if (category == "Message") {
133 // Text yellow - communicative, bright
134 return {0.95f, 0.90f, 0.40f, 1.0f, 0.95f, 0.90f, 0.40f};
135 }
136 if (category == "Screen") {
137 // TV white/silver - display, clean
138 return {0.90f, 0.92f, 0.95f, 1.0f, 0.90f, 0.92f, 0.95f};
139 }
140 if (category == "Emulator") {
141 // Game red - playful, active
142 return {0.90f, 0.35f, 0.40f, 1.0f, 0.90f, 0.35f, 0.40f};
143 }
144 if (category == "Assembly") {
145 // Code green - technical, precise
146 return {0.40f, 0.90f, 0.50f, 1.0f, 0.40f, 0.90f, 0.50f};
147 }
148 if (category == "Settings") {
149 // Gear gray/blue - utility, system
150 return {0.60f, 0.70f, 0.80f, 1.0f, 0.60f, 0.70f, 0.80f};
151 }
152 if (category == "Memory") {
153 // Memory orange - data, technical
154 return {0.95f, 0.60f, 0.25f, 1.0f, 0.95f, 0.60f, 0.25f};
155 }
156 if (category == "Agent") {
157 // AI purple/violet - intelligent, futuristic
158 return {0.60f, 0.40f, 0.95f, 1.0f, 0.60f, 0.40f, 0.95f};
159 }
160 if (category == "Planning") {
161 // Map amber - routes, goals
162 return {0.95f, 0.65f, 0.35f, 1.0f, 0.95f, 0.65f, 0.35f};
163 }
164
165 // Default - neutral blue
166 return {0.50f, 0.60f, 0.80f, 1.0f, 0.50f, 0.60f, 0.80f};
167}
168
171 const float fallback = GetSidePanelWidthForViewport(viewport_width);
172 if (viewport_width <= 0.0f) {
173 SidePanelWidthBounds bounds{};
174 bounds.min_width = 220.0f;
175 bounds.max_width = std::max(520.0f, fallback);
176 return bounds;
177 }
178
179 const float min_width = std::max(220.0f, viewport_width * 0.18f);
180 const float max_width = std::max(min_width + 20.0f, viewport_width * 0.62f);
181 SidePanelWidthBounds bounds{};
182 bounds.min_width = min_width;
183 bounds.max_width = max_width;
184 return bounds;
185}
186
188 float viewport_width) const {
189 const float default_width = GetSidePanelWidthForViewport(viewport_width);
190 if (browser_state_.sidebar_width <= 0.0f) {
191 return default_width;
192 }
193 const auto bounds = GetSidePanelWidthBounds(viewport_width);
194 return std::clamp(browser_state_.sidebar_width, bounds.min_width,
195 bounds.max_width);
196}
197
199 float viewport_width,
200 bool notify) {
201 if (width <= 0.0f) {
205 }
206 return;
207 }
208
209 const auto bounds = GetSidePanelWidthBounds(viewport_width);
210 const float clamped = std::clamp(width, bounds.min_width, bounds.max_width);
211 if (std::abs(browser_state_.sidebar_width - clamped) < 0.5f) {
212 return;
213 }
217 }
218}
219
229
231 bool notify) {
232 const float fallback = GetDefaultPanelBrowserCategoryWidth();
233 const float clamped = std::max(180.0f, width > 0.0f ? width : fallback);
234 if (std::abs(browser_state_.window_browser_category_width - clamped) < 0.5f) {
235 return;
236 }
241 }
242}
243
245 const std::string& prefixed_id) {
246 auto it = registry_state_.descriptors.find(prefixed_id);
247 return it != registry_state_.descriptors.end() ? &it->second : nullptr;
248}
249
251 const std::string& prefixed_id) const {
252 auto it = registry_state_.descriptors.find(prefixed_id);
253 return it != registry_state_.descriptors.end() ? &it->second : nullptr;
254}
255
257 size_t session_id) {
258 auto it = session_state_.session_windows.find(session_id);
259 return it != session_state_.session_windows.end() ? &it->second : nullptr;
260}
261
262const std::vector<std::string>* WorkspaceWindowManager::FindSessionWindowIds(
263 size_t session_id) const {
264 auto it = session_state_.session_windows.find(session_id);
265 return it != session_state_.session_windows.end() ? &it->second : nullptr;
266}
267
268std::unordered_map<std::string, std::string>*
270 auto it = session_state_.session_window_mapping.find(session_id);
271 return it != session_state_.session_window_mapping.end() ? &it->second
272 : nullptr;
273}
274
275const std::unordered_map<std::string, std::string>*
277 auto it = session_state_.session_window_mapping.find(session_id);
278 return it != session_state_.session_window_mapping.end() ? &it->second
279 : nullptr;
280}
281
282std::unordered_map<std::string, std::string>*
284 auto it = session_state_.session_reverse_window_mapping.find(session_id);
285 return it != session_state_.session_reverse_window_mapping.end() ? &it->second
286 : nullptr;
287}
288
289const std::unordered_map<std::string, std::string>*
291 size_t session_id) const {
292 auto it = session_state_.session_reverse_window_mapping.find(session_id);
293 return it != session_state_.session_reverse_window_mapping.end() ? &it->second
294 : nullptr;
295}
296
298 size_t session_id, const std::string& prefixed_window_id,
299 const std::string& base_window_id, const std::string& category,
300 bool visible) const {
301 if (auto* bus = ContentRegistry::Context::event_bus()) {
303 prefixed_window_id, base_window_id, category, visible, session_id));
304 }
305}
306
308 const std::string& resource_type) const {
309 if (resource_type == "room") {
311 }
312 if (resource_type == "song") {
314 }
315 if (resource_type == "sheet") {
317 }
318 if (resource_type == "map") {
320 }
322}
323
325 const std::string& panel_id, ResourceWindowContent* resource_panel) {
326 if (!resource_panel) {
327 return;
328 }
329 const std::string resource_type = resource_panel->GetResourceType();
330 resource_panels_[resource_type].push_back(panel_id);
331 panel_resource_types_[panel_id] = resource_type;
332}
333
335 const std::string& panel_id) {
336 auto type_it = panel_resource_types_.find(panel_id);
337 if (type_it == panel_resource_types_.end()) {
338 return;
339 }
340
341 const std::string resource_type = type_it->second;
342 auto panels_it = resource_panels_.find(resource_type);
343 if (panels_it != resource_panels_.end()) {
344 panels_it->second.remove(panel_id);
345 if (panels_it->second.empty()) {
346 resource_panels_.erase(panels_it);
347 }
348 }
349 panel_resource_types_.erase(type_it);
350}
351
353 const std::list<std::string>& panel_ids) const {
354 for (const auto& panel_id : panel_ids) {
355 if (!IsWindowPinnedImpl(panel_id)) {
356 return panel_id;
357 }
358 }
359
360 return panel_ids.empty() ? std::string{} : panel_ids.front();
361}
362
363// ============================================================================
364// Session Lifecycle Management
365// ============================================================================
366
367// ============================================================================
368// Panel Registration
369// ============================================================================
370
372 const WindowDescriptor& base_info) {
373 RegisterSession(session_id); // Ensure session exists
374
375 WindowDescriptor canonical_info = base_info;
376 canonical_info.card_id = ResolveBaseWindowId(base_info.card_id);
377
378 std::string panel_id =
379 MakeWindowId(session_id, canonical_info.card_id, canonical_info.scope);
380
381 bool already_registered = (cards_.find(panel_id) != cards_.end());
382 if (already_registered && canonical_info.scope != WindowScope::kGlobal) {
383 LOG_WARN("WorkspaceWindowManager",
384 "Panel '%s' already registered, skipping duplicate",
385 panel_id.c_str());
386 }
387
388 if (!already_registered) {
389 // Create new WindowDescriptor with final ID
390 WindowDescriptor panel_info = canonical_info;
391 panel_info.card_id = panel_id;
392
393 // If no visibility_flag provided, create centralized one
394 if (!panel_info.visibility_flag) {
395 centralized_visibility_[panel_id] = false; // Hidden by default
396 panel_info.visibility_flag = &centralized_visibility_[panel_id];
397 }
398
399 // Register the card
400 cards_[panel_id] = panel_info;
401
402 LOG_INFO("WorkspaceWindowManager",
403 "Registered card %s -> %s for session %zu",
404 canonical_info.card_id.c_str(), panel_id.c_str(), session_id);
405 }
406
407 if (canonical_info.scope == WindowScope::kGlobal) {
408 global_panel_ids_.insert(panel_id);
409 for (const auto& [mapped_session, _] : session_cards_) {
410 TrackPanelForSession(mapped_session, canonical_info.card_id, panel_id);
411 }
412 } else {
413 TrackPanelForSession(session_id, canonical_info.card_id, panel_id);
414 }
415}
416
418 size_t session_id, const std::string& card_id,
419 const std::string& display_name, const std::string& icon,
420 const std::string& category, const std::string& shortcut_hint, int priority,
421 std::function<void()> on_show, std::function<void()> on_hide,
422 bool visible_by_default) {
423 WindowDescriptor info;
424 info.card_id = ResolveBaseWindowId(card_id);
425 info.display_name = display_name;
426 info.icon = icon;
427 info.category = category;
428 info.shortcut_hint = shortcut_hint;
429 info.priority = priority;
430 info.visibility_flag = nullptr; // Will be created in RegisterPanel
431 info.on_show = on_show;
432 info.on_hide = on_hide;
433
434 RegisterPanel(session_id, info);
435
436 // Set initial visibility if requested
437 if (visible_by_default) {
438 OpenWindowImpl(session_id, info.card_id);
439 }
440}
441
443 const std::string& base_card_id) {
444 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
445 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
446 if (prefixed_id.empty()) {
447 return;
448 }
449
450 pending_float_window_ids_.erase(prefixed_id);
451
452 auto it = cards_.find(prefixed_id);
453 if (it != cards_.end()) {
454 LOG_INFO("WorkspaceWindowManager", "Unregistered card: %s",
455 prefixed_id.c_str());
456 RememberPinnedStateForRemovedWindow(session_id, canonical_base_id,
457 prefixed_id);
458 UntrackResourceWindow(prefixed_id);
459 cards_.erase(it);
460 centralized_visibility_.erase(prefixed_id);
461 pinned_panels_.erase(prefixed_id);
462 if (global_panel_ids_.find(prefixed_id) != global_panel_ids_.end()) {
463 global_panel_ids_.erase(prefixed_id);
464 for (auto& [mapped_session, card_list] : session_cards_) {
465 card_list.erase(
466 std::remove(card_list.begin(), card_list.end(), prefixed_id),
467 card_list.end());
468 }
469 for (auto& [mapped_session, mapping] : session_card_mapping_) {
470 mapping.erase(canonical_base_id);
471 }
472 for (auto& [mapped_session, reverse_mapping] :
474 reverse_mapping.erase(prefixed_id);
475 }
476 return;
477 }
478
479 // Remove from session tracking
480 auto& session_card_list = session_cards_[session_id];
481 session_card_list.erase(std::remove(session_card_list.begin(),
482 session_card_list.end(), prefixed_id),
483 session_card_list.end());
484
485 session_card_mapping_[session_id].erase(canonical_base_id);
486 }
487}
488
490 const std::string& prefix) {
491 struct RemovalInfo {
492 std::string prefixed_id;
493 size_t session_id = 0;
494 std::string base_id;
495 };
496 std::vector<RemovalInfo> to_remove;
497
498 // Find all cards with the given prefix
499 for (const auto& [card_id, card_info] : cards_) {
500 (void)card_info;
501 if (card_id.find(prefix) == 0) { // Starts with prefix
502 RemovalInfo info;
503 info.prefixed_id = card_id;
504 for (const auto& [session_id, reverse_mapping] :
506 auto reverse_it = reverse_mapping.find(card_id);
507 if (reverse_it != reverse_mapping.end()) {
508 info.session_id = session_id;
509 info.base_id = reverse_it->second;
510 break;
511 }
512 }
513 to_remove.push_back(std::move(info));
514 }
515 }
516
517 // Remove them
518 for (const auto& info : to_remove) {
519 if (!info.base_id.empty()) {
520 RememberPinnedStateForRemovedWindow(info.session_id, info.base_id,
521 info.prefixed_id);
522 }
523 UntrackResourceWindow(info.prefixed_id);
524 pending_float_window_ids_.erase(info.prefixed_id);
525 cards_.erase(info.prefixed_id);
526 centralized_visibility_.erase(info.prefixed_id);
527 pinned_panels_.erase(info.prefixed_id);
528 LOG_INFO("WorkspaceWindowManager", "Unregistered card with prefix '%s': %s",
529 prefix.c_str(), info.prefixed_id.c_str());
530 }
531
532 // Also clean up session tracking
533 for (auto& [session_id, card_list] : session_cards_) {
534 card_list.erase(std::remove_if(card_list.begin(), card_list.end(),
535 [&prefix](const std::string& id) {
536 return id.find(prefix) == 0;
537 }),
538 card_list.end());
539 }
540}
541
543 cards_.clear();
545 pinned_panels_.clear();
546 session_cards_.clear();
547 session_card_mapping_.clear();
549 session_context_keys_.clear();
550 panel_instances_.clear();
551 registry_panel_ids_.clear();
552 global_panel_ids_.clear();
553 resource_panels_.clear();
554 panel_resource_types_.clear();
555 session_count_ = 0;
556 LOG_INFO("WorkspaceWindowManager", "Cleared all cards");
557}
558
559// ============================================================================
560// WindowContent Instance Management (Phase 4)
561// ============================================================================
562
564 std::unique_ptr<WindowContent> panel) {
565 if (!panel) {
566 LOG_ERROR("WorkspaceWindowManager",
567 "Attempted to register null WindowContent");
568 return;
569 }
570
571 auto* resource_panel = dynamic_cast<ResourceWindowContent*>(panel.get());
572 if (resource_panel) {
573 EnforceResourceWindowLimits(resource_panel->GetResourceType());
574 }
575
576 std::string panel_id = panel->GetId();
577
578 // Check if already registered
579 if (panel_instances_.find(panel_id) != panel_instances_.end()) {
580 LOG_WARN("WorkspaceWindowManager",
581 "WindowContent '%s' already registered, skipping registry add",
582 panel_id.c_str());
583 return;
584 }
585
586 if (panel->GetScope() == WindowScope::kGlobal) {
587 global_panel_ids_.insert(panel_id);
588 }
589 registry_panel_ids_.insert(panel_id);
590
591 // Store the WindowContent instance
592 panel_instances_[panel_id] = std::move(panel);
593
594 TrackResourceWindow(panel_id, resource_panel);
595
596 LOG_INFO("WorkspaceWindowManager", "Registered registry WindowContent: %s",
597 panel_id.c_str());
598}
599
601 size_t session_id) {
602 RegisterSession(session_id);
603 for (const auto& panel_id : registry_panel_ids_) {
604 auto it = panel_instances_.find(panel_id);
605 if (it == panel_instances_.end()) {
606 continue;
607 }
608 RegisterPanelDescriptorForSession(session_id, *it->second);
609 }
610}
611
613 std::unique_ptr<WindowContent> panel) {
614 const WindowPresentationPolicy presentation =
615 panel ? panel->GetPresentationPolicy()
617 RegisterWindowContent(std::move(panel), presentation);
618}
619
621 std::unique_ptr<WindowContent> panel,
622 WindowPresentationPolicy presentation) {
623 if (!panel) {
624 LOG_ERROR("WorkspaceWindowManager",
625 "Attempted to register null WindowContent");
626 return;
627 }
628
629 auto* resource_panel = dynamic_cast<ResourceWindowContent*>(panel.get());
630 if (resource_panel) {
631 EnforceResourceWindowLimits(resource_panel->GetResourceType());
632 }
633
634 const std::string panel_id = ResolveBaseWindowId(panel->GetId());
635
636 // Auto-register WindowDescriptor for sidebar/menu visibility
637 WindowDescriptor descriptor = BuildDescriptorFromPanel(*panel, presentation);
638
639 // Check if panel should be visible by default
640 bool visible_by_default = panel->IsVisibleByDefault();
641
642 // Store session content under the same final ID as its descriptor. Otherwise
643 // loading this editor in a second ROM session replaces the first session's
644 // instance because both use the same unprefixed base ID.
645 // Reuse an existing mapping even if the total session count changed since
646 // initial registration; switching between prefixed and unprefixed IDs during
647 // a reload would leave two live instances for the same session.
648 std::string instance_id;
649 bool descriptor_preexisting = false;
650 if (panel->GetScope() == WindowScope::kSession) {
651 if (const auto* session_mapping =
653 if (auto mapping_it = session_mapping->find(panel_id);
654 mapping_it != session_mapping->end() &&
655 cards_.contains(mapping_it->second)) {
656 instance_id = mapping_it->second;
657 descriptor_preexisting = true;
658 }
659 }
660 } else {
661 descriptor_preexisting = cards_.contains(panel_id);
662 }
663 if (instance_id.empty()) {
664 // Register the descriptor (creates visibility storage and session mapping).
665 RegisterPanel(active_session_, descriptor);
666 instance_id = GetPrefixedWindowId(active_session_, panel_id);
667 }
668 if (instance_id.empty()) {
669 instance_id = MakeWindowId(active_session_, panel_id, panel->GetScope());
670 }
671
672 // An editor may pre-register a descriptor that names no presentation policy
673 // and then register the content that does. The content's declared policy
674 // fills the gap; a descriptor that already declared one keeps it.
675 if (descriptor_preexisting && presentation.declared) {
676 if (auto descriptor_it = cards_.find(instance_id);
677 descriptor_it != cards_.end() &&
678 !descriptor_it->second.presentation.declared) {
679 descriptor_it->second.presentation = presentation;
680 }
681 }
682
683 // Descriptors created here must not capture the first content instance.
684 // Resolve IsEnabled through the owning slot so same-session reloads can
685 // replace content without leaving a dangling callback. Descriptors that an
686 // editor registered explicitly keep their richer editor-owned condition.
687 if (!descriptor_preexisting) {
688 if (auto descriptor_it = cards_.find(instance_id);
689 descriptor_it != cards_.end()) {
690 descriptor_it->second.enabled_condition = [this, instance_id]() {
691 auto instance_it = panel_instances_.find(instance_id);
692 return instance_it != panel_instances_.end() &&
693 instance_it->second->IsEnabled();
694 };
695 }
696 }
697
698 auto existing = panel_instances_.find(instance_id);
699 if (existing != panel_instances_.end()) {
700 // Editor content objects capture the owning editor/session. When a project
701 // or session reloads, keeping the old instance leaves stale callbacks that
702 // can dereference freed editor state on the next draw.
703 existing->second->OnClose();
705 UntrackResourceWindow(instance_id);
706 existing->second = std::move(panel);
707 TrackResourceWindow(instance_id, resource_panel);
708 if (visible_by_default) {
710 }
711 LOG_INFO("WorkspaceWindowManager", "Replaced WindowContent: %s",
712 instance_id.c_str());
713 return;
714 }
715
716 // Set initial visibility if panel should be visible by default
717 if (visible_by_default) {
719 }
720
721 // Store the WindowContent instance
722 panel_instances_[instance_id] = std::move(panel);
723
724 TrackResourceWindow(instance_id, resource_panel);
725
726 LOG_INFO("WorkspaceWindowManager", "Registered WindowContent: %s (%s)",
727 instance_id.c_str(), descriptor.display_name.c_str());
728}
729
730// ============================================================================
731// Resource Management (Phase 6)
732// ============================================================================
733
735 const std::string& resource_type) {
736 auto it = resource_panels_.find(resource_type);
737 if (it == resource_panels_.end())
738 return;
739
740 auto& panel_list = it->second;
741 const size_t limit = GetResourceWindowLimit(resource_type);
742
743 // Evict panels until we have room for one more (current count < limit)
744 while (panel_list.size() >= limit) {
745 std::string panel_to_evict = SelectResourceWindowForEviction(panel_list);
746 if (panel_to_evict.empty()) {
747 break;
748 }
749
750 // Remove from LRU list first to avoid iterator issues
751 panel_list.remove(panel_to_evict);
752
753 UnregisterWindowContent(panel_to_evict);
754 }
755}
756
757void WorkspaceWindowManager::MarkPanelUsed(const std::string& panel_id) {
758 auto type_it = panel_resource_types_.find(panel_id);
759 if (type_it == panel_resource_types_.end())
760 return;
761
762 std::string type = type_it->second;
763 auto& list = resource_panels_[type];
764
765 // Move to back (MRU)
766 // std::list::remove is slow (linear), but list size is small (<10)
767 list.remove(panel_id);
768 list.push_back(panel_id);
769}
770
772 const std::string& panel_id) {
773 size_t owner_session = active_session_;
774 std::string base_panel_id;
775 std::string instance_id;
776
777 // Resource eviction supplies the exact tracked instance ID. Resolve that
778 // identity before treating an unprefixed ID as a base ID in the active
779 // session; otherwise an inactive single-session-era instance can evict the
780 // active session's newer prefixed instance with the same base ID.
781 if (panel_instances_.contains(panel_id)) {
782 for (const auto& [session_id, reverse_mapping] :
784 if (auto reverse_it = reverse_mapping.find(panel_id);
785 reverse_it != reverse_mapping.end()) {
786 owner_session = session_id;
787 base_panel_id = reverse_it->second;
788 instance_id = panel_id;
789 break;
790 }
791 }
792 }
793
794 if (instance_id.empty()) {
795 const std::string requested_base_id = ResolveBaseWindowId(panel_id);
796 if (const auto* active_mapping =
798 if (auto mapping_it = active_mapping->find(requested_base_id);
799 mapping_it != active_mapping->end()) {
800 base_panel_id = requested_base_id;
801 instance_id = mapping_it->second;
802 }
803 }
804 }
805
806 // Resolve a qualified descriptor without a live content instance as a
807 // fallback for callers cleaning up partially registered content.
808 if (instance_id.empty()) {
809 for (const auto& [session_id, reverse_mapping] :
811 if (auto reverse_it = reverse_mapping.find(panel_id);
812 reverse_it != reverse_mapping.end()) {
813 owner_session = session_id;
814 base_panel_id = reverse_it->second;
815 instance_id = panel_id;
816 break;
817 }
818 }
819 }
820 if (instance_id.empty()) {
821 base_panel_id = ResolveBaseWindowId(panel_id);
822 instance_id = GetPrefixedWindowId(owner_session, base_panel_id);
823 }
824 if (instance_id.empty()) {
825 instance_id = panel_id;
826 }
827
828 UntrackResourceWindow(instance_id);
829 auto it = panel_instances_.find(instance_id);
830 if (it != panel_instances_.end()) {
831 // Call OnClose before removing
832 it->second->OnClose();
834 panel_instances_.erase(it);
835 registry_panel_ids_.erase(base_panel_id);
836 global_panel_ids_.erase(instance_id);
837 LOG_INFO("WorkspaceWindowManager", "Unregistered WindowContent: %s",
838 instance_id.c_str());
839 }
840
841 // Also unregister the descriptor
842 UnregisterPanel(owner_session, base_panel_id);
843}
844
846 const std::string& panel_id) {
847 return GetWindowContent(active_session_, panel_id);
848}
849
851 size_t session_id, const std::string& panel_id) {
852 const auto* session_mapping = FindSessionWindowMapping(session_id);
853 if (!session_mapping) {
854 return nullptr;
855 }
856
857 std::string base_panel_id = GetBaseIdForPrefixedId(session_id, panel_id);
858 if (base_panel_id.empty()) {
859 base_panel_id = ResolveBaseWindowId(panel_id);
860 }
861 auto mapping_it = session_mapping->find(base_panel_id);
862 if (mapping_it == session_mapping->end()) {
863 return nullptr;
864 }
865 return FindPanelInstance(mapping_it->second, base_panel_id);
866}
867
869 const std::string& prefixed_panel_id, const std::string& base_panel_id) {
870 auto prefixed_it = panel_instances_.find(prefixed_panel_id);
871 if (prefixed_it != panel_instances_.end()) {
872 return prefixed_it->second.get();
873 }
874 auto base_it = panel_instances_.find(base_panel_id);
875 if (base_it != panel_instances_.end()) {
876 return base_it->second.get();
877 }
878 return nullptr;
879}
880
882 const std::string& prefixed_panel_id,
883 const std::string& base_panel_id) const {
884 auto prefixed_it = panel_instances_.find(prefixed_panel_id);
885 if (prefixed_it != panel_instances_.end()) {
886 return prefixed_it->second.get();
887 }
888 auto base_it = panel_instances_.find(base_panel_id);
889 if (base_it != panel_instances_.end()) {
890 return base_it->second.get();
891 }
892 return nullptr;
893}
894
896 // Suppress panel drawing when dashboard is active (no editor selected yet)
897 // This ensures panels don't appear until user selects an editor
898 if (browser_state_.active_category.empty() ||
900 return;
901 }
902
903 auto session_it = session_cards_.find(active_session_);
904 if (session_it == session_cards_.end()) {
905 return;
906 }
907
908 auto& animator = gui::GetAnimator();
909 bool animations_enabled = animator.IsEnabled();
910 const bool touch_device = gui::LayoutHelpers::IsTouchDevice();
911
912 const auto resolve_switch_speed = [&](float snappy, float standard,
913 float relaxed) {
914 switch (animator.motion_profile()) {
916 return snappy;
918 return relaxed;
920 default:
921 return standard;
922 }
923 };
924 const float category_transition_speed =
925 resolve_switch_speed(8.5f, 6.0f, 4.5f);
926 const float panel_fade_speed = resolve_switch_speed(11.0f, 8.0f, 6.0f);
927
928 // Animate global category transition alpha
929 float global_alpha = 1.0f;
930 if (animations_enabled) {
931 global_alpha = animator.Animate("global", "category_transition", 1.0f,
932 category_transition_speed);
933 }
934
935 for (const auto& prefixed_panel_id : session_it->second) {
936 auto descriptor_it = cards_.find(prefixed_panel_id);
937 if (descriptor_it == cards_.end()) {
938 continue;
939 }
940 WindowDescriptor& descriptor = descriptor_it->second;
941
942 std::string base_panel_id =
943 GetBaseIdForPrefixedId(active_session_, prefixed_panel_id);
944 if (base_panel_id.empty()) {
945 base_panel_id = prefixed_panel_id;
946 }
947
948 WindowContent* panel = FindPanelInstance(prefixed_panel_id, base_panel_id);
949 if (!panel) {
950 continue;
951 }
952
953 bool is_visible = descriptor.visibility_flag && *descriptor.visibility_flag;
954
955 // Category filtering: draw if matches active category or user has pinned.
956 // Previously a Persistent lifecycle was a third branch here; collapsed into
957 // CrossEditor + default-pin via UserSettings revision-7 migration.
958 bool should_draw = false;
959 if (is_visible) {
962 should_draw = true;
963 } else if (IsWindowPinnedImpl(active_session_, base_panel_id)) {
964 should_draw = true;
965 }
966 }
967
968 // Compute target alpha: 1.0 if should draw, 0.0 if should hide
969 float target_alpha = should_draw ? 1.0f : 0.0f;
970
971 // Animate alpha towards target (or snap if animations disabled)
972 float current_alpha = 1.0f;
973 if (animations_enabled) {
974 current_alpha = animator.Animate(prefixed_panel_id, "panel_alpha",
975 target_alpha, panel_fade_speed);
976 current_alpha *= global_alpha; // Apply global category fade
977 } else {
978 current_alpha = target_alpha;
979 }
980
981 // Skip drawing if alpha is effectively zero
982 if (current_alpha < 0.01f) {
983 continue;
984 }
985
986 // Get visibility flag for the panel window
987 bool* visibility_flag = descriptor.visibility_flag;
988
989 // Get display name without icon - PanelWindow will add the icon
990 // This fixes the double-icon issue where both descriptor and PanelWindow added icons
991 std::string display_name = descriptor.display_name.empty()
992 ? panel->GetDisplayName()
993 : descriptor.display_name;
994 std::string icon =
995 descriptor.icon.empty() ? panel->GetIcon() : descriptor.icon;
996
997 if (editor_resolver_) {
998 if (Editor* editor = editor_resolver_(panel->GetEditorCategory())) {
1000 }
1001 }
1002
1003 // Create PanelWindow and draw content
1004 gui::PanelWindow window(display_name.c_str(), icon.c_str(),
1005 visibility_flag);
1006 window.SetStableId(prefixed_panel_id);
1007 const bool force_float =
1008 pending_float_window_ids_.count(prefixed_panel_id) > 0;
1009 if (panel->PrefersFloating() || force_float) {
1011 } else if (touch_device) {
1013 }
1014 // Only undock on explicit OpenWindowFloating — PrefersFloating alone sets
1015 // the default floating position without blocking user docking.
1016 if (force_float) {
1017 window.RequestForceUndock();
1018 pending_float_window_ids_.erase(prefixed_panel_id);
1019 }
1020
1021 // Use preferred size from WindowContent if specified.
1022 float preferred_width = panel->GetPreferredWidth();
1023 float preferred_height = panel->GetPreferredHeight();
1024 if (preferred_width > 0.0f || preferred_height > 0.0f) {
1025 window.SetDefaultSize(
1026 preferred_width > 0.0f ? preferred_width : 400.0f,
1027 preferred_height > 0.0f ? preferred_height : 300.0f);
1028 }
1029
1030 // Enable pin functionality for cross-editor persistence
1031 window.SetPinnable(true);
1032 window.SetPinned(IsWindowPinnedImpl(active_session_, base_panel_id));
1033
1034 // Wire up pin state change callback to persist to WorkspaceWindowManager
1035 window.SetPinChangedCallback([this, base_panel_id](bool pinned) {
1036 SetWindowPinnedImpl(base_panel_id, pinned);
1037 });
1038
1039 // Apply fade alpha for smooth transitions
1040 std::optional<gui::StyleVarGuard> alpha_guard;
1041 if (current_alpha < 1.0f) {
1042 alpha_guard.emplace(ImGuiStyleVar_Alpha, current_alpha);
1043 }
1044
1045 animator.ApplyPanelTransitionPre(prefixed_panel_id);
1046 if (window.Begin(visibility_flag)) {
1047 panel->DrawWithLazyInit(visibility_flag);
1048 }
1049 window.End();
1050 animator.ApplyPanelTransitionPost(prefixed_panel_id);
1051
1052 alpha_guard.reset();
1053
1054 // Handle visibility change (window closed via X button)
1055 if (visibility_flag && !*visibility_flag) {
1056 panel->OnClose();
1057 }
1058 }
1059}
1060
1061void WorkspaceWindowManager::OnEditorSwitch(const std::string& from_category,
1062 const std::string& to_category) {
1063 if (from_category == to_category) {
1064 return; // No switch needed
1065 }
1066
1067 LOG_INFO("WorkspaceWindowManager", "Switching from category '%s' to '%s'",
1068 from_category.c_str(), to_category.c_str());
1069
1070 // IMPORTANT:
1071 // Editor switching must *not* be treated as a user-initiated hide/show.
1072 //
1073 // Historically this function toggled visibility flags (CloseWindowImpl/OpenWindowImpl),
1074 // which publishes PanelVisibilityChangedEvent and permanently overwrote user
1075 // prefs. Worse, dynamic "resource windows" (rooms/songs) interpret
1076 // IsWindowVisibleImpl()==false as "user closed", unregistering themselves.
1077 //
1078 // Reset global category transition to trigger fade-in
1079 if (gui::GetAnimator().IsEnabled()) {
1082
1083 // Also begin a per-panel fade for every visible window in the incoming
1084 // category. Without this, individual panels use whatever alpha the
1085 // animator happened to have for their workspace fade state from the
1086 // previous session, which was the source of the "old bitmap still
1087 // visible during switch" ghosting. EditorActivator clears that narrow
1088 // transition state just before this call, so each transition starts from
1089 // alpha=0.
1090 const auto incoming =
1092 for (const auto& descriptor : incoming) {
1093 if (descriptor.visibility_flag && *descriptor.visibility_flag) {
1095 GetPrefixedWindowId(active_session_, descriptor.card_id),
1097 }
1098 }
1099 }
1100
1101 SetActiveCategory(to_category);
1102}
1103
1104// ============================================================================
1105// Panel Control (Programmatic, No GUI)
1106// ============================================================================
1107
1109 size_t session_id, const std::string& base_window_id) {
1110 const auto* descriptor = GetWindowDescriptorImpl(session_id, base_window_id);
1111 if (descriptor == nullptr || !descriptor->IsAdmitted() ||
1112 !descriptor->presentation.allow_popout) {
1113 return false;
1114 }
1115 if (!OpenWindowImpl(session_id, base_window_id)) {
1116 return false;
1117 }
1119 GetPrefixedWindowId(session_id, ResolveBaseWindowId(base_window_id)));
1120 return true;
1121}
1122
1124 const std::string& base_card_id) {
1125 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1126 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1127 if (prefixed_id.empty()) {
1128 return false;
1129 }
1130
1131 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1132 if (descriptor) {
1133 if (!descriptor->IsAdmitted()) {
1134 return false;
1135 }
1136 const bool was_visible =
1137 (descriptor->visibility_flag && *descriptor->visibility_flag);
1138 if (descriptor->visibility_flag) {
1139 *descriptor->visibility_flag = true;
1140 }
1141 if (descriptor->on_show) {
1142 descriptor->on_show();
1143 }
1144 if (!was_visible) {
1145 if (WindowContent* panel =
1146 FindPanelInstance(prefixed_id, canonical_base_id)) {
1147 panel->OnOpen();
1148 }
1149 }
1150
1151 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1152 descriptor->category, true);
1153 return true;
1154 }
1155 return false;
1156}
1157
1159 const std::string& base_card_id) {
1160 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1161 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1162 if (prefixed_id.empty()) {
1163 return false;
1164 }
1165
1166 pending_float_window_ids_.erase(prefixed_id);
1167
1168 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1169 if (descriptor) {
1170 const bool was_visible =
1171 (descriptor->visibility_flag && *descriptor->visibility_flag);
1172 if (descriptor->visibility_flag) {
1173 *descriptor->visibility_flag = false;
1174 }
1175 if (descriptor->on_hide) {
1176 descriptor->on_hide();
1177 }
1178 if (was_visible) {
1179 if (WindowContent* panel =
1180 FindPanelInstance(prefixed_id, canonical_base_id)) {
1181 panel->OnClose();
1182 }
1183 }
1184
1185 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1186 descriptor->category, false);
1187 return true;
1188 }
1189 return false;
1190}
1191
1193 const std::string& base_card_id) {
1194 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1195 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1196 if (prefixed_id.empty()) {
1197 return false;
1198 }
1199
1200 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1201 if (descriptor && descriptor->visibility_flag) {
1202 bool new_state = !(*descriptor->visibility_flag);
1203 *descriptor->visibility_flag = new_state;
1204 if (!new_state)
1205 pending_float_window_ids_.erase(prefixed_id);
1206
1207 if (new_state && descriptor->on_show) {
1208 descriptor->on_show();
1209 } else if (!new_state && descriptor->on_hide) {
1210 descriptor->on_hide();
1211 }
1212 if (WindowContent* panel =
1213 FindPanelInstance(prefixed_id, canonical_base_id)) {
1214 if (new_state) {
1215 panel->OnOpen();
1216 } else {
1217 panel->OnClose();
1218 }
1219 }
1220
1221 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1222 descriptor->category, new_state);
1223 return true;
1224 }
1225 return false;
1226}
1227
1229 size_t session_id, const std::string& base_card_id) const {
1230 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1231 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1232 if (prefixed_id.empty()) {
1233 return false;
1234 }
1235
1236 const auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1237 if (descriptor && descriptor->visibility_flag) {
1238 return *descriptor->visibility_flag;
1239 }
1240 return false;
1241}
1242
1244 size_t session_id, const std::string& base_card_id) {
1245 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1246 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1247 if (prefixed_id.empty()) {
1248 return nullptr;
1249 }
1250
1251 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1252 if (descriptor) {
1253 return descriptor->visibility_flag;
1254 }
1255 return nullptr;
1256}
1257
1258// ============================================================================
1259// Batch Operations
1260// ============================================================================
1261
1263 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1264 for (const auto& prefixed_card_id : *session_windows) {
1265 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1266 if (!descriptor->IsListedInWindowBrowser()) {
1267 continue;
1268 }
1269 if (descriptor->visibility_flag) {
1270 *descriptor->visibility_flag = true;
1271 }
1272 if (descriptor->on_show) {
1273 descriptor->on_show();
1274 }
1275 }
1276 }
1277 }
1278}
1279
1281 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1282 for (const auto& prefixed_card_id : *session_windows) {
1283 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1284 if (descriptor->visibility_flag) {
1285 pending_float_window_ids_.erase(prefixed_card_id);
1286 *descriptor->visibility_flag = false;
1287 }
1288 if (descriptor->on_hide) {
1289 descriptor->on_hide();
1290 }
1291 }
1292 }
1293 }
1294}
1295
1297 size_t session_id, const std::string& category) {
1298 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1299 for (const auto& prefixed_card_id : *session_windows) {
1300 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1301 if (descriptor->category != category) {
1302 continue;
1303 }
1304 if (!descriptor->IsListedInWindowBrowser()) {
1305 continue;
1306 }
1307 if (descriptor->visibility_flag) {
1308 *descriptor->visibility_flag = true;
1309 }
1310 if (descriptor->on_show) {
1311 descriptor->on_show();
1312 }
1313 }
1314 }
1315 }
1316}
1317
1319 size_t session_id, const std::string& category) {
1320 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1321 for (const auto& prefixed_card_id : *session_windows) {
1322 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1323 if (descriptor->category != category) {
1324 continue;
1325 }
1326 if (descriptor->visibility_flag) {
1327 pending_float_window_ids_.erase(prefixed_card_id);
1328 *descriptor->visibility_flag = false;
1329 }
1330 if (descriptor->on_hide) {
1331 descriptor->on_hide();
1332 }
1333 }
1334 }
1335 }
1336}
1337
1339 const std::string& base_card_id) {
1340 // First get the category of the target card
1341 std::string prefixed_id = GetPrefixedWindowId(session_id, base_card_id);
1342 if (prefixed_id.empty()) {
1343 return;
1344 }
1345
1346 const auto* target = FindDescriptorByPrefixedId(prefixed_id);
1347 if (!target) {
1348 return;
1349 }
1350
1351 std::string category = target->category;
1352
1353 // Hide all cards in the same category
1354 HideAllWindowsInCategory(session_id, category);
1355
1356 // Show the target card
1357 OpenWindowImpl(session_id, base_card_id);
1358}
1359
1360// ============================================================================
1361// Query Methods
1362// ============================================================================
1363
1365 size_t session_id) const {
1366 std::vector<std::string> result;
1367 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1368 result.reserve(session_windows->size());
1369 for (const auto& prefixed_window_id : *session_windows) {
1370 const std::string base_window_id =
1371 GetBaseIdForPrefixedId(session_id, prefixed_window_id);
1372 result.push_back(base_window_id.empty() ? prefixed_window_id
1373 : base_window_id);
1374 }
1375 }
1376 return result;
1377}
1378
1380 size_t session_id, const std::string& category) const {
1381 std::vector<WindowDescriptor> result;
1382
1383 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1384 for (const auto& prefixed_window_id : *session_windows) {
1385 if (const auto* descriptor =
1386 FindDescriptorByPrefixedId(prefixed_window_id)) {
1387 if (descriptor->category == category) {
1388 WindowDescriptor window = *descriptor;
1389 const std::string base_window_id =
1390 GetBaseIdForPrefixedId(session_id, prefixed_window_id);
1391 if (!base_window_id.empty()) {
1392 window.card_id = base_window_id;
1393 }
1394 result.push_back(std::move(window));
1395 }
1396 }
1397 }
1398 }
1399
1400 // Sort by priority
1401 std::sort(result.begin(), result.end(),
1402 [](const WindowDescriptor& a, const WindowDescriptor& b) {
1403 return a.priority < b.priority;
1404 });
1405
1406 return result;
1407}
1408
1410 size_t session_id) const {
1411 std::vector<std::string> categories;
1412
1413 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1414 for (const auto& prefixed_window_id : *session_windows) {
1415 if (const auto* descriptor =
1416 FindDescriptorByPrefixedId(prefixed_window_id)) {
1417 if (std::find(categories.begin(), categories.end(),
1418 descriptor->category) == categories.end()) {
1419 categories.push_back(descriptor->category);
1420 }
1421 }
1422 }
1423 }
1424 return categories;
1425}
1426
1428 size_t session_id, const std::string& base_card_id) const {
1429 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1430 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1431 if (prefixed_id.empty()) {
1432 return nullptr;
1433 }
1434
1435 return FindDescriptorByPrefixedId(prefixed_id);
1436}
1437
1438std::vector<std::string> WorkspaceWindowManager::GetAllCategories() const {
1439 std::vector<std::string> categories;
1440 for (const auto& [card_id, card_info] : registry_state_.descriptors) {
1441 if (std::find(categories.begin(), categories.end(), card_info.category) ==
1442 categories.end()) {
1443 categories.push_back(card_info.category);
1444 }
1445 }
1446 return categories;
1447}
1448
1449// ============================================================================
1450// State Persistence
1451// ============================================================================
1452
1454 size_t session_id) const {
1455 std::vector<std::string> visible_panels;
1456
1457 const auto* window_mapping = FindSessionWindowMapping(session_id);
1458 if (!window_mapping) {
1459 return visible_panels;
1460 }
1461
1462 for (const auto& [base_id, prefixed_id] : *window_mapping) {
1463 if (const auto* descriptor = FindDescriptorByPrefixedId(prefixed_id)) {
1464 if (descriptor->visibility_flag && *descriptor->visibility_flag) {
1465 visible_panels.push_back(base_id);
1466 }
1467 }
1468 }
1469
1470 return visible_panels;
1471}
1472
1473// ============================================================================
1474// Workspace Presets
1475// ============================================================================
1476
1477void WorkspaceWindowManager::SavePreset(const std::string& name,
1478 const std::string& description) {
1479 WorkspacePreset preset;
1480 preset.name = name;
1481 preset.description = description;
1482
1483 // Collect all visible cards across all sessions
1484 for (const auto& [card_id, card_info] : cards_) {
1485 if (card_info.visibility_flag && *card_info.visibility_flag) {
1486 preset.visible_cards.push_back(card_id);
1487 }
1488 }
1489
1490 presets_[name] = preset;
1492 LOG_INFO("WorkspaceWindowManager", "Saved preset: %s (%zu cards)",
1493 name.c_str(), preset.visible_cards.size());
1494}
1495
1496bool WorkspaceWindowManager::LoadPreset(const std::string& name) {
1497 auto it = presets_.find(name);
1498 if (it == presets_.end()) {
1499 return false;
1500 }
1501
1502 // First hide all cards
1503 for (auto& [card_id, card_info] : cards_) {
1504 if (card_info.visibility_flag) {
1505 pending_float_window_ids_.erase(card_id);
1506 *card_info.visibility_flag = false;
1507 }
1508 }
1509
1510 // Then show preset cards
1511 for (const auto& card_id : it->second.visible_cards) {
1512 auto card_it = cards_.find(card_id);
1513 if (card_it != cards_.end() && card_it->second.visibility_flag) {
1514 *card_it->second.visibility_flag = true;
1515 if (card_it->second.on_show) {
1516 card_it->second.on_show();
1517 }
1518 }
1519 }
1520
1521 LOG_INFO("WorkspaceWindowManager", "Loaded preset: %s", name.c_str());
1522 return true;
1523}
1524
1525void WorkspaceWindowManager::DeletePreset(const std::string& name) {
1526 presets_.erase(name);
1528}
1529
1530std::vector<WorkspaceWindowManager::WorkspacePreset>
1532 std::vector<WorkspacePreset> result;
1533 for (const auto& [name, preset] : presets_) {
1534 result.push_back(preset);
1535 }
1536 return result;
1537}
1538
1539// ============================================================================
1540// Quick Actions
1541// ============================================================================
1542
1543void WorkspaceWindowManager::ShowAll(size_t session_id) {
1544 ShowAllWindowsInSession(session_id);
1545}
1546
1547void WorkspaceWindowManager::HideAll(size_t session_id) {
1548 HideAllWindowsInSession(session_id);
1549}
1550
1552 // Hide all cards first
1553 HideAllWindowsInSession(session_id);
1554
1555 // TODO: Load default visibility from config file or hardcoded defaults
1556 LOG_INFO("WorkspaceWindowManager", "Reset to defaults for session %zu",
1557 session_id);
1558}
1559
1561 EditorType editor_type) {
1562 // Get category for this editor
1563 std::string category = EditorRegistry::GetEditorCategory(editor_type);
1564 if (category.empty()) {
1565 LOG_WARN("WorkspaceWindowManager",
1566 "No category found for editor type %d, skipping reset",
1567 static_cast<int>(editor_type));
1568 return;
1569 }
1570
1571 // Publish every close so the next editor switch cannot restore stale prefs.
1572 for (const auto& window : GetWindowsInCategory(session_id, category)) {
1573 CloseWindow(session_id, window.card_id);
1574 }
1575
1576 // Get default cards from LayoutPresets
1577 auto default_panels = LayoutPresets::GetDefaultPanels(editor_type);
1578
1579 // Show each default card
1580 for (const auto& card_id : default_panels) {
1581 if (OpenWindowImpl(session_id, card_id)) {
1582 LOG_INFO("WorkspaceWindowManager", "Showing default card: %s",
1583 card_id.c_str());
1584 }
1585 }
1586
1587 LOG_INFO("WorkspaceWindowManager",
1588 "Reset %s editor to defaults (%zu cards visible)", category.c_str(),
1589 default_panels.size());
1590}
1591
1592// ============================================================================
1593// Session Prefixing Utilities
1594// ============================================================================
1595
1597 size_t session_id, const std::string& base_id) const {
1598 return MakeWindowId(session_id, base_id, WindowScope::kSession);
1599}
1600
1601std::string WorkspaceWindowManager::MakeWindowId(size_t session_id,
1602 const std::string& base_id,
1603 WindowScope scope) const {
1604 const std::string canonical_base_id = ResolveBaseWindowId(base_id);
1605 if (scope == WindowScope::kGlobal) {
1606 return canonical_base_id;
1607 }
1608 if (ShouldPrefixPanels()) {
1609 return absl::StrFormat("s%zu.%s", session_id, canonical_base_id);
1610 }
1611 return canonical_base_id;
1612}
1613
1614} // namespace editor
1615} // namespace yaze
static std::string GetEditorCategory(EditorType type)
Interface for editor classes.
Definition editor.h:249
static std::vector< std::string > GetDefaultPanels(EditorType type)
Get default visible panels for an editor.
Base class for windows that edit specific ROM resources.
virtual std::string GetResourceType() const =0
The resource type name.
Base interface for all logical window content components.
virtual std::string GetWorkflowDescription() const
Optional workflow description for menus/command palette.
virtual std::string GetDisabledTooltip() const
Get tooltip text when panel is disabled.
virtual int GetWorkflowPriority() const
Optional workflow ordering priority (lower sorts first).
virtual std::string GetDisplayName() const =0
Human-readable name shown in menus and title bars.
virtual WindowLifecycle GetWindowLifecycle() const
Get the lifecycle category for this window.
virtual std::string GetEditorCategory() const =0
Editor category this panel belongs to.
virtual int GetPriority() const
Get display priority for menu ordering.
virtual void OnClose()
Called when panel is hidden.
virtual WindowScope GetScope() const
Get the registration scope for this window.
virtual std::string GetIcon() const =0
Material Design icon for this panel.
virtual float GetPreferredHeight() const
Get preferred height for this panel (optional)
virtual float GetPreferredWidth() const
Get preferred width for this panel (optional)
virtual WindowContextScope GetContextScope() const
Optional context binding for this window (room/selection/etc)
virtual std::string GetWorkflowLabel() const
Optional workflow label for menus/command palette.
void DrawWithLazyInit(bool *p_open)
Execute lazy initialization if needed, then call Draw()
virtual bool PrefersFloating() const
virtual std::string GetId() const =0
Unique identifier for this panel.
virtual std::string GetWorkflowGroup() const
Optional workflow group for hack-centric actions.
virtual std::string GetShortcutHint() const
Get keyboard shortcut hint for display.
std::unordered_set< std::string > & pending_float_window_ids_
void RegisterPanel(size_t session_id, const WindowDescriptor &base_info)
bool IsWindowPinnedImpl(size_t session_id, const std::string &base_card_id) const
void SavePreset(const std::string &name, const std::string &description="")
std::vector< std::string > GetWindowsInSessionImpl(size_t session_id) const
void SetActiveSidePanelWidth(float width, float viewport_width=0.0f, bool notify=true)
void HideAllWindowsInCategory(size_t session_id, const std::string &category)
bool CloseWindowImpl(size_t session_id, const std::string &base_card_id)
size_t GetResourceWindowLimit(const std::string &resource_type) const
std::unordered_map< size_t, std::unordered_map< std::string, std::string > > & session_reverse_card_mapping_
static SidePanelWidthBounds GetSidePanelWidthBounds(float viewport_width)
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
std::vector< WorkspacePreset > GetPresets() const
void UnregisterWindowContent(const std::string &window_id)
Unregister and destroy a WindowContent instance.
std::unordered_map< std::string, std::string > * FindSessionWindowMapping(size_t session_id)
std::string SelectResourceWindowForEviction(const std::list< std::string > &panel_ids) const
bool * GetVisibilityFlag(size_t session_id, const std::string &base_card_id)
void UntrackResourceWindow(const std::string &panel_id)
std::string ResolveBaseWindowId(const std::string &panel_id) const
std::unordered_map< std::string, WindowDescriptor > & cards_
std::string GetPrefixedWindowId(size_t session_id, const std::string &base_id) const
bool OpenWindowFloating(size_t session_id, const std::string &base_window_id)
static float GetSidePanelWidthForViewport(float viewport_width)
std::vector< std::string > * FindSessionWindowIds(size_t session_id)
const WindowDescriptor * GetWindowDescriptorImpl(size_t session_id, const std::string &base_card_id) const
void RememberPinnedStateForRemovedWindow(size_t session_id, const std::string &base_card_id, const std::string &prefixed_id)
std::string GetBaseIdForPrefixedId(size_t session_id, const std::string &prefixed_id) const
static CategoryTheme GetCategoryTheme(const std::string &category)
std::unordered_map< std::string, std::string > & panel_resource_types_
std::vector< WindowDescriptor > GetWindowsInCategoryImpl(size_t session_id, const std::string &category) const
std::unordered_map< std::string, bool > & centralized_visibility_
std::unordered_map< size_t, std::unordered_map< std::string, std::string > > & session_card_mapping_
void MarkPanelUsed(const std::string &panel_id)
Mark a panel as recently used (for LRU)
void TrackPanelForSession(size_t session_id, const std::string &base_id, const std::string &panel_id)
bool CloseWindow(size_t session_id, const std::string &base_window_id)
std::unordered_map< std::string, std::list< std::string > > & resource_panels_
void TrackResourceWindow(const std::string &panel_id, ResourceWindowContent *resource_panel)
std::vector< std::string > GetAllCategories() const
void SetActiveCategory(const std::string &category, bool notify=true)
void RegisterRegistryWindowContent(std::unique_ptr< WindowContent > window)
Register a ContentRegistry-managed WindowContent instance.
static std::string GetCategoryIcon(const std::string &category)
void UnregisterPanel(size_t session_id, const std::string &base_card_id)
void ShowOnlyWindow(size_t session_id, const std::string &base_window_id)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::function< Editor *(const std::string &) editor_resolver_)
static constexpr const char * kDashboardCategory
std::unordered_map< std::string, WorkspacePreset > presets_
void EnforceResourceLimits(const std::string &resource_type)
Enforce limits on resource panels (LRU eviction)
WindowDescriptor * FindDescriptorByPrefixedId(const std::string &prefixed_id)
void SetPanelBrowserCategoryWidth(float width, bool notify=true)
bool IsWindowVisibleImpl(size_t session_id, const std::string &base_card_id) const
void DrawAllVisiblePanels()
Draw all visible WindowContent instances (central drawing)
std::unordered_map< std::string, std::unique_ptr< WindowContent > > & panel_instances_
std::unordered_set< std::string > & registry_panel_ids_
std::vector< std::string > GetVisibleWindowIdsImpl(size_t session_id) const
void SetWindowPinnedImpl(size_t session_id, const std::string &base_card_id, bool pinned)
bool ToggleWindowImpl(size_t session_id, const std::string &base_card_id)
std::unordered_map< size_t, std::vector< std::string > > & session_cards_
void EnforceResourceWindowLimits(const std::string &resource_type)
std::unordered_map< std::string, std::string > * FindSessionReverseWindowMapping(size_t session_id)
float GetActiveSidePanelWidth(float viewport_width) const
std::string MakeWindowId(size_t session_id, const std::string &base_id) const
void OnEditorSwitch(const std::string &from_category, const std::string &to_category)
Handle editor/category switching for panel visibility.
std::unordered_map< std::string, bool > & pinned_panels_
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
void RegisterRegistryWindowContentsForSession(size_t session_id)
Register descriptors for all registry window contents in a session.
std::unordered_map< size_t, std::unordered_map< WindowContextScope, std::string, WindowContextScopeHash > > & session_context_keys_
WindowContent * FindPanelInstance(const std::string &prefixed_panel_id, const std::string &base_panel_id)
void UnregisterPanelsWithPrefix(const std::string &prefix)
void PublishWindowVisibilityChanged(size_t session_id, const std::string &prefixed_window_id, const std::string &base_window_id, const std::string &category, bool visible) const
void RegisterPanelDescriptorForSession(size_t session_id, const WindowContent &panel)
bool OpenWindowImpl(size_t session_id, const std::string &base_card_id)
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
std::unordered_set< std::string > & global_panel_ids_
void ClearAnimationsForPanel(const std::string &panel_id)
Definition animator.cc:83
void BeginPanelTransition(const std::string &panel_id, TransitionType type)
Definition animator.cc:126
Draggable, dockable panel for editor sub-windows.
void SetPinChangedCallback(std::function< void(bool)> callback)
void SetPinned(bool pinned)
void SetStableId(const std::string &stable_id)
void SetPinnable(bool pinnable)
void SetPosition(Position pos)
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_MESSAGE
Definition icons.h:1201
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_ROUTE
Definition icons.h:1627
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_TV
Definition icons.h:2032
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
::yaze::EventBus * event_bus()
Get the current EventBus instance.
void SetCurrentEditor(Editor *editor)
Set the currently active editor.
constexpr size_t kMaxTotalResourcePanels
Maximum total resource panels across all types.
constexpr size_t kMaxSongPanels
Maximum open song panels (music editor)
constexpr size_t kMaxSheetPanels
Maximum open graphics sheet panels.
constexpr size_t kMaxRoomPanels
Maximum open room panels (dungeon editor)
constexpr size_t kMaxMapPanels
Maximum open map panels (overworld editor)
WindowDescriptor BuildDescriptorFromPanel(const WindowContent &panel, const WindowPresentationPolicy &presentation)
WindowScope
Defines whether a window is session-scoped or global.
Animator & GetAnimator()
Definition animator.cc:318
static PanelVisibilityChangedEvent Create(const std::string &id, const std::string &base_id, const std::string &cat, bool vis, size_t session=0)
Metadata for a dockable editor window (formerly PanelInfo)
static WindowPresentationPolicy OptionalPopOut()
Get the expressive theme color for a category.
std::unordered_map< std::string, WindowDescriptor > descriptors
std::unordered_map< size_t, std::vector< std::string > > session_windows
std::unordered_map< size_t, std::unordered_map< std::string, std::string > > session_reverse_window_mapping
std::unordered_map< size_t, std::unordered_map< std::string, std::string > > session_window_mapping