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.
This commit is contained in:
Christopher Linnett
2026-10-24 00:03:10 +08:00
parent 0558c9427c
commit 0635649e42
2 changed files with 28 additions and 4 deletions
+6 -4
View File
@@ -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 => {
+22
View File
@@ -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();