Files
OrcaSlicer/docs/HLSD/keyboard-shortcuts.md
Kris Austin 42608687e5 Fix AltGr typing in settings boxes switching tabs, and add go-to shortcuts for the other tabs (#16918)
Remove the old Ctrl+1 to Ctrl+4 key handler on settings fields. wxWidgets reports AltGr as Ctrl+Alt, so on AZERTY and similar layouts, typing `{`, `~` or `#` in a multi-line settings box switched the main tab. The handler was also outside the shortcut registry and clashed with the Ctrl+1 to Ctrl+4 camera shortcuts.

Add unbound "Go to the ... tab" shortcuts for Home, Design, Multi-device and Calibration, next to the Prepare, Preview, Device and Project ones from #16301.

Fixes #9649
2026-10-28 07:17:52 -05:00

166 lines
11 KiB
Markdown

# Keyboard Shortcuts
## Why it exists
Key events arrive in several windows (the main frame's char hook, the 3D canvases, the
gizmo manager and the object list), and the same keys are shown again in menu labels,
toolbar tooltips, gizmo names and the shortcuts dialog. The registry is the one table all
of them read. Each binding is defined once; dispatchers look key events up there, labels
are derived from it, and a change the user makes updates all of them.
## Data model
`KeyChord` (`src/slic3r/GUI/KeyChord.hpp`) is one key press: the key code as
`wxEVT_KEY_DOWN` reports it, plus the `wxMOD_*` modifiers held with it. It has two
text forms. The canonical one (`Ctrl+Shift+S`) is platform-neutral and doubles as the wx
accelerator string and the config format. The display one uses translated modifier
names and the command and option glyphs on macOS. `KeyChord::from_event()` turns any wx
key event into the same key code and modifiers, so a chord recorded in the dialog is
equal to the chord a dispatcher builds from the key press.
`Shortcut` is the enum of every user-facing binding. `shortcut_table` in
`src/slic3r/GUI/Shortcuts.cpp` gives each one a config key, a description, a context
mask, a default chord, a `repeatable` flag and a `modifier_variants` flag, in the order
the dialog lists them; a `static_assert` keeps the table and the enum in step.
`ShortcutRegistry` overlays the user's overrides on the defaults and keeps a
chord-to-shortcut index for lookups. It reads and writes the `shortcuts` section of
`AppConfig`. Only overrides are stored, so a default can change between releases
without touching anyone's config; `none` records a shortcut the user unbound.
## Contexts
A key press is looked up in the context of the window that received it.
| Context | Dispatcher | Examples |
|--------------|-----------------------------------------------------------|-------------------------------|
| `Global` | `MainFrame`'s `wxEVT_CHAR_HOOK`, before any child sees it | New project, camera views |
| `Plater` | `GLCanvas3D` of the 3D and assembly views | Arrange, gizmo activation |
| `Preview` | `GLCanvas3D` of the G-code preview | One-layer mode, jump to layer |
| `ObjectList` | the object list | Copy, delete, auto drop |
| `Painting` | `GLGizmosManager` while a painting gizmo is open | Circle, sphere, fill tools |
A shortcut can belong to several contexts, which is how copy and paste are a single
binding for the canvas and the object list. Two shortcuts can share a chord when their
contexts do not overlap; `C` is the cut gizmo in the 3D view, the G-code window in the
preview and the circle tool while painting. A Global chord is dispatched before every
other context, so the dialog treats it as conflicting with all of them.
A Global shortcut has to include Ctrl or Alt or use a key that types nothing, since a
bare printable key in the frame hook would swallow that character in every text field.
The dialog refuses such chords and `ShortcutRegistry::load()` drops them from the config.
Space counts as typing. The speed dial's default is the one bare Space, and
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
combo boxes), so it opens the dial from the canvases and the tab strip only.
`MainFrame::handle_global_shortcut` skips a chord that `KeyChord::edits_text()` reports
while a text field has the focus (a wx text entry, spin control, web view or ImGui
InputText), and skips every chord while an ImGui modal is open, as a wx modal dialog
would. `ImGuiWrapper::owns_key()` reads the text input request and the popup stack of the
last drawn frame, since `WantCaptureKeyboard` and `WantTextInput` only catch up when the
next frame starts, and is called only when the focus is on a visible canvas from
`OpenGLManager::create_wxglcanvas()`.
The `Plater`, `Preview` and `Painting` shortcuts act on the 3D view, so they run only while
the 3D view has the keyboard. `GLCanvas3D` feeds every key event to ImGui and asks
`ImGuiWrapper::claim_key_event()` whether to act on it as well. While an ImGui popup (a
dropdown, a menu or a modal) is open or an ImGui InputText is active, ImGui claims every key,
as a focused sidebar text field or an open native dropdown keeps every key from the canvas.
Esc then closes the topmost dropdown or menu, since this ImGui closes popups on Esc only
through keyboard navigation, which is off; a modal handles Esc itself. A second Esc reaches
the canvas once nothing claims it. A slider or button held with the mouse also claims every
key until it is released, since a shortcut that switches the view or closes the tool would
leave the drag behind. One side handles a key from its key-down to its release. Its
repeats, its char event and its release follow the key-down's decision even when a field or
modal opens or closes in between, so a key never both runs a shortcut and types, and a
keyboard move of the selection always sees the release of the key that started it.
Key-ups go to whatever window has the focus, so a shortcut that opens a dialog, such as
Ctrl+P, takes the release of its modifier with it. When the canvas loses the focus it sends
itself a key-up for each Shift, Ctrl or Alt still held, which ends a gizmo's Shift mode and
clears ImGui's own copy of the modifier, and then drops its record of held keys.
`ImGuiWrapper::update_mouse_data()` also takes ImGui's modifiers from each mouse event, which
carries the real state, since a stale Ctrl would turn every slider click into a Ctrl+click
and open the slider's text box. The macOS
Edit menu, whose key equivalents run before the canvas sees the key, follows the same rule
as the canvas.
## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
on the keyboard layout: the key labelled `Q` on an AZERTY keyboard and the key in the
same position under a Cyrillic layout both report `Q`. Numpad keys fold onto their main
keyboard equivalents, so `Ctrl+1` and `Ctrl+Numpad 1` are one binding.
Punctuation matches on `wxEVT_CHAR`, because only the char event knows which character
a key produced under the active layout. `+` is Shift and `=` on a US keyboard and a key
of its own on a German one, and the binding means the character in both cases. The
canvas looks a key up on key-down first and, when nothing matched, once more on the char
event, for punctuation chords only. The dialog records chords the same way: a printable
non-alphanumeric key pressed with nothing but Shift is taken from the char event that
follows.
wxGTK does not report key auto-repeat, so the canvases share one record of the keys
seen going down and swallow the repeats of every shortcut not marked `repeatable`. Zoom
and undo repeat, for example; a toggle such as Tab does not. The record is shared because
a shortcut can move the focus to another canvas while its key is still held; a key
released while no canvas had the focus is dropped on the next press.
A few shortcuts have `modifier_variants`: Shift or Ctrl added to their binding selects a
step of the same action (1 mm and camera-space moves of the selection, five-step slider
moves). Only a binding without Shift or Ctrl of its own has steps, so no two bindings
share one. `ShortcutRegistry::match()` looks the exact chord up first and only then, when
nothing is bound to it, looks for such a shortcut whose binding is the chord minus those
modifiers, reporting which were added; a binding on Ctrl+Shift+key therefore wins over
the combined step. The Shift and Ctrl steps themselves are reserved. `step_owner()` names
the shortcut they belong to, the capture dialog refuses to assign them, and
`conflicts()` reports exact chords only. A binding made before its key became a stepping
key keeps its chord and shadows that one step. A move or rotation of the selection
started from the keyboard runs until the key that started it is released, or the
canvas loses focus, so a held key is one undo step.
## Labels
Menu labels, toolbar tooltips, gizmo names, the context menu and the shortcuts dialog
read the registry, so a rebinding shows up in all of them. Each tracked menu item keeps
its base label; `MainFrame::update_shortcut_labels()` appends the current binding
again after an edit, which also installs the new wx accelerator.
A chord that is unsafe as a menu accelerator, meaning a bare printable key, is appended
to the label as plain text so the menu cannot take it away from text fields. The macOS
edit menu shows its clipboard and undo entries that way, because a system-menu key
equivalent for Cmd+C would run instead of the text field's own copy.
On macOS the object list receives no key events at all, so its bindings are installed as
a `wxAcceleratorTable`, regenerated from the registry after each edit.
## Editing
The shortcuts dialog has a page per context, each opening with a line that says when its
keys apply. A page lists the shortcuts under the headings of `section_table`, with the
fixed keys that cannot change (mouse buttons, the step modifiers, Esc, the digit keys
that pick a filament) sorted into the same sections. The mouse drag rows describe the
camera actions chosen in Preferences; their button opens Preferences > Control with that
option scrolled into view and focused, instead of editing a key.
Editing a row opens a capture dialog that records the next chord, names the shortcuts it
would take the chord from, and on confirmation unbinds those and binds this one.
Resetting a row asks the same question when its default is now held by another
shortcut, so a reset cannot leave two shortcuts on one chord. Each change is written to
the config at once and pushed to the menus, tooltips and accelerator tables through
`GUI_App::on_shortcuts_changed()`. The dialog opens from the Help menu and Preferences >
Control on the Global page, and from the `?` key on the page of the view that received it.
## Adding a shortcut
1. Add the enum value to `Shortcut` and its row to `shortcut_table`, in the position
the dialog should list it; the row's section heading is the `section_table` entry
above it, so a new section needs an entry there too. Pick a default that does not
collide inside its contexts; the `[Shortcuts]` tests check every default against the
others. An empty chord `{}` leaves a shortcut unbound until the user picks a key,
as the jump-to-tab shortcuts are.
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
3. Where the UI shows the key, ask the registry (`display()` for tooltips,
`accelerator()` for menu labels); no label holds a literal key name.