29. Keyboard shortcuts are declared by components¶
Date: 2026-10-09
Status¶
Accepted. Amended on 25 October 2026: a presenter's coroutine method can be
a shortcut, and runs on the session's loop. A view's can be one only on a
frontend whose view_coroutines is true, which the Qt frontend's is not.
Context¶
No key was bound in a redsun window. The positioner stepped an axis on
Left and Right by reading key presses inside its widget, which nothing else
could see, list or change. A window built from several plugins needs more:
each component brings its own keys, two of them can ask for the same one,
and the user needs to see what's bound and, later, change it.
napari, which redsun may later bring in as a provider, keeps the keys of
its menu commands in app-model and its canvas keys in an older registry of
its own, and plans to move everything to app-model.
Decision¶
- A component marks a method with
@shortcut(key, title=..., scope=...). The decorator only records the key on the method, as@slotdoes, so it lives in the core with noapp-modelimport. - A key is written in
app-model's spelling, whereCtrlis Command on macOS, withmac=,win=andlinux=for a platform that differs. - A key acts in the whole window. Marked
scope="view", it acts only while the component's view has focus. A presenter's keys always act in the whole window. - Each shortcut is an
app-modelcommand named<component>.<method>, in the session'sApplication, beside the session file's actions. - Keys are layered: what the component declares, then the session file's
shortcutssection, then, in a later release, the user's own changes. - When two commands ask for one key that acts in the same place, both in
the whole window or both in one view, the first declared keeps it and the
log names both.
strictstops the build instead. A view may bind a combination the window binds too: while the view has focus, the view's binding acts. - Window -> Keyboard shortcuts lists every key, grouped by view.
Other options were rejected:
- A table of keys on the class. The key would be written away from the method it runs, and a method name in a string goes stale.
- Components registering
app-modelactions themselves. Every component would depend onapp-modeland Qt, and the core couldn't check or list the keys. - Refusing the build on any conflict. A shortcut shouldn't stop an instrument from starting.
Before¶
class MyView(QWidget):
def keyPressEvent(self, event: QKeyEvent | None) -> None:
if event is not None and event.key() == Qt.Key.Key_R:
self.run_selected()
else:
super().keyPressEvent(event)
After¶
class MyView(QWidget):
@shortcut("Ctrl+R", title="Run the selected plan")
def run_selected(self) -> None: ...
The key now shows in the list, a session file can change it, and a second component asking for it is caught when the session builds.
Consequences¶
- A provider whose keys live in
app-modelcan be listed beside the session's own. - A coroutine method can't be a shortcut yet: a key fires on Qt's thread, while coroutines run on the session's own loop.
- Keys inside the built-in views, and changing keys in the list, come in later releases.