GUI shortcut architecture

Goal

Perastage uses a centralized GUI shortcut registry to keep keyboard behavior explicit, validated and consistent across modules.

Central registry

The canonical shortcut map lives in:

  • gui/shortcut_registry.h
  • gui/shortcut_registry.cpp

Each shortcut entry defines:

  • Key combination (keyCode for non-modifier shortcuts).
  • Action (ShortcutAction).
  • Owner module (ownerModule).
  • Scope (ShortcutScope: Global, Viewer2D, Viewer3D, Cli).
  • Focus policy (ShortcutFocusPolicy: allowed or blocked in editable widgets).
  • Priority (used when multiple entries share the same key across scopes).

Current shortcut map

Menu accelerators are owned by their menu entries and are not routed through ShortcutAction. The Edit menu includes Ctrl+G for Group and Ctrl+U for Ungroup; both commands are global menu accelerators, operate on the active cross-table selection, and keep the existing editable-widget behavior provided by wxWidgets menu accelerator handling.

Key Action Owner Scope Focus policy
Z FitView viewer2d Viewer2D Block in editable widgets
Z FitView viewer3d Viewer3D Block in editable widgets
P CliPrefillPos cli Cli Block in editable widgets
R CliPrefillRot cli Cli Block in editable widgets
F CliPrefillFixture cli Cli Block in editable widgets
1 SelectFixturesTab gui Global Block in editable widgets
2 SelectTrussesTab gui Global Block in editable widgets
3 SelectSupportsTab gui Global Block in editable widgets
4 SelectObjectsTab gui Global Block in editable widgets
NumPad1 ViewportFront viewer2d Viewer2D Block in editable widgets
NumPad3 ViewportSide viewer2d Viewer2D Block in editable widgets
NumPad7 ViewportTop viewer2d Viewer2D Block in editable widgets
NumPad1 ViewportFront viewer3d Viewer3D Block in editable widgets
NumPad3 ViewportSide viewer3d Viewer3D Block in editable widgets
NumPad7 ViewportTop viewer3d Viewer3D Block in editable widgets
NumPad5 ViewportReset3D viewer3d Viewer3D Block in editable widgets

Priority and resolution rules

Resolution is performed by ResolveShortcut(...).

  1. Normalize key code to uppercase.
  2. Filter shortcuts with matching key.
  3. Apply focus gating through CanExecuteShortcut(...):
    • reject when modifiers are active,
    • reject when policy blocks editable-focus and focus is in editable text,
    • require scope/focus match (for example, Viewer2D requires focus in 2D viewer).
  4. Select the highest-priority shortcut among eligible entries.
  5. Return one execution decision for MainWindow::OnGlobalCharHook(...).

This makes shortcut behavior deterministic and keeps one single decision point for "can this shortcut run under current focus?".

Startup/test validation

ValidateShortcutRegistry() detects collisions by (scope, normalized key).

  • Called at MainWindow startup (debug assertion + error log).
  • Covered by tests/shortcut_registry_test.cpp.

Execution flow in GUI

  1. MainWindow::OnGlobalCharHook(...) builds a ShortcutExecutionContext from focus state.
    • Editable focus is detected via gui::IsEditableWidgetFocused(...) in gui/editable_focus_utils.{h,cpp}.
    • The helper covers wxTextEntry-based controls (for example wxTextCtrl and custom text-entry widgets), combo boxes without the wxCB_READONLY style, spin controls, and active wxGrid cell editors. Read-only combo focus does not suppress shortcuts, even on ports where the combo's generic text-entry abstraction reports editable.
  2. The hook asks ResolveShortcut(...) for one decision.
  3. MainWindow::ApplyShortcutDecision(...) executes the routed action:
    • ApplyFitShortcut() for focused viewer fit (selection-aware: when there is an active selection, fit targets selected fixtures/trusses/scene objects; otherwise it falls back to fitting the full scene),
    • FocusConsoleForQuickCommand(...) for CLI-prefill actions,
    • notebook-tab selection commands for 1..4,
    • ApplyViewportShortcut(...) for top/front/side numpad view actions in the active viewer,
    • 3D viewport reset for NumPad5.
  4. Viewport orientation parity rule:
    • ViewportFront in Viewer2D and Viewer3D must represent the same front-facing convention (camera from negative Y looking toward the origin) so shortcut behavior is consistent across both viewers.

Viewer-specific direct handling for these migrated shortcuts is intentionally avoided, so routing stays centralized in GUI.

Continuous placement cancellation

Escape remains a viewer-local interaction key rather than a registered global shortcut. While continuous placement from Add Fixture, Add Truss, or Add Object owns the focused 2D or 3D viewer, it takes precedence over measure-tool cancellation and discards only the element currently attached to the pointer. Elements already confirmed with left-click remain in the scene. Right-click provides the same cancellation behavior without changing shortcut routing.

