Step 354: Menu Bar with Key Symbols (12/12 tests)

This commit is contained in:
Bill
2026-02-16 09:07:20 -07:00
parent e79e42bab1
commit b089fa0cd0
4 changed files with 394 additions and 0 deletions

View File

@@ -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)

217
editor/src/MenuBar.h Normal file
View File

@@ -0,0 +1,217 @@
#pragma once
// Step 354: Menu Bar with key symbols.
// Headless data model used by tests and UI integration points.
#include <optional>
#include <string>
#include <vector>
#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<MenuItem> submenuItems;
static MenuItem separator() {
MenuItem m;
m.isSeparator = true;
return m;
}
};
struct Menu {
std::string title;
std::vector<MenuItem> items;
};
class MenuBar {
public:
void addMenu(const Menu& menu) { menus_.push_back(menu); }
const std::vector<Menu>& 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<std::string> 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<Menu> menus_;
std::string openMenu_;
};

View File

@@ -0,0 +1,136 @@
// Step 354: Menu Bar with Key Symbols (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#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;
}

View File

@@ -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)