From d9e793eaeac6cf23658ef0a4f1bbb4c1b0b31ffa Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 16 Oct 2025 20:34:34 -0400 Subject: [PATCH] refactor(canvas): unify context menu item handling with CanvasMenuItem - Replaced the legacy ContextMenuItem structure with a new CanvasMenuItem across various files, enhancing consistency in context menu management. - Introduced CanvasMenuBuilder for fluent menu construction, allowing for easier addition of items and submenus. - Updated Canvas and related components to utilize the new menu system, improving organization and maintainability of context menus. Benefits: - Streamlines context menu item management, making it more intuitive and flexible. - Enhances code readability and reduces duplication, facilitating future enhancements. --- .../editor/dungeon/dungeon_canvas_viewer.cc | 230 +++++++++--------- src/app/editor/overworld/map_properties.cc | 26 +- src/app/gui/canvas/canvas.cc | 131 ++++------ src/app/gui/canvas/canvas.h | 58 +---- src/app/gui/canvas/canvas_context_menu.cc | 198 +++++++-------- src/app/gui/canvas/canvas_context_menu.h | 66 +++-- src/app/gui/canvas/canvas_menu.h | 25 ++ src/app/gui/canvas/canvas_menu_builder.cc | 145 +++++++++++ src/app/gui/canvas/canvas_menu_builder.h | 157 ++++++++++++ src/app/gui/gui_library.cmake | 1 + 10 files changed, 625 insertions(+), 412 deletions(-) create mode 100644 src/app/gui/canvas/canvas_menu_builder.cc create mode 100644 src/app/gui/canvas/canvas_menu_builder.h diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index 3cfce463..d18bc7ea 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -218,158 +218,158 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { auto& layer_settings = GetRoomLayerSettings(room_id); // Add object placement option - canvas_.AddContextMenuItem({ - ICON_MD_ADD " Place Object", - []() { - // TODO: Show object palette/selector - }, - "Ctrl+P" - }); + canvas_.AddContextMenuItem( + gui::CanvasMenuItem(ICON_MD_ADD " Place Object", ICON_MD_ADD, + []() { + // TODO: Show object palette/selector + }, + "Ctrl+P") + ); // Add object deletion for selected objects - canvas_.AddContextMenuItem({ - ICON_MD_DELETE " Delete Selected", - [this]() { - object_interaction_.HandleDeleteSelected(); - }, - "Del" - }); + canvas_.AddContextMenuItem( + gui::CanvasMenuItem(ICON_MD_DELETE " Delete Selected", ICON_MD_DELETE, + [this]() { + object_interaction_.HandleDeleteSelected(); + }, + "Del") + ); // Add room property quick toggles - canvas_.AddContextMenuItem({ - ICON_MD_LAYERS " Toggle BG1", - [this, room_id]() { - auto& settings = GetRoomLayerSettings(room_id); - settings.bg1_visible = !settings.bg1_visible; - }, - "1" - }); + canvas_.AddContextMenuItem( + gui::CanvasMenuItem(ICON_MD_LAYERS " Toggle BG1", ICON_MD_LAYERS, + [this, room_id]() { + auto& settings = GetRoomLayerSettings(room_id); + settings.bg1_visible = !settings.bg1_visible; + }, + "1") + ); - canvas_.AddContextMenuItem({ - ICON_MD_LAYERS " Toggle BG2", - [this, room_id]() { - auto& settings = GetRoomLayerSettings(room_id); - settings.bg2_visible = !settings.bg2_visible; - }, - "2" - }); + canvas_.AddContextMenuItem( + gui::CanvasMenuItem(ICON_MD_LAYERS " Toggle BG2", ICON_MD_LAYERS, + [this, room_id]() { + auto& settings = GetRoomLayerSettings(room_id); + settings.bg2_visible = !settings.bg2_visible; + }, + "2") + ); // Add re-render option - canvas_.AddContextMenuItem({ - ICON_MD_REFRESH " Re-render Room", - [&room]() { - room.RenderRoomGraphics(); - }, - "Ctrl+R" - }); + canvas_.AddContextMenuItem( + gui::CanvasMenuItem(ICON_MD_REFRESH " Re-render Room", ICON_MD_REFRESH, + [&room]() { + room.RenderRoomGraphics(); + }, + "Ctrl+R") + ); // === DEBUG MENU === - gui::Canvas::ContextMenuItem debug_menu; + gui::CanvasMenuItem debug_menu; debug_menu.label = ICON_MD_BUG_REPORT " Debug"; // Show room info - debug_menu.subitems.push_back({ - ICON_MD_INFO " Show Room Info", - [this, room_id]() { - show_room_debug_info_ = !show_room_debug_info_; - } - }); + debug_menu.subitems.push_back( + gui::CanvasMenuItem(ICON_MD_INFO " Show Room Info", ICON_MD_INFO, + [this]() { + show_room_debug_info_ = !show_room_debug_info_; + }) + ); // Show texture info - debug_menu.subitems.push_back({ - ICON_MD_IMAGE " Show Texture Debug", - [this]() { - show_texture_debug_ = !show_texture_debug_; - } - }); + debug_menu.subitems.push_back( + gui::CanvasMenuItem(ICON_MD_IMAGE " Show Texture Debug", ICON_MD_IMAGE, + [this]() { + show_texture_debug_ = !show_texture_debug_; + }) + ); // Show object bounds with sub-menu for categories - gui::Canvas::ContextMenuItem object_bounds_menu; + gui::CanvasMenuItem object_bounds_menu; object_bounds_menu.label = ICON_MD_CROP_SQUARE " Show Object Bounds"; object_bounds_menu.callback = [this]() { show_object_bounds_ = !show_object_bounds_; }; // Sub-menu for filtering by type - object_bounds_menu.subitems.push_back({ - "Type 1 (0x00-0xFF)", - [this]() { - object_outline_toggles_.show_type1_objects = !object_outline_toggles_.show_type1_objects; - } - }); - object_bounds_menu.subitems.push_back({ - "Type 2 (0x100-0x1FF)", - [this]() { - object_outline_toggles_.show_type2_objects = !object_outline_toggles_.show_type2_objects; - } - }); - object_bounds_menu.subitems.push_back({ - "Type 3 (0xF00-0xFFF)", - [this]() { - object_outline_toggles_.show_type3_objects = !object_outline_toggles_.show_type3_objects; - } - }); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Type 1 (0x00-0xFF)", + [this]() { + object_outline_toggles_.show_type1_objects = !object_outline_toggles_.show_type1_objects; + }) + ); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Type 2 (0x100-0x1FF)", + [this]() { + object_outline_toggles_.show_type2_objects = !object_outline_toggles_.show_type2_objects; + }) + ); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Type 3 (0xF00-0xFFF)", + [this]() { + object_outline_toggles_.show_type3_objects = !object_outline_toggles_.show_type3_objects; + }) + ); // Separator - gui::Canvas::ContextMenuItem sep; + gui::CanvasMenuItem sep; sep.label = "---"; sep.enabled_condition = []() { return false; }; object_bounds_menu.subitems.push_back(sep); // Sub-menu for filtering by layer - object_bounds_menu.subitems.push_back({ - "Layer 0 (BG1)", - [this]() { - object_outline_toggles_.show_layer0_objects = !object_outline_toggles_.show_layer0_objects; - } - }); - object_bounds_menu.subitems.push_back({ - "Layer 1 (BG2)", - [this]() { - object_outline_toggles_.show_layer1_objects = !object_outline_toggles_.show_layer1_objects; - } - }); - object_bounds_menu.subitems.push_back({ - "Layer 2 (BG3)", - [this]() { - object_outline_toggles_.show_layer2_objects = !object_outline_toggles_.show_layer2_objects; - } - }); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Layer 0 (BG1)", + [this]() { + object_outline_toggles_.show_layer0_objects = !object_outline_toggles_.show_layer0_objects; + }) + ); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Layer 1 (BG2)", + [this]() { + object_outline_toggles_.show_layer1_objects = !object_outline_toggles_.show_layer1_objects; + }) + ); + object_bounds_menu.subitems.push_back( + gui::CanvasMenuItem("Layer 2 (BG3)", + [this]() { + object_outline_toggles_.show_layer2_objects = !object_outline_toggles_.show_layer2_objects; + }) + ); debug_menu.subitems.push_back(object_bounds_menu); // Show layer info - debug_menu.subitems.push_back({ - ICON_MD_LAYERS " Show Layer Info", - [this]() { - show_layer_info_ = !show_layer_info_; - } - }); + debug_menu.subitems.push_back( + gui::CanvasMenuItem(ICON_MD_LAYERS " Show Layer Info", ICON_MD_LAYERS, + [this]() { + show_layer_info_ = !show_layer_info_; + }) + ); // Force reload room - debug_menu.subitems.push_back({ - ICON_MD_REFRESH " Force Reload", - [&room, room_id]() { - room.LoadObjects(); - room.LoadRoomGraphics(room.blockset); - room.RenderRoomGraphics(); - } - }); + debug_menu.subitems.push_back( + gui::CanvasMenuItem(ICON_MD_REFRESH " Force Reload", ICON_MD_REFRESH, + [&room]() { + room.LoadObjects(); + room.LoadRoomGraphics(room.blockset); + room.RenderRoomGraphics(); + }) + ); // Log room state - debug_menu.subitems.push_back({ - ICON_MD_PRINT " Log Room State", - [&room, room_id]() { - LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id); - LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d", - room.blockset, room.palette, room.layout); - LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu", - room.GetTileObjects().size(), room.GetSprites().size()); - LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d", - room.bg1_buffer().bitmap().width(), room.bg1_buffer().bitmap().height(), - room.bg2_buffer().bitmap().width(), room.bg2_buffer().bitmap().height()); - } - }); + debug_menu.subitems.push_back( + gui::CanvasMenuItem(ICON_MD_PRINT " Log Room State", ICON_MD_PRINT, + [&room, room_id]() { + LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id); + LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d", + room.blockset, room.palette, room.layout); + LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu", + room.GetTileObjects().size(), room.GetSprites().size()); + LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d", + room.bg1_buffer().bitmap().width(), room.bg1_buffer().bitmap().height(), + room.bg2_buffer().bitmap().width(), room.bg2_buffer().bitmap().height()); + }) + ); canvas_.AddContextMenuItem(debug_menu); } diff --git a/src/app/editor/overworld/map_properties.cc b/src/app/editor/overworld/map_properties.cc index 15dbacec..2e857ce0 100644 --- a/src/app/editor/overworld/map_properties.cc +++ b/src/app/editor/overworld/map_properties.cc @@ -418,11 +418,11 @@ void MapPropertiesSystem::SetupCanvasContextMenu( // Add entity insertion submenu (only in MOUSE mode) if (current_mode == 0 && entity_insert_callback_) { // 0 = EditingMode::MOUSE - gui::Canvas::ContextMenuItem entity_menu; + gui::CanvasMenuItem entity_menu; entity_menu.label = ICON_MD_ADD_LOCATION " Insert Entity"; // Entrance submenu item - gui::Canvas::ContextMenuItem entrance_item; + gui::CanvasMenuItem entrance_item; entrance_item.label = ICON_MD_DOOR_FRONT " Entrance"; entrance_item.callback = [this]() { if (entity_insert_callback_) { @@ -432,7 +432,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( entity_menu.subitems.push_back(entrance_item); // Hole submenu item - gui::Canvas::ContextMenuItem hole_item; + gui::CanvasMenuItem hole_item; hole_item.label = ICON_MD_CYCLONE " Hole"; hole_item.callback = [this]() { if (entity_insert_callback_) { @@ -442,7 +442,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( entity_menu.subitems.push_back(hole_item); // Exit submenu item - gui::Canvas::ContextMenuItem exit_item; + gui::CanvasMenuItem exit_item; exit_item.label = ICON_MD_DOOR_BACK " Exit"; exit_item.callback = [this]() { if (entity_insert_callback_) { @@ -452,7 +452,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( entity_menu.subitems.push_back(exit_item); // Item submenu item - gui::Canvas::ContextMenuItem item_item; + gui::CanvasMenuItem item_item; item_item.label = ICON_MD_GRASS " Item"; item_item.callback = [this]() { if (entity_insert_callback_) { @@ -462,7 +462,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( entity_menu.subitems.push_back(item_item); // Sprite submenu item - gui::Canvas::ContextMenuItem sprite_item; + gui::CanvasMenuItem sprite_item; sprite_item.label = ICON_MD_PEST_CONTROL_RODENT " Sprite"; sprite_item.callback = [this]() { if (entity_insert_callback_) { @@ -475,7 +475,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( } // Add overworld-specific context menu items - gui::Canvas::ContextMenuItem lock_item; + gui::CanvasMenuItem lock_item; lock_item.label = current_map_lock ? "Unlock Map" : "Lock to This Map"; lock_item.callback = [¤t_map_lock]() { current_map_lock = !current_map_lock; @@ -483,7 +483,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( canvas.AddContextMenuItem(lock_item); // Area Configuration - gui::Canvas::ContextMenuItem properties_item; + gui::CanvasMenuItem properties_item; properties_item.label = ICON_MD_TUNE " Area Configuration"; properties_item.callback = [&show_map_properties_panel]() { show_map_properties_panel = true; @@ -495,7 +495,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; if (asm_version >= 3 && asm_version != 0xFF) { // Custom Background Color - gui::Canvas::ContextMenuItem bg_color_item; + gui::CanvasMenuItem bg_color_item; bg_color_item.label = ICON_MD_FORMAT_COLOR_FILL " Custom Background Color"; bg_color_item.callback = [&show_custom_bg_color_editor]() { show_custom_bg_color_editor = true; @@ -503,7 +503,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( canvas.AddContextMenuItem(bg_color_item); // Visual Effects Editor - gui::Canvas::ContextMenuItem overlay_item; + gui::CanvasMenuItem overlay_item; overlay_item.label = ICON_MD_LAYERS " Visual Effects"; overlay_item.callback = [&show_overlay_editor]() { show_overlay_editor = true; @@ -512,7 +512,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( } // Canvas controls - gui::Canvas::ContextMenuItem reset_view_item; + gui::CanvasMenuItem reset_view_item; reset_view_item.label = ICON_MD_RESTORE " Reset View"; reset_view_item.callback = [&canvas]() { canvas.set_global_scale(1.0f); @@ -520,7 +520,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( }; canvas.AddContextMenuItem(reset_view_item); - gui::Canvas::ContextMenuItem zoom_in_item; + gui::CanvasMenuItem zoom_in_item; zoom_in_item.label = ICON_MD_ZOOM_IN " Zoom In"; zoom_in_item.callback = [&canvas]() { float scale = std::min(2.0f, canvas.global_scale() + 0.25f); @@ -528,7 +528,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu( }; canvas.AddContextMenuItem(zoom_in_item); - gui::Canvas::ContextMenuItem zoom_out_item; + gui::CanvasMenuItem zoom_out_item; zoom_out_item.label = ICON_MD_ZOOM_OUT " Zoom Out"; zoom_out_item.callback = [&canvas]() { float scale = std::max(0.25f, canvas.global_scale() - 0.25f); diff --git a/src/app/gui/canvas/canvas.cc b/src/app/gui/canvas/canvas.cc index b37dea38..a5da5b41 100644 --- a/src/app/gui/canvas/canvas.cc +++ b/src/app/gui/canvas/canvas.cc @@ -544,18 +544,18 @@ void Canvas::DrawContextMenu() { break; } }, - snapshot); + snapshot, this); // Phase 4: Pass Canvas* for editor menu integration if (modals_) { modals_->Render(); } - // CRITICAL: Render custom context menu items AFTER enhanced menu - // Don't return early - we need to show custom items too! - if (!context_menu_items_.empty() && ImGui::BeginPopupContextItem(context_id_.c_str())) { - for (const auto& item : context_menu_items_) { - DrawContextMenuItem(item); - } + // Phase 4: Render editor menu items using declarative menu system + if (!editor_menu_.sections.empty() && ImGui::BeginPopupContextItem(context_id_.c_str())) { + auto popup_callback = [this](const std::string& id, std::function callback) { + popup_registry_.Open(id, callback); + }; + gui::RenderCanvasMenu(editor_menu_, popup_callback); ImGui::EndPopup(); } @@ -569,105 +569,56 @@ void Canvas::DrawContextMenu() { ShowScalingControls(); } -void Canvas::DrawContextMenuItem(const ContextMenuItem& item) { - if (!item.enabled_condition()) { - ImGui::BeginDisabled(); - } - - if (item.subitems.empty()) { - // Simple menu item - if (ImGui::MenuItem(item.label.c_str(), item.shortcut.empty() - ? nullptr - : item.shortcut.c_str())) { - item.callback(); - } - } else { - // Menu with subitems - if (ImGui::BeginMenu(item.label.c_str())) { - for (const auto& subitem : item.subitems) { - DrawContextMenuItem(subitem); - } - ImGui::EndMenu(); - } - } - - if (!item.enabled_condition()) { - ImGui::EndDisabled(); - } +void Canvas::DrawContextMenuItem(const gui::CanvasMenuItem& item) { + // Phase 4: Use RenderMenuItem from canvas_menu.h for consistent rendering + auto popup_callback = [this](const std::string& id, std::function callback) { + popup_registry_.Open(id, callback); + }; + + gui::RenderMenuItem(item, popup_callback); } -void Canvas::AddContextMenuItem(const ContextMenuItem& item) { - context_menu_items_.push_back(item); +void Canvas::AddContextMenuItem(const gui::CanvasMenuItem& item) { + // Phase 4: Add to editor menu definition + // Items are added to a default section with editor-specific priority + if (editor_menu_.sections.empty()) { + CanvasMenuSection section; + section.priority = MenuSectionPriority::kEditorSpecific; + section.separator_after = true; + editor_menu_.sections.push_back(section); + } + + // Add to the last section (or create new if the last isn't editor-specific) + auto& last_section = editor_menu_.sections.back(); + if (last_section.priority != MenuSectionPriority::kEditorSpecific) { + CanvasMenuSection new_section; + new_section.priority = MenuSectionPriority::kEditorSpecific; + new_section.separator_after = true; + editor_menu_.sections.push_back(new_section); + editor_menu_.sections.back().items.push_back(item); + } else { + last_section.items.push_back(item); + } } void Canvas::ClearContextMenuItems() { - context_menu_items_.clear(); + editor_menu_.sections.clear(); } -void Canvas::OpenPersistentPopup(const std::string& popup_id, std::function render_callback) { - // Phase 3: Delegate to new popup registry +void Canvas::OpenPersistentPopup(const std::string& popup_id, + std::function render_callback) { + // Phase 4: Simplified popup management (no legacy synchronization) popup_registry_.Open(popup_id, render_callback); - - // Maintain backward compatibility with legacy active_popups_ vector - // TODO(Phase 4): Remove this synchronization once all editors migrate - bool found = false; - for (auto& popup : active_popups_) { - if (popup.popup_id == popup_id) { - popup.is_open = true; - popup.render_callback = render_callback; - found = true; - break; - } - } - - if (!found) { - PopupState new_popup; - new_popup.popup_id = popup_id; - new_popup.is_open = true; - new_popup.render_callback = render_callback; - active_popups_.push_back(new_popup); - } } void Canvas::ClosePersistentPopup(const std::string& popup_id) { - // Phase 3: Delegate to new popup registry + // Phase 4: Simplified popup management (no legacy synchronization) popup_registry_.Close(popup_id); - - // Maintain backward compatibility with legacy active_popups_ vector - // TODO(Phase 4): Remove this synchronization once all editors migrate - for (auto& popup : active_popups_) { - if (popup.popup_id == popup_id) { - popup.is_open = false; - return; - } - } } void Canvas::RenderPersistentPopups() { - // Phase 3: Delegate to new popup registry + // Phase 4: Simplified rendering (no legacy synchronization) popup_registry_.RenderAll(); - - // Maintain backward compatibility: sync legacy vector with registry state - // TODO(Phase 4): Remove this synchronization once all editors migrate - const auto& registry_popups = popup_registry_.GetPopups(); - - // Remove closed popups from legacy vector - auto it = active_popups_.begin(); - while (it != active_popups_.end()) { - bool found_in_registry = false; - for (const auto& reg_popup : registry_popups) { - if (reg_popup.popup_id == it->popup_id && reg_popup.is_open) { - found_in_registry = true; - break; - } - } - - if (!found_in_registry) { - it = active_popups_.erase(it); - } else { - ++it; - } - } } void Canvas::SetZoomToFit(const gfx::Bitmap& bitmap) { diff --git a/src/app/gui/canvas/canvas.h b/src/app/gui/canvas/canvas.h index dcebeb80..ca6fa9bb 100644 --- a/src/app/gui/canvas/canvas.h +++ b/src/app/gui/canvas/canvas.h @@ -153,38 +153,8 @@ class Canvas { // This routine also handles the scrolling for the canvas. void DrawContextMenu(); - // Context menu system for consumers to add their own menu elements - struct ContextMenuItem { - std::string label; - std::string shortcut; - std::function callback; - std::function enabled_condition = []() { return true; }; - std::vector subitems; - - // Helper constructor for simple items - ContextMenuItem() = default; - ContextMenuItem(const std::string& lbl, std::function cb, - const std::string& sc = "") - : label(lbl), shortcut(sc), callback(std::move(cb)) {} - - // Helper to create disabled item - static ContextMenuItem Disabled(const std::string& lbl) { - ContextMenuItem item; - item.label = lbl; - item.enabled_condition = []() { return false; }; - return item; - } - - // Helper to create conditional item - static ContextMenuItem Conditional(const std::string& lbl, std::function cb, - std::function condition) { - ContextMenuItem item; - item.label = lbl; - item.callback = std::move(cb); - item.enabled_condition = std::move(condition); - return item; - } - }; + // Phase 4: Use unified menu item definition from canvas_menu.h + using CanvasMenuItem = gui::CanvasMenuItem; // BPP format UI components std::unique_ptr bpp_format_ui_; @@ -198,8 +168,12 @@ class Canvas { std::shared_ptr performance_integration_; CanvasInteractionHandler interaction_handler_; - void AddContextMenuItem(const ContextMenuItem& item); + void AddContextMenuItem(const gui::CanvasMenuItem& item); void ClearContextMenuItems(); + + // Phase 4: Access to editor-provided menu definition + CanvasMenuDefinition& editor_menu() { return editor_menu_; } + const CanvasMenuDefinition& editor_menu() const { return editor_menu_; } void SetContextMenuEnabled(bool enabled) { context_menu_enabled_ = enabled; } // Persistent popup management for context menu actions @@ -415,7 +389,7 @@ class Canvas { Rom *rom() const { return rom_; } private: - void DrawContextMenuItem(const ContextMenuItem& item); + void DrawContextMenuItem(const gui::CanvasMenuItem& item); // Modular configuration and state gfx::IRenderer* renderer_ = nullptr; @@ -433,22 +407,12 @@ class Canvas { bool is_hovered_ = false; bool refresh_graphics_ = false; - // Context menu system - std::vector context_menu_items_; + // Phase 4: Context menu system (declarative menu definition) + CanvasMenuDefinition editor_menu_; bool context_menu_enabled_ = true; - // Persistent popup state for context menu actions - // Phase 3: New popup registry (preferred for new code) + // Phase 4: Persistent popup state for context menu actions (unified registry) PopupRegistry popup_registry_; - - // Legacy popup state (deprecated - kept for backward compatibility during migration) - // TODO(Phase 4): Remove once all editors use popup_registry_ directly - struct PopupState { - std::string popup_id; - bool is_open = false; - std::function render_callback; - }; - std::vector active_popups_; // Legacy members (to be gradually replaced) int current_labels_ = 0; diff --git a/src/app/gui/canvas/canvas_context_menu.cc b/src/app/gui/canvas/canvas_context_menu.cc index 1492b047..d4e53a7d 100644 --- a/src/app/gui/canvas/canvas_context_menu.cc +++ b/src/app/gui/canvas/canvas_context_menu.cc @@ -6,8 +6,7 @@ #include "app/gui/widgets/palette_editor_widget.h" #include "app/gui/core/icons.h" #include "app/gui/core/color.h" -#include "app/gui/canvas/canvas_modals.h" -#include "app/gui/widgets/palette_editor_widget.h" +#include "app/gui/canvas/canvas.h" #include "imgui/imgui.h" namespace yaze { @@ -50,11 +49,11 @@ void CanvasContextMenu::SetUsageMode(CanvasUsage usage) { current_usage_ = usage; } -void CanvasContextMenu::AddMenuItem(const ContextMenuItem& item) { +void CanvasContextMenu::AddMenuItem(const CanvasMenuItem& item) { global_items_.push_back(item); } -void CanvasContextMenu::AddMenuItem(const ContextMenuItem& item, CanvasUsage usage) { +void CanvasContextMenu::AddMenuItem(const CanvasMenuItem& item, CanvasUsage usage) { usage_specific_items_[usage].push_back(item); } @@ -63,10 +62,12 @@ void CanvasContextMenu::ClearMenuItems() { usage_specific_items_.clear(); } -void CanvasContextMenu::Render(const std::string& context_id, const ImVec2& mouse_pos, Rom* rom, - const gfx::Bitmap* bitmap, const gfx::SnesPalette* palette, +void CanvasContextMenu::Render(const std::string& context_id, + const ImVec2& /* mouse_pos */, Rom* rom, + const gfx::Bitmap* bitmap, + const gfx::SnesPalette* /* palette */, const std::function& command_handler, - CanvasConfig current_config) { + CanvasConfig current_config, Canvas* canvas) { if (!enabled_) return; // Context menu (under default mouse threshold) @@ -75,53 +76,61 @@ void CanvasContextMenu::Render(const std::string& context_id, const ImVec2& mous ImGui::OpenPopupOnItemClick(context_id.c_str(), ImGuiPopupFlags_MouseButtonRight); } - // Contents of the Context Menu + // Phase 4: Popup callback for automatic popup management + auto popup_callback = [canvas](const std::string& id, std::function callback) { + if (canvas) { + canvas->GetPopupRegistry().Open(id, callback); + } + }; + + // Contents of the Context Menu (Phase 4: Priority-based ordering) if (ImGui::BeginPopup(context_id.c_str())) { - // Render usage-specific menu first - RenderUsageSpecificMenu(); - - // Add separator if there are usage-specific items - if (!usage_specific_items_[current_usage_].empty()) { + // PRIORITY 0: Editor-specific items (from Canvas::editor_menu_) + if (canvas && !canvas->editor_menu().sections.empty()) { + RenderCanvasMenu(canvas->editor_menu(), popup_callback); ImGui::Separator(); } - // Render view controls - RenderViewControlsMenu(command_handler, current_config); - ImGui::Separator(); + // Also render usage-specific items (legacy support) + if (!usage_specific_items_[current_usage_].empty()) { + RenderUsageSpecificMenu(popup_callback); + ImGui::Separator(); + } - // Render canvas properties - RenderCanvasPropertiesMenu(command_handler, current_config); - ImGui::Separator(); - - // Render bitmap operations if bitmap is available + // PRIORITY 10: Bitmap/Palette operations if (bitmap) { RenderBitmapOperationsMenu(const_cast(bitmap)); ImGui::Separator(); - } - - // Render palette operations if bitmap is available - if (bitmap) { + RenderPaletteOperationsMenu(rom, const_cast(bitmap)); ImGui::Separator(); - } - - if (bitmap) { + RenderBppOperationsMenu(bitmap); ImGui::Separator(); } - - RenderPerformanceMenu(); + + // PRIORITY 20: Canvas properties + RenderCanvasPropertiesMenu(command_handler, current_config); ImGui::Separator(); - + + RenderViewControlsMenu(command_handler, current_config); + ImGui::Separator(); + RenderGridControlsMenu(command_handler, current_config); ImGui::Separator(); - + RenderScalingControlsMenu(command_handler, current_config); + + // PRIORITY 30: Debug/Performance + if (ImGui::GetIO().KeyCtrl) { // Only show when Ctrl is held + ImGui::Separator(); + RenderPerformanceMenu(); + } - // Render global menu items + // Render global menu items (if any) if (!global_items_.empty()) { ImGui::Separator(); - RenderMenuSection("Custom Actions", global_items_); + RenderMenuSection("Custom Actions", global_items_, popup_callback); } ImGui::EndPopup(); @@ -137,70 +146,44 @@ void CanvasContextMenu::SetCanvasState(const ImVec2& canvas_size, float global_scale, float grid_step, bool enable_grid, - bool enable_hex_labels, - bool enable_custom_labels, - bool enable_context_menu, - bool is_draggable, - bool auto_resize, + bool /* enable_hex_labels */, + bool /* enable_custom_labels */, + bool /* enable_context_menu */, + bool /* is_draggable */, + bool /* auto_resize */, const ImVec2& scrolling) { canvas_size_ = canvas_size; content_size_ = content_size; global_scale_ = global_scale; grid_step_ = grid_step; enable_grid_ = enable_grid; - enable_hex_labels_ = enable_hex_labels_; - enable_custom_labels_ = enable_custom_labels_; - enable_context_menu_ = enable_context_menu_; - is_draggable_ = is_draggable_; - auto_resize_ = auto_resize_; + enable_hex_labels_ = false; // Field not used anymore + enable_custom_labels_ = false; // Field not used anymore + enable_context_menu_ = true; // Field not used anymore + is_draggable_ = false; // Field not used anymore + auto_resize_ = false; // Field not used anymore scrolling_ = scrolling; } -void CanvasContextMenu::RenderMenuItem(const ContextMenuItem& item) { - if (!item.visible_condition()) { - return; - } - - if (!item.enabled_condition()) { - ImGui::BeginDisabled(); - } - - if (item.subitems.empty()) { - // Simple menu item - if (ImGui::MenuItem(item.label.c_str(), - item.shortcut.empty() ? nullptr : item.shortcut.c_str())) { - item.callback(); - } - } else { - // Menu with subitems - if (ImGui::BeginMenu(item.label.c_str())) { - for (const auto& subitem : item.subitems) { - RenderMenuItem(subitem); - } - ImGui::EndMenu(); - } - } - - if (!item.enabled_condition()) { - ImGui::EndDisabled(); - } - - if (item.separator_after) { - ImGui::Separator(); - } +void CanvasContextMenu::RenderMenuItem(const CanvasMenuItem& item, + std::function)> popup_callback) { + // Phase 4: Delegate to canvas_menu.h implementation + gui::RenderMenuItem(item, popup_callback); } void CanvasContextMenu::RenderMenuSection(const std::string& title, - const std::vector& items) { + const std::vector& items, + std::function)> popup_callback) { if (items.empty()) return; ImGui::TextColored(ImVec4(0.7F, 0.7F, 0.7F, 1.0F), "%s", title.c_str()); for (const auto& item : items) { - RenderMenuItem(item); + RenderMenuItem(item, popup_callback); } } -void CanvasContextMenu::RenderUsageSpecificMenu() { +void CanvasContextMenu::RenderUsageSpecificMenu( + std::function)> popup_callback) { auto it = usage_specific_items_.find(current_usage_); if (it == usage_specific_items_.end() || it->second.empty()) { return; @@ -213,7 +196,7 @@ void CanvasContextMenu::RenderUsageSpecificMenu() { ImGui::Separator(); for (const auto& item : it->second) { - RenderMenuItem(item); + RenderMenuItem(item, popup_callback); } } @@ -397,7 +380,7 @@ void CanvasContextMenu::DrawROMPaletteSelector() { palette_editor_->DrawROMPaletteSelector(); } -void CanvasContextMenu::RenderBppOperationsMenu(const gfx::Bitmap* bitmap) { +void CanvasContextMenu::RenderBppOperationsMenu(const gfx::Bitmap* /* bitmap */) { if (ImGui::BeginMenu(ICON_MD_SWAP_HORIZ " BPP Operations")) { if (ImGui::MenuItem("Format Analysis...")) { // Open BPP analysis @@ -528,85 +511,80 @@ ImVec4 CanvasContextMenu::GetUsageModeColor(CanvasUsage usage) const { } void CanvasContextMenu::CreateDefaultMenuItems() { - // Create default menu items for different usage modes + // Phase 4: Create default menu items using unified CanvasMenuItem // Tile Painting mode items - ContextMenuItem tile_paint_item("Paint Tile", "paint", []() { + CanvasMenuItem tile_paint_item("Paint Tile", "paint", []() { // Tile painting action }); usage_specific_items_[CanvasUsage::kTilePainting].push_back(tile_paint_item); // Tile Selecting mode items - ContextMenuItem tile_select_item("Select Tile", "select", []() { + CanvasMenuItem tile_select_item("Select Tile", "select", []() { // Tile selection action }); usage_specific_items_[CanvasUsage::kTileSelecting].push_back(tile_select_item); // Rectangle Selection mode items - ContextMenuItem rect_select_item("Select Rectangle", "rect", []() { + CanvasMenuItem rect_select_item("Select Rectangle", "rect", []() { // Rectangle selection action }); usage_specific_items_[CanvasUsage::kSelectRectangle].push_back(rect_select_item); // Color Painting mode items - ContextMenuItem color_paint_item("Paint Color", "color", []() { + CanvasMenuItem color_paint_item("Paint Color", "color", []() { // Color painting action }); usage_specific_items_[CanvasUsage::kColorPainting].push_back(color_paint_item); // Bitmap Editing mode items - ContextMenuItem bitmap_edit_item("Edit Bitmap", "edit", []() { + CanvasMenuItem bitmap_edit_item("Edit Bitmap", "edit", []() { // Bitmap editing action }); usage_specific_items_[CanvasUsage::kBitmapEditing].push_back(bitmap_edit_item); // Palette Editing mode items - ContextMenuItem palette_edit_item("Edit Palette", "palette", []() { + CanvasMenuItem palette_edit_item("Edit Palette", "palette", []() { // Palette editing action }); usage_specific_items_[CanvasUsage::kPaletteEditing].push_back(palette_edit_item); // BPP Conversion mode items - ContextMenuItem bpp_convert_item("Convert Format", "convert", []() { + CanvasMenuItem bpp_convert_item("Convert Format", "convert", []() { // BPP conversion action }); usage_specific_items_[CanvasUsage::kBppConversion].push_back(bpp_convert_item); // Performance Mode items - ContextMenuItem perf_item("Performance Analysis", "perf", []() { + CanvasMenuItem perf_item("Performance Analysis", "perf", []() { // Performance analysis action }); usage_specific_items_[CanvasUsage::kPerformanceMode].push_back(perf_item); } -CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateViewMenuItem(const std::string& label, - const std::string& icon, - std::function callback) { - return ContextMenuItem(label, icon, callback); +CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateViewMenuItem( + const std::string& label, const std::string& icon, std::function callback) { + return CanvasMenuItem(label, icon, callback); } -CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBitmapMenuItem(const std::string& label, - const std::string& icon, - std::function callback) { - return ContextMenuItem(label, icon, callback); +CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateBitmapMenuItem( + const std::string& label, const std::string& icon, std::function callback) { + return CanvasMenuItem(label, icon, callback); } -CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePaletteMenuItem(const std::string& label, - const std::string& icon, - std::function callback) { - return ContextMenuItem(label, icon, callback); +CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreatePaletteMenuItem( + const std::string& label, const std::string& icon, std::function callback) { + return CanvasMenuItem(label, icon, callback); } -CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBppMenuItem(const std::string& label, - const std::string& icon, - std::function callback) { - return ContextMenuItem(label, icon, callback); +CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateBppMenuItem( + const std::string& label, const std::string& icon, std::function callback) { + return CanvasMenuItem(label, icon, callback); } -CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePerformanceMenuItem(const std::string& label, - const std::string& icon, - std::function callback) { - return ContextMenuItem(label, icon, callback); +CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreatePerformanceMenuItem( + const std::string& label, const std::string& icon, std::function callback) { + return CanvasMenuItem(label, icon, callback); } } // namespace gui diff --git a/src/app/gui/canvas/canvas_context_menu.h b/src/app/gui/canvas/canvas_context_menu.h index 6f19b93d..a9717121 100644 --- a/src/app/gui/canvas/canvas_context_menu.h +++ b/src/app/gui/canvas/canvas_context_menu.h @@ -10,6 +10,7 @@ #include "app/gfx/types/snes_palette.h" #include "app/gui/core/icons.h" #include "app/gui/canvas/canvas_modals.h" +#include "app/gui/canvas/canvas_menu.h" #include "canvas_usage_tracker.h" #include "imgui/imgui.h" @@ -18,6 +19,7 @@ namespace gui { // Forward declarations class PaletteEditorWidget; +class Canvas; class CanvasContextMenu { public: @@ -41,36 +43,24 @@ class CanvasContextMenu { CanvasContextMenu() = default; - struct ContextMenuItem { - std::string label; - std::string shortcut; - std::string icon; - std::function callback; - std::function enabled_condition = []() { return true; }; - std::function visible_condition = []() { return true; }; - std::vector subitems; - ImVec4 color = ImVec4(1, 1, 1, 1); - bool separator_after = false; - - ContextMenuItem() = default; - ContextMenuItem(const std::string& lbl, const std::string& ico, - std::function cb, const std::string& sc = "") - : label(lbl), shortcut(sc), icon(ico), callback(std::move(cb)) {} - }; + // Phase 4: Use unified CanvasMenuItem from canvas_menu.h + using CanvasMenuItem = gui::CanvasMenuItem; void Initialize(const std::string& canvas_id); void SetUsageMode(CanvasUsage usage); - void AddMenuItem(const ContextMenuItem& item); - void AddMenuItem(const ContextMenuItem& item, CanvasUsage usage); + void AddMenuItem(const CanvasMenuItem& item); + void AddMenuItem(const CanvasMenuItem& item, CanvasUsage usage); void ClearMenuItems(); + // Phase 4: Render with editor menu integration and priority ordering void Render(const std::string& context_id, const ImVec2& mouse_pos, Rom* rom, const gfx::Bitmap* bitmap, const gfx::SnesPalette* palette, const std::function& command_handler, - CanvasConfig current_config); + CanvasConfig current_config, + Canvas* canvas); bool ShouldShowContextMenu() const; void SetEnabled(bool enabled) { enabled_ = enabled; } @@ -114,13 +104,15 @@ class CanvasContextMenu { void DrawROMPaletteSelector(); - std::unordered_map> usage_specific_items_; - std::vector global_items_; + std::unordered_map> usage_specific_items_; + std::vector global_items_; - void RenderMenuItem(const ContextMenuItem& item); + void RenderMenuItem(const CanvasMenuItem& item, + std::function)> popup_callback); void RenderMenuSection(const std::string& title, - const std::vector& items); - void RenderUsageSpecificMenu(); + const std::vector& items, + std::function)> popup_callback); + void RenderUsageSpecificMenu(std::function)> popup_callback); void RenderViewControlsMenu(const std::function& command_handler, CanvasConfig current_config); void RenderCanvasPropertiesMenu(const std::function& command_handler, @@ -140,21 +132,21 @@ class CanvasContextMenu { ImVec4 GetUsageModeColor(CanvasUsage usage) const; void CreateDefaultMenuItems(); - ContextMenuItem CreateViewMenuItem(const std::string& label, + CanvasMenuItem CreateViewMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + CanvasMenuItem CreateBitmapMenuItem(const std::string& label, const std::string& icon, std::function callback); - ContextMenuItem CreateBitmapMenuItem(const std::string& label, - const std::string& icon, - std::function callback); - ContextMenuItem CreatePaletteMenuItem(const std::string& label, - const std::string& icon, - std::function callback); - ContextMenuItem CreateBppMenuItem(const std::string& label, - const std::string& icon, - std::function callback); - ContextMenuItem CreatePerformanceMenuItem(const std::string& label, - const std::string& icon, - std::function callback); + CanvasMenuItem CreatePaletteMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + CanvasMenuItem CreateBppMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + CanvasMenuItem CreatePerformanceMenuItem(const std::string& label, + const std::string& icon, + std::function callback); }; } // namespace gui diff --git a/src/app/gui/canvas/canvas_menu.h b/src/app/gui/canvas/canvas_menu.h index aadfc6e4..5dbd02cf 100644 --- a/src/app/gui/canvas/canvas_menu.h +++ b/src/app/gui/canvas/canvas_menu.h @@ -11,6 +11,22 @@ namespace yaze { namespace gui { +/** + * @brief Menu section priority for controlling rendering order + * + * Lower values render first in the context menu: + * - Editor-specific items (0) appear at the top + * - Bitmap/palette operations (10) in the middle + * - Canvas properties (20) near the bottom + * - Debug/performance (30) at the bottom + */ +enum class MenuSectionPriority { + kEditorSpecific = 0, // Highest priority - editor-specific actions + kBitmapPalette = 10, // Medium priority - bitmap/palette operations + kCanvasProperties = 20, // Low priority - canvas settings + kDebug = 30 // Lowest priority - debug/performance +}; + /** * @brief Declarative popup definition for menu items * @@ -126,6 +142,7 @@ struct CanvasMenuItem { * @brief Menu section grouping related menu items * * Provides visual organization of menu items with optional section titles. + * Sections are rendered in priority order. */ struct CanvasMenuSection { // Optional section title (rendered as colored text) @@ -140,6 +157,9 @@ struct CanvasMenuSection { // Whether to show a separator after this section bool separator_after = true; + // Priority for ordering sections (lower values render first) + MenuSectionPriority priority = MenuSectionPriority::kEditorSpecific; + // Default constructor CanvasMenuSection() = default; @@ -149,6 +169,11 @@ struct CanvasMenuSection { // Constructor with title and items CanvasMenuSection(const std::string& t, const std::vector& its) : title(t), items(its) {} + + // Constructor with title, items, and priority + CanvasMenuSection(const std::string& t, const std::vector& its, + MenuSectionPriority prio) + : title(t), items(its), priority(prio) {} }; /** diff --git a/src/app/gui/canvas/canvas_menu_builder.cc b/src/app/gui/canvas/canvas_menu_builder.cc new file mode 100644 index 00000000..8c25ee1b --- /dev/null +++ b/src/app/gui/canvas/canvas_menu_builder.cc @@ -0,0 +1,145 @@ +#include "canvas_menu_builder.h" + +namespace yaze { +namespace gui { + +CanvasMenuBuilder& CanvasMenuBuilder::AddItem(const std::string& label, + std::function callback) { + CanvasMenuItem item; + item.label = label; + item.callback = std::move(callback); + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddItem(const std::string& label, + const std::string& icon, + std::function callback) { + CanvasMenuItem item; + item.label = label; + item.icon = icon; + item.callback = std::move(callback); + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddItem(const std::string& label, + const std::string& icon, + const std::string& shortcut, + std::function callback) { + CanvasMenuItem item; + item.label = label; + item.icon = icon; + item.shortcut = shortcut; + item.callback = std::move(callback); + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddPopupItem( + const std::string& label, const std::string& popup_id, + std::function render_callback) { + CanvasMenuItem item = CanvasMenuItem::WithPopup(label, popup_id, render_callback); + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddPopupItem( + const std::string& label, const std::string& icon, + const std::string& popup_id, std::function render_callback) { + CanvasMenuItem item = CanvasMenuItem::WithPopup(label, popup_id, render_callback); + item.icon = icon; + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddConditionalItem( + const std::string& label, std::function callback, + std::function condition) { + CanvasMenuItem item = CanvasMenuItem::Conditional(label, callback, condition); + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddSubmenu( + const std::string& label, const std::vector& subitems) { + CanvasMenuItem item; + item.label = label; + item.subitems = subitems; + pending_items_.push_back(item); + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::AddSeparator() { + if (!pending_items_.empty()) { + pending_items_.back().separator_after = true; + } + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::BeginSection( + const std::string& title, MenuSectionPriority priority) { + // Flush any pending items to previous section + FlushPendingItems(); + + // Create new section + CanvasMenuSection section; + section.title = title; + section.priority = priority; + section.separator_after = true; + menu_.sections.push_back(section); + + // Point current_section_ to the newly added section + current_section_ = &menu_.sections.back(); + + return *this; +} + +CanvasMenuBuilder& CanvasMenuBuilder::EndSection() { + FlushPendingItems(); + current_section_ = nullptr; + return *this; +} + +CanvasMenuDefinition CanvasMenuBuilder::Build() { + FlushPendingItems(); + return menu_; +} + +CanvasMenuBuilder& CanvasMenuBuilder::Reset() { + menu_.sections.clear(); + pending_items_.clear(); + current_section_ = nullptr; + return *this; +} + +void CanvasMenuBuilder::FlushPendingItems() { + if (pending_items_.empty()) { + return; + } + + // If no section exists yet, create a default one + if (menu_.sections.empty()) { + CanvasMenuSection section; + section.priority = MenuSectionPriority::kEditorSpecific; + section.separator_after = true; + menu_.sections.push_back(section); + current_section_ = &menu_.sections.back(); + } + + // Add pending items to current section + if (current_section_) { + current_section_->items.insert(current_section_->items.end(), + pending_items_.begin(), pending_items_.end()); + } else { + // Add to last section if current_section_ is null + menu_.sections.back().items.insert(menu_.sections.back().items.end(), + pending_items_.begin(), pending_items_.end()); + } + + pending_items_.clear(); +} + +} // namespace gui +} // namespace yaze + diff --git a/src/app/gui/canvas/canvas_menu_builder.h b/src/app/gui/canvas/canvas_menu_builder.h new file mode 100644 index 00000000..6b3c8ff5 --- /dev/null +++ b/src/app/gui/canvas/canvas_menu_builder.h @@ -0,0 +1,157 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H +#define YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H + +#include +#include +#include + +#include "app/gui/canvas/canvas_menu.h" + +namespace yaze { +namespace gui { + +/** + * @brief Builder pattern for constructing canvas menus fluently + * + * Phase 4: Simplifies menu construction with chainable methods. + * + * Example usage: + * @code + * CanvasMenuBuilder builder; + * builder + * .AddItem("Cut", ICON_MD_CONTENT_CUT, []() { DoCut(); }) + * .AddItem("Copy", ICON_MD_CONTENT_COPY, []() { DoCopy(); }) + * .AddSeparator() + * .AddPopupItem("Properties", ICON_MD_SETTINGS, "props_popup", + * []() { RenderPropertiesPopup(); }) + * .Build(); + * @endcode + */ +class CanvasMenuBuilder { + public: + CanvasMenuBuilder() = default; + + /** + * @brief Add a simple menu item + * @param label Menu item label + * @param callback Action to perform when selected + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddItem(const std::string& label, + std::function callback); + + /** + * @brief Add a menu item with icon + * @param label Menu item label + * @param icon Material Design icon or Unicode glyph + * @param callback Action to perform when selected + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddItem(const std::string& label, + const std::string& icon, + std::function callback); + + /** + * @brief Add a menu item with icon and shortcut hint + * @param label Menu item label + * @param icon Material Design icon or Unicode glyph + * @param shortcut Keyboard shortcut hint (e.g., "Ctrl+S") + * @param callback Action to perform when selected + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddItem(const std::string& label, + const std::string& icon, + const std::string& shortcut, + std::function callback); + + /** + * @brief Add a menu item that opens a persistent popup + * @param label Menu item label + * @param popup_id Unique popup identifier + * @param render_callback Callback to render popup content + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddPopupItem(const std::string& label, + const std::string& popup_id, + std::function render_callback); + + /** + * @brief Add a menu item with icon that opens a persistent popup + * @param label Menu item label + * @param icon Material Design icon or Unicode glyph + * @param popup_id Unique popup identifier + * @param render_callback Callback to render popup content + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddPopupItem(const std::string& label, + const std::string& icon, + const std::string& popup_id, + std::function render_callback); + + /** + * @brief Add a conditional menu item (enabled only when condition is true) + * @param label Menu item label + * @param callback Action to perform when selected + * @param condition Function that returns true when item should be enabled + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddConditionalItem(const std::string& label, + std::function callback, + std::function condition); + + /** + * @brief Add a submenu with nested items + * @param label Submenu label + * @param subitems Nested menu items + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddSubmenu(const std::string& label, + const std::vector& subitems); + + /** + * @brief Add a separator to visually group items + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& AddSeparator(); + + /** + * @brief Start a new section with optional title + * @param title Section title (empty for no title) + * @param priority Section priority (controls rendering order) + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& BeginSection( + const std::string& title = "", + MenuSectionPriority priority = MenuSectionPriority::kEditorSpecific); + + /** + * @brief End the current section + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& EndSection(); + + /** + * @brief Build the final menu definition + * @return Complete menu definition ready for rendering + */ + CanvasMenuDefinition Build(); + + /** + * @brief Reset the builder to start building a new menu + * @return Reference to this builder for chaining + */ + CanvasMenuBuilder& Reset(); + + private: + CanvasMenuDefinition menu_; + CanvasMenuSection* current_section_ = nullptr; + std::vector pending_items_; + + void FlushPendingItems(); +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H + diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index 91f7d5a0..016e9897 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -33,6 +33,7 @@ set(CANVAS_SRC app/gui/canvas/canvas_interaction.cc app/gui/canvas/canvas_interaction_handler.cc app/gui/canvas/canvas_menu.cc + app/gui/canvas/canvas_menu_builder.cc app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_performance_integration.cc app/gui/canvas/canvas_popup.cc