A left-button press is confirmed as placement only when it is released without navigation movement. Holding and dragging keeps the normal viewer-local navigation behavior: pan in the 2D viewer, and orbit or Shift-pan in the 3D viewer. Releasing the button after navigation immediately realigns the pending element beneath the pointer without confirming it. This mouse gesture does not add a shortcut-registry entry.

Middle Mouse drag also pans either viewport during continuous placement. Its release realigns the provisional element beneath the pointer and never confirms or changes the current selection.

The global Ctrl+Z/Undo command is session-aware while continuous placement is active. It restores the previous placement snapshot, removes the latest confirmed element, and keeps that element as the provisional pointer-following copy. When Undo reaches the first confirmed element, or when no element has yet been confirmed, it restores the pre-dialog scene and ends placement. Redo is ignored until the placement session ends so it cannot replace the scene behind an active provisional element.

Scene clipboard accelerators

Ctrl+X, Ctrl+C, and Ctrl+V are standard Edit-menu accelerators for scene Cut, Copy, and Paste. They use the central mixed scene selection rather than the active table's local row state. These modifier shortcuts deliberately do not belong to the no-modifier shortcut registry.

Editable text controls and active cell editors retain precedence. The scene handlers skip their menu command in that context so native editing clipboard behavior remains available. A non-editing table may invoke the scene commands.

Consistent editable-focus guard across local key handlers

Panels that handle local key events also reuse gui::IsEditableWidgetFocused(...) before executing panel-level actions, avoiding divergent focus checks:

  • Viewer2DPanel::OnKeyDown(...)
  • Viewer3DPanel::OnKeyDown(...)
  • LayoutViewerPanel::OnKeyDown(...)

The modal 2D View Editor additionally routes Z fit, ←, →, ↑, ↓, and their Alt zoom variants through the embedded Viewer2DPanel even when focus is in the editor side panels. Editable text controls keep priority and receive their normal key input. This makes keyboard fit, pan, and zoom behavior match the standalone 2D viewport while preserving text-editing safety.

This keeps key behavior consistent while editing text or cell editors, and prevents global-style actions from stealing keys during edit sessions.

Mouse modifier gestures tied to selection behavior

The following mouse gestures are implemented directly in viewer panels (not in the keyboard shortcut registry), and must stay aligned between viewers when they represent equivalent actions:

  • Viewer2D: Left Mouse drag retains its existing view-navigation and selection priorities, Middle Mouse drag always pans the view, and the wheel zooms.

  • Viewer3D: Left Mouse drag orbits, Shift + Left Mouse drag and Middle Mouse drag pan, and the wheel zooms.

  • The 3D navigation preferences invert horizontal yaw and vertical pitch independently. The legacy viewer3d_invert_orbit setting remains the vertical preference, while viewer3d_invert_orbit_horizontal controls only horizontal movement and defaults to off when absent. Both are user-level preferences and are preserved when a project package is opened, regardless of values stored by the project author.

  • Ctrl + Left Drag: rectangle selection in the active table context.

  • Ctrl + Shift + Left Drag: transversal rectangle selection across all selectable object tables in both Viewer2D and Viewer3D.

Truss-line fixture distribution

The Tools menu owns Alt+D, which opens the fixture distribution dialog for the current fixture selection. The default exact-spacing system supports a configurable center-to-center or edge-to-edge gap and can place fixtures either between two limits from the outside inward or from one start point in a chosen direction. The dialog also exposes the existing full-truss and uniform two-point systems.

This combination is a wx menu accelerator rather than an entry in the no-modifier shortcut registry. They do not alter existing shortcut priority. During two-point selection, the active scene viewport owns Esc and uses it only to cancel that transient interaction. Both distribution commands require focus inside an existing scene viewport and never open, close, or switch panes.

Local shortcuts in "Create scene from text" autocomplete

RiderTextDialog adds local key handling in the multiline text box for autocomplete navigation, and keeps Esc scoped to autocomplete behavior:

  • ↑ / ↓: move the active suggestion.
  • Enter or Tab: accept the selected suggestion and replace the active token.
  • Esc: if suggestions are open, close them without inserting; if suggestions are closed, keep focus in the dialog editor (do not close the dialog).

The dialog also shows this shortcut help inline under the editor so users can discover the local autocomplete controls without leaving the workflow.

Priority and focus impact:

  • Esc is always consumed by the dialog to avoid accidental window close while editing rider text.
  • Navigation/accept keys are consumed only when the dropdown is open.
  • Outside that state, the text box keeps its standard free-text editing behavior.
  • Global shortcut routing is unchanged because the dialog key handling stays local.
  • Suggestion ordering uses local relevance ranking (exact/prefix/fuzzy + recent usage + syntax context), but key routing/priority rules above remain unchanged.
Back to top