2026-02-16 09:07:20 -07:00
|
|
|
#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;
|
2026-02-16 09:23:29 -07:00
|
|
|
appendFileMenu(bar, reg, theme);
|
|
|
|
|
appendEditMenu(bar, reg, theme);
|
|
|
|
|
appendViewMenu(bar, reg, theme);
|
|
|
|
|
appendToolsMenu(bar, reg, theme);
|
|
|
|
|
appendWorkflowMenu(bar, reg, theme);
|
|
|
|
|
appendHelpMenu(bar, reg, theme);
|
|
|
|
|
return bar;
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
private:
|
|
|
|
|
static MenuItem makeActionItem(const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme,
|
|
|
|
|
const std::string& id,
|
|
|
|
|
const std::string& fallback) {
|
|
|
|
|
MenuItem item;
|
|
|
|
|
item.id = id;
|
|
|
|
|
item.label = fallback;
|
|
|
|
|
|
|
|
|
|
for (const auto& action : reg.getActions()) {
|
|
|
|
|
if (action.id == id) {
|
|
|
|
|
item.label = action.label;
|
|
|
|
|
break;
|
2026-02-16 09:07:20 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
auto layout = KeySymbolRenderer::renderActionWithKey(item.label, id, reg, theme);
|
|
|
|
|
item.shortcut = layout.shortcut;
|
|
|
|
|
item.symbols = layout.symbols;
|
|
|
|
|
return item;
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
static void appendFileMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "File";
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "new-file", "New"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "open-file", "Open File"));
|
|
|
|
|
m.items.push_back(MenuItem::separator());
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "save-buffer", "Save"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "save-all", "Save All"));
|
|
|
|
|
m.items.push_back(MenuItem::separator());
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "close-buffer", "Close"));
|
|
|
|
|
bar.addMenu(m);
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
static void appendEditMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "Edit";
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "undo", "Undo"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "redo", "Redo"));
|
|
|
|
|
m.items.push_back(MenuItem::separator());
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "find-in-file", "Find"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "find-in-project", "Find in Project"));
|
|
|
|
|
bar.addMenu(m);
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
static void appendViewMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "View";
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "toggle-file-tree", "Toggle File Tree"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "toggle-ast-view", "Toggle AST"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "toggle-diagnostics", "Toggle Diagnostics"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "toggle-annotations", "Toggle Annotations"));
|
|
|
|
|
m.items.push_back(MenuItem::separator());
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "reset-layout", "Reset Layout"));
|
|
|
|
|
bar.addMenu(m);
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
static void appendToolsMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "Tools";
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "run-pipeline", "Run Pipeline"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "generate-code", "Generate Code"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "infer-annotations", "Infer Annotations"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "validate", "Validate"));
|
|
|
|
|
m.items.push_back(MenuItem::separator());
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "export-semanno", "Export Semanno"));
|
|
|
|
|
bar.addMenu(m);
|
|
|
|
|
}
|
2026-02-16 09:07:20 -07:00
|
|
|
|
2026-02-16 09:23:29 -07:00
|
|
|
static void appendWorkflowMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "Workflow";
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "create-workflow", "Create Workflow"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "route-all", "Route All"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "view-ready-tasks", "View Ready Tasks"));
|
|
|
|
|
m.items.push_back(makeActionItem(reg, theme, "save-workflow", "Save Workflow"));
|
|
|
|
|
bar.addMenu(m);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void appendHelpMenu(MenuBar& bar,
|
|
|
|
|
const KeybindingRegistry& reg,
|
|
|
|
|
const WhetstoneTheme& theme) {
|
|
|
|
|
Menu m;
|
|
|
|
|
m.title = "Help";
|
|
|
|
|
MenuItem about = makeActionItem(reg, theme, "about", "About");
|
|
|
|
|
MenuItem shortcuts = makeActionItem(reg, theme, "keyboard-shortcuts", "Keyboard Shortcuts");
|
|
|
|
|
|
|
|
|
|
MenuItem docs;
|
|
|
|
|
docs.id = "docs";
|
|
|
|
|
docs.label = "Documentation";
|
|
|
|
|
docs.hasSubmenu = true;
|
|
|
|
|
docs.submenuItems.push_back(makeActionItem(reg, theme, "docs-getting-started", "Getting Started"));
|
|
|
|
|
docs.submenuItems.push_back(makeActionItem(reg, theme, "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);
|
2026-02-16 09:07:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<Menu> menus_;
|
|
|
|
|
std::string openMenu_;
|
|
|
|
|
};
|