From e93b672b3bf73a15417f235cb854f94536c46285 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:36:59 -0800 Subject: [PATCH] Keyboard Shortcuts: panel menu commands with typed optional params can take a shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An undoable command without a menu path or a default key was listed only when every field of its params doc ended with `?`. A typed optional field such as `on?: bool`, `ids?: [id…]` or `artboard?: index=0` counted as required, so Fractional Widths, System Layout, Align to Pixel Grid, Embed Image, Update Links, Delete Slices, Switch Orientation, Distribute Spacing for artboards, Target Contents, Invert and other panel menu commands that act on the selection could not be given a shortcut. `runs_without_params` now reads the params doc with `names_a_required_field`, the parser the editor already uses for menu commands. The commands the new rule would list that set nothing, fail or only give data back when a key runs them with no params (the panel setters such as Fill and Character, the exports and saves without a path, the undo group's begin and end, Create Dimension) join `NEEDS_A_PARAM`, so they stay out of the list until the user gives one a key. New Symbol joins it too, since the Symbols panel runs it through Symbol Options. docs/mcp.md describes the rule and the full `NEEDS_A_PARAM` list. --- crates/ui-egui/src/shortcut_editor.rs | 107 +++++++++++++++++++++++--- docs/mcp.md | 26 ++++++- 2 files changed, 121 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 4e02180017..26606c2763 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -765,12 +765,10 @@ fn chosen_path(items: &[(String, Value)]) -> Option<&str> { items.iter().find(|(_, p)| is_bare(p)).or(items.first()).map(|(path, _)| path.as_str()) } -/// Does a command's params doc (`{a?, b?} …`) say it runs with no params, as a shortcut runs it? +/// Does a command's params doc (`{a?, b?: bool} …`) say it runs with no params, as a shortcut runs +/// it? It does when it starts with `{` and [`names_a_required_field`] finds no field a call must give. fn runs_without_params(params: &str) -> bool { - params - .strip_prefix('{') - .and_then(|r| r.split_once('}')) - .is_some_and(|(fields, _)| fields.split(',').all(|f| f.trim().is_empty() || f.trim_end().ends_with('?'))) + params.starts_with('{') && !names_a_required_field(params) } /// Does the editor list `e` under `overrides`? An entry that [`needs_a_value`] is listed only while @@ -783,8 +781,13 @@ fn listed(overrides: &Overrides, e: &Entry) -> bool { /// with none, and that no item of the menu bar runs: Transparency and Opacity Mask Options need /// a value to set, Align and Distribute a direction, Delete Swatch and Move Swatch a swatch name, /// and the library, data set, text style and SVG filter loaders a file, which their menu items ask -/// for. -const NEEDS_A_PARAM: [&str; 20] = [ +/// for. The commands after them have no menu path: the setters of panels and dialogs (Fill, Stroke +/// Options, Character, Set Bounds), the exports and saves that give the file back without a `path` +/// (Save Document gives the file back when the document has no file yet; File › Save takes a key +/// instead), the undo group's begin and end, and the commands that need a point, a value or a name +/// (Create Dimension, Edit Freeform Point, Select by Color, Move Brush). New Symbol is left out +/// because the Symbols panel runs it through the Symbol Options dialog. +const NEEDS_A_PARAM: [&str; 74] = [ "transparency.set", "transparency.setOpacityMask", "stroke.set", @@ -805,6 +808,60 @@ const NEEDS_A_PARAM: [&str; 20] = [ "paraStyle.load", "textStyle.loadAll", "svgFilter.import", + "addon.pathtools.setAnchor", + "addon.shapes.configure", + "appearance.addEffect", + "artboard.align", + "artboard.distribute", + "assets.export", + "assets.export3D", + "assets.settings.set", + "attributes.set", + "brush.move", + "css.export", + "dimension.create", + "document.export", + "document.exportDxf", + "document.exportEps", + "document.exportForWeb", + "document.exportPdf", + "document.save", + "document.setup", + "edit.undoGroup.begin", + "edit.undoGroup.end", + "eyedropper.setOptions", + "file.package", + "library.createGroup", + "links.placementOptions", + "links.unembed", + "livePaint.gapOptions", + "object.blend.createSpine", + "object.blend.setEasing", + "object.intertwine.arrange", + "object.objectsOnPath.options", + "object.puppetWarp.pins", + "object.setBounds", + "object.setLiveShape", + "object.setOverprint", + "object.setProps", + "object.slice.select", + "paint.freeform.setPoint", + "paint.setFill", + "paint.setGradientGeom", + "paint.setStroke", + "perspective.grid.define", + "perspective.grid.set", + "perspective.plane.move", + "print.setup", + "select.color", + "select.globalEditOptions", + "stroke.setAdvanced", + "symbol.new", + "symbol.options", + "text.setFormat", + "text.setRangeStyle", + "text.setStyle", + "type.ruby", ]; /// The (label, params) of each menu bar item, by command id. @@ -907,7 +964,7 @@ fn a_menu_bar_item_runs(id: &str, params: &str) -> bool { /// Is engine command `c` listed in Edit › Keyboard Shortcuts whether or not an item of the menu bar /// runs it? A command with a menu path of its own is. One without (a panel menu item) is when a key /// alone can run it: it has a default key, or it is an undoable command, not an alias, whose params -/// doc ends each field with `?`. +/// doc names no field a call must give ([`runs_without_params`]). fn listed_on_its_own(c: &vectorcraft_engine::CommandSpec) -> bool { !c.menu.is_empty() || c.shortcut.is_some() || (c.journal && !vectorcraft_engine::cmd::is_alias(c.id) && runs_without_params(c.params)) } @@ -2309,11 +2366,39 @@ mod tests { for id in ["brush.duplicate", "path.convertAnchors", "document.json", "prefs.list"] { assert!(entry(id).is_none(), "{id}"); } - for (doc, bare) in - [("{}", true), ("{dx?, dy?} x", true), ("{id?|ids?, clip?}", true), ("{name, newName?}", false), ("{a?: {b}}", false), ("same as", false)] - { + // The Symbols panel runs New Symbol through Symbol Options, so it waits for a key the user gives it. + assert!(entry("symbol.new").is_some_and(|e| !listed(&Overrides::new(), &e))); + for (doc, bare) in [ + ("{}", true), + ("{dx?, dy?} x", true), + ("{id?|ids?, clip?}", true), + ("{name, newName?}", false), + ("{a?: {b}}", true), + ("{ids?|id?, on?: bool} turn it on or off", true), + ("{ids?: [id…] (the selection), anchors?: [index…]}", true), + ("{format?: a|b, size?: 1|3|5}", true), + ("{artboard?: index=0}", true), + ("{mode: a|b, on?: bool}", false), + ("same as", false), + ] { assert_eq!(runs_without_params(doc), bare, "{doc}"); } + // Panel menu commands whose params doc gives each optional field a type are listed too. + let none = Overrides::new(); + for id in [ + "type.fractionalWidths", + "type.systemLayout", + "object.setPixelAligned", + "links.embed", + "links.update", + "object.slice.delete", + "artboard.switchOrientation", + "appearance.targetContents", + "paint.invert", + "artboard.distributeSpacing", + ] { + assert!(entry(id).is_some_and(|e| listed(&none, &e)), "{id}"); + } let mut ov = Overrides::new(); assert!(assign(&mut ov, "symbol.breakLink", Some("Cmd+Alt+Shift+F9"), true).is_ok()); assert_eq!(effective_in(&ov, "symbol.breakLink").as_deref(), Some("Cmd+Alt+Shift+F9")); diff --git a/docs/mcp.md b/docs/mcp.md index 84c75a81d0..7a0d5192b9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5616,6 +5616,15 @@ tool's active artboard in place, and the Distribute buttons then send the Distri can then be negative. Align to Selection sends `to: "selection"`. The Distribute buttons need two or more selected artboards with Align to Artboard, and three or more with Align to Selection. +Edit › Keyboard Shortcuts and `shortcuts.list` list an undoable command that is not an alias and has no menu path or +default shortcut, such as a panel menu's Break Link to Symbol (`symbol.breakLink`), when its params doc names no +required field and it is not one of the commands left out below. A field is optional when each of its names ends with +`?`, whatever type follows it (`on?: bool`, `ids?: [id…]`), when its name is followed directly by an `=` default +(`rows=10`), or when it starts with `…`. Such a command is listed in the group named by the first part of its id. +Commands listed this way include `type.fractionalWidths`, `type.systemLayout`, `object.setPixelAligned`, +`links.embed`, `links.update`, `object.slice.delete`, `artboard.switchOrientation`, `artboard.distributeSpacing`, +`appearance.targetContents` and `paint.invert`. + Edit › Keyboard Shortcuts and `shortcuts.list` leave out a command with no default shortcut when a key press cannot run it: no item of the menu bar runs it, it opens no dialog when run without params, and it is one of these: @@ -5625,7 +5634,22 @@ run it: no item of the menu bar runs it, it opens no dialog when run without par - a command that sets nothing or fails without a param: `transparency.set`, `transparency.setOpacityMask`, `stroke.set`, `layer.setProps`, `object.align`, `object.distribute`, `swatch.delete`, `swatch.move`, `swatch.spotOptions`, `edit.colors.adjustBalance`, `swatch.library.load`, `graphicStyle.loadLibrary`, - `symbol.loadLibrary`, `brush.loadLibrary`, `dataset.import` and `variable.loadLibrary`. + `symbol.loadLibrary`, `brush.loadLibrary`, `dataset.import`, `variable.loadLibrary`, `charStyle.load`, + `paraStyle.load`, `textStyle.loadAll` and `svgFilter.import`; +- a command without a menu path that sets nothing, fails or only gives data back without a param: + `addon.pathtools.setAnchor`, `addon.shapes.configure`, `appearance.addEffect`, `artboard.align`, + `artboard.distribute`, `assets.export`, `assets.export3D`, `assets.settings.set`, + `attributes.set`, `brush.move`, `css.export`, `dimension.create`, `document.export`, `document.exportDxf`, + `document.exportEps`, `document.exportForWeb`, `document.exportPdf`, `document.save`, `document.setup`, + `edit.undoGroup.begin`, `edit.undoGroup.end`, `eyedropper.setOptions`, `file.package`, `library.createGroup`, + `links.placementOptions`, `links.unembed`, `livePaint.gapOptions`, `object.blend.createSpine`, + `object.blend.setEasing`, `object.intertwine.arrange`, `object.objectsOnPath.options`, `object.puppetWarp.pins`, + `object.setBounds`, `object.setLiveShape`, `object.setOverprint`, `object.setProps`, `object.slice.select`, + `paint.freeform.setPoint`, `paint.setFill`, `paint.setGradientGeom`, `paint.setStroke`, `perspective.grid.define`, + `perspective.grid.set`, `perspective.plane.move`, `print.setup`, `select.color`, `select.globalEditOptions`, + `stroke.setAdvanced`, `symbol.options`, `text.setFormat`, `text.setRangeStyle`, `text.setStyle` and `type.ruby`; +- `symbol.new`, which the Symbols panel's New Symbol button and its New Symbol… menu item run through the Symbol + Options dialog. 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. When every menu bar item that runs a command gives it the same params, a