From acb017e1f14cf7b963d9bf092bd47dc8980ecfe0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 21 Oct 2026 02:28:36 -0700 Subject: [PATCH] Type: a character typed with AltGr while editing text runs no command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firefox on Windows reports AltGr as Ctrl and Alt held. In the web build, while the Type tool edited text, a character typed with AltGr was typed and also ran the command chord of its key: AltGr+L (ł on a Polish layout) added a layer through Cmd+L, AltGr+Q (@ on a German layout) ran Reset Tracking and Kerning, AltGr+E (€) opened Export for Screens, AltGr+A (ą) ran Select All on Active Artboard, and AltGr+Z (ż) undid the typing through Cmd+Z. While the Type tool edits text, a key press with Ctrl and Alt held that is followed by the text it typed is now dropped before the command chords are matched. A Ctrl or Ctrl+Alt chord that types no character runs its command as before, and key presses outside text editing are handled as before. The desktop app receives AltGr without Ctrl and Alt, and a press with Ctrl held types no text there, so it behaves as before. Docs: docs/target-app-parity.md. --- crates/ui-egui/src/shortcuts.rs | 96 +++++++++++++++++++++++++++++++++ docs/target-app-parity.md | 1 + 2 files changed, 97 insertions(+) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 76e079932..35f757f8b 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -150,6 +150,25 @@ fn unclip_chords(i: &mut egui::InputState) { } } +/// Drop each key press made with Ctrl and Alt held that is followed by the text it typed. Firefox +/// on Windows reports AltGr as Ctrl and Alt held, so AltGr+L on a Polish layout arrives as a +/// Ctrl+Alt+L press and then the text "ł", which the Type tool types. Without the press no command +/// chord runs for it: neither Cmd+Alt+L nor Cmd+L, which egui matches with the extra Alt held. +fn drop_typing_presses(i: &mut egui::InputState) { + let next = i.events.iter().skip(1).map(Some).chain([None]); + let typed: Vec = i + .events + .iter() + .zip(next) + .map(|(e, next)| { + matches!(e, egui::Event::Key { pressed: true, modifiers, .. } if modifiers.ctrl && modifiers.alt) + && matches!(next, Some(egui::Event::Text(t)) if !t.is_empty()) + }) + .collect(); + let mut typed = typed.into_iter(); + i.events.retain(|_| !typed.next().unwrap_or(false)); +} + /// Keyboard pastes of something other than text. egui swallows a paste chord's key press and /// sends a Paste event only when the clipboard holds text, so a paste key released without its /// press (or a Paste event) reported was a paste of a bitmap or a PDF. @@ -378,6 +397,10 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { if ctx.input(|i| i.key_pressed(Key::Tab)) { ctx.memory_mut(|m| m.move_focus(egui::FocusDirection::None)); } + // While the Type tool edits text, a character typed with AltGr runs no command chord. + if app.session.tool_wants_text() { + ctx.input_mut(drop_typing_presses); + } // Type tool editing: text, IME composition and editing keys go to the tool. if app.session.tool_wants_text() { type_text(app, ctx); @@ -775,6 +798,79 @@ mod tests { assert!(!is_shortcut_chord(M::CTRL | M::ALT), "AltGr types @ on a German keyboard"); } + /// A press of `key` with Ctrl and Alt held (as Firefox on Windows reports AltGr), followed by + /// the text it typed when `typed` is given. + fn ctrl_alt_press(key: Key, typed: Option<&str>) -> Vec { + let m = Modifiers::CTRL | Modifiers::ALT | Modifiers::COMMAND; + let mut v = + vec![egui::Event::ModifiersChanged(m), egui::Event::Key { key, physical_key: Some(key), pressed: true, repeat: false, modifiers: m }]; + v.extend(typed.map(|t| egui::Event::Text(t.into()))); + v + } + + /// The text of the type object `id`, and the document's layer count. + fn text_and_layers(app: &mut VectorcraftApp, id: u64) -> (String, usize) { + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + let text = r["runs"].as_array().unwrap().iter().filter_map(|run| run["text"].as_str()).collect(); + (text, app.session.active().unwrap().doc.layers.len()) + } + + /// While the Type tool edits text, a character typed with AltGr (Ctrl+Alt in Firefox on + /// Windows) is typed and runs no command: not New Layer (Cmd+L), Reset Tracking (Cmd+Alt+Q), + /// Export for Screens (Cmd+Alt+E), Select All on Active Artboard (Cmd+Alt+A) or Undo (Cmd+Z). + #[test] + fn altgr_characters_typed_in_text_run_no_command() { + let mut wrong = vec![]; + for (key, typed) in [(Key::L, "ł"), (Key::Q, "@"), (Key::E, "€"), (Key::A, "ą"), (Key::Z, "ż")] { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("text.setRangeStyle", &json!({"id": id, "tracking": 200, "kerning": -40})).unwrap(); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + app.session.set_tool_option("select", &json!({"start": 2, "end": 2})); + assert!(app.session.tool_wants_text()); + let layers = text_and_layers(&mut app, id).1; + let selected = |app: &VectorcraftApp| app.session.active().unwrap().selection.objects.len(); + let before = selected(&app); + frame(&mut app, ctrl_alt_press(key, Some(typed))); + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + let spaced = + r["runs"].as_array().unwrap().iter().all(|run| run["style"]["tracking"] == json!(200.0) && run["style"]["kerning"] == json!(-40.0)); + let got = (text_and_layers(&mut app, id), spaced, selected(&app) == before, app.ui.dialog.is_none(), app.session.tool_wants_text()); + if got != ((format!("Hi{typed}"), layers), true, true, true, true) { + wrong.push(format!("{key:?}: (text, layers), tracking and kerning kept, selection kept, no dialog, still editing = {got:?}")); + } + } + assert!(wrong.is_empty(), "{wrong:#?}"); + } + + /// Ctrl and Ctrl+Alt chords that type no text still run their commands while the Type tool + /// edits, and with the Selection tool a Ctrl+Alt press runs its chord as before. + #[test] + fn ctrl_alt_chords_without_typed_text_still_run() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + assert!(app.session.tool_wants_text()); + let layers = text_and_layers(&mut app, id).1; + let m = Modifiers::COMMAND | Modifiers::CTRL; + let ctrl_l = egui::Event::Key { key: Key::L, physical_key: Some(Key::L), pressed: true, repeat: false, modifiers: m }; + frame(&mut app, vec![egui::Event::ModifiersChanged(m), ctrl_l]); + assert_eq!(text_and_layers(&mut app, id), ("Hi".into(), layers + 1), "Ctrl+L while editing: New Layer"); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + assert!(app.session.tool_wants_text()); + frame(&mut app, ctrl_alt_press(Key::L, None)); + assert_eq!(text_and_layers(&mut app, id), ("Hi".into(), layers + 2), "Ctrl+Alt+L with no text typed while editing: New Layer"); + app.select_tool("selection"); + frame(&mut app, ctrl_alt_press(Key::L, Some("ł"))); + assert_eq!(text_and_layers(&mut app, id), ("Hi".into(), layers + 3), "Selection tool: Ctrl+Alt+L runs New Layer as before"); + } + /// #2094: Cmd+Shift+O is Type › Create Outlines (not File › Open's Cmd+O, not the Artboard /// tool's Shift+O), with the Selection tool or while the Type tool edits the text. #[test] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0b7f59506..c5701f353 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -518,6 +518,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column. - The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown. - Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected). + - While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also run New Layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command. - With the Artboard tool, Cmd/Ctrl+A after a click on a row of the Artboards panel selects every artboard, as the macOS menu bar's Select All does then; the active artboard stays active, and the art's selection does not change. After a press outside the panel's list, Cmd/Ctrl+A selects every object again. It used to select every object after a click in the panel too. Public reference: [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html). - Cmd/Ctrl+/ (`appearance.addFill`) and Cmd/Ctrl+Alt+/ (`appearance.addStroke`) add a new fill and a new stroke to the selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do, on the keys the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for "Add new fill" and "Add new stroke". Keyboard Shortcuts lists both under Window › Appearance. - Cmd/Ctrl+Alt+Shift+. and , (`type.size.increaseByFive`, `type.size.decreaseByFive`) step the font size of the selected type, or of the text the Type tool has selected, by five times the Size/Leading increment (Preferences › Type); Cmd/Ctrl+Shift+. and , still step it by one increment.