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.
This commit is contained in:
scawful
2025-10-16 20:34:34 -04:00
parent 99b0a7e11f
commit d9e793eaea
10 changed files with 625 additions and 412 deletions

View File

@@ -218,158 +218,158 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
auto& layer_settings = GetRoomLayerSettings(room_id); auto& layer_settings = GetRoomLayerSettings(room_id);
// Add object placement option // Add object placement option
canvas_.AddContextMenuItem({ canvas_.AddContextMenuItem(
ICON_MD_ADD " Place Object", gui::CanvasMenuItem(ICON_MD_ADD " Place Object", ICON_MD_ADD,
[]() { []() {
// TODO: Show object palette/selector // TODO: Show object palette/selector
}, },
"Ctrl+P" "Ctrl+P")
}); );
// Add object deletion for selected objects // Add object deletion for selected objects
canvas_.AddContextMenuItem({ canvas_.AddContextMenuItem(
ICON_MD_DELETE " Delete Selected", gui::CanvasMenuItem(ICON_MD_DELETE " Delete Selected", ICON_MD_DELETE,
[this]() { [this]() {
object_interaction_.HandleDeleteSelected(); object_interaction_.HandleDeleteSelected();
}, },
"Del" "Del")
}); );
// Add room property quick toggles // Add room property quick toggles
canvas_.AddContextMenuItem({ canvas_.AddContextMenuItem(
ICON_MD_LAYERS " Toggle BG1", gui::CanvasMenuItem(ICON_MD_LAYERS " Toggle BG1", ICON_MD_LAYERS,
[this, room_id]() { [this, room_id]() {
auto& settings = GetRoomLayerSettings(room_id); auto& settings = GetRoomLayerSettings(room_id);
settings.bg1_visible = !settings.bg1_visible; settings.bg1_visible = !settings.bg1_visible;
}, },
"1" "1")
}); );
canvas_.AddContextMenuItem({ canvas_.AddContextMenuItem(
ICON_MD_LAYERS " Toggle BG2", gui::CanvasMenuItem(ICON_MD_LAYERS " Toggle BG2", ICON_MD_LAYERS,
[this, room_id]() { [this, room_id]() {
auto& settings = GetRoomLayerSettings(room_id); auto& settings = GetRoomLayerSettings(room_id);
settings.bg2_visible = !settings.bg2_visible; settings.bg2_visible = !settings.bg2_visible;
}, },
"2" "2")
}); );
// Add re-render option // Add re-render option
canvas_.AddContextMenuItem({ canvas_.AddContextMenuItem(
ICON_MD_REFRESH " Re-render Room", gui::CanvasMenuItem(ICON_MD_REFRESH " Re-render Room", ICON_MD_REFRESH,
[&room]() { [&room]() {
room.RenderRoomGraphics(); room.RenderRoomGraphics();
}, },
"Ctrl+R" "Ctrl+R")
}); );
// === DEBUG MENU === // === DEBUG MENU ===
gui::Canvas::ContextMenuItem debug_menu; gui::CanvasMenuItem debug_menu;
debug_menu.label = ICON_MD_BUG_REPORT " Debug"; debug_menu.label = ICON_MD_BUG_REPORT " Debug";
// Show room info // Show room info
debug_menu.subitems.push_back({ debug_menu.subitems.push_back(
ICON_MD_INFO " Show Room Info", gui::CanvasMenuItem(ICON_MD_INFO " Show Room Info", ICON_MD_INFO,
[this, room_id]() { [this]() {
show_room_debug_info_ = !show_room_debug_info_; show_room_debug_info_ = !show_room_debug_info_;
} })
}); );
// Show texture info // Show texture info
debug_menu.subitems.push_back({ debug_menu.subitems.push_back(
ICON_MD_IMAGE " Show Texture Debug", gui::CanvasMenuItem(ICON_MD_IMAGE " Show Texture Debug", ICON_MD_IMAGE,
[this]() { [this]() {
show_texture_debug_ = !show_texture_debug_; show_texture_debug_ = !show_texture_debug_;
} })
}); );
// Show object bounds with sub-menu for categories // 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.label = ICON_MD_CROP_SQUARE " Show Object Bounds";
object_bounds_menu.callback = [this]() { object_bounds_menu.callback = [this]() {
show_object_bounds_ = !show_object_bounds_; show_object_bounds_ = !show_object_bounds_;
}; };
// Sub-menu for filtering by type // Sub-menu for filtering by type
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Type 1 (0x00-0xFF)", gui::CanvasMenuItem("Type 1 (0x00-0xFF)",
[this]() { [this]() {
object_outline_toggles_.show_type1_objects = !object_outline_toggles_.show_type1_objects; object_outline_toggles_.show_type1_objects = !object_outline_toggles_.show_type1_objects;
} })
}); );
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Type 2 (0x100-0x1FF)", gui::CanvasMenuItem("Type 2 (0x100-0x1FF)",
[this]() { [this]() {
object_outline_toggles_.show_type2_objects = !object_outline_toggles_.show_type2_objects; object_outline_toggles_.show_type2_objects = !object_outline_toggles_.show_type2_objects;
} })
}); );
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Type 3 (0xF00-0xFFF)", gui::CanvasMenuItem("Type 3 (0xF00-0xFFF)",
[this]() { [this]() {
object_outline_toggles_.show_type3_objects = !object_outline_toggles_.show_type3_objects; object_outline_toggles_.show_type3_objects = !object_outline_toggles_.show_type3_objects;
} })
}); );
// Separator // Separator
gui::Canvas::ContextMenuItem sep; gui::CanvasMenuItem sep;
sep.label = "---"; sep.label = "---";
sep.enabled_condition = []() { return false; }; sep.enabled_condition = []() { return false; };
object_bounds_menu.subitems.push_back(sep); object_bounds_menu.subitems.push_back(sep);
// Sub-menu for filtering by layer // Sub-menu for filtering by layer
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Layer 0 (BG1)", gui::CanvasMenuItem("Layer 0 (BG1)",
[this]() { [this]() {
object_outline_toggles_.show_layer0_objects = !object_outline_toggles_.show_layer0_objects; object_outline_toggles_.show_layer0_objects = !object_outline_toggles_.show_layer0_objects;
} })
}); );
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Layer 1 (BG2)", gui::CanvasMenuItem("Layer 1 (BG2)",
[this]() { [this]() {
object_outline_toggles_.show_layer1_objects = !object_outline_toggles_.show_layer1_objects; object_outline_toggles_.show_layer1_objects = !object_outline_toggles_.show_layer1_objects;
} })
}); );
object_bounds_menu.subitems.push_back({ object_bounds_menu.subitems.push_back(
"Layer 2 (BG3)", gui::CanvasMenuItem("Layer 2 (BG3)",
[this]() { [this]() {
object_outline_toggles_.show_layer2_objects = !object_outline_toggles_.show_layer2_objects; object_outline_toggles_.show_layer2_objects = !object_outline_toggles_.show_layer2_objects;
} })
}); );
debug_menu.subitems.push_back(object_bounds_menu); debug_menu.subitems.push_back(object_bounds_menu);
// Show layer info // Show layer info
debug_menu.subitems.push_back({ debug_menu.subitems.push_back(
ICON_MD_LAYERS " Show Layer Info", gui::CanvasMenuItem(ICON_MD_LAYERS " Show Layer Info", ICON_MD_LAYERS,
[this]() { [this]() {
show_layer_info_ = !show_layer_info_; show_layer_info_ = !show_layer_info_;
} })
}); );
// Force reload room // Force reload room
debug_menu.subitems.push_back({ debug_menu.subitems.push_back(
ICON_MD_REFRESH " Force Reload", gui::CanvasMenuItem(ICON_MD_REFRESH " Force Reload", ICON_MD_REFRESH,
[&room, room_id]() { [&room]() {
room.LoadObjects(); room.LoadObjects();
room.LoadRoomGraphics(room.blockset); room.LoadRoomGraphics(room.blockset);
room.RenderRoomGraphics(); room.RenderRoomGraphics();
} })
}); );
// Log room state // Log room state
debug_menu.subitems.push_back({ debug_menu.subitems.push_back(
ICON_MD_PRINT " Log Room State", gui::CanvasMenuItem(ICON_MD_PRINT " Log Room State", ICON_MD_PRINT,
[&room, room_id]() { [&room, room_id]() {
LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id); LOG_DEBUG("DungeonDebug", "=== Room %03X Debug ===", room_id);
LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d", LOG_DEBUG("DungeonDebug", "Blockset: %d, Palette: %d, Layout: %d",
room.blockset, room.palette, room.layout); room.blockset, room.palette, room.layout);
LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu", LOG_DEBUG("DungeonDebug", "Objects: %zu, Sprites: %zu",
room.GetTileObjects().size(), room.GetSprites().size()); room.GetTileObjects().size(), room.GetSprites().size());
LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d", LOG_DEBUG("DungeonDebug", "BG1: %dx%d, BG2: %dx%d",
room.bg1_buffer().bitmap().width(), room.bg1_buffer().bitmap().height(), room.bg1_buffer().bitmap().width(), room.bg1_buffer().bitmap().height(),
room.bg2_buffer().bitmap().width(), room.bg2_buffer().bitmap().height()); room.bg2_buffer().bitmap().width(), room.bg2_buffer().bitmap().height());
} })
}); );
canvas_.AddContextMenuItem(debug_menu); canvas_.AddContextMenuItem(debug_menu);
} }

