mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Appearance: Cmd+/ adds a new fill and Cmd+Alt+/ adds a new stroke
The reference app adds a new fill with Ctrl+/ on Windows and Command+/ on macOS, and it adds a new stroke with Ctrl+Alt+/ on Windows and Option+Command+/ on macOS. VectorCraft had Add New Fill and Add New Stroke on the Appearance panel, with no default key for either. appearance.addFill now has the default Cmd+/ and appearance.addStroke has Cmd+Alt+/, which are Ctrl+/ and Ctrl+Alt+/ on Windows and Linux. Like the panel's buttons and menu items, each key runs its command with no params and adds the item to every selected object. Keyboard Shortcuts lists both under Window › Appearance with their keys, and press_key runs them for agents. Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
This commit is contained in:
@@ -37,7 +37,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"appearance.addFill",
|
||||
"Add New Fill",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
Some("Cmd+/"),
|
||||
"{ids?|id?, target?: \"object\"|\"contents\"} add a fill on top of each selected object's own stack, on an image right below its Contents row (a copy of its top fill; type without one: its characters' fill; else the default fill)",
|
||||
has_doc,
|
||||
|s, p| add_item(s, p, true)
|
||||
@@ -46,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"appearance.addStroke",
|
||||
"Add New Stroke",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
Some("Cmd+Alt+/"),
|
||||
"{ids?|id?, target?} add a stroke on top of each selected object's own stack, on an image right below its Contents row (a copy of its top stroke, else the default stroke)",
|
||||
has_doc,
|
||||
|s, p| add_item(s, p, false)
|
||||
|
||||
@@ -1184,6 +1184,31 @@ fn cmd_shift_x_resets_horizontal_scale_for_agents() {
|
||||
assert_eq!(runs[0]["style"]["h_scale"], json!(100.0), "{runs}");
|
||||
}
|
||||
|
||||
/// `press_key` runs Cmd+/ and Cmd+Alt+/ for agents: a new fill and then a new stroke go on top of
|
||||
/// the selected rectangle's appearance stack.
|
||||
#[test]
|
||||
fn cmd_slash_and_cmd_alt_slash_add_a_fill_and_a_stroke_for_agents() {
|
||||
let mut s = server();
|
||||
let r = call(&mut s, 1, "run_command", json!({"command": "shape.rectangle", "params": {"x": 10, "y": 10, "width": 100, "height": 100}}));
|
||||
let id = serde_json::from_str::<Value>(&text_of(&r)).unwrap()["id"].as_u64().unwrap();
|
||||
let r = call(&mut s, 2, "run_command", json!({"command": "select.set", "params": {"ids": [id]}}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
let kinds = |s: &mut Server, n: u64| {
|
||||
let r = call(s, n, "run_command", json!({"command": "effect.list", "params": {}}));
|
||||
let items = serde_json::from_str::<Value>(&text_of(&r)).unwrap()["applied"][0]["items"].clone();
|
||||
items.as_array().unwrap().iter().map(|it| it["kind"].as_str().unwrap().to_string()).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(kinds(&mut s, 3), ["fill", "stroke"]);
|
||||
let r = call(&mut s, 4, "press_key", json!({"key": "/", "mods": {"cmd": true}}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
assert_eq!(serde_json::from_str::<Value>(&text_of(&r)).unwrap()["command"], "appearance.addFill", "{r}");
|
||||
assert_eq!(kinds(&mut s, 5), ["fill", "stroke", "fill"]);
|
||||
let r = call(&mut s, 6, "press_key", json!({"key": "/", "mods": {"cmd": true, "alt": true}}));
|
||||
assert_eq!(r["isError"], false, "{r}");
|
||||
assert_eq!(serde_json::from_str::<Value>(&text_of(&r)).unwrap()["command"], "appearance.addStroke", "{r}");
|
||||
assert_eq!(kinds(&mut s, 7), ["fill", "stroke", "fill", "stroke"]);
|
||||
}
|
||||
|
||||
/// The drawing tools snap to Smart Guides over MCP as with the mouse (#506): a Rectangle drawn
|
||||
/// from near another object's corner starts on it, and a Pen anchor placed beside that object's
|
||||
/// centre lines up with it.
|
||||
|
||||
@@ -102,6 +102,106 @@ fn stroke_panel_shows_the_active_rows_weight() {
|
||||
assert!(!frame_texts(&mut app, stroke::show).contains(&six));
|
||||
}
|
||||
|
||||
/// Cmd+/ is Add New Fill and Cmd+Alt+/ is Add New Stroke by default. On Windows and Linux the keys
|
||||
/// are Ctrl+/ and Ctrl+Alt+/, and on macOS they are Command+/ and Option+Command+/. No other
|
||||
/// command, tool or panel has either key by default.
|
||||
#[test]
|
||||
fn cmd_slash_is_add_new_fill_and_cmd_alt_slash_add_new_stroke() {
|
||||
use crate::shortcut_editor::{command_for_key, default_command_shortcut};
|
||||
let mac = cfg!(target_os = "macos");
|
||||
let (cmd, alt) = (egui::Modifiers::COMMAND, egui::Modifiers::ALT);
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
for (id, key, label, mods, shown) in [
|
||||
("appearance.addFill", "Cmd+/", "Add New Fill", cmd, if mac { "⌘/" } else { "Ctrl+/" }),
|
||||
("appearance.addStroke", "Cmd+Alt+/", "Add New Stroke", cmd | alt, if mac { "⌥⌘/" } else { "Ctrl+Alt+/" }),
|
||||
] {
|
||||
assert_eq!(default_command_shortcut(id), Some(key));
|
||||
assert_eq!(command_for_key(key), Some(id));
|
||||
let chord = crate::shortcuts::parse(key).expect("parses");
|
||||
assert_eq!((chord.modifiers, chord.logical_key), (mods, egui::Key::Slash));
|
||||
assert_eq!(crate::menus::pretty_shortcut(key), shown);
|
||||
let listed = app.run("shortcuts.list", json!({ "query": id })).unwrap();
|
||||
let rows: Vec<_> = listed.as_array().expect("rows").iter().map(|r| (r["label"].clone(), r["group"].clone(), r["default"].clone())).collect();
|
||||
assert_eq!(rows, [(json!(label), json!("Window › Appearance"), json!(key))], "Keyboard Shortcuts lists {id}");
|
||||
}
|
||||
assert_eq!(app.run("shortcuts.conflicts", json!({})).unwrap(), json!([]), "no other default has either key");
|
||||
}
|
||||
|
||||
/// Cmd+/ and Cmd+Alt+/, pressed through the shortcut handler, add a fill and a stroke to the
|
||||
/// selected objects as the Appearance panel's Add New Fill and Add New Stroke buttons do, on a path
|
||||
/// and on type. With nothing selected the buttons are off, and the keys leave the objects as they are.
|
||||
#[test]
|
||||
fn cmd_slash_adds_a_fill_and_cmd_alt_slash_a_stroke_as_the_panel_buttons_do() {
|
||||
fn press(app: &mut VectorcraftApp, modifiers: egui::Modifiers) {
|
||||
let ctx = egui::Context::default();
|
||||
let ev = egui::Event::Key { key: egui::Key::Slash, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
let mut out = ctx.run_ui(egui::RawInput { events: vec![ev], ..Default::default() }, |ui| crate::shortcuts::handle(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
/// The Appearance panel's button named `label`: the center of its rect and whether it is on.
|
||||
fn button(ctx: &egui::Context, app: &mut VectorcraftApp, label: &str) -> (egui::Pos2, bool) {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| appearance::show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
|
||||
let (_, n) = nodes
|
||||
.iter()
|
||||
.find(|(_, n)| n.label() == Some(label) && n.role() == egui::accesskit::Role::Button)
|
||||
.unwrap_or_else(|| panic!("no button named {label:?}"));
|
||||
let b = n.bounds().expect("bounds");
|
||||
(egui::pos2((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5), !n.is_disabled())
|
||||
}
|
||||
/// A rectangle and a line of type, with the rectangle (`type_too`: both) selected.
|
||||
fn two_objects(type_too: bool) -> (VectorcraftApp, Vec<Value>) {
|
||||
let mut app = app_with_rect();
|
||||
let rect = first_selected(&app).expect("the rectangle").id.0;
|
||||
let text = run(&mut app, "text.create", json!({"x": 10, "y": 150, "text": "Hi"}))["id"].clone();
|
||||
let ids = if type_too { vec![json!(rect), text.clone()] } else { vec![json!(rect)] };
|
||||
run(&mut app, "select.set", json!({ "ids": ids }));
|
||||
(app, vec![json!(rect), text])
|
||||
}
|
||||
let stacks = |app: &VectorcraftApp, ids: &[Value]| -> Vec<vectorcraft_doc::Appearance> {
|
||||
let d = &app.session.active().expect("a document").doc;
|
||||
ids.iter().map(|id| (*d.node(vectorcraft_doc::NodeId(id.as_u64().expect("an id"))).expect("the object").appearance).clone()).collect()
|
||||
};
|
||||
let cmd = egui::Modifiers::COMMAND;
|
||||
for (type_too, mods, label, fill) in [
|
||||
(false, cmd, "Add New Fill", true),
|
||||
(false, cmd | egui::Modifiers::ALT, "Add New Stroke", false),
|
||||
(true, cmd, "Add New Fill", true),
|
||||
(true, cmd | egui::Modifiers::ALT, "Add New Stroke", false),
|
||||
] {
|
||||
let (mut keyed, ids) = two_objects(type_too);
|
||||
let before = stacks(&keyed, &ids);
|
||||
press(&mut keyed, mods);
|
||||
let (mut clicked, _) = two_objects(type_too);
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
let (at, on) = button(&ctx, &mut clicked, label);
|
||||
assert!(on, "{label} is on with a selection");
|
||||
click(&ctx, &mut clicked, at, appearance::show);
|
||||
let after = stacks(&keyed, &ids);
|
||||
assert_eq!(after, stacks(&clicked, &ids), "{label}: the key adds what the button adds");
|
||||
let selected = if type_too { 2 } else { 1 };
|
||||
for (i, (a, b)) in after.iter().zip(&before).enumerate() {
|
||||
let added = if i < selected { 1 } else { 0 };
|
||||
assert_eq!(a.items.len(), b.items.len() + added, "{label}, object {i}: {a:?}");
|
||||
if added == 1 {
|
||||
assert_eq!(a.items.last().map(|it| it.is_fill()), Some(fill), "{label}: on top of the stack");
|
||||
}
|
||||
}
|
||||
}
|
||||
// With nothing selected, both buttons are off and neither key changes the objects.
|
||||
let (mut app, ids) = two_objects(true);
|
||||
run(&mut app, "select.none", json!({}));
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
assert!(!button(&ctx, &mut app, "Add New Fill").1 && !button(&ctx, &mut app, "Add New Stroke").1);
|
||||
let before = stacks(&app, &ids);
|
||||
press(&mut app, cmd);
|
||||
press(&mut app, cmd | egui::Modifiers::ALT);
|
||||
assert_eq!(stacks(&app, &ids), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparency_panel_reads_the_active_item() {
|
||||
let mut app = app_with_rect();
|
||||
|
||||
+2
-1
@@ -415,7 +415,8 @@ mode. To press the key during a drag, end one
|
||||
With nothing being dragged, `e` chooses the tool bound to E.
|
||||
|
||||
Appearance stacks: an object can carry several fills and strokes (`appearance.addFill`, `appearance.addStroke`), indexed
|
||||
in paint order (0 is painted first, the bottom row of the Appearance panel). `paint.setFill`, `paint.setStroke`,
|
||||
in paint order (0 is painted first, the bottom row of the Appearance panel). Cmd+/ runs `appearance.addFill` and
|
||||
Cmd+Alt+/ runs `appearance.addStroke` (`press_key {key: "/", mods: {cmd: true, alt: true}}`). `paint.setFill`, `paint.setStroke`,
|
||||
`stroke.set`, `stroke.setAdvanced`, `paint.editGradient`, `paint.setGradientGeom` and `transparency.set` take
|
||||
`item` to edit one of them; `run_command appearance.setActiveItem {"index": n}` makes that row the target of later
|
||||
calls that omit `item` (as clicking the row in the Appearance panel does) until the selection changes; the proxy of
|
||||
|
||||
@@ -512,6 +512,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- 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).
|
||||
- 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.
|
||||
- Cmd/Ctrl+Alt+Q (`type.resetTracking`) sets the tracking of the selected type, or of the text the Type tool has selected, to 0 and its kerning to Auto (0 in the Character panel), in one undo step; at a caret it resets the kerning of the character before it.
|
||||
- Cmd/Ctrl+Shift+X (`type.resetHorizontalScale`) sets the horizontal scale of the selected type, or of the text the Type tool has selected, to 100% as the Character panel shows it (the object's own stretch included), in one undo step; at a caret it leaves the text as it is.
|
||||
|
||||
@@ -189,6 +189,9 @@ tools, never checked side by side) isn't captured row by row.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
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.
|
||||
|
||||
~110 distinct modified shortcuts on menu commands plus single-key tool shortcuts for the default toolbar
|
||||
(`crates/tools/src/catalog.rs`), a shortcut editor with chords, and saved sets. Not yet diffed item by item
|
||||
against Illustrator's published default shortcut list; estimated ~70% of it. Remaining: the panel shortcuts,
|
||||
|
||||
Reference in New Issue
Block a user