From b089fa0cd0b46f9eeeb52505628a4fe5519bed66 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 09:07:20 -0700 Subject: [PATCH] Step 354: Menu Bar with Key Symbols (12/12 tests) --- editor/CMakeLists.txt | 4 + editor/src/MenuBar.h | 217 ++++++++++++++++++++++++++++++++++ editor/tests/step354_test.cpp | 136 +++++++++++++++++++++ progress.md | 37 ++++++ 4 files changed, 394 insertions(+) create mode 100644 editor/src/MenuBar.h create mode 100644 editor/tests/step354_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 89651f5..aa1893f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2110,4 +2110,8 @@ add_executable(step353_test tests/step353_test.cpp) target_include_directories(step353_test PRIVATE src) target_link_libraries(step353_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step354_test tests/step354_test.cpp) +target_include_directories(step354_test PRIVATE src) +target_link_libraries(step354_test PRIVATE nlohmann_json::nlohmann_json) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/MenuBar.h b/editor/src/MenuBar.h new file mode 100644 index 0000000..0c75d11 --- /dev/null +++ b/editor/src/MenuBar.h @@ -0,0 +1,217 @@ +#pragma once +// Step 354: Menu Bar with key symbols. +// Headless data model used by tests and UI integration points. + +#include +#include +#include +#include "KeySymbolRenderer.h" + +struct MenuItem { + std::string id; + std::string label; + std::string shortcut; // text form, e.g. Ctrl+S + std::string symbols; // symbol/text depending platform, e.g. ⌃S + bool isSeparator = false; + bool enabled = true; + bool hasSubmenu = false; + std::vector submenuItems; + + static MenuItem separator() { + MenuItem m; + m.isSeparator = true; + return m; + } +}; + +struct Menu { + std::string title; + std::vector items; +}; + +class MenuBar { +public: + void addMenu(const Menu& menu) { menus_.push_back(menu); } + + const std::vector& getMenus() const { return menus_; } + + size_t menuCount() const { return menus_.size(); } + + const Menu* getMenu(const std::string& title) const { + for (const auto& m : menus_) { + if (m.title == title) return &m; + } + return nullptr; + } + + bool hasMenu(const std::string& title) const { + return getMenu(title) != nullptr; + } + + MenuItem findItem(const std::string& id) const { + for (const auto& menu : menus_) { + for (const auto& item : menu.items) { + if (item.id == id) return item; + if (item.hasSubmenu) { + for (const auto& sub : item.submenuItems) { + if (sub.id == id) return sub; + } + } + } + } + return {}; + } + + bool hasItem(const std::string& id) const { + return !findItem(id).id.empty(); + } + + int separatorCount(const std::string& menuTitle) const { + const Menu* menu = getMenu(menuTitle); + if (!menu) return 0; + int count = 0; + for (const auto& item : menu->items) { + if (item.isSeparator) count++; + } + return count; + } + + bool hasSubmenu(const std::string& itemId) const { + for (const auto& menu : menus_) { + for (const auto& item : menu.items) { + if (item.id == itemId) return item.hasSubmenu && !item.submenuItems.empty(); + } + } + return false; + } + + void openMenu(const std::string& title) { + openMenu_ = title; + } + + bool isMenuOpen(const std::string& title) const { + return openMenu_ == title; + } + + bool isAnyMenuOpen() const { return !openMenu_.empty(); } + + std::optional selectItem(const std::string& itemId) { + MenuItem item = findItem(itemId); + if (item.id.empty() || !item.enabled || item.isSeparator) return std::nullopt; + openMenu_.clear(); // selecting an action closes menus + return item.id; + } + + static MenuBar buildDefaults(const KeybindingRegistry& reg, + const WhetstoneTheme& theme = getDefaultDarkTheme()) { + MenuBar bar; + + auto makeAction = [&](const std::string& id, const std::string& fallback) -> MenuItem { + MenuItem item; + item.id = id; + item.label = fallback; + + for (const auto& action : reg.getActions()) { + if (action.id == id) { + item.label = action.label; + break; + } + } + + auto layout = KeySymbolRenderer::renderActionWithKey(item.label, id, reg, theme); + item.shortcut = layout.shortcut; + item.symbols = layout.symbols; + return item; + }; + + // File + { + Menu m; + m.title = "File"; + m.items.push_back(makeAction("new-file", "New")); + m.items.push_back(makeAction("open-file", "Open File")); + m.items.push_back(MenuItem::separator()); + m.items.push_back(makeAction("save-buffer", "Save")); + m.items.push_back(makeAction("save-all", "Save All")); + m.items.push_back(MenuItem::separator()); + m.items.push_back(makeAction("close-buffer", "Close")); + bar.addMenu(m); + } + + // Edit + { + Menu m; + m.title = "Edit"; + m.items.push_back(makeAction("undo", "Undo")); + m.items.push_back(makeAction("redo", "Redo")); + m.items.push_back(MenuItem::separator()); + m.items.push_back(makeAction("find-in-file", "Find")); + m.items.push_back(makeAction("find-in-project", "Find in Project")); + bar.addMenu(m); + } + + // View + { + Menu m; + m.title = "View"; + m.items.push_back(makeAction("toggle-file-tree", "Toggle File Tree")); + m.items.push_back(makeAction("toggle-ast-view", "Toggle AST")); + m.items.push_back(makeAction("toggle-diagnostics", "Toggle Diagnostics")); + m.items.push_back(makeAction("toggle-annotations", "Toggle Annotations")); + m.items.push_back(MenuItem::separator()); + m.items.push_back(makeAction("reset-layout", "Reset Layout")); + bar.addMenu(m); + } + + // Tools + { + Menu m; + m.title = "Tools"; + m.items.push_back(makeAction("run-pipeline", "Run Pipeline")); + m.items.push_back(makeAction("generate-code", "Generate Code")); + m.items.push_back(makeAction("infer-annotations", "Infer Annotations")); + m.items.push_back(makeAction("validate", "Validate")); + m.items.push_back(MenuItem::separator()); + m.items.push_back(makeAction("export-semanno", "Export Semanno")); + bar.addMenu(m); + } + + // Workflow + { + Menu m; + m.title = "Workflow"; + m.items.push_back(makeAction("create-workflow", "Create Workflow")); + m.items.push_back(makeAction("route-all", "Route All")); + m.items.push_back(makeAction("view-ready-tasks", "View Ready Tasks")); + m.items.push_back(makeAction("save-workflow", "Save Workflow")); + bar.addMenu(m); + } + + // Help with submenu + { + Menu m; + m.title = "Help"; + MenuItem about = makeAction("about", "About"); + MenuItem shortcuts = makeAction("keyboard-shortcuts", "Keyboard Shortcuts"); + + MenuItem docs; + docs.id = "docs"; + docs.label = "Documentation"; + docs.hasSubmenu = true; + docs.submenuItems.push_back(makeAction("docs-getting-started", "Getting Started")); + docs.submenuItems.push_back(makeAction("docs-keybindings", "Keybindings")); + + m.items.push_back(about); + m.items.push_back(shortcuts); + m.items.push_back(MenuItem::separator()); + m.items.push_back(docs); + bar.addMenu(m); + } + + return bar; + } + +private: + std::vector menus_; + std::string openMenu_; +}; diff --git a/editor/tests/step354_test.cpp b/editor/tests/step354_test.cpp new file mode 100644 index 0000000..b67fb8f --- /dev/null +++ b/editor/tests/step354_test.cpp @@ -0,0 +1,136 @@ +// Step 354: Menu Bar with Key Symbols (12 tests) + +#include +#include +#include +#include "MenuBar.h" +#include "KeybindingRegistry.h" + +int main() { + int passed = 0; + KeybindingRegistry reg; + reg.loadDefaults(); + MenuBar bar = MenuBar::buildDefaults(reg); + + // Test 1: all 5 core menus render (+ Help) + { + assert(bar.hasMenu("File")); + assert(bar.hasMenu("Edit")); + assert(bar.hasMenu("View")); + assert(bar.hasMenu("Tools")); + assert(bar.hasMenu("Workflow")); + assert(bar.hasMenu("Help")); + std::cout << "Test 1 PASSED: all menus render\n"; + passed++; + } + + // Test 2: File menu Save has Ctrl+S + { + MenuItem save = bar.findItem("save-buffer"); + assert(save.id == "save-buffer"); + assert(save.shortcut == "Ctrl+S"); + std::cout << "Test 2 PASSED: File Save shortcut\n"; + passed++; + } + + // Test 3: Edit menu Undo has Ctrl+Z + { + MenuItem undo = bar.findItem("undo"); + assert(undo.id == "undo"); + assert(undo.shortcut == "Ctrl+Z"); + std::cout << "Test 3 PASSED: Edit Undo shortcut\n"; + passed++; + } + + // Test 4: View menu panel toggles present + { + assert(bar.hasItem("toggle-file-tree")); + assert(bar.hasItem("toggle-ast-view")); + assert(bar.hasItem("toggle-diagnostics")); + assert(bar.hasItem("toggle-annotations")); + std::cout << "Test 4 PASSED: View panel toggles present\n"; + passed++; + } + + // Test 5: Tools menu run pipeline present + { + MenuItem run = bar.findItem("run-pipeline"); + assert(run.id == "run-pipeline"); + assert(run.label.find("Run") != std::string::npos); + std::cout << "Test 5 PASSED: Tools run pipeline present\n"; + passed++; + } + + // Test 6: menu item executes correct action id + { + bar.openMenu("File"); + auto selected = bar.selectItem("save-buffer"); + assert(selected.has_value()); + assert(selected.value() == "save-buffer"); + std::cout << "Test 6 PASSED: menu selection executes action\n"; + passed++; + } + + // Test 7: key symbols visible on bound actions + { + MenuItem save = bar.findItem("save-buffer"); + MenuItem run = bar.findItem("run-pipeline"); + assert(!save.symbols.empty()); + assert(!run.symbols.empty()); + std::cout << "Test 7 PASSED: key symbols visible\n"; + passed++; + } + + // Test 8: menu closes after selection + { + bar.openMenu("Edit"); + assert(bar.isMenuOpen("Edit")); + auto selected = bar.selectItem("undo"); + assert(selected.has_value()); + assert(!bar.isAnyMenuOpen()); + std::cout << "Test 8 PASSED: menu closes after selection\n"; + passed++; + } + + // Test 9: separators between groups + { + assert(bar.separatorCount("File") >= 2); + assert(bar.separatorCount("Edit") >= 1); + assert(bar.separatorCount("Tools") >= 1); + std::cout << "Test 9 PASSED: separators between groups\n"; + passed++; + } + + // Test 10: workflow menu items present + { + assert(bar.hasItem("create-workflow")); + assert(bar.hasItem("route-all")); + assert(bar.hasItem("view-ready-tasks")); + assert(bar.hasItem("save-workflow")); + std::cout << "Test 10 PASSED: workflow items present\n"; + passed++; + } + + // Test 11: submenu rendering metadata + { + assert(bar.hasSubmenu("docs")); + assert(bar.hasItem("docs-getting-started")); + assert(bar.hasItem("docs-keybindings")); + std::cout << "Test 11 PASSED: submenu metadata\n"; + passed++; + } + + // Test 12: selecting separator/unknown is ignored + { + auto bad = bar.selectItem("not-found"); + assert(!bad.has_value()); + auto sep = bar.selectItem(""); + assert(!sep.has_value()); + std::cout << "Test 12 PASSED: invalid selections ignored\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 8c9fd08..4c7b30d 100644 --- a/progress.md +++ b/progress.md @@ -1790,6 +1790,43 @@ ids, categories, and aliases. Added population helpers that pull actions from - `step346_test` — PASS (8/8) layout + palette integration regression - `step353_test` — PASS (12/12) new step coverage +### Step 354: Menu Bar with Key Symbols +**Status:** PASS (12/12 tests) + +Added a structured menu bar model with File/Edit/View/Tools/Workflow/Help menus, +key-symbol metadata on actions, separator grouping, action selection handling, +and submenu support for help/documentation. Menu action labels/shortcuts are +derived from `KeybindingRegistry` and rendered via `KeySymbolRenderer` so the +symbol display path is consistent with step 352. + +**Files created:** +- `editor/src/MenuBar.h` — headless menu model: + - `MenuItem` / `Menu` / `MenuBar` + - `buildDefaults(...)` for the full menu set + - selection API (`openMenu`, `selectItem`) with close-on-select behavior + - submenu metadata support and separator counting helpers +- `editor/tests/step354_test.cpp` — 12 tests covering: + 1. core menus present + 2. File Save shortcut + 3. Edit Undo shortcut + 4. View panel toggle actions + 5. Tools pipeline action + 6. correct action execution id + 7. key symbol visibility + 8. menu close after selection + 9. separator grouping + 10. workflow action presence + 11. submenu metadata + 12. invalid selection handling + +**Files modified:** +- `editor/CMakeLists.txt` — `step354_test` target + +**Verification run:** +- `step352_test` — PASS (12/12) regression coverage +- `step353_test` — PASS (12/12) regression coverage +- `step354_test` — PASS (12/12) new step coverage + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)