From 610e4523e91199ac3a163af33512e37ef38885a0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 23:46:30 -0700 Subject: [PATCH] Keyboard Shortcuts: commands that need a value are not listed unless given a shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edit › Keyboard Shortcuts listed every engine command with a menu path, whatever its params. A shortcut runs a command with no params, so a key given to File › Document Setup › Units ran document.setUnits with {} and failed with "unknown units". The dialog and shortcuts.list now leave out a command with a menu path and no default shortcut when no item of the menu bar runs it without params, it opens no dialog when run without them, and its params doc names a required field. This leaves out 38 commands, among them the Graphic Styles, Swatches and character and paragraph style commands that take a name. A command whose shortcut the user has changed is still listed, where the key can be changed or cleared, and a saved key still runs and conflicts as before. Every command with a default shortcut stays listed. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/ui-egui/src/shortcut_editor.rs | 144 +++++++++++++++++++++++++- docs/mcp.md | 6 ++ docs/target-app-parity.md | 1 + 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index ccfd76550..83046a3b4 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -283,7 +283,7 @@ pub struct Entry { pub is_tool: bool, } -/// Every tool and menu command the editor lists. +/// Every tool, command and panel the editor can give a shortcut; [`listed`] says which it lists. pub fn entries() -> &'static [Entry] { static E: OnceLock> = OnceLock::new(); E.get_or_init(|| { @@ -335,6 +335,80 @@ fn runs_without_params(params: &str) -> bool { .is_some_and(|(fields, _)| fields.split(',').all(|f| f.trim().is_empty() || f.trim_end().ends_with('?'))) } +/// Does the editor list `e` under `overrides`? An entry that [`needs_a_value`] is listed only while +/// the user has changed its shortcut. +fn listed(overrides: &Overrides, e: &Entry) -> bool { + !needs_a_value(&e.key) || crate::keymap::has_override(overrides, &e.key) +} + +/// Is `key` a command with a menu path and no default shortcut that a key press can't run: no +/// item of the menu bar runs it without params, it opens no dialog when run without them (as the +/// Variables panel's commands do), and its params doc names a field a call must give (File › +/// Document Setup › Units, `{units}`)? +fn needs_a_value(key: &str) -> bool { + fn run_bare(items: &[menus::Item], out: &mut HashSet<&'static str>) { + for it in items { + match it { + menus::Item::Cmd(_, id, p) if p.as_object().is_none_or(|o| o.is_empty()) => { + out.insert(id); + } + menus::Item::Sub(_, children) => run_bare(children, out), + _ => {} + } + } + } + static KEYS: OnceLock> = OnceLock::new(); + KEYS.get_or_init(|| { + let mut bare = HashSet::new(); + for (_, items) in menus::menu_tree() { + run_bare(&items, &mut bare); + } + vectorcraft_engine::command_specs() + .iter() + .filter(|c| { + !c.menu.is_empty() + && c.shortcut.is_none() + && !bare.contains(c.id) + && !crate::dialogs::variables::opens(c.id) + && names_a_required_field(c.params) + }) + .map(|c| c.id) + .collect() + }) + .contains(key) +} + +/// Does a params doc name a field a call must give? Its fields are the top-level parts of its +/// first `{…}`. A field is optional when it is empty, starts with `…`, gives a default (`rows=10`) +/// or each of its names ends with `?` (`id?|ids?: [id…]`); `{index | indices}` names a required +/// field. A doc that does not start with `{` names none. +fn names_a_required_field(params: &str) -> bool { + let required = |f: &str| { + let f = f.trim(); + let end = f.find([':', '=', '(']).unwrap_or(f.len()); + !(f.is_empty() || f.starts_with('…') || f[end..].starts_with('=') || f[..end].split('|').all(|n| n.trim().ends_with('?'))) + }; + let Some(rest) = params.strip_prefix('{') else { return false }; + let (mut depth, mut quoted, mut start) = (0usize, false, 0); + for (i, ch) in rest.char_indices() { + match ch { + '"' => quoted = !quoted, + _ if quoted => {} + '{' | '[' | '(' => depth += 1, + '}' if depth == 0 => return required(&rest[start..i]), + '}' | ']' | ')' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + if required(&rest[start..i]) { + return true; + } + start = i + 1; + } + _ => {} + } + } + false +} + pub fn entry(key: &str) -> Option<&'static Entry> { entries().iter().find(|e| e.key == key) } @@ -510,6 +584,7 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option view.texts.get(i).cloned().flatten(), Cow::Owned(km) => effective_with(km, &e.key), @@ -1126,6 +1201,71 @@ mod tests { assert_eq!(effective_in(&ov, "symbol.breakLink").as_deref(), Some("Cmd+Alt+Shift+F9")); } + /// The Menu Commands rows the dialog draws for `query`. + fn menu_rows(app: &mut VectorcraftApp, query: &str) -> Vec { + open(app); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("tab".into(), json!("menu")); + d.fields.insert("query".into(), json!(query)); + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + // The modal draws nothing in its first frame, which only sizes it. + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui.ctx())); + out.textures_delta.clear(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui.ctx())); + out.textures_delta.clear(); + app.ui.dialog = None; + out.shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text().contains(" › ") => Some(t.galley.text().to_string()), + _ => None, + }) + .collect() + } + + /// A menu command whose params need a value (Units needs `{units}`) is not listed while it has + /// no shortcut. A shortcut the user gave it keeps it listed, where it can be changed or cleared. + #[test] + fn commands_that_need_a_value_are_listed_only_with_the_users_shortcut() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + let list = run_command(&mut app, "shortcuts.list", &json!({"query": "document.setUnits"})).unwrap().unwrap(); + assert_eq!(list, json!([])); + assert!(!menu_rows(&mut app, "units").iter().any(|r| r.ends_with(" › Units"))); + // A key saved for it before stays listed. + crate::keymap::set_override(&mut app.ui.shortcut_overrides, "document.setUnits", "Cmd+Alt+Shift+F12"); + let list = run_command(&mut app, "shortcuts.list", &json!({"query": "document.setUnits"})).unwrap().unwrap(); + let e = &list[0]; + assert_eq!( + (e["id"].as_str(), e["shortcut"].as_str(), e["overridden"].as_bool()), + (Some("document.setUnits"), Some("Cmd+Alt+Shift+F12"), Some(true)) + ); + assert!(menu_rows(&mut app, "units").iter().any(|r| r == "File › Document Setup › Units")); + // Every entry with a default shortcut is listed, and so are commands a key press runs: one + // whose params are all optional, one the menu bar runs without params (Rotate… opens its + // dialog) and one that opens a dialog for its values (New Variable). + let none = Overrides::new(); + let dropped: Vec<&str> = entries().iter().filter(|e| default_of(&e.key).is_some() && !listed(&none, e)).map(|e| e.key.as_str()).collect(); + assert!(dropped.is_empty(), "{dropped:?}"); + for id in ["edit.duplicate", "object.pathfinder.unite", "object.rotate", "variable.define"] { + assert!(entry(id).is_some_and(|e| listed(&none, e)), "{id}"); + } + for (doc, required) in [ + ("{}", false), + ("{units: \"Points\"|\"Picas\"} the units", true), + ("{ids?: [id…] (selected first), dx?, dy?}", false), + ("{name? (trimmed, as `to`), attrs?: {…}}", false), + ("{columns=10, rows=10 (whole numbers)}", false), + ("{index | indices: [index…]}", true), + ("{index?: n | indices?: [n…]}", false), + ("{…as document.save} {path?}", false), + ("{find, replace?: \"\"}", true), + ("same as file.save", false), + ] { + assert_eq!(names_a_required_field(doc), required, "{doc}"); + } + } + /// #2991: Enter Isolation Mode has no default key but runs on the selection, so it belongs in /// Edit › Keyboard Shortcuts. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index e4cd6d637..5efcf1231 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2732,6 +2732,12 @@ the Align To choice, and while there is a key object or a key guide they send th `spacing`. Edit › Keyboard Shortcuts lists them under Align, with no default shortcut, and `shortcuts.set {id, shortcut}` gives one a shortcut. +Edit › Keyboard Shortcuts and `shortcuts.list` leave out a command with a menu path and no default shortcut when its +params doc has a required field (one with no `?` and no `=` default), no item of the menu bar runs it without params, +and it opens no dialog when run without them, such as `document.setUnits {units}` (File › Document Setup › Units). +They list such a command while the user has changed its shortcut (`overridden: true` in `shortcuts.list`), and +`shortcuts.set` can change or clear it. + The desktop app adds its active artboard, the one the Artboards panel highlights, to the `object.align`, `object.distribute`, `object.distributeSpacing` and `select.allOnArtboard` calls it runs without `artboard`, as it does for `edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack`, and to each `command.batch`. A batch passes its diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5439f0edc..ffead3844 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -616,6 +616,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Cmd/Ctrl+Alt+Shift+U (`document.switchUnits`) switches the document to the next unit of measurement, in one undo step, on the key 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 switching units of measurement. The page does not give an order. VectorCraft steps through the units in the order a right-click on a ruler lists them (Points, Picas, Inches, Millimeters, Centimeters, Pixels, Feet & Inches, Meters, Yards, Feet) and goes from Feet back to Points. - Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`). - On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one. + - Edit › Keyboard Shortcuts lists items of the menu bar and panel menus, as the reference app's [Customize keyboard shortcuts](https://web.archive.org/web/20221205131209id_/https://helpx.adobe.com/illustrator/using/customizing-keyboard-shortcuts.html) page describes for its Menu Commands. A menu command with no default shortcut that needs a value a key press does not give, such as File › Document Setup › Units (`document.setUnits {units}`), is listed only while the user has changed its shortcut. - On Windows a maximized window stays inside the work area with the taskbar on any edge (#3242, #3723). - On Windows a maximized window stops one pixel short of each monitor edge where an auto-hidden taskbar sits. - **Tools:**