mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #487: Keyboard Shortcuts records chords with modifiers
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 <[email protected]>
This commit is contained in:
1 parent
4974d658e5
commit
6dc6b8600b
4 files changed
+50
-4
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -40,11 +40,20 @@ pub fn parse(s: &str) -> Option<KeyboardShortcut> {
|
||||
"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)> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user