diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index aa1893f..bb656c0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2114,4 +2114,8 @@ 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) +add_executable(step355_test tests/step355_test.cpp) +target_include_directories(step355_test PRIVATE src) +target_link_libraries(step355_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/CommandPalette.h b/editor/src/CommandPalette.h index 146565a..4942308 100644 --- a/editor/src/CommandPalette.h +++ b/editor/src/CommandPalette.h @@ -12,6 +12,7 @@ struct CommandEntry { std::string id; std::string label; std::string shortcut; + std::string symbols; std::string icon; std::string category; std::string inputHint; @@ -64,7 +65,8 @@ public: const std::string& inputHint = "", bool inlineInput = false, const std::vector& aliases = {}, - const std::string& icon = "") { + const std::string& icon = "", + const std::string& symbols = "") { auto& entry = commands_[id]; entry.id = id; entry.label = label; @@ -75,6 +77,7 @@ public: entry.inlineInput = inlineInput; entry.aliases = aliases; entry.icon = icon; + entry.symbols = symbols; } void registerFromKeybindings(const KeybindingRegistry& registry) { @@ -89,7 +92,8 @@ public: "", false, {action.id}, - WhetstoneIcons::Search + WhetstoneIcons::Search, + binding.toSymbolsAuto() ); } } diff --git a/editor/src/KeybindingRegistry.h b/editor/src/KeybindingRegistry.h index 74d35bf..d6fba32 100644 --- a/editor/src/KeybindingRegistry.h +++ b/editor/src/KeybindingRegistry.h @@ -205,6 +205,7 @@ public: // View actions registerAction("command-palette", "Command Palette", "View"); + registerAction("keyboard-shortcuts", "Keyboard Shortcuts", "Help"); registerAction("toggle-file-tree", "Toggle File Tree", "View"); registerAction("toggle-ast-view", "Toggle AST View", "View"); registerAction("toggle-diagnostics", "Toggle Diagnostics", "View"); @@ -234,6 +235,7 @@ public: bind("find-in-file", KeyCombo::ctrl("F")); bind("find-in-project", KeyCombo::ctrlShift("F")); bind("command-palette", KeyCombo::ctrl("P")); + bind("keyboard-shortcuts", KeyCombo::ctrlShift("?")); bind("toggle-file-tree", KeyCombo::ctrl("B")); bind("toggle-diagnostics", KeyCombo::ctrlShift("D")); bind("run-pipeline", KeyCombo::fkey("F5")); diff --git a/editor/src/KeyboardShortcutsPanel.h b/editor/src/KeyboardShortcutsPanel.h new file mode 100644 index 0000000..e7f7bac --- /dev/null +++ b/editor/src/KeyboardShortcutsPanel.h @@ -0,0 +1,85 @@ +#pragma once +// Step 355: Keyboard shortcuts panel model. +// Headless grouping/filtering/customize behavior for shortcut display. + +#include +#include +#include +#include +#include "KeybindingRegistry.h" + +struct ShortcutRow { + std::string actionId; + std::string label; + std::string category; + std::string symbols; + std::string description; +}; + +class KeyboardShortcutsPanel { +public: + void open() { visible_ = true; } + void close() { visible_ = false; } + bool isVisible() const { return visible_; } + + void setFilter(const std::string& text) { filter_ = text; } + const std::string& filter() const { return filter_; } + + void loadFromRegistry(const KeybindingRegistry& registry, bool macOS = false) { + rows_.clear(); + for (const auto& action : registry.getActions()) { + ShortcutRow row; + row.actionId = action.id; + row.label = action.label; + row.category = action.category; + row.description = action.description; + row.symbols = registry.getSymbols(action.id, macOS); + rows_.push_back(row); + } + std::sort(rows_.begin(), rows_.end(), [](const ShortcutRow& a, const ShortcutRow& b) { + if (a.category != b.category) return a.category < b.category; + return a.label < b.label; + }); + } + + const std::vector& rows() const { return rows_; } + + std::vector filteredRows() const { + if (filter_.empty()) return rows_; + std::string needle = toLower(filter_); + std::vector out; + for (const auto& row : rows_) { + std::string hay = toLower(row.label + " " + row.category + " " + row.actionId); + if (hay.find(needle) != std::string::npos) out.push_back(row); + } + return out; + } + + std::map> groupedRows() const { + std::map> groups; + for (const auto& row : filteredRows()) { + groups[row.category].push_back(row); + } + return groups; + } + + void requestCustomize() { customizeRequested_ = true; } + bool consumeCustomizeRequest() { + bool value = customizeRequested_; + customizeRequested_ = false; + return value; + } + +private: + static std::string toLower(const std::string& s) { + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return out; + } + + bool visible_ = false; + bool customizeRequested_ = false; + std::string filter_; + std::vector rows_; +}; diff --git a/editor/tests/step355_test.cpp b/editor/tests/step355_test.cpp new file mode 100644 index 0000000..beb6c8c --- /dev/null +++ b/editor/tests/step355_test.cpp @@ -0,0 +1,161 @@ +// Step 355: Phase 13c Integration — Keyboard Shortcuts Help Panel (8 tests) + +#include +#include +#include +#include "KeybindingRegistry.h" +#include "CommandPalette.h" +#include "MenuBar.h" +#include "KeyboardShortcutsPanel.h" + +int main() { + int passed = 0; + + // Test 1: all default bindings displayed in shortcuts panel + { + KeybindingRegistry reg; + reg.loadDefaults(); + KeyboardShortcutsPanel panel; + panel.loadFromRegistry(reg); + assert(!panel.rows().empty()); + assert(panel.rows().size() >= static_cast(reg.bindingCount())); + std::cout << "Test 1 PASSED: default bindings displayed\n"; + passed++; + } + + // Test 2: Ctrl+S triggers save action + { + KeybindingRegistry reg; + reg.loadDefaults(); + KeyCombo::InputState in; + in.ctrl = true; + in.key = "s"; + auto action = reg.processInput(in); + assert(action.has_value()); + assert(action.value() == "save-buffer"); + std::cout << "Test 2 PASSED: Ctrl+S triggers save\n"; + passed++; + } + + // Test 3: Ctrl+P opens command palette + { + KeybindingRegistry reg; + reg.loadDefaults(); + CommandPalette palette; + assert(!palette.isOpen()); + KeyCombo::InputState in; + in.ctrl = true; + in.key = "P"; + auto action = reg.processInput(in); + if (action.has_value() && action.value() == "command-palette") { + palette.open(); + } + assert(palette.isOpen()); + std::cout << "Test 3 PASSED: Ctrl+P opens palette\n"; + passed++; + } + + // Test 4: command palette shows key symbols for registered actions + { + KeybindingRegistry reg; + reg.loadDefaults(); + CommandPalette palette; + palette.registerFromKeybindings(reg); + auto all = palette.all(); + assert(!all.empty()); + int symbolCount = 0; + for (const auto& c : all) { + if (!c.shortcut.empty()) { + assert(!c.symbols.empty()); + symbolCount++; + } + } + assert(symbolCount >= 10); + std::cout << "Test 4 PASSED: palette shows key symbols\n"; + passed++; + } + + // Test 5: menu items show correct key symbols + { + KeybindingRegistry reg; + reg.loadDefaults(); + auto menu = MenuBar::buildDefaults(reg); + auto save = menu.findItem("save-buffer"); + auto undo = menu.findItem("undo"); + assert(!save.symbols.empty()); + assert(!undo.symbols.empty()); + std::cout << "Test 5 PASSED: menu symbols correct\n"; + passed++; + } + + // Test 6: rebind action updates shortcuts panel + { + KeybindingRegistry reg; + reg.loadDefaults(); + KeyboardShortcutsPanel panel; + panel.loadFromRegistry(reg); + std::string before; + for (const auto& row : panel.rows()) { + if (row.actionId == "save-buffer") before = row.symbols; + } + reg.bind("save-buffer", KeyCombo::ctrl("W")); + panel.loadFromRegistry(reg); + std::string after; + for (const auto& row : panel.rows()) { + if (row.actionId == "save-buffer") after = row.symbols; + } + assert(before != after); + assert(after.find("W") != std::string::npos); + std::cout << "Test 6 PASSED: rebind updates panel\n"; + passed++; + } + + // Test 7: key symbols render on current platform mode + { + KeybindingRegistry reg; + reg.loadDefaults(); + KeyboardShortcutsPanel panel; + panel.loadFromRegistry(reg); + bool hasAnySymbol = false; + for (const auto& row : panel.rows()) { + if (!row.symbols.empty()) { + hasAnySymbol = true; + break; + } + } + assert(hasAnySymbol); + std::cout << "Test 7 PASSED: platform symbols render\n"; + passed++; + } + + // Test 8: full keyboard workflow Ctrl+P -> "pipeline" -> Enter -> run-pipeline + { + KeybindingRegistry reg; + reg.loadDefaults(); + CommandPalette palette; + palette.registerFromKeybindings(reg); + + bool pipelineRan = false; + KeyCombo::InputState openIn; + openIn.ctrl = true; + openIn.key = "p"; + auto openAction = reg.processInput(openIn); + assert(openAction.has_value()); + assert(openAction.value() == "command-palette"); + palette.open(); + assert(palette.isOpen()); + + auto matches = palette.search("pipeline"); + assert(!matches.empty()); + auto selected = palette.executeSelected(matches); + assert(selected.has_value()); + if (selected.value() == "run-pipeline") pipelineRan = true; + assert(pipelineRan); + std::cout << "Test 8 PASSED: keyboard workflow executes pipeline\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/8\n"; + assert(passed == 8); + return 0; +} diff --git a/progress.md b/progress.md index 4c7b30d..95277c0 100644 --- a/progress.md +++ b/progress.md @@ -1827,6 +1827,42 @@ symbol display path is consistent with step 352. - `step353_test` — PASS (12/12) regression coverage - `step354_test` — PASS (12/12) new step coverage +### Step 355: Phase 13c Integration — Keyboard Shortcuts Help Panel +**Status:** PASS (8/8 tests) + +Integrated a dedicated keyboard-shortcuts panel model and connected the end-to-end +keyboard flow across the keybinding registry, command palette, and menu bar. +This provides grouped/filterable shortcut rows plus refresh-on-rebind behavior and +a full keyboard-driven command execution path. + +**Files created:** +- `editor/src/KeyboardShortcutsPanel.h` — panel data model: + - visibility state, filter text, grouped rows by category + - row projection from `KeybindingRegistry` (label/category/symbols) + - customize-request signal (`requestCustomize` / `consumeCustomizeRequest`) +- `editor/tests/step355_test.cpp` — 8 integration tests: + 1. defaults appear in shortcuts panel + 2. Ctrl+S resolves to save action + 3. Ctrl+P opens command palette + 4. command palette entries include key symbols + 5. menu entries include key symbols + 6. rebind propagates to shortcuts panel + 7. symbol rendering works for current platform mode + 8. keyboard workflow: Ctrl+P → query pipeline → execute run-pipeline + +**Files modified:** +- `editor/src/KeybindingRegistry.h` — added `keyboard-shortcuts` action and default + binding (`Ctrl+Shift+?`) to support help-panel keyboard access +- `editor/src/CommandPalette.h` — added `symbols` field on command entries and + populated symbol strings from keybindings +- `editor/CMakeLists.txt` — `step355_test` target + +**Verification run:** +- `step351_test` — PASS (12/12) regression coverage +- `step353_test` — PASS (12/12) regression coverage +- `step354_test` — PASS (12/12) regression coverage +- `step355_test` — PASS (8/8) new integration coverage + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)