From 6dc6b8600b75ec5ffc151ca638416e2ac4e1ffa1 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 18:54:42 +0700 Subject: [PATCH] Fix #487: Keyboard Shortcuts records chords with modifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit egui reports a modifier's own key press (ShiftLeft, ControlLeft…) as a key event, and the recorder took the first key press it saw, so pressing Shift+A saved `ShiftLeft` as the chord: a shortcut that can never fire. The recorder now skips modifier key presses, so the chord completes on the first other key with the modifiers held (Esc still cancels). A modifier is never a chord's key in the shortcut parser either, so `shortcuts.set` refuses one, and overrides saved that way by older versions are dropped on start, giving the default back. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/shortcut_editor.rs | 38 +++++++++++++++++++++++++-- crates/ui-egui/src/shortcuts.rs | 11 +++++++- crates/ui-egui/src/state.rs | 3 +++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 99412d9c..a608bc65 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469, #474); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 8f105210..d327b916 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -515,11 +515,12 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let mut ok = false; let mut cancel = false; let recording = d.str("__recording"); - // Record a chord (before widgets see the keys). + // Record a chord (before widgets see the keys). Modifiers held down are part of the next key's + // chord, not a chord of their own. if !recording.is_empty() { let ev = ctx.input(|i| { i.events.iter().find_map(|e| match e { - egui::Event::Key { key, pressed: true, modifiers, .. } => Some((*key, *modifiers)), + egui::Event::Key { key, pressed: true, modifiers, .. } if !crate::shortcuts::is_modifier(*key) => Some((*key, *modifiers)), _ => None, }) }); @@ -789,6 +790,39 @@ mod tests { assert_eq!(chord_from_event(Key::K, Modifiers::COMMAND | Modifiers::SHIFT).as_deref(), Some("Cmd+Shift+K")); assert_eq!(chord_from_event(Key::P, Modifiers::NONE).as_deref(), Some("P")); assert_eq!(chord_from_event(Key::Escape, Modifiers::NONE), None); + // A modifier's own key press is no chord (#487). + assert_eq!(chord_from_event(Key::ShiftLeft, Modifiers::SHIFT), None); + assert!(normalize("ShiftLeft").is_none() && normalize("Cmd+ControlRight").is_none()); + } + + #[test] + fn a_modifier_pressed_while_recording_waits_for_the_key() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + open(&mut app); + app.ui.dialog.as_mut().unwrap().fields.insert("__recording".into(), json!("tool:groupSelection")); + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + let mut press = |key, modifiers| { + let events = vec![egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }]; + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(&mut app, ui.ctx())); + out.textures_delta.clear(); + app.ui.dialog.clone().unwrap() + }; + // egui reports Shift's own key press first (#487): the recording goes on. + assert_eq!(press(Key::ShiftLeft, Modifiers::SHIFT).str("__recording"), "tool:groupSelection"); + let d = press(Key::A, Modifiers::SHIFT); + assert_eq!(d.str("__recording"), ""); + assert_eq!(dialog_overrides(&d).get("tool:groupSelection").map(String::as_str), Some("Shift+A")); + } + + #[test] + fn saved_modifier_only_overrides_give_the_default_back() { + let mut ui = UiState::default(); + ui.shortcut_overrides.insert("tool:groupSelection".into(), "Shift+ShiftLeft".into()); + ui.shortcut_overrides.insert("edit.preferences".into(), String::new()); + let ui = ui.sanitized(); + assert!(!ui.shortcut_overrides.contains_key("tool:groupSelection")); + assert_eq!(ui.shortcut_overrides.get("edit.preferences").map(String::as_str), Some("")); } #[test] diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 9c145735..c4cb88f8 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -40,11 +40,20 @@ pub fn parse(s: &str) -> Option { "Backspace" => Key::Backspace, "Tab" => Key::Tab, "~" => Key::Backtick, - k => Key::from_name(k)?, + // A modifier alone is never a chord's key: it couldn't fire (#487). + k => Key::from_name(k).filter(|k| !is_modifier(*k))?, }; Some(KeyboardShortcut::new(m, key)) } +/// The modifier keys, which egui also reports as key presses of their own. +pub(crate) fn is_modifier(key: Key) -> bool { + matches!( + key, + Key::ShiftLeft | Key::ShiftRight | Key::ControlLeft | Key::ControlRight | Key::AltLeft | Key::AltRight | Key::SuperLeft | Key::SuperRight + ) +} + /// Every command shortcut in effect (user overrides from Edit → Keyboard Shortcuts win), with the /// command's params: `{}`, or `{panel}` for the panel shortcuts (`window.panel`). pub(crate) fn all_shortcuts() -> Vec<(KeyboardShortcut, &'static str, serde_json::Value)> { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index ed1e1d0b..0856f52d 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -407,6 +407,9 @@ impl UiState { f.tools.retain(|id| vectorcraft_tools::tool_info(id).is_some()); f.tools.first().is_some_and(|k| seen.insert(k.clone())) }); + // Overrides that can't fire (modifier-only chords recorded by older versions, #487) give + // the default back. + self.shortcut_overrides.retain(|_, c| c.is_empty() || crate::shortcut_editor::normalize(c).is_some()); self } }