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