shortcuts: recording a chord skips the modifier key presses (#164)

egui 0.36 reports pressing Shift, Ctrl, Alt or the Windows / Cmd key as a
key event of its own, before the key they modify. The Keyboard Shortcuts
recorder took that first press and refused it ("AltLeft can't be used as a
shortcut"), so no chord with a modifier could be recorded. Modifier keys on
their own are now skipped; the chord is recorded with the key they modify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
carlitosteso3
2026-10-07 12:37:49 -05:00
co-authored by Claude Opus 5.5
parent 8fcad73c92
commit 98c022e0b0
2 changed files with 40 additions and 2 deletions
+10 -2
View File
@@ -105,13 +105,21 @@ fn assign(app: &mut FilmcraftApp, command: &str, keys: &str, add: bool) {
}
}
/// Take the next key press while recording (Esc cancels).
/// A modifier key on its own. egui reports pressing Shift, Ctrl, Alt or the Windows / Cmd key as a
/// key event of its own, before the key it modifies arrives (#164).
fn is_modifier_key(key: egui::Key) -> bool {
use egui::Key::*;
matches!(key, ShiftLeft | ShiftRight | ControlLeft | ControlRight | AltLeft | AltRight | SuperLeft | SuperRight)
}
/// Take the next key press while recording (Esc cancels). Modifier keys on their own are skipped:
/// the chord is recorded when the key they modify is pressed, with them as its modifiers.
fn record(app: &mut FilmcraftApp, ctx: &egui::Context) {
let Some((cmd, add)) = app.shortcut_editor.recording.clone() else { return };
let got = ctx.input_mut(|i| {
let mut got = None;
i.events.retain(|e| match e {
egui::Event::Key { key, pressed: true, modifiers, .. } if got.is_none() => {
egui::Event::Key { key, pressed: true, modifiers, .. } if got.is_none() && !is_modifier_key(*key) => {
got = Some((*key, *modifiers));
false
}
+30
View File
@@ -512,3 +512,33 @@ fn waveform_peaks_do_not_survive_opening_another_project() {
let now = d.harness.state().tl.peaks.lock().unwrap().get(&item).cloned();
assert!(now.is_none_or(|p| !std::sync::Arc::ptr_eq(&p, &old)), "peaks from the previous project were reused");
}
/// #164: on a real keyboard, pressing Alt+Shift+J sends each modifier key on its own first (egui
/// reports modifier keys as key events). The shortcut recorder took the Alt press and refused it
/// ("AltLeft can't be used as a shortcut"), so no chord with Shift, Ctrl or Alt could be recorded.
#[test]
fn recording_a_shortcut_skips_the_modifier_key_presses() {
let mut d = Driver::demo();
d.ok("ui.menu.invoke", json!({"id": "app.keyboardShortcuts"}));
d.frames(2);
d.ok("ui.click", json!({"id": "shortcuts.search"}));
d.ok("ui.type", json!({"text": "add edit"}));
d.frames(2);
d.ok("ui.click", json!({"id": "shortcuts.add.sequence.addEdit"}));
d.frames(1);
// what a real keyboard sends for Alt+Shift+J: Alt, then Shift, then J
let alt = egui::Modifiers { alt: true, ..Default::default() };
let alt_shift = egui::Modifiers { alt: true, shift: true, ..Default::default() };
let key = |key, modifiers| egui::Event::Key { key, physical_key: Some(key), pressed: true, repeat: false, modifiers };
let events = &mut d.harness.input_mut().events;
events.push(key(egui::Key::AltLeft, alt));
events.push(key(egui::Key::ShiftLeft, alt_shift));
events.push(key(egui::Key::J, alt_shift));
d.frames(2);
d.ok("ui.click", json!({"id": "shortcuts.ok"}));
d.frames(2);
// added next to Cmd+K (`shortcuts.get` lists every chord of the command)
let got = d.ok("engine.execute", json!({"command": "shortcuts.get", "params": {"command": "sequence.addEdit"}})).to_string();
assert!(got.contains("Alt+Shift+J"), "{got}");
assert!(got.contains("Cmd+K"), "{got}");
}