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_DEBUG("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_DEBUG("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_DEBUG("WorkspaceWindowManager",
529 "Unregistered card with prefix '%s': %s", prefix.c_str(),
530 info.prefixed_id.c_str());
531 }
532
533 // Also clean up session tracking
534 for (auto& [session_id, card_list] : session_cards_) {
535 card_list.erase(std::remove_if(card_list.begin(), card_list.end(),
536 [&prefix](const std::string& id) {
537 return id.find(prefix) == 0;
538 }),
539 card_list.end());
540 }
541}
542
544 cards_.clear();
546 pinned_panels_.clear();
547 session_cards_.clear();
548 session_card_mapping_.clear();
550 session_context_keys_.clear();
551 panel_instances_.clear();
552 registry_panel_ids_.clear();
553 global_panel_ids_.clear();
554 resource_panels_.clear();
555 panel_resource_types_.clear();
556 session_count_ = 0;
557 LOG_INFO("WorkspaceWindowManager", "Cleared all cards");
558}
559
560// ============================================================================
561// WindowContent Instance Management (Phase 4)
562// ============================================================================
563
565 std::unique_ptr<WindowContent> panel) {
566 if (!panel) {
567 LOG_ERROR("WorkspaceWindowManager",
568 "Attempted to register null WindowContent");
569 return;
570 }
571
572 auto* resource_panel = dynamic_cast<ResourceWindowContent*>(panel.get());
573 if (resource_panel) {
574 EnforceResourceWindowLimits(resource_panel->GetResourceType());
575 }
576
577 std::string panel_id = panel->GetId();
578
579 // Check if already registered
580 if (panel_instances_.find(panel_id) != panel_instances_.end()) {
581 LOG_WARN("WorkspaceWindowManager",
582 "WindowContent '%s' already registered, skipping registry add",
583 panel_id.c_str());
584 return;
585 }
586
587 if (panel->GetScope() == WindowScope::kGlobal) {
588 global_panel_ids_.insert(panel_id);
589 }
590 registry_panel_ids_.insert(panel_id);
591
592 // Store the WindowContent instance
593 panel_instances_[panel_id] = std::move(panel);
594
595 TrackResourceWindow(panel_id, resource_panel);
596
597 LOG_DEBUG("WorkspaceWindowManager", "Registered registry WindowContent: %s",
598 panel_id.c_str());
599}
600
602 size_t session_id) {
603 RegisterSession(session_id);
604 for (const auto& panel_id : registry_panel_ids_) {
605 auto it = panel_instances_.find(panel_id);
606 if (it == panel_instances_.end()) {
607 continue;
608 }
609 RegisterPanelDescriptorForSession(session_id, *it->second);
610 }
611}
612
614 std::unique_ptr<WindowContent> panel) {
615 const WindowPresentationPolicy presentation =
616 panel ? panel->GetPresentationPolicy()
618 RegisterWindowContent(std::move(panel), presentation);
619}
620
622 std::unique_ptr<WindowContent> panel,
623 WindowPresentationPolicy presentation) {
624 if (!panel) {
625 LOG_ERROR("WorkspaceWindowManager",
626 "Attempted to register null WindowContent");
627 return;
628 }
629
630 auto* resource_panel = dynamic_cast<ResourceWindowContent*>(panel.get());
631 if (resource_panel) {
632 EnforceResourceWindowLimits(resource_panel->GetResourceType());
633 }
634
635 const std::string panel_id = ResolveBaseWindowId(panel->GetId());
636
637 // Auto-register WindowDescriptor for sidebar/menu visibility
638 WindowDescriptor descriptor = BuildDescriptorFromPanel(*panel, presentation);
639
640 // Check if panel should be visible by default
641 bool visible_by_default = panel->IsVisibleByDefault();
642
643 // Store session content under the same final ID as its descriptor. Otherwise
644 // loading this editor in a second ROM session replaces the first session's
645 // instance because both use the same unprefixed base ID.
646 // Reuse an existing mapping even if the total session count changed since
647 // initial registration; switching between prefixed and unprefixed IDs during
648 // a reload would leave two live instances for the same session.
649 std::string instance_id;
650 bool descriptor_preexisting = false;
651 if (panel->GetScope() == WindowScope::kSession) {
652 if (const auto* session_mapping =
654 if (auto mapping_it = session_mapping->find(panel_id);
655 mapping_it != session_mapping->end() &&
656 cards_.contains(mapping_it->second)) {
657 instance_id = mapping_it->second;
658 descriptor_preexisting = true;
659 }
660 }
661 } else {
662 descriptor_preexisting = cards_.contains(panel_id);
663 }
664 if (instance_id.empty()) {
665 // Register the descriptor (creates visibility storage and session mapping).
666 RegisterPanel(active_session_, descriptor);
667 instance_id = GetPrefixedWindowId(active_session_, panel_id);
668 }
669 if (instance_id.empty()) {
670 instance_id = MakeWindowId(active_session_, panel_id, panel->GetScope());
671 }
672
673 // An editor may pre-register a descriptor that names no presentation policy
674 // and then register the content that does. The content's declared policy
675 // fills the gap; a descriptor that already declared one keeps it.
676 if (descriptor_preexisting && presentation.declared) {
677 if (auto descriptor_it = cards_.find(instance_id);
678 descriptor_it != cards_.end() &&
679 !descriptor_it->second.presentation.declared) {
680 descriptor_it->second.presentation = presentation;
681 }
682 }
683
684 // Descriptors created here must not capture the first content instance.
685 // Resolve IsEnabled through the owning slot so same-session reloads can
686 // replace content without leaving a dangling callback. Descriptors that an
687 // editor registered explicitly keep their richer editor-owned condition.
688 if (!descriptor_preexisting) {
689 if (auto descriptor_it = cards_.find(instance_id);
690 descriptor_it != cards_.end()) {
691 descriptor_it->second.enabled_condition = [this, instance_id]() {
692 auto instance_it = panel_instances_.find(instance_id);
693 return instance_it != panel_instances_.end() &&
694 instance_it->second->IsEnabled();
695 };
696 }
697 }
698
699 auto existing = panel_instances_.find(instance_id);
700 if (existing != panel_instances_.end()) {
701 // Editor content objects capture the owning editor/session. When a project
702 // or session reloads, keeping the old instance leaves stale callbacks that
703 // can dereference freed editor state on the next draw.
704 existing->second->OnClose();
706 UntrackResourceWindow(instance_id);
707 existing->second = std::move(panel);
708 TrackResourceWindow(instance_id, resource_panel);
709 if (visible_by_default) {
711 }
712 LOG_DEBUG("WorkspaceWindowManager", "Replaced WindowContent: %s",
713 instance_id.c_str());
714 return;
715 }
716
717 // Set initial visibility if panel should be visible by default
718 if (visible_by_default) {
720 }
721
722 // Store the WindowContent instance
723 panel_instances_[instance_id] = std::move(panel);
724
725 TrackResourceWindow(instance_id, resource_panel);
726
727 LOG_DEBUG("WorkspaceWindowManager", "Registered WindowContent: %s (%s)",
728 instance_id.c_str(), descriptor.display_name.c_str());
729}
730
731// ============================================================================
732// Resource Management (Phase 6)
733// ============================================================================
734
736 const std::string& resource_type) {
737 auto it = resource_panels_.find(resource_type);
738 if (it == resource_panels_.end())
739 return;
740
741 auto& panel_list = it->second;
742 const size_t limit = GetResourceWindowLimit(resource_type);
743
744 // Evict panels until we have room for one more (current count < limit)
745 while (panel_list.size() >= limit) {
746 std::string panel_to_evict = SelectResourceWindowForEviction(panel_list);
747 if (panel_to_evict.empty()) {
748 break;
749 }
750
751 // Remove from LRU list first to avoid iterator issues
752 panel_list.remove(panel_to_evict);
753
754 UnregisterWindowContent(panel_to_evict);
755 }
756}
757
758void WorkspaceWindowManager::MarkPanelUsed(const std::string& panel_id) {
759 auto type_it = panel_resource_types_.find(panel_id);
760 if (type_it == panel_resource_types_.end())
761 return;
762
763 std::string type = type_it->second;
764 auto& list = resource_panels_[type];
765
766 // Move to back (MRU)
767 // std::list::remove is slow (linear), but list size is small (<10)
768 list.remove(panel_id);
769 list.push_back(panel_id);
770}
771
773 const std::string& panel_id) {
774 size_t owner_session = active_session_;
775 std::string base_panel_id;
776 std::string instance_id;
777
778 // Resource eviction supplies the exact tracked instance ID. Resolve that
779 // identity before treating an unprefixed ID as a base ID in the active
780 // session; otherwise an inactive single-session-era instance can evict the
781 // active session's newer prefixed instance with the same base ID.
782 if (panel_instances_.contains(panel_id)) {
783 for (const auto& [session_id, reverse_mapping] :
785 if (auto reverse_it = reverse_mapping.find(panel_id);
786 reverse_it != reverse_mapping.end()) {
787 owner_session = session_id;
788 base_panel_id = reverse_it->second;
789 instance_id = panel_id;
790 break;
791 }
792 }
793 }
794
795 if (instance_id.empty()) {
796 const std::string requested_base_id = ResolveBaseWindowId(panel_id);
797 if (const auto* active_mapping =
799 if (auto mapping_it = active_mapping->find(requested_base_id);
800 mapping_it != active_mapping->end()) {
801 base_panel_id = requested_base_id;
802 instance_id = mapping_it->second;
803 }
804 }
805 }
806
807 // Resolve a qualified descriptor without a live content instance as a
808 // fallback for callers cleaning up partially registered content.
809 if (instance_id.empty()) {
810 for (const auto& [session_id, reverse_mapping] :
812 if (auto reverse_it = reverse_mapping.find(panel_id);
813 reverse_it != reverse_mapping.end()) {
814 owner_session = session_id;
815 base_panel_id = reverse_it->second;
816 instance_id = panel_id;
817 break;
818 }
819 }
820 }
821 if (instance_id.empty()) {
822 base_panel_id = ResolveBaseWindowId(panel_id);
823 instance_id = GetPrefixedWindowId(owner_session, base_panel_id);
824 }
825 if (instance_id.empty()) {
826 instance_id = panel_id;
827 }
828
829 UntrackResourceWindow(instance_id);
830 auto it = panel_instances_.find(instance_id);
831 if (it != panel_instances_.end()) {
832 // Call OnClose before removing
833 it->second->OnClose();
835 panel_instances_.erase(it);
836 registry_panel_ids_.erase(base_panel_id);
837 global_panel_ids_.erase(instance_id);
838 LOG_DEBUG("WorkspaceWindowManager", "Unregistered WindowContent: %s",
839 instance_id.c_str());
840 }
841
842 // Also unregister the descriptor
843 UnregisterPanel(owner_session, base_panel_id);
844}
845
847 const std::string& panel_id) {
848 return GetWindowContent(active_session_, panel_id);
849}
850
852 size_t session_id, const std::string& panel_id) {
853 const auto* session_mapping = FindSessionWindowMapping(session_id);
854 if (!session_mapping) {
855 return nullptr;
856 }
857
858 std::string base_panel_id = GetBaseIdForPrefixedId(session_id, panel_id);
859 if (base_panel_id.empty()) {
860 base_panel_id = ResolveBaseWindowId(panel_id);
861 }
862 auto mapping_it = session_mapping->find(base_panel_id);
863 if (mapping_it == session_mapping->end()) {
864 return nullptr;
865 }
866 return FindPanelInstance(mapping_it->second, base_panel_id);
867}
868
870 const std::string& prefixed_panel_id, const std::string& base_panel_id) {
871 auto prefixed_it = panel_instances_.find(prefixed_panel_id);
872 if (prefixed_it != panel_instances_.end()) {
873 return prefixed_it->second.get();
874 }
875 auto base_it = panel_instances_.find(base_panel_id);
876 if (base_it != panel_instances_.end()) {
877 return base_it->second.get();
878 }
879 return nullptr;
880}
881
883 const std::string& prefixed_panel_id,
884 const std::string& base_panel_id) const {
885 auto prefixed_it = panel_instances_.find(prefixed_panel_id);
886 if (prefixed_it != panel_instances_.end()) {
887 return prefixed_it->second.get();
888 }
889 auto base_it = panel_instances_.find(base_panel_id);
890 if (base_it != panel_instances_.end()) {
891 return base_it->second.get();
892 }
893 return nullptr;
894}
895
897 // Suppress panel drawing when dashboard is active (no editor selected yet)
898 // This ensures panels don't appear until user selects an editor
899 if (browser_state_.active_category.empty() ||
901 return;
902 }
903
904 auto session_it = session_cards_.find(active_session_);
905 if (session_it == session_cards_.end()) {
906 return;
907 }
908
909 auto& animator = gui::GetAnimator();
910 bool animations_enabled = animator.IsEnabled();
911 const bool touch_device = gui::LayoutHelpers::IsTouchDevice();
912
913 const auto resolve_switch_speed = [&](float snappy, float standard,
914 float relaxed) {
915 switch (animator.motion_profile()) {
917 return snappy;
919 return relaxed;
921 default:
922 return standard;
923 }
924 };
925 const float category_transition_speed =
926 resolve_switch_speed(8.5f, 6.0f, 4.5f);
927 const float panel_fade_speed = resolve_switch_speed(11.0f, 8.0f, 6.0f);
928
929 // Animate global category transition alpha
930 float global_alpha = 1.0f;
931 if (animations_enabled) {
932 global_alpha = animator.Animate("global", "category_transition", 1.0f,
933 category_transition_speed);
934 }
935
936 for (const auto& prefixed_panel_id : session_it->second) {
937 auto descriptor_it = cards_.find(prefixed_panel_id);
938 if (descriptor_it == cards_.end()) {
939 continue;
940 }
941 WindowDescriptor& descriptor = descriptor_it->second;
942
943 std::string base_panel_id =
944 GetBaseIdForPrefixedId(active_session_, prefixed_panel_id);
945 if (base_panel_id.empty()) {
946 base_panel_id = prefixed_panel_id;
947 }
948
949 WindowContent* panel = FindPanelInstance(prefixed_panel_id, base_panel_id);
950 if (!panel) {
951 continue;
952 }
953
954 bool is_visible = descriptor.visibility_flag && *descriptor.visibility_flag;
955
956 // Category filtering: draw if matches active category or user has pinned.
957 // Previously a Persistent lifecycle was a third branch here; collapsed into
958 // CrossEditor + default-pin via UserSettings revision-7 migration.
959 bool should_draw = false;
960 if (is_visible) {
963 should_draw = true;
964 } else if (IsWindowPinnedImpl(active_session_, base_panel_id)) {
965 should_draw = true;
966 }
967 }
968
969 // Compute target alpha: 1.0 if should draw, 0.0 if should hide
970 float target_alpha = should_draw ? 1.0f : 0.0f;
971
972 // Animate alpha towards target (or snap if animations disabled)
973 float current_alpha = 1.0f;
974 if (animations_enabled) {
975 current_alpha = animator.Animate(prefixed_panel_id, "panel_alpha",
976 target_alpha, panel_fade_speed);
977 current_alpha *= global_alpha; // Apply global category fade
978 } else {
979 current_alpha = target_alpha;
980 }
981
982 // Skip drawing if alpha is effectively zero
983 if (current_alpha < 0.01f) {
984 continue;
985 }
986
987 // Get visibility flag for the panel window
988 bool* visibility_flag = descriptor.visibility_flag;
989
990 // Get display name without icon - PanelWindow will add the icon
991 // This fixes the double-icon issue where both descriptor and PanelWindow added icons
992 std::string display_name = descriptor.display_name.empty()
993 ? panel->GetDisplayName()
994 : descriptor.display_name;
995 std::string icon =
996 descriptor.icon.empty() ? panel->GetIcon() : descriptor.icon;
997
998 if (editor_resolver_) {
999 if (Editor* editor = editor_resolver_(panel->GetEditorCategory())) {
1001 }
1002 }
1003
1004 // Create PanelWindow and draw content
1005 gui::PanelWindow window(display_name.c_str(), icon.c_str(),
1006 visibility_flag);
1007 window.SetStableId(prefixed_panel_id);
1008 const bool force_float =
1009 pending_float_window_ids_.count(prefixed_panel_id) > 0;
1010 if (panel->PrefersFloating() || force_float) {
1012 } else if (touch_device) {
1014 }
1015 // Only undock on explicit OpenWindowFloating — PrefersFloating alone sets
1016 // the default floating position without blocking user docking.
1017 if (force_float) {
1018 window.RequestForceUndock();
1019 pending_float_window_ids_.erase(prefixed_panel_id);
1020 }
1021
1022 // Use preferred size from WindowContent if specified.
1023 float preferred_width = panel->GetPreferredWidth();
1024 float preferred_height = panel->GetPreferredHeight();
1025 if (preferred_width > 0.0f || preferred_height > 0.0f) {
1026 window.SetDefaultSize(
1027 preferred_width > 0.0f ? preferred_width : 400.0f,
1028 preferred_height > 0.0f ? preferred_height : 300.0f);
1029 }
1030
1031 // Enable pin functionality for cross-editor persistence
1032 window.SetPinnable(true);
1033 window.SetPinned(IsWindowPinnedImpl(active_session_, base_panel_id));
1034
1035 // Wire up pin state change callback to persist to WorkspaceWindowManager
1036 window.SetPinChangedCallback([this, base_panel_id](bool pinned) {
1037 SetWindowPinnedImpl(base_panel_id, pinned);
1038 });
1039
1040 // Apply fade alpha for smooth transitions
1041 std::optional<gui::StyleVarGuard> alpha_guard;
1042 if (current_alpha < 1.0f) {
1043 alpha_guard.emplace(ImGuiStyleVar_Alpha, current_alpha);
1044 }
1045
1046 animator.ApplyPanelTransitionPre(prefixed_panel_id);
1047 if (window.Begin(visibility_flag)) {
1048 panel->DrawWithLazyInit(visibility_flag);
1049 }
1050 window.End();
1051 animator.ApplyPanelTransitionPost(prefixed_panel_id);
1052
1053 alpha_guard.reset();
1054
1055 // ImGui's title-bar X and content close actions change the flag directly.
1056 // Persist that transition just like CloseWindowImpl, once rather than on
1057 // every remaining fade-out frame, so a session switch cannot reopen it.
1058 if (is_visible && visibility_flag && !*visibility_flag) {
1059 pending_float_window_ids_.erase(prefixed_panel_id);
1060 if (descriptor.on_hide) {
1061 descriptor.on_hide();
1062 }
1063 panel->OnClose();
1065 base_panel_id, descriptor.category, false);
1066 }
1067 }
1068}
1069
1070void WorkspaceWindowManager::OnEditorSwitch(const std::string& from_category,
1071 const std::string& to_category) {
1072 if (from_category == to_category) {
1073 return; // No switch needed
1074 }
1075
1076 LOG_INFO("WorkspaceWindowManager", "Switching from category '%s' to '%s'",
1077 from_category.c_str(), to_category.c_str());
1078
1079 // IMPORTANT:
1080 // Editor switching must *not* be treated as a user-initiated hide/show.
1081 //
1082 // Historically this function toggled visibility flags (CloseWindowImpl/OpenWindowImpl),
1083 // which publishes PanelVisibilityChangedEvent and permanently overwrote user
1084 // prefs. Worse, dynamic "resource windows" (rooms/songs) interpret
1085 // IsWindowVisibleImpl()==false as "user closed", unregistering themselves.
1086 //
1087 // Reset global category transition to trigger fade-in
1088 if (gui::GetAnimator().IsEnabled()) {
1091
1092 // Also begin a per-panel fade for every visible window in the incoming
1093 // category. Without this, individual panels use whatever alpha the
1094 // animator happened to have for their workspace fade state from the
1095 // previous session, which was the source of the "old bitmap still
1096 // visible during switch" ghosting. EditorActivator clears that narrow
1097 // transition state just before this call, so each transition starts from
1098 // alpha=0.
1099 const auto incoming =
1101 for (const auto& descriptor : incoming) {
1102 if (descriptor.visibility_flag && *descriptor.visibility_flag) {
1104 GetPrefixedWindowId(active_session_, descriptor.card_id),
1106 }
1107 }
1108 }
1109
1110 SetActiveCategory(to_category);
1111}
1112
1113// ============================================================================
1114// Panel Control (Programmatic, No GUI)
1115// ============================================================================
1116
1118 size_t session_id, const std::string& base_window_id) {
1119 const auto* descriptor = GetWindowDescriptorImpl(session_id, base_window_id);
1120 if (descriptor == nullptr || !descriptor->IsAdmitted() ||
1121 !descriptor->presentation.allow_popout) {
1122 return false;
1123 }
1124 if (!OpenWindowImpl(session_id, base_window_id)) {
1125 return false;
1126 }
1128 GetPrefixedWindowId(session_id, ResolveBaseWindowId(base_window_id)));
1129 return true;
1130}
1131
1133 const std::string& base_card_id) {
1134 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1135 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1136 if (prefixed_id.empty()) {
1137 return false;
1138 }
1139
1140 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1141 if (descriptor) {
1142 if (!descriptor->IsAdmitted()) {
1143 return false;
1144 }
1145 const bool was_visible =
1146 (descriptor->visibility_flag && *descriptor->visibility_flag);
1147 if (descriptor->visibility_flag) {
1148 *descriptor->visibility_flag = true;
1149 }
1150 if (descriptor->on_show) {
1151 descriptor->on_show();
1152 }
1153 if (!was_visible) {
1154 if (WindowContent* panel =
1155 FindPanelInstance(prefixed_id, canonical_base_id)) {
1156 panel->OnOpen();
1157 }
1158 }
1159
1160 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1161 descriptor->category, true);
1162 return true;
1163 }
1164 return false;
1165}
1166
1168 const std::string& base_card_id) {
1169 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1170 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1171 if (prefixed_id.empty()) {
1172 return false;
1173 }
1174
1175 pending_float_window_ids_.erase(prefixed_id);
1176
1177 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1178 if (descriptor) {
1179 const bool was_visible =
1180 (descriptor->visibility_flag && *descriptor->visibility_flag);
1181 if (descriptor->visibility_flag) {
1182 *descriptor->visibility_flag = false;
1183 }
1184 if (descriptor->on_hide) {
1185 descriptor->on_hide();
1186 }
1187 if (was_visible) {
1188 if (WindowContent* panel =
1189 FindPanelInstance(prefixed_id, canonical_base_id)) {
1190 panel->OnClose();
1191 }
1192 }
1193
1194 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1195 descriptor->category, false);
1196 return true;
1197 }
1198 return false;
1199}
1200
1202 const std::string& base_card_id) {
1203 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1204 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1205 if (prefixed_id.empty()) {
1206 return false;
1207 }
1208
1209 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1210 if (descriptor && descriptor->visibility_flag) {
1211 bool new_state = !(*descriptor->visibility_flag);
1212 *descriptor->visibility_flag = new_state;
1213 if (!new_state)
1214 pending_float_window_ids_.erase(prefixed_id);
1215
1216 if (new_state && descriptor->on_show) {
1217 descriptor->on_show();
1218 } else if (!new_state && descriptor->on_hide) {
1219 descriptor->on_hide();
1220 }
1221 if (WindowContent* panel =
1222 FindPanelInstance(prefixed_id, canonical_base_id)) {
1223 if (new_state) {
1224 panel->OnOpen();
1225 } else {
1226 panel->OnClose();
1227 }
1228 }
1229
1230 PublishWindowVisibilityChanged(session_id, prefixed_id, canonical_base_id,
1231 descriptor->category, new_state);
1232 return true;
1233 }
1234 return false;
1235}
1236
1238 size_t session_id, const std::string& base_card_id) const {
1239 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1240 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1241 if (prefixed_id.empty()) {
1242 return false;
1243 }
1244
1245 const auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1246 if (descriptor && descriptor->visibility_flag) {
1247 return *descriptor->visibility_flag;
1248 }
1249 return false;
1250}
1251
1253 size_t session_id, const std::string& base_card_id) {
1254 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1255 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1256 if (prefixed_id.empty()) {
1257 return nullptr;
1258 }
1259
1260 auto* descriptor = FindDescriptorByPrefixedId(prefixed_id);
1261 if (descriptor) {
1262 return descriptor->visibility_flag;
1263 }
1264 return nullptr;
1265}
1266
1267// ============================================================================
1268// Batch Operations
1269// ============================================================================
1270
1272 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1273 for (const auto& prefixed_card_id : *session_windows) {
1274 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1275 if (!descriptor->IsListedInWindowBrowser()) {
1276 continue;
1277 }
1278 if (descriptor->visibility_flag) {
1279 *descriptor->visibility_flag = true;
1280 }
1281 if (descriptor->on_show) {
1282 descriptor->on_show();
1283 }
1284 }
1285 }
1286 }
1287}
1288
1290 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1291 for (const auto& prefixed_card_id : *session_windows) {
1292 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1293 if (descriptor->visibility_flag) {
1294 pending_float_window_ids_.erase(prefixed_card_id);
1295 *descriptor->visibility_flag = false;
1296 }
1297 if (descriptor->on_hide) {
1298 descriptor->on_hide();
1299 }
1300 }
1301 }
1302 }
1303}
1304
1306 size_t session_id, const std::string& category) {
1307 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1308 for (const auto& prefixed_card_id : *session_windows) {
1309 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1310 if (descriptor->category != category) {
1311 continue;
1312 }
1313 if (!descriptor->IsListedInWindowBrowser()) {
1314 continue;
1315 }
1316 if (descriptor->visibility_flag) {
1317 *descriptor->visibility_flag = true;
1318 }
1319 if (descriptor->on_show) {
1320 descriptor->on_show();
1321 }
1322 }
1323 }
1324 }
1325}
1326
1328 size_t session_id, const std::string& category) {
1329 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1330 for (const auto& prefixed_card_id : *session_windows) {
1331 if (auto* descriptor = FindDescriptorByPrefixedId(prefixed_card_id)) {
1332 if (descriptor->category != category) {
1333 continue;
1334 }
1335 if (descriptor->visibility_flag) {
1336 pending_float_window_ids_.erase(prefixed_card_id);
1337 *descriptor->visibility_flag = false;
1338 }
1339 if (descriptor->on_hide) {
1340 descriptor->on_hide();
1341 }
1342 }
1343 }
1344 }
1345}
1346
1348 const std::string& base_card_id) {
1349 // First get the category of the target card
1350 std::string prefixed_id = GetPrefixedWindowId(session_id, base_card_id);
1351 if (prefixed_id.empty()) {
1352 return;
1353 }
1354
1355 const auto* target = FindDescriptorByPrefixedId(prefixed_id);
1356 if (!target) {
1357 return;
1358 }
1359
1360 std::string category = target->category;
1361
1362 // Hide all cards in the same category
1363 HideAllWindowsInCategory(session_id, category);
1364
1365 // Show the target card
1366 OpenWindowImpl(session_id, base_card_id);
1367}
1368
1369// ============================================================================
1370// Query Methods
1371// ============================================================================
1372
1374 size_t session_id) const {
1375 std::vector<std::string> result;
1376 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1377 result.reserve(session_windows->size());
1378 for (const auto& prefixed_window_id : *session_windows) {
1379 const std::string base_window_id =
1380 GetBaseIdForPrefixedId(session_id, prefixed_window_id);
1381 result.push_back(base_window_id.empty() ? prefixed_window_id
1382 : base_window_id);
1383 }
1384 }
1385 return result;
1386}
1387
1389 size_t session_id, const std::string& category) const {
1390 std::vector<WindowDescriptor> result;
1391
1392 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1393 for (const auto& prefixed_window_id : *session_windows) {
1394 if (const auto* descriptor =
1395 FindDescriptorByPrefixedId(prefixed_window_id)) {
1396 if (descriptor->category == category) {
1397 WindowDescriptor window = *descriptor;
1398 const std::string base_window_id =
1399 GetBaseIdForPrefixedId(session_id, prefixed_window_id);
1400 if (!base_window_id.empty()) {
1401 window.card_id = base_window_id;
1402 }
1403 result.push_back(std::move(window));
1404 }
1405 }
1406 }
1407 }
1408
1409 // Sort by priority
1410 std::sort(result.begin(), result.end(),
1411 [](const WindowDescriptor& a, const WindowDescriptor& b) {
1412 return a.priority < b.priority;
1413 });
1414
1415 return result;
1416}
1417
1419 size_t session_id) const {
1420 std::vector<std::string> categories;
1421
1422 if (const auto* session_windows = FindSessionWindowIds(session_id)) {
1423 for (const auto& prefixed_window_id : *session_windows) {
1424 if (const auto* descriptor =
1425 FindDescriptorByPrefixedId(prefixed_window_id)) {
1426 if (std::find(categories.begin(), categories.end(),
1427 descriptor->category) == categories.end()) {
1428 categories.push_back(descriptor->category);
1429 }
1430 }
1431 }
1432 }
1433 return categories;
1434}
1435
1437 size_t session_id, const std::string& base_card_id) const {
1438 const std::string canonical_base_id = ResolveBaseWindowId(base_card_id);
1439 std::string prefixed_id = GetPrefixedWindowId(session_id, canonical_base_id);
1440 if (prefixed_id.empty()) {
1441 return nullptr;
1442 }
1443
1444 return FindDescriptorByPrefixedId(prefixed_id);
1445}
1446
1447std::vector<std::string> WorkspaceWindowManager::GetAllCategories() const {
1448 std::vector<std::string> categories;
1449 for (const auto& [card_id, card_info] : registry_state_.descriptors) {
1450 if (std::find(categories.begin(), categories.end(), card_info.category) ==
1451 categories.end()) {
1452 categories.push_back(card_info.category);
1453 }
1454 }
1455 return categories;
1456}
1457
1458// ============================================================================
1459// State Persistence
1460// ============================================================================
1461
1463 size_t session_id) const {
1464 std::vector<std::string> visible_panels;
1465
1466 const auto* window_mapping = FindSessionWindowMapping(session_id);
1467 if (!window_mapping) {
1468 return visible_panels;
1469 }
1470
1471 for (const auto& [base_id, prefixed_id] : *window_mapping) {
1472 if (const auto* descriptor = FindDescriptorByPrefixedId(prefixed_id)) {
1473 if (descriptor->visibility_flag && *descriptor->visibility_flag) {
1474 visible_panels.push_back(base_id);
1475 }
1476 }
1477 }
1478
1479 return visible_panels;
1480}
1481
1482// ============================================================================
1483// Workspace Presets
1484// ============================================================================
1485
1486void WorkspaceWindowManager::SavePreset(const std::string& name,
1487 const std::string& description) {
1488 WorkspacePreset preset;
1489 preset.name = name;
1490 preset.description = description;
1491
1492 // Collect all visible cards across all sessions
1493 for (const auto& [card_id, card_info] : cards_) {
1494 if (card_info.visibility_flag && *card_info.visibility_flag) {
1495 preset.visible_cards.push_back(card_id);
1496 }
1497 }
1498
1499 presets_[name] = preset;
1501 LOG_INFO("WorkspaceWindowManager", "Saved preset: %s (%zu cards)",
1502 name.c_str(), preset.visible_cards.size());
1503}
1504
1505bool WorkspaceWindowManager::LoadPreset(const std::string& name) {
1506 auto it = presets_.find(name);
1507 if (it == presets_.end()) {
1508 return false;
1509 }
1510
1511 // First hide all cards
1512 for (auto& [card_id, card_info] : cards_) {
1513 if (card_info.visibility_flag) {
1514 pending_float_window_ids_.erase(card_id);
1515 *card_info.visibility_flag = false;
1516 }
1517 }
1518
1519 // Then show preset cards
1520 for (const auto& card_id : it->second.visible_cards) {
1521 auto card_it = cards_.find(card_id);
1522 if (card_it != cards_.end() && card_it->second.visibility_flag) {
1523 *card_it->second.visibility_flag = true;
1524 if (card_it->second.on_show) {
1525 card_it->second.on_show();
1526 }
1527 }
1528 }
1529
1530 LOG_INFO("WorkspaceWindowManager", "Loaded preset: %s", name.c_str());
1531 return true;
1532}
1533
1534void WorkspaceWindowManager::DeletePreset(const std::string& name) {
1535 presets_.erase(name);
1537}
1538
1539std::vector<WorkspaceWindowManager::WorkspacePreset>
1541 std::vector<WorkspacePreset> result;
1542 for (const auto& [name, preset] : presets_) {
1543 result.push_back(preset);
1544 }
1545 return result;
1546}
1547
1548// ============================================================================
1549// Quick Actions
1550// ============================================================================
1551
1552void WorkspaceWindowManager::ShowAll(size_t session_id) {
1553 ShowAllWindowsInSession(session_id);
1554}
1555
1556void WorkspaceWindowManager::HideAll(size_t session_id) {
1557 HideAllWindowsInSession(session_id);
1558}
1559
1561 // Hide all cards first
1562 HideAllWindowsInSession(session_id);
1563
1564 // TODO: Load default visibility from config file or hardcoded defaults
1565 LOG_INFO("WorkspaceWindowManager", "Reset to defaults for session %zu",
1566 session_id);
1567}
1568
1570 EditorType editor_type) {
1571 // Get category for this editor
1572 std::string category = EditorRegistry::GetEditorCategory(editor_type);
1573 if (category.empty()) {
1574 LOG_WARN("WorkspaceWindowManager",
1575 "No category found for editor type %d, skipping reset",
1576 static_cast<int>(editor_type));
1577 return;
1578 }
1579
1580 // Publish every close so the next editor switch cannot restore stale prefs.
1581 for (const auto& window : GetWindowsInCategory(session_id, category)) {
1582 CloseWindow(session_id, window.card_id);
1583 }
1584
1585 // Get default cards from LayoutPresets
1586 auto default_panels = LayoutPresets::GetDefaultPanels(editor_type);
1587
1588 // Show each default card
1589 for (const auto& card_id : default_panels) {
1590 if (OpenWindowImpl(session_id, card_id)) {
1591 LOG_DEBUG("WorkspaceWindowManager", "Showing default card: %s",
1592 card_id.c_str());
1593 }
1594 }
1595
1596 LOG_INFO("WorkspaceWindowManager",
1597 "Reset %s editor to defaults (%zu cards visible)", category.c_str(),
1598 default_panels.size());
1599}
1600
1601// ============================================================================
1602// Session Prefixing Utilities
1603// ============================================================================
1604
1606 size_t session_id, const std::string& base_id) const {
1607 return MakeWindowId(session_id, base_id, WindowScope::kSession);
1608}
1609
1610std::string WorkspaceWindowManager::MakeWindowId(size_t session_id,
1611 const std::string& base_id,
1612 WindowScope scope) const {
1613 const std::string canonical_base_id = ResolveBaseWindowId(base_id);
1614 if (scope == WindowScope::kGlobal) {
1615 return canonical_base_id;
1616 }
1617 if (ShouldPrefixPanels()) {
1618 return absl::StrFormat("s%zu.%s", session_id, canonical_base_id);
1619 }
1620 return canonical_base_id;
1621}
1622
1623} // namespace editor
1624} // namespace yaze
static std::string GetEditorCategory(EditorType type)
Interface for editor classes.
Definition editor.h:252
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)
Update the active category and start panel transitions.
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_DEBUG(category, format,...)
Definition log.h:133
#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