feat/keyboard-shortcuts #3

Manually merged
rob merged 5 commits from feat/keyboard-shortcuts into main 2026-09-09 15:08:38 +02:00
Collaborator
No description provided.
Escape was handled locally in seven modules, each unaware of the
others, and most toggles had no Escape at all — the DSL inspector,
group select, the preset sidebar, notation edit/create and the
rehearsal chooser could only be left via their own button.

New js/system/keyboardShortcuts.js owns global key handling. Modules
register dismissible layers with a tier; one press backs out of
exactly the innermost active layer, so nested states unwind instead
of collapsing at once:

  overlay    fullscreen cue video
  popup      rehearsal mark chooser
  substate   DSL path-pick, preset click-to-place, group wrap panel
  selection  group selection, audio-object and annotation selection
  panel      DSL editor sidebar, preset sidebar, live console
  mode       group select, DSL inspector, draw/erase/select,
             annotation edit, audio-object edit, notation create/edit

Deliberately not on the stack: freeze animations, mic analysis, and
marker/playhead visibility. Escape dismisses things that capture
input or cover the score, never a setting you switched on — escaping
a freeze mid-edit would undo the reason you froze.

The listener runs at window capture but only consumes the event when
one of its layers is active; otherwise Escape falls through untouched
to the handlers that own it (inline dialogs, the live console's cue
browser, stopwatch fullscreen). It also defers whenever focus is in a
text field, so Escape still cancels an inline edit, and ignores
modifier chords. Equal-priority layers go in registration order.

Supporting changes:

- dslInspector: the exit paths its toggle buttons were inlining are
  now named functions (_cancelPick, _exitInspector, _closePresetPanel,
  _exitGroupMode) so button and Escape share one path; the module
  exports getDslEscapeLayers() rather than its private state.
  Cancelling a preset placement also clears the 'click in the score
  to place…' status, which used to linger.
- video.js registers the fullscreen overlay as a layer instead of its
  own window listener, so it outranks the rest rather than racing it.
- oscillaLive exports isLiveConsoleOpen/hideLiveConsole.
- drawing.js and interactionSurface.js lose their Escape branches —
  the central handler supersedes them — with pointers left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The module was only half its name. UIBindings carried a second function
also called initKeyboardShortcuts, holding the Space play/pause binding,
the isTypingInInput guard, and a commented-out H/F/T/R block behind
"Keybindings disabled -- uncomment to re-enable". Two owners, one name,
no shared ordering.

Both move here, and the dispatcher splits into an Escape path and a
plain-key path so there is somewhere for a global keymap to live. Space
is its first and, at this commit, only entry — behaviour is unchanged,
including going inert on a locked client and while typing.

isTypingInInput is the canonical guard now and still publishes
window.isTypingInInput, which the transport listeners already call.

Also drops the onToggleSplashScreen/onOpenRehearsalPopup callbacks:
only the commented-out block read them, so they were dead weight in the
initUIBindings signature. app.js stops passing them.

No new bindings here — the letters and the help overlay follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviving a design that was already here and switched off: the H/F/T/R
block UIBindings carried commented out, an exported toggleKeybindingsPopup
with nothing behind it, and a #keybindings-popup stub whose whole body
read <!-- left unchanged -->. The docs already promised shortcuts that
did not exist.

Keys now dispatch in three stages:

  1. Escape      unwinds one layer of the Escape stack
  2. Contexts    an active tool gets first refusal on a key
  3. Global map  whatever no context claimed

Stage 2 is what makes bare letters safe. drawing's handleDrawingKeydown
already returned handled/not-handled, so it registers as a context and
keeps D/E/V/S while a drawing mode runs, while the same letters fall
through to the global map otherwise. So D enters draw mode, D inside
eraser returns to draw (context), D inside draw exits (global) — one
binding, not a collision.

  Tools    D draw · A annotations · O audio objects · N notation edit
           I DSL editor · G group select · P presets · L live console
  Session  M drop marker · Shift+M marker visibility · R rehearsal marks
  View     F fullscreen · Shift+F freeze anims · T splash · H this list

Left on the mouse deliberately: reset animations (destructive), audio
analysis (fires a mic permission prompt), waveform labels (rare). A
stray keypress on those costs more than the reach saves.

Bare letters fire during performance, so the authoring tools (I G P N)
carry authoring:true and are ignored while window.transportControlsDisabled
is set — locked performer clients keep the session keys and lose the
score-editing ones.

H renders #keybindings-popup from the keymap itself, including the keys
other modules still dispatch, so the help cannot drift from the bindings.
The module header lists those outliers (transport seek/speed, the Ctrl
chords, Alt+Shift preset panels) with the reason they stay put: they own
state machines, not keymap concerns.

Also:
- dslInspector exports toggleDslInspector/togglePresetPanel/
  toggleGroupMode/toggleAnimFreeze so button and key share one path.
- _closePanel is null-safe: it is now reachable from `i` before the
  panel has ever been built, which a button click could never do.
- docs: notation.md no longer claims there is no shortcut; audioObject
  and dev-dsl-inspector name the actual keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overlay that documents the shortcuts could only be opened with one
of them. H was its sole caller — no button, no menu entry — so it was
unreachable unless you already knew what it would have told you.

- Hamburger menu gains "Keyboard shortcuts", next to Local help.
- Escape closes it, as a popup-tier layer alongside the rehearsal
  chooser.
- It renders above both transport bars now. It carried two competing
  z-index declarations (20000, then 11200) and the later one won, so
  it sat under #controls at 30000 and #top-bar at 35000. Now declared
  once in the documented layering block at 45000 — above the transport
  bars and the controlXY panels, below the screen-lock overlay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking an element with no cues opens the Add Cue panel, which
autofocuses its filter box. The global Escape stack always defers to a
focused text field, so the key blurred the input and left the panel
open. Not specific to any one element — "grid" was just a plain path
with no cues on it.

The panel now handles Escape itself, one listener covering both cue
pickers and any field added to it later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rob closed this pull request 2026-09-09 14:45:39 +02:00
rob reopened this pull request 2026-09-09 15:07:31 +02:00
rob manually merged commit b2203239e1 into main 2026-09-09 15:08:38 +02:00
filip deleted branch feat/keyboard-shortcuts 2026-09-09 15:24:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/oscilla!3
No description provided.