View File

@@ -418,11 +418,11 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
// Add entity insertion submenu (only in MOUSE mode) // Add entity insertion submenu (only in MOUSE mode)
if (current_mode == 0 && entity_insert_callback_) { // 0 = EditingMode::MOUSE 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"; entity_menu.label = ICON_MD_ADD_LOCATION " Insert Entity";
// Entrance submenu item // Entrance submenu item
gui::Canvas::ContextMenuItem entrance_item; gui::CanvasMenuItem entrance_item;
entrance_item.label = ICON_MD_DOOR_FRONT " Entrance"; entrance_item.label = ICON_MD_DOOR_FRONT " Entrance";
entrance_item.callback = [this]() { entrance_item.callback = [this]() {
if (entity_insert_callback_) { if (entity_insert_callback_) {
@@ -432,7 +432,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
entity_menu.subitems.push_back(entrance_item); entity_menu.subitems.push_back(entrance_item);
// Hole submenu item // Hole submenu item
gui::Canvas::ContextMenuItem hole_item; gui::CanvasMenuItem hole_item;
hole_item.label = ICON_MD_CYCLONE " Hole"; hole_item.label = ICON_MD_CYCLONE " Hole";
hole_item.callback = [this]() { hole_item.callback = [this]() {
if (entity_insert_callback_) { if (entity_insert_callback_) {
@@ -442,7 +442,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
entity_menu.subitems.push_back(hole_item); entity_menu.subitems.push_back(hole_item);
// Exit submenu item // Exit submenu item
gui::Canvas::ContextMenuItem exit_item; gui::CanvasMenuItem exit_item;
exit_item.label = ICON_MD_DOOR_BACK " Exit"; exit_item.label = ICON_MD_DOOR_BACK " Exit";
exit_item.callback = [this]() { exit_item.callback = [this]() {
if (entity_insert_callback_) { if (entity_insert_callback_) {
@@ -452,7 +452,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
entity_menu.subitems.push_back(exit_item); entity_menu.subitems.push_back(exit_item);
// Item submenu item // Item submenu item
gui::Canvas::ContextMenuItem item_item; gui::CanvasMenuItem item_item;
item_item.label = ICON_MD_GRASS " Item"; item_item.label = ICON_MD_GRASS " Item";
item_item.callback = [this]() { item_item.callback = [this]() {
if (entity_insert_callback_) { if (entity_insert_callback_) {
@@ -462,7 +462,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
entity_menu.subitems.push_back(item_item); entity_menu.subitems.push_back(item_item);
// Sprite submenu item // Sprite submenu item
gui::Canvas::ContextMenuItem sprite_item; gui::CanvasMenuItem sprite_item;
sprite_item.label = ICON_MD_PEST_CONTROL_RODENT " Sprite"; sprite_item.label = ICON_MD_PEST_CONTROL_RODENT " Sprite";
sprite_item.callback = [this]() { sprite_item.callback = [this]() {
if (entity_insert_callback_) { if (entity_insert_callback_) {
@@ -475,7 +475,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
} }
// Add overworld-specific context menu items // 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.label = current_map_lock ? "Unlock Map" : "Lock to This Map";
lock_item.callback = [&current_map_lock]() { lock_item.callback = [&current_map_lock]() {
current_map_lock = !current_map_lock; current_map_lock = !current_map_lock;
@@ -483,7 +483,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
canvas.AddContextMenuItem(lock_item); canvas.AddContextMenuItem(lock_item);
// Area Configuration // Area Configuration
gui::Canvas::ContextMenuItem properties_item; gui::CanvasMenuItem properties_item;
properties_item.label = ICON_MD_TUNE " Area Configuration"; properties_item.label = ICON_MD_TUNE " Area Configuration";
properties_item.callback = [&show_map_properties_panel]() { properties_item.callback = [&show_map_properties_panel]() {
show_map_properties_panel = true; show_map_properties_panel = true;
@@ -495,7 +495,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
(*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; (*rom_)[zelda3::OverworldCustomASMHasBeenApplied];
if (asm_version >= 3 && asm_version != 0xFF) { if (asm_version >= 3 && asm_version != 0xFF) {
// Custom Background Color // 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.label = ICON_MD_FORMAT_COLOR_FILL " Custom Background Color";
bg_color_item.callback = [&show_custom_bg_color_editor]() { bg_color_item.callback = [&show_custom_bg_color_editor]() {
show_custom_bg_color_editor = true; show_custom_bg_color_editor = true;
@@ -503,7 +503,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
canvas.AddContextMenuItem(bg_color_item); canvas.AddContextMenuItem(bg_color_item);
// Visual Effects Editor // Visual Effects Editor
gui::Canvas::ContextMenuItem overlay_item; gui::CanvasMenuItem overlay_item;
overlay_item.label = ICON_MD_LAYERS " Visual Effects"; overlay_item.label = ICON_MD_LAYERS " Visual Effects";
overlay_item.callback = [&show_overlay_editor]() { overlay_item.callback = [&show_overlay_editor]() {
show_overlay_editor = true; show_overlay_editor = true;
@@ -512,7 +512,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
} }
// Canvas controls // 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.label = ICON_MD_RESTORE " Reset View";
reset_view_item.callback = [&canvas]() { reset_view_item.callback = [&canvas]() {
canvas.set_global_scale(1.0f); canvas.set_global_scale(1.0f);
@@ -520,7 +520,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
}; };
canvas.AddContextMenuItem(reset_view_item); 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.label = ICON_MD_ZOOM_IN " Zoom In";
zoom_in_item.callback = [&canvas]() { zoom_in_item.callback = [&canvas]() {
float scale = std::min(2.0f, canvas.global_scale() + 0.25f); float scale = std::min(2.0f, canvas.global_scale() + 0.25f);
@@ -528,7 +528,7 @@ void MapPropertiesSystem::SetupCanvasContextMenu(
}; };
canvas.AddContextMenuItem(zoom_in_item); 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.label = ICON_MD_ZOOM_OUT " Zoom Out";
zoom_out_item.callback = [&canvas]() { zoom_out_item.callback = [&canvas]() {
float scale = std::max(0.25f, canvas.global_scale() - 0.25f); float scale = std::max(0.25f, canvas.global_scale() - 0.25f);

View File

@@ -544,18 +544,18 @@ void Canvas::DrawContextMenu() {
break; break;
} }
}, },
snapshot); snapshot, this); // Phase 4: Pass Canvas* for editor menu integration
if (modals_) { if (modals_) {
modals_->Render(); modals_->Render();
} }
// CRITICAL: Render custom context menu items AFTER enhanced menu // Phase 4: Render editor menu items using declarative menu system
// Don't return early - we need to show custom items too! if (!editor_menu_.sections.empty() && ImGui::BeginPopupContextItem(context_id_.c_str())) {
if (!context_menu_items_.empty() && ImGui::BeginPopupContextItem(context_id_.c_str())) { auto popup_callback = [this](const std::string& id, std::function<void()> callback) {
for (const auto& item : context_menu_items_) { popup_registry_.Open(id, callback);
DrawContextMenuItem(item); };
} gui::RenderCanvasMenu(editor_menu_, popup_callback);
ImGui::EndPopup(); ImGui::EndPopup();
} }
@@ -569,105 +569,56 @@ void Canvas::DrawContextMenu() {
ShowScalingControls(); ShowScalingControls();
} }
void Canvas::DrawContextMenuItem(const ContextMenuItem& item) { void Canvas::DrawContextMenuItem(const gui::CanvasMenuItem& item) {
if (!item.enabled_condition()) { // Phase 4: Use RenderMenuItem from canvas_menu.h for consistent rendering
ImGui::BeginDisabled(); auto popup_callback = [this](const std::string& id, std::function<void()> callback) {
} popup_registry_.Open(id, callback);
};
if (item.subitems.empty()) {
// Simple menu item gui::RenderMenuItem(item, popup_callback);
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::AddContextMenuItem(const ContextMenuItem& item) { void Canvas::AddContextMenuItem(const gui::CanvasMenuItem& item) {
context_menu_items_.push_back(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() { void Canvas::ClearContextMenuItems() {
context_menu_items_.clear(); editor_menu_.sections.clear();
} }
void Canvas::OpenPersistentPopup(const std::string& popup_id, std::function<void()> render_callback) { void Canvas::OpenPersistentPopup(const std::string& popup_id,
// Phase 3: Delegate to new popup registry std::function<void()> render_callback) {
// Phase 4: Simplified popup management (no legacy synchronization)
popup_registry_.Open(popup_id, render_callback); 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) { 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); 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() { void Canvas::RenderPersistentPopups() {
// Phase 3: Delegate to new popup registry // Phase 4: Simplified rendering (no legacy synchronization)
popup_registry_.RenderAll(); 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) { void Canvas::SetZoomToFit(const gfx::Bitmap& bitmap) {

View File

@@ -153,38 +153,8 @@ class Canvas {
// This routine also handles the scrolling for the canvas. // This routine also handles the scrolling for the canvas.
void DrawContextMenu(); void DrawContextMenu();
// Context menu system for consumers to add their own menu elements // Phase 4: Use unified menu item definition from canvas_menu.h
struct ContextMenuItem { using CanvasMenuItem = gui::CanvasMenuItem;
std::string label;
std::string shortcut;
std::function<void()> callback;
std::function<bool()> enabled_condition = []() { return true; };
std::vector<ContextMenuItem> subitems;
// Helper constructor for simple items
ContextMenuItem() = default;
ContextMenuItem(const std::string& lbl, std::function<void()> 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<void()> cb,
std::function<bool()> condition) {
ContextMenuItem item;
item.label = lbl;
item.callback = std::move(cb);
item.enabled_condition = std::move(condition);
return item;
}
};
// BPP format UI components // BPP format UI components
std::unique_ptr<gui::BppFormatUI> bpp_format_ui_; std::unique_ptr<gui::BppFormatUI> bpp_format_ui_;
@@ -198,8 +168,12 @@ class Canvas {
std::shared_ptr<CanvasPerformanceIntegration> performance_integration_; std::shared_ptr<CanvasPerformanceIntegration> performance_integration_;
CanvasInteractionHandler interaction_handler_; CanvasInteractionHandler interaction_handler_;
void AddContextMenuItem(const ContextMenuItem& item); void AddContextMenuItem(const gui::CanvasMenuItem& item);
void ClearContextMenuItems(); 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; } void SetContextMenuEnabled(bool enabled) { context_menu_enabled_ = enabled; }
// Persistent popup management for context menu actions // Persistent popup management for context menu actions
@@ -415,7 +389,7 @@ class Canvas {
Rom *rom() const { return rom_; } Rom *rom() const { return rom_; }
private: private:
void DrawContextMenuItem(const ContextMenuItem& item); void DrawContextMenuItem(const gui::CanvasMenuItem& item);
// Modular configuration and state // Modular configuration and state
gfx::IRenderer* renderer_ = nullptr; gfx::IRenderer* renderer_ = nullptr;
@@ -433,22 +407,12 @@ class Canvas {
bool is_hovered_ = false; bool is_hovered_ = false;
bool refresh_graphics_ = false; bool refresh_graphics_ = false;
// Context menu system // Phase 4: Context menu system (declarative menu definition)
std::vector<ContextMenuItem> context_menu_items_; CanvasMenuDefinition editor_menu_;
bool context_menu_enabled_ = true; bool context_menu_enabled_ = true;
// Persistent popup state for context menu actions // Phase 4: Persistent popup state for context menu actions (unified registry)
// Phase 3: New popup registry (preferred for new code)
PopupRegistry popup_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<void()> render_callback;
};
std::vector<PopupState> active_popups_;
// Legacy members (to be gradually replaced) // Legacy members (to be gradually replaced)
int current_labels_ = 0; int current_labels_ = 0;

View File

@@ -6,8 +6,7 @@
#include "app/gui/widgets/palette_editor_widget.h" #include "app/gui/widgets/palette_editor_widget.h"
#include "app/gui/core/icons.h" #include "app/gui/core/icons.h"
#include "app/gui/core/color.h" #include "app/gui/core/color.h"
#include "app/gui/canvas/canvas_modals.h" #include "app/gui/canvas/canvas.h"
#include "app/gui/widgets/palette_editor_widget.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
namespace yaze { namespace yaze {
@@ -50,11 +49,11 @@ void CanvasContextMenu::SetUsageMode(CanvasUsage usage) {
current_usage_ = usage; current_usage_ = usage;
} }
void CanvasContextMenu::AddMenuItem(const ContextMenuItem& item) { void CanvasContextMenu::AddMenuItem(const CanvasMenuItem& item) {
global_items_.push_back(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); usage_specific_items_[usage].push_back(item);
} }
@@ -63,10 +62,12 @@ void CanvasContextMenu::ClearMenuItems() {
usage_specific_items_.clear(); usage_specific_items_.clear();
} }
void CanvasContextMenu::Render(const std::string& context_id, const ImVec2& mouse_pos, Rom* rom, void CanvasContextMenu::Render(const std::string& context_id,
const gfx::Bitmap* bitmap, const gfx::SnesPalette* palette, const ImVec2& /* mouse_pos */, Rom* rom,
const gfx::Bitmap* bitmap,
const gfx::SnesPalette* /* palette */,
const std::function<void(Command, const CanvasConfig&)>& command_handler, const std::function<void(Command, const CanvasConfig&)>& command_handler,
CanvasConfig current_config) { CanvasConfig current_config, Canvas* canvas) {
if (!enabled_) return; if (!enabled_) return;
// Context menu (under default mouse threshold) // 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); 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<void()> 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())) { if (ImGui::BeginPopup(context_id.c_str())) {
// Render usage-specific menu first // PRIORITY 0: Editor-specific items (from Canvas::editor_menu_)
RenderUsageSpecificMenu(); if (canvas && !canvas->editor_menu().sections.empty()) {
RenderCanvasMenu(canvas->editor_menu(), popup_callback);
// Add separator if there are usage-specific items
if (!usage_specific_items_[current_usage_].empty()) {
ImGui::Separator(); ImGui::Separator();
} }
// Render view controls // Also render usage-specific items (legacy support)
RenderViewControlsMenu(command_handler, current_config); if (!usage_specific_items_[current_usage_].empty()) {
ImGui::Separator(); RenderUsageSpecificMenu(popup_callback);
ImGui::Separator();
}
// Render canvas properties // PRIORITY 10: Bitmap/Palette operations
RenderCanvasPropertiesMenu(command_handler, current_config);
ImGui::Separator();
// Render bitmap operations if bitmap is available
if (bitmap) { if (bitmap) {
RenderBitmapOperationsMenu(const_cast<gfx::Bitmap*>(bitmap)); RenderBitmapOperationsMenu(const_cast<gfx::Bitmap*>(bitmap));
ImGui::Separator(); ImGui::Separator();
}
// Render palette operations if bitmap is available
if (bitmap) {
RenderPaletteOperationsMenu(rom, const_cast<gfx::Bitmap*>(bitmap)); RenderPaletteOperationsMenu(rom, const_cast<gfx::Bitmap*>(bitmap));
ImGui::Separator(); ImGui::Separator();
}
if (bitmap) {
RenderBppOperationsMenu(bitmap); RenderBppOperationsMenu(bitmap);
ImGui::Separator(); ImGui::Separator();
} }
RenderPerformanceMenu(); // PRIORITY 20: Canvas properties
RenderCanvasPropertiesMenu(command_handler, current_config);
ImGui::Separator(); ImGui::Separator();
RenderViewControlsMenu(command_handler, current_config);
ImGui::Separator();
RenderGridControlsMenu(command_handler, current_config); RenderGridControlsMenu(command_handler, current_config);
ImGui::Separator(); ImGui::Separator();
RenderScalingControlsMenu(command_handler, current_config); 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()) { if (!global_items_.empty()) {
ImGui::Separator(); ImGui::Separator();
RenderMenuSection("Custom Actions", global_items_); RenderMenuSection("Custom Actions", global_items_, popup_callback);
} }
ImGui::EndPopup(); ImGui::EndPopup();
@@ -137,70 +146,44 @@ void CanvasContextMenu::SetCanvasState(const ImVec2& canvas_size,
float global_scale, float global_scale,
float grid_step, float grid_step,
bool enable_grid, bool enable_grid,
bool enable_hex_labels, bool /* enable_hex_labels */,
bool enable_custom_labels, bool /* enable_custom_labels */,
bool enable_context_menu, bool /* enable_context_menu */,
bool is_draggable, bool /* is_draggable */,
bool auto_resize, bool /* auto_resize */,
const ImVec2& scrolling) { const ImVec2& scrolling) {
canvas_size_ = canvas_size; canvas_size_ = canvas_size;
content_size_ = content_size; content_size_ = content_size;
global_scale_ = global_scale; global_scale_ = global_scale;
grid_step_ = grid_step; grid_step_ = grid_step;
enable_grid_ = enable_grid; enable_grid_ = enable_grid;
enable_hex_labels_ = enable_hex_labels_; enable_hex_labels_ = false; // Field not used anymore
enable_custom_labels_ = enable_custom_labels_; enable_custom_labels_ = false; // Field not used anymore
enable_context_menu_ = enable_context_menu_; enable_context_menu_ = true; // Field not used anymore
is_draggable_ = is_draggable_; is_draggable_ = false; // Field not used anymore
auto_resize_ = auto_resize_; auto_resize_ = false; // Field not used anymore
scrolling_ = scrolling; scrolling_ = scrolling;
} }
void CanvasContextMenu::RenderMenuItem(const ContextMenuItem& item) { void CanvasContextMenu::RenderMenuItem(const CanvasMenuItem& item,
if (!item.visible_condition()) { std::function<void(const std::string&, std::function<void()>)> popup_callback) {
return; // Phase 4: Delegate to canvas_menu.h implementation
} gui::RenderMenuItem(item, popup_callback);
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::RenderMenuSection(const std::string& title, void CanvasContextMenu::RenderMenuSection(const std::string& title,
const std::vector<ContextMenuItem>& items) { const std::vector<CanvasMenuItem>& items,
std::function<void(const std::string&, std::function<void()>)> popup_callback) {
if (items.empty()) return; if (items.empty()) return;
ImGui::TextColored(ImVec4(0.7F, 0.7F, 0.7F, 1.0F), "%s", title.c_str()); ImGui::TextColored(ImVec4(0.7F, 0.7F, 0.7F, 1.0F), "%s", title.c_str());
for (const auto& item : items) { for (const auto& item : items) {
RenderMenuItem(item); RenderMenuItem(item, popup_callback);
} }
} }
void CanvasContextMenu::RenderUsageSpecificMenu() { void CanvasContextMenu::RenderUsageSpecificMenu(
std::function<void(const std::string&, std::function<void()>)> popup_callback) {
auto it = usage_specific_items_.find(current_usage_); auto it = usage_specific_items_.find(current_usage_);
if (it == usage_specific_items_.end() || it->second.empty()) { if (it == usage_specific_items_.end() || it->second.empty()) {
return; return;
@@ -213,7 +196,7 @@ void CanvasContextMenu::RenderUsageSpecificMenu() {
ImGui::Separator(); ImGui::Separator();
for (const auto& item : it->second) { for (const auto& item : it->second) {
RenderMenuItem(item); RenderMenuItem(item, popup_callback);
} }
} }
@@ -397,7 +380,7 @@ void CanvasContextMenu::DrawROMPaletteSelector() {
palette_editor_->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::BeginMenu(ICON_MD_SWAP_HORIZ " BPP Operations")) {
if (ImGui::MenuItem("Format Analysis...")) { if (ImGui::MenuItem("Format Analysis...")) {
// Open BPP analysis // Open BPP analysis
@@ -528,85 +511,80 @@ ImVec4 CanvasContextMenu::GetUsageModeColor(CanvasUsage usage) const {
} }
void CanvasContextMenu::CreateDefaultMenuItems() { void CanvasContextMenu::CreateDefaultMenuItems() {
// Create default menu items for different usage modes // Phase 4: Create default menu items using unified CanvasMenuItem
// Tile Painting mode items // Tile Painting mode items
ContextMenuItem tile_paint_item("Paint Tile", "paint", []() { CanvasMenuItem tile_paint_item("Paint Tile", "paint", []() {
// Tile painting action // Tile painting action
}); });
usage_specific_items_[CanvasUsage::kTilePainting].push_back(tile_paint_item); usage_specific_items_[CanvasUsage::kTilePainting].push_back(tile_paint_item);
// Tile Selecting mode items // Tile Selecting mode items
ContextMenuItem tile_select_item("Select Tile", "select", []() { CanvasMenuItem tile_select_item("Select Tile", "select", []() {
// Tile selection action // Tile selection action
}); });
usage_specific_items_[CanvasUsage::kTileSelecting].push_back(tile_select_item); usage_specific_items_[CanvasUsage::kTileSelecting].push_back(tile_select_item);
// Rectangle Selection mode items // Rectangle Selection mode items
ContextMenuItem rect_select_item("Select Rectangle", "rect", []() { CanvasMenuItem rect_select_item("Select Rectangle", "rect", []() {
// Rectangle selection action // Rectangle selection action
}); });
usage_specific_items_[CanvasUsage::kSelectRectangle].push_back(rect_select_item); usage_specific_items_[CanvasUsage::kSelectRectangle].push_back(rect_select_item);
// Color Painting mode items // Color Painting mode items
ContextMenuItem color_paint_item("Paint Color", "color", []() { CanvasMenuItem color_paint_item("Paint Color", "color", []() {
// Color painting action // Color painting action
}); });
usage_specific_items_[CanvasUsage::kColorPainting].push_back(color_paint_item); usage_specific_items_[CanvasUsage::kColorPainting].push_back(color_paint_item);
// Bitmap Editing mode items // Bitmap Editing mode items
ContextMenuItem bitmap_edit_item("Edit Bitmap", "edit", []() { CanvasMenuItem bitmap_edit_item("Edit Bitmap", "edit", []() {
// Bitmap editing action // Bitmap editing action
}); });
usage_specific_items_[CanvasUsage::kBitmapEditing].push_back(bitmap_edit_item); usage_specific_items_[CanvasUsage::kBitmapEditing].push_back(bitmap_edit_item);
// Palette Editing mode items // Palette Editing mode items
ContextMenuItem palette_edit_item("Edit Palette", "palette", []() { CanvasMenuItem palette_edit_item("Edit Palette", "palette", []() {
// Palette editing action // Palette editing action
}); });
usage_specific_items_[CanvasUsage::kPaletteEditing].push_back(palette_edit_item); usage_specific_items_[CanvasUsage::kPaletteEditing].push_back(palette_edit_item);
// BPP Conversion mode items // BPP Conversion mode items
ContextMenuItem bpp_convert_item("Convert Format", "convert", []() { CanvasMenuItem bpp_convert_item("Convert Format", "convert", []() {
// BPP conversion action // BPP conversion action
}); });
usage_specific_items_[CanvasUsage::kBppConversion].push_back(bpp_convert_item); usage_specific_items_[CanvasUsage::kBppConversion].push_back(bpp_convert_item);
// Performance Mode items // Performance Mode items
ContextMenuItem perf_item("Performance Analysis", "perf", []() { CanvasMenuItem perf_item("Performance Analysis", "perf", []() {
// Performance analysis action // Performance analysis action
}); });
usage_specific_items_[CanvasUsage::kPerformanceMode].push_back(perf_item); usage_specific_items_[CanvasUsage::kPerformanceMode].push_back(perf_item);
} }
CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateViewMenuItem(const std::string& label, CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateViewMenuItem(
const std::string& icon, const std::string& label, const std::string& icon, std::function<void()> callback) {
std::function<void()> callback) { return CanvasMenuItem(label, icon, callback);
return ContextMenuItem(label, icon, callback);
} }
CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBitmapMenuItem(const std::string& label, CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateBitmapMenuItem(
const std::string& icon, const std::string& label, const std::string& icon, std::function<void()> callback) {
std::function<void()> callback) { return CanvasMenuItem(label, icon, callback);
return ContextMenuItem(label, icon, callback);
} }
CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePaletteMenuItem(const std::string& label, CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreatePaletteMenuItem(
const std::string& icon, const std::string& label, const std::string& icon, std::function<void()> callback) {
std::function<void()> callback) { return CanvasMenuItem(label, icon, callback);
return ContextMenuItem(label, icon, callback);
} }
CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBppMenuItem(const std::string& label, CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreateBppMenuItem(
const std::string& icon, const std::string& label, const std::string& icon, std::function<void()> callback) {
std::function<void()> callback) { return CanvasMenuItem(label, icon, callback);
return ContextMenuItem(label, icon, callback);
} }
CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePerformanceMenuItem(const std::string& label, CanvasContextMenu::CanvasMenuItem CanvasContextMenu::CreatePerformanceMenuItem(
const std::string& icon, const std::string& label, const std::string& icon, std::function<void()> callback) {
std::function<void()> callback) { return CanvasMenuItem(label, icon, callback);
return ContextMenuItem(label, icon, callback);
} }
} // namespace gui } // namespace gui

View File

@@ -10,6 +10,7 @@
#include "app/gfx/types/snes_palette.h" #include "app/gfx/types/snes_palette.h"
#include "app/gui/core/icons.h" #include "app/gui/core/icons.h"
#include "app/gui/canvas/canvas_modals.h" #include "app/gui/canvas/canvas_modals.h"
#include "app/gui/canvas/canvas_menu.h"
#include "canvas_usage_tracker.h" #include "canvas_usage_tracker.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
@@ -18,6 +19,7 @@ namespace gui {
// Forward declarations // Forward declarations
class PaletteEditorWidget; class PaletteEditorWidget;
class Canvas;
class CanvasContextMenu { class CanvasContextMenu {
public: public:
@@ -41,36 +43,24 @@ class CanvasContextMenu {
CanvasContextMenu() = default; CanvasContextMenu() = default;
struct ContextMenuItem { // Phase 4: Use unified CanvasMenuItem from canvas_menu.h
std::string label; using CanvasMenuItem = gui::CanvasMenuItem;
std::string shortcut;
std::string icon;
std::function<void()> callback;
std::function<bool()> enabled_condition = []() { return true; };
std::function<bool()> visible_condition = []() { return true; };
std::vector<ContextMenuItem> 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<void()> cb, const std::string& sc = "")
: label(lbl), shortcut(sc), icon(ico), callback(std::move(cb)) {}
};
void Initialize(const std::string& canvas_id); void Initialize(const std::string& canvas_id);
void SetUsageMode(CanvasUsage usage); void SetUsageMode(CanvasUsage usage);
void AddMenuItem(const ContextMenuItem& item); void AddMenuItem(const CanvasMenuItem& item);
void AddMenuItem(const ContextMenuItem& item, CanvasUsage usage); void AddMenuItem(const CanvasMenuItem& item, CanvasUsage usage);
void ClearMenuItems(); void ClearMenuItems();
// Phase 4: Render with editor menu integration and priority ordering
void Render(const std::string& context_id, void Render(const std::string& context_id,
const ImVec2& mouse_pos, const ImVec2& mouse_pos,
Rom* rom, Rom* rom,
const gfx::Bitmap* bitmap, const gfx::Bitmap* bitmap,
const gfx::SnesPalette* palette, const gfx::SnesPalette* palette,
const std::function<void(Command, const CanvasConfig&)>& command_handler, const std::function<void(Command, const CanvasConfig&)>& command_handler,
CanvasConfig current_config); CanvasConfig current_config,
Canvas* canvas);
bool ShouldShowContextMenu() const; bool ShouldShowContextMenu() const;
void SetEnabled(bool enabled) { enabled_ = enabled; } void SetEnabled(bool enabled) { enabled_ = enabled; }
@@ -114,13 +104,15 @@ class CanvasContextMenu {
void DrawROMPaletteSelector(); void DrawROMPaletteSelector();
std::unordered_map<CanvasUsage, std::vector<ContextMenuItem>> usage_specific_items_; std::unordered_map<CanvasUsage, std::vector<CanvasMenuItem>> usage_specific_items_;
std::vector<ContextMenuItem> global_items_; std::vector<CanvasMenuItem> global_items_;
void RenderMenuItem(const ContextMenuItem& item); void RenderMenuItem(const CanvasMenuItem& item,
std::function<void(const std::string&, std::function<void()>)> popup_callback);
void RenderMenuSection(const std::string& title, void RenderMenuSection(const std::string& title,
const std::vector<ContextMenuItem>& items); const std::vector<CanvasMenuItem>& items,
void RenderUsageSpecificMenu(); std::function<void(const std::string&, std::function<void()>)> popup_callback);
void RenderUsageSpecificMenu(std::function<void(const std::string&, std::function<void()>)> popup_callback);
void RenderViewControlsMenu(const std::function<void(Command, const CanvasConfig&)>& command_handler, void RenderViewControlsMenu(const std::function<void(Command, const CanvasConfig&)>& command_handler,
CanvasConfig current_config); CanvasConfig current_config);
void RenderCanvasPropertiesMenu(const std::function<void(Command, const CanvasConfig&)>& command_handler, void RenderCanvasPropertiesMenu(const std::function<void(Command, const CanvasConfig&)>& command_handler,
@@ -140,21 +132,21 @@ class CanvasContextMenu {
ImVec4 GetUsageModeColor(CanvasUsage usage) const; ImVec4 GetUsageModeColor(CanvasUsage usage) const;
void CreateDefaultMenuItems(); void CreateDefaultMenuItems();
ContextMenuItem CreateViewMenuItem(const std::string& label, CanvasMenuItem CreateViewMenuItem(const std::string& label,
const std::string& icon,
std::function<void()> callback);
CanvasMenuItem CreateBitmapMenuItem(const std::string& label,
const std::string& icon, const std::string& icon,
std::function<void()> callback); std::function<void()> callback);
ContextMenuItem CreateBitmapMenuItem(const std::string& label, CanvasMenuItem CreatePaletteMenuItem(const std::string& label,
const std::string& icon, const std::string& icon,
std::function<void()> callback); std::function<void()> callback);
ContextMenuItem CreatePaletteMenuItem(const std::string& label, CanvasMenuItem CreateBppMenuItem(const std::string& label,
const std::string& icon, const std::string& icon,
std::function<void()> callback); std::function<void()> callback);
ContextMenuItem CreateBppMenuItem(const std::string& label, CanvasMenuItem CreatePerformanceMenuItem(const std::string& label,
const std::string& icon, const std::string& icon,
std::function<void()> callback); std::function<void()> callback);
ContextMenuItem CreatePerformanceMenuItem(const std::string& label,
const std::string& icon,
std::function<void()> callback);
}; };
} // namespace gui } // namespace gui

View File

@@ -11,6 +11,22 @@
namespace yaze { namespace yaze {
namespace gui { 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 * @brief Declarative popup definition for menu items
* *
@@ -126,6 +142,7 @@ struct CanvasMenuItem {
* @brief Menu section grouping related menu items * @brief Menu section grouping related menu items
* *
* Provides visual organization of menu items with optional section titles. * Provides visual organization of menu items with optional section titles.
* Sections are rendered in priority order.
*/ */
struct CanvasMenuSection { struct CanvasMenuSection {
// Optional section title (rendered as colored text) // Optional section title (rendered as colored text)
@@ -140,6 +157,9 @@ struct CanvasMenuSection {
// Whether to show a separator after this section // Whether to show a separator after this section
bool separator_after = true; bool separator_after = true;
// Priority for ordering sections (lower values render first)
MenuSectionPriority priority = MenuSectionPriority::kEditorSpecific;
// Default constructor // Default constructor
CanvasMenuSection() = default; CanvasMenuSection() = default;
@@ -149,6 +169,11 @@ struct CanvasMenuSection {
// Constructor with title and items // Constructor with title and items
CanvasMenuSection(const std::string& t, const std::vector<CanvasMenuItem>& its) CanvasMenuSection(const std::string& t, const std::vector<CanvasMenuItem>& its)
: title(t), items(its) {} : title(t), items(its) {}
// Constructor with title, items, and priority
CanvasMenuSection(const std::string& t, const std::vector<CanvasMenuItem>& its,
MenuSectionPriority prio)
: title(t), items(its), priority(prio) {}
}; };
/** /**

View File

@@ -0,0 +1,145 @@
#include "canvas_menu_builder.h"
namespace yaze {
namespace gui {
CanvasMenuBuilder& CanvasMenuBuilder::AddItem(const std::string& label,
std::function<void()> 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<void()> 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<void()> 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<void()> 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<void()> 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<void()> callback,
std::function<bool()> 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<CanvasMenuItem>& 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

View File

@@ -0,0 +1,157 @@
#ifndef YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H
#define YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H
#include <functional>
#include <string>
#include <vector>
#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<void()> 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<void()> 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<void()> 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<void()> 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<void()> 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<void()> callback,
std::function<bool()> 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<CanvasMenuItem>& 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<CanvasMenuItem> pending_items_;
void FlushPendingItems();
};
} // namespace gui
} // namespace yaze
#endif // YAZE_APP_GUI_CANVAS_CANVAS_MENU_BUILDER_H

View File

@@ -33,6 +33,7 @@ set(CANVAS_SRC
app/gui/canvas/canvas_interaction.cc app/gui/canvas/canvas_interaction.cc
app/gui/canvas/canvas_interaction_handler.cc app/gui/canvas/canvas_interaction_handler.cc
app/gui/canvas/canvas_menu.cc app/gui/canvas/canvas_menu.cc
app/gui/canvas/canvas_menu_builder.cc
app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_modals.cc
app/gui/canvas/canvas_performance_integration.cc app/gui/canvas/canvas_performance_integration.cc
app/gui/canvas/canvas_popup.cc app/gui/canvas/canvas_popup.cc