mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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