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.