Files
OrcaSlicer/docs/HLSD/keyboard-shortcuts.md
T
ExPikaPaka a800e0c186 feat: add an Image gizmo for zero-displacement colour
A separate toolbar tool for the simple case of texture displacement: put a
colour picture on the model with no relief at all.

It is a thin wrapper, not a fork. GLGizmoImage subclasses
GLGizmoTextureDisplacement and overrides one new protected virtual,
image_mode(). Painting, the layer stack, the paint masks, the texture
library, the previews and the bake are the same code reached the same way;
a zero-depth bake refines the painted area and writes the per-triangle
filament indices exactly as a relief bake does, it just displaces by
nothing.

In the base gizmo, image_mode() does three things:

- apply_image_mode_presets() pins every layer to depth 0 and turns colour on
  wherever the image has any, alongside the existing Standard-mode presets.
- The panel leaves out Depth, Midlevel, Invert, Edge fade and Blend, and the
  Standard/Pro switch, which is what keeps the mesh-preparation sections
  hidden. Bake is disabled when no image carries colour, since that bake
  would refine the mesh and produce nothing.
- add_texture_layer() seeds a new layer from the first library texture that
  has colour, rather than the first one, because most of the shipped library
  is grayscale height maps.

The two tools share one layer stack, so opening the Image tool on a volume
whose layers carry relief flattens them. Only depth changes: the paint, the
image, the tiling, the mapping and any relief already baked into the mesh
are untouched.

The toolbar icon follows Orca's menu-icon style: 16 px, 1 px #949494 frame,
#009688 on the picture inside.

The shortcut is registered but unbound: every single letter the Plater
context can reach is already taken, and a default that shadows one of them
is worse than none. docs/HLSD/keyboard-shortcuts.md now documents that an
empty default chord is a supported choice.

Measured with a probe built against this tree in the texture-bake harness:
a depth-0 bake of a painted cube face leaves every output vertex within
7 nm of the original surface (tolerance 1 um) while colouring 17090 of
24226 output triangles across 3 filaments; the same scene at depth 0.4 mm
moves vertices up to 0.216 mm.
2026-10-12 08:29:10 +02:00

8.5 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

  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. A default KeyChord{} leaves the action unbound: it is listed in the dialog for the user to bind and matches nothing until they do, which is the honest answer when every key its contexts can reach is already spoken for.
  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.