8.3 KiB
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.
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
- Add the enum value to
Shortcutand its row toshortcut_table, in the position the dialog should list it; the row's section heading is thesection_tableentry 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. - Handle it in the dispatcher of its context:
MainFrame::handle_global_shortcut,GLCanvas3D::handle_shortcut,ObjectList::dispatch_shortcut, or a gizmo'son_tool_shortcut. A gizmo that opens on a key setsm_shortcutin its constructor. - Where the UI shows the key, ask the registry (
display()for tooltips,accelerator()for menu labels); no label holds a literal key name.