Step 197: keyboard navigation audit
This commit is contained in:
@@ -544,4 +544,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Step 194: Semantic-filtered library browser (tag filter bar, active tag chips, tag badges, context-aware completion boosting, agent tag context). 1/1 tests pass (step194_test). |
|
||||
| 2026-02-10 | Codex | Step 195: Security & semantic UX tests (vulnerability badges/advisories, security diagnostics severity mapping, semantic tag auto-assign, library tag filtering, vulnerable import deprioritization, safe upgrade writeback). 1/1 tests pass (step195_test). |
|
||||
| 2026-02-10 | Codex | Step 196: High contrast + colorblind modes (high contrast theme, annotation shapes toggle, diagnostic pattern underlines). 1/1 tests pass (step196_test). |
|
||||
| 2026-02-10 | Codex | Step 197: Keyboard navigation audit (F6 panel cycle, Esc focus return, focus ring defaults, nav focus enabled on panels). 1/1 tests pass (step197_test). |
|
||||
| 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. |
|
||||
|
||||
@@ -1128,6 +1128,10 @@ target_include_directories(step195_test PRIVATE src)
|
||||
add_executable(step196_test tests/step196_test.cpp)
|
||||
target_include_directories(step196_test PRIVATE src)
|
||||
|
||||
add_executable(step197_test tests/step197_test.cpp)
|
||||
target_include_directories(step197_test PRIVATE src)
|
||||
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -932,6 +932,38 @@ struct EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
bool hasSidePanels() const {
|
||||
if (ui.showOutline) return true;
|
||||
if (library.showDependencyPanel) return true;
|
||||
if (library.showLibraryBrowserPanel) return true;
|
||||
if (library.showCompositionPanel) return true;
|
||||
if (emacsState.showEmacsPackagesPanel) return true;
|
||||
if (emacsState.showEmacsBridgePanel) return true;
|
||||
return true; // Memory Strategies panel is always present.
|
||||
}
|
||||
|
||||
void cyclePanelFocus() {
|
||||
FocusRegion order[] = {
|
||||
FocusRegion::Editor,
|
||||
FocusRegion::Explorer,
|
||||
FocusRegion::Side,
|
||||
FocusRegion::Bottom
|
||||
};
|
||||
int current = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (ui.focusedRegion == order[i]) {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int step = 1; step <= 4; ++step) {
|
||||
FocusRegion next = order[(current + step) % 4];
|
||||
if (next == FocusRegion::Side && !hasSidePanels()) continue;
|
||||
ui.focusTarget = next;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void applySettingsToState() {
|
||||
ui.showMinimap = settings.getShowMinimap();
|
||||
ui.showLineNumbers = settings.getShowLineNumbers();
|
||||
|
||||
@@ -274,6 +274,8 @@ private:
|
||||
if (name == "Separator") return ImGuiCol_Separator;
|
||||
if (name == "SeparatorHovered") return ImGuiCol_SeparatorHovered;
|
||||
if (name == "SeparatorActive") return ImGuiCol_SeparatorActive;
|
||||
if (name == "NavHighlight") return ImGuiCol_NavHighlight;
|
||||
if (name == "NavWindowingHighlight") return ImGuiCol_NavWindowingHighlight;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -345,6 +347,12 @@ private:
|
||||
style.Colors[colId] = color;
|
||||
}
|
||||
}
|
||||
if (theme.imguiColors.find(ImGuiCol_NavHighlight) == theme.imguiColors.end()) {
|
||||
style.Colors[ImGuiCol_NavHighlight] = ImVec4(1.0f, 0.85f, 0.2f, 1.0f);
|
||||
}
|
||||
if (theme.imguiColors.find(ImGuiCol_NavWindowingHighlight) == theme.imguiColors.end()) {
|
||||
style.Colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.0f, 0.85f, 0.2f, 1.0f);
|
||||
}
|
||||
style.WindowPadding = theme.panelPadding;
|
||||
style.ItemSpacing = theme.panelSpacing;
|
||||
style.WindowBorderSize = theme.windowBorderSize;
|
||||
|
||||
@@ -258,6 +258,14 @@ int main(int, char**) {
|
||||
if (sdlMod & KMOD_SHIFT) mods |= WMOD_SHIFT;
|
||||
if (sdlMod & KMOD_ALT) mods |= WMOD_ALT;
|
||||
|
||||
if (sym == SDLK_ESCAPE && mods == WMOD_NONE) {
|
||||
state.ui.focusTarget = FocusRegion::Editor;
|
||||
}
|
||||
|
||||
if (sym == SDLK_F6 && mods == WMOD_NONE) {
|
||||
state.cyclePanelFocus();
|
||||
}
|
||||
|
||||
if ((sdlMod & KMOD_CTRL) && (sdlMod & KMOD_SHIFT) && sym == SDLK_p) {
|
||||
state.showCommandPalette = true;
|
||||
state.commandQuery[0] = '\0';
|
||||
|
||||
@@ -52,10 +52,17 @@ static std::vector<SimpleDiagnostic> collectDiagnostics(const EditorState& state
|
||||
}
|
||||
|
||||
static void renderBottomPanel(EditorState& state) {
|
||||
if (state.ui.focusTarget == FocusRegion::Bottom) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
// ---------------------------------------------------------------
|
||||
// Bottom panel — Output / AST / Highlighted Preview / Terminal
|
||||
// ---------------------------------------------------------------
|
||||
ImGui::Begin("Panel");
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Bottom;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("PanelTabs")) {
|
||||
// Output log
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <map>
|
||||
|
||||
static void renderEditorPanel(EditorState& state) {
|
||||
if (state.ui.focusTarget == FocusRegion::Editor) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
if (state.ui.showAnnotations) {
|
||||
queueFeatureHint(state.featureHints,
|
||||
"hint.annotations",
|
||||
@@ -18,6 +22,9 @@ static void renderEditorPanel(EditorState& state) {
|
||||
// Editor (center) -- editable text area
|
||||
// ---------------------------------------------------------------
|
||||
ImGui::Begin("Editor");
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Editor;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
ImGui::BeginChild("##breadcrumbs", ImVec2(0, 26.0f), false, ImGuiWindowFlags_NoScrollbar);
|
||||
if (!state.active()) {
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
#include "../EditorUtils.h"
|
||||
|
||||
static void renderExplorerPanel(EditorState& state) {
|
||||
if (state.ui.focusTarget == FocusRegion::Explorer) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Explorer");
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Explorer;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "OPEN EDITORS");
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
|
||||
static void renderOutlinePanel(EditorState& state) {
|
||||
if (!state.ui.showOutline) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Outline", &state.ui.showOutline);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
queueFeatureHint(state.featureHints,
|
||||
"hint.outline",
|
||||
"Tip: Use the Outline to jump between symbols quickly.");
|
||||
@@ -81,7 +88,14 @@ static void renderOutlinePanel(EditorState& state) {
|
||||
|
||||
static void renderDependenciesPanel(EditorState& state) {
|
||||
if (!state.library.showDependencyPanel) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Dependencies", &state.library.showDependencyPanel);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
renderDependencyPanel(state.library.dependencyPanel,
|
||||
state.workspaceRoot,
|
||||
@@ -99,7 +113,14 @@ static void renderDependenciesPanel(EditorState& state) {
|
||||
|
||||
static void renderLibrariesPanel(EditorState& state) {
|
||||
if (!state.library.showLibraryBrowserPanel) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Libraries", &state.library.showLibraryBrowserPanel);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
std::string insertText;
|
||||
std::string insertLibrary;
|
||||
@@ -118,7 +139,14 @@ static void renderLibrariesPanel(EditorState& state) {
|
||||
|
||||
static void renderCompositionPanel(EditorState& state) {
|
||||
if (!state.library.showCompositionPanel) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Compose", &state.library.showCompositionPanel);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
std::string nodeId;
|
||||
if (state.activeAST()) {
|
||||
@@ -140,7 +168,14 @@ static void renderCompositionPanel(EditorState& state) {
|
||||
|
||||
static void renderEmacsPackagesPanel(EditorState& state) {
|
||||
if (!state.emacsState.showEmacsPackagesPanel) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Emacs Packages", &state.emacsState.showEmacsPackagesPanel);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
if (renderEmacsPackageBrowser(state.emacsState.emacsPackages,
|
||||
state.emacsState.emacs,
|
||||
@@ -153,7 +188,14 @@ static void renderEmacsPackagesPanel(EditorState& state) {
|
||||
|
||||
static void renderEmacsBridgePanel(EditorState& state) {
|
||||
if (!state.emacsState.showEmacsBridgePanel) return;
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Emacs Bridge", &state.emacsState.showEmacsBridgePanel);
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
if (state.active()) {
|
||||
ImGui::Text("Active file: %s", state.active()->path.c_str());
|
||||
@@ -182,7 +224,7 @@ static void renderMinibuffer(EditorState& state) {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGuiWindowFlags mbFlags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNavFocus;
|
||||
ImGuiWindowFlags_NoScrollbar;
|
||||
float mbHeight = ImGui::GetFrameHeight() + 10;
|
||||
ImVec2 mbPos(viewport->WorkPos.x,
|
||||
viewport->WorkPos.y + viewport->WorkSize.y - mbHeight - 24.0f);
|
||||
@@ -207,7 +249,14 @@ static void renderMinibuffer(EditorState& state) {
|
||||
}
|
||||
|
||||
static void renderMemoryStrategiesPanel(EditorState& state) {
|
||||
if (state.ui.focusTarget == FocusRegion::Side) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
state.ui.focusTarget = FocusRegion::None;
|
||||
}
|
||||
ImGui::Begin("Memory Strategies");
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
state.ui.focusedRegion = FocusRegion::Side;
|
||||
}
|
||||
ImGui::PushFont(state.uiFont);
|
||||
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
|
||||
if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#pragma once
|
||||
#include "LayoutManager.h"
|
||||
|
||||
enum class FocusRegion {
|
||||
None,
|
||||
Editor,
|
||||
Explorer,
|
||||
Side,
|
||||
Bottom
|
||||
};
|
||||
|
||||
struct UIFlags {
|
||||
bool showWhitespace = false;
|
||||
bool showMinimap = false;
|
||||
@@ -17,4 +25,6 @@ struct UIFlags {
|
||||
bool showAgentWizard = false;
|
||||
int bottomTab = 0; // 0=Output,1=AST,2=Highlighted
|
||||
LayoutPreset layoutPreset = LayoutPreset::VSCode;
|
||||
FocusRegion focusTarget = FocusRegion::None;
|
||||
FocusRegion focusedRegion = FocusRegion::Editor;
|
||||
};
|
||||
|
||||
48
editor/tests/step197_test.cpp
Normal file
48
editor/tests/step197_test.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Step 197: Keyboard navigation completeness audit.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static void assertContains(const std::string& text, const std::string& needle) {
|
||||
assert(text.find(needle) != std::string::npos);
|
||||
}
|
||||
|
||||
static void assertNotContains(const std::string& text, const std::string& needle) {
|
||||
assert(text.find(needle) == std::string::npos);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const std::string uiFlags = readFile("src/state/UIFlags.h");
|
||||
const std::string editorState = readFile("src/EditorState.h");
|
||||
const std::string mainSrc = readFile("src/main.cpp");
|
||||
const std::string theme = readFile("src/ThemeEngine.h");
|
||||
const std::string explorer = readFile("src/panels/ExplorerPanel.h");
|
||||
const std::string editor = readFile("src/panels/EditorPanel.h");
|
||||
const std::string bottom = readFile("src/panels/BottomPanel.h");
|
||||
const std::string side = readFile("src/panels/SidePanels.h");
|
||||
|
||||
assertContains(uiFlags, "FocusRegion");
|
||||
assertContains(uiFlags, "focusTarget");
|
||||
assertContains(editorState, "cyclePanelFocus");
|
||||
assertContains(mainSrc, "SDLK_F6");
|
||||
assertContains(mainSrc, "SDLK_ESCAPE");
|
||||
assertContains(theme, "NavHighlight");
|
||||
assertContains(theme, "NavWindowingHighlight");
|
||||
|
||||
assertContains(explorer, "SetNextWindowFocus");
|
||||
assertContains(editor, "SetNextWindowFocus");
|
||||
assertContains(bottom, "SetNextWindowFocus");
|
||||
assertContains(side, "SetNextWindowFocus");
|
||||
assertNotContains(side, "NoNavFocus");
|
||||
|
||||
printf("step197_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -393,7 +393,7 @@ Make Whetstone usable by everyone and fast with large files.
|
||||
- Test: all information conveyed by color also conveyed by shape/pattern
|
||||
*New:* High contrast theme. *Modifies:* annotation/diagnostic rendering
|
||||
|
||||
- [ ] **Step 197: Keyboard navigation completeness audit**
|
||||
- [x] **Step 197: Keyboard navigation completeness audit**
|
||||
Ensure every feature is keyboard-accessible:
|
||||
- All panels navigable with Tab/Shift+Tab
|
||||
- Focus ring indicator on active element (visible in all themes)
|
||||
|
||||
Reference in New Issue
Block a user