From 0635649e42dd49d24d93d8fb9672dec99bede46e Mon Sep 17 00:00:00 2001 From: Christopher Linnett Date: Sat, 24 Oct 2026 00:03:10 +0800 Subject: [PATCH] fix: let a rebound Deselect All use Alt variants of Cmd+A and Cmd+Z (#2096) The text editor's Select All and Undo arms ignored Alt, so a Deselect All rebound to Cmd+Alt+A or Cmd+Alt+Z was taken as Select All or Undo while a text edit was open. The arms now match exactly the chords the editor owns (Cmd/Ctrl+A, and Cmd/Ctrl+Z with or without Shift); their Alt variants fall through to the Deselect All binding match. Chords the editor owns still win over a rebound Deselect All, so Undo keeps working while typing. Test: a UI kittest rebinds Deselect All to each Alt variant and checks that Cmd+A and Cmd+Z still edit the text while the rebound chord ends the edit. --- crates/ui-egui/src/panels/viewer_text.rs | 10 ++++++---- crates/ui-egui/tests/ui_text_edit.rs | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/viewer_text.rs b/crates/ui-egui/src/panels/viewer_text.rs index 23e938f3..c7169f41 100644 --- a/crates/ui-egui/src/panels/viewer_text.rs +++ b/crates/ui-egui/src/panels/viewer_text.rs @@ -486,12 +486,14 @@ fn keyboard(app: &mut EffectcraftApp, ui: &mut egui::Ui, viewer: Rect) { Key::Escape => { exec(app, "text.endEdit", json!({})); } - // Cmd/Ctrl alone is Select All; with Shift it belongs to Deselect All, - // which the binding match below handles, so don't eat the chord here. - Key::A if cmd && !m.shift => { + // The editor owns exactly Cmd/Ctrl+A (Select All) and Cmd/Ctrl+Z with or + // without Shift (Undo, Redo). Other modifiers on those keys aren't editing + // chords, so they fall through to the binding match below: Shift+A is + // Deselect All's default, and Alt variants are free for a rebound one. + Key::A if cmd && !m.shift && !m.alt => { exec(app, "text.setSelection", json!({"select": "all"})); } - Key::Z if cmd => { + Key::Z if cmd && !m.alt => { let _ = crate::menus::invoke(app, &ctx, if m.shift { "edit.redo" } else { "edit.undo" }, json!({})); } Key::Comma | Key::Period if cmd && m.shift => { diff --git a/crates/ui-egui/tests/ui_text_edit.rs b/crates/ui-egui/tests/ui_text_edit.rs index da78aa46..ed5baca2 100644 --- a/crates/ui-egui/tests/ui_text_edit.rs +++ b/crates/ui-egui/tests/ui_text_edit.rs @@ -634,6 +634,28 @@ fn rebound_deselect_all_chord_forwards_from_inside_the_edit() { assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, "Hello", "the text is kept"); } +#[test] +fn deselect_all_rebound_onto_an_alt_variant_of_an_editor_chord_forwards() { + let alt_cmd = |k| (k, Modifiers { alt: true, ..Modifiers::COMMAND }); + for (bound, (k, mods)) in [("Cmd+Alt+Z", alt_cmd(Key::Z)), ("Cmd+Alt+A", alt_cmd(Key::A))] { + let mut a = app(); + a.session.execute("shortcuts.set", json!({"command": "edit.deselectAll", "keys": [bound]})).unwrap(); + let (mut h, lid) = type_tool_edit(a, "Hello"); + // The editor owns Cmd+A and Cmd+Z themselves, whatever Deselect All is bound to… + key(&mut h, Key::A, Modifiers::COMMAND); + let (_, _, a0, c0) = edited(&h); + assert_eq!((a0, c0), (0, 5), "{bound}: Cmd+A still selects every character"); + type_text(&mut h, "Hi"); + key(&mut h, Key::Z, Modifiers::COMMAND); + assert_eq!(edited(&h).1, "Hello", "{bound}: Cmd+Z still undoes the typing"); + // …but not their Alt variants, which belong to the rebound Deselect All. + key(&mut h, k, mods); + assert!(h.state().session.state.text_edit.is_none(), "{bound}: the rebound chord ends the edit"); + assert!(h.state().session.state.selected_layers.is_empty(), "{bound}: and deselects"); + assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, "Hello", "{bound}: without undoing or selecting text"); + } +} + #[test] fn bare_letter_rebinding_stays_typing_but_a_function_key_rebinding_forwards() { let mut a = app();