Merge pull request #3341 from dotCooCoo/feat/appearance-new-fill-and-stroke-keys

Appearance: Cmd+/ adds a new fill and Cmd+Alt+/ adds a new stroke
This commit is contained in:
BFlat
2026-10-21 16:15:14 +07:00
committed by GitHub
6 changed files with 133 additions and 3 deletions
+2 -2
View File
@@ -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)
+25
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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.
+3
View File
@@ -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,