Keyboard Shortcuts: each Align and Distribute button of the Align panel can be given a key

Squashed from #3317 onto main.
This commit is contained in:
bflatastic
2026-10-21 23:38:02 +07:00
parent 5e24e59bd3
commit 9c20027778
6 changed files with 350 additions and 24 deletions
+92
View File
@@ -224,6 +224,96 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("type.alignRight", "Align right", "Cmd+Shift+R", "{} align the selected type's paragraphs (or those the Type tool's selection touches) right"),
("type.justifyLeft", "Justify with last line aligned left", "Cmd+Shift+J", "{} justify the selected type's paragraphs, last line aligned left"),
("type.justifyAll", "Justify all lines", "Cmd+Shift+F", "{} justify every line of the selected type's paragraphs"),
(
"align.horizontalLeft",
"Horizontal Align Left",
"",
"{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.horizontalCenter",
"Horizontal Align Center",
"",
"{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.horizontalRight",
"Horizontal Align Right",
"",
"{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.horizontalVerticalCenter",
"Horizontal & Vertical Align Center",
"",
"{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.verticalTop",
"Vertical Align Top",
"",
"{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.verticalCenter",
"Vertical Align Center",
"",
"{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.verticalBottom",
"Vertical Align Bottom",
"",
"{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
),
(
"align.distributeVerticalTop",
"Vertical Distribute Top",
"",
"{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeVerticalCenter",
"Vertical Distribute Center",
"",
"{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeVerticalBottom",
"Vertical Distribute Bottom",
"",
"{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeHorizontalLeft",
"Horizontal Distribute Left",
"",
"{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeHorizontalCenter",
"Horizontal Distribute Center",
"",
"{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeHorizontalRight",
"Horizontal Distribute Right",
"",
"{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeVerticalSpace",
"Vertical Distribute Space",
"",
"{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"align.distributeHorizontalSpace",
"Horizontal Distribute Space",
"",
"{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
),
(
"type.bold",
"Bold",
@@ -1248,6 +1338,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
id if let Some((_, justify)) = crate::panels::paragraph::ALIGN_COMMANDS.iter().find(|c| c.0 == id) => {
crate::panels::paragraph::align(app, justify)
}
id if let Some(b) = crate::panels::align::button(id) => crate::panels::align::run_button(app, b),
"view.gradientAnnotator" => flag(&mut app.ui.view.gradient_annotator),
"view.livePaintGaps" => flag(&mut app.ui.view.live_paint_gaps),
"effect.last" => match app.last_effect.clone() {
@@ -2304,6 +2395,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"file.save" => app.session.active().is_some_and(|d| d.path.is_none() || d.converted || d.is_dirty()),
"type.bold" | "type.italic" => crate::panels::character::text_style(app).is_some(),
id if crate::panels::paragraph::ALIGN_COMMANDS.iter().any(|c| c.0 == id) => crate::panels::character::text_style(app).is_some(),
id if let Some(b) = crate::panels::align::button(id) => enabled(app, b.command()),
"file.reveal" => app.services.reveal.is_some() && app.session.active().is_some_and(|d| d.path.is_some()),
"file.cancelExport" | "file.exportInBackground" => app.background.jobs.iter().any(|j| j.progress.is_some() && !j.cancelled()),
"file.place"
+236 -23
View File
@@ -1,5 +1,6 @@
//! Align panel: Align Objects, Distribute Objects and Distribute Spacing (both with the spacing
//! value when aligning to a key object) and Align To (selection / key object / artboard).
//! value when aligning to a key object or a key guide) and Align To (selection / key object /
//! artboard).
use egui::Ui;
use serde::Serialize;
@@ -51,9 +52,9 @@ pub fn align_params(base: Value, to: AlignTo, n_selected: usize) -> Value {
}
/// `object.align` params `base` with the `to` that the align buttons of the Align panel, the Control
/// bar and the Properties panel send: the artboard for a single selected object, else the key object
/// while there is one, else the Align To choice. Object → Align sends it too while every selected
/// object is selected whole ([`crate::menus::invoke`]).
/// bar and the Properties panel send: the key while there is one (a key object, or a key guide:
/// #2143), else the artboard for a single selected object, else the Align To choice. Object → Align
/// sends it too while every selected object is selected whole ([`crate::menus::invoke`]).
pub fn with_align_to(app: &VectorcraftApp, base: Value) -> Value {
align_params(base, align_to(app), objects_aligned(app))
}
@@ -88,6 +89,85 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [
("dc-dist-right", "Horizontal Distribute Right", "horizontal", "right"),
];
/// What a button of the Align panel's Align Objects, Distribute Objects and Distribute Spacing
/// rows runs ([`run_button`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Button {
/// An Align Objects button: `object.align {axis: value}`.
Align(&'static str, &'static str),
/// Horizontal & Vertical Align Center: `object.align {horizontal: "center", vertical: "center"}`.
AlignCenters,
/// A Distribute Objects button: `object.distribute {axis: value}`.
Distribute(&'static str, &'static str),
/// A Distribute Spacing button: `object.distributeSpacing {axis}`.
Spacing(&'static str),
}
impl Button {
/// The engine command the button runs.
pub(crate) fn command(self) -> &'static str {
match self {
Button::Align(..) | Button::AlignCenters => "object.align",
Button::Distribute(..) => "object.distribute",
Button::Spacing(_) => "object.distributeSpacing",
}
}
}
/// The Align panel's buttons as UI commands ([`crate::menus::UI_COMMANDS`]), in the panel's order:
/// (command id, button). Edit › Keyboard Shortcuts lists each under Align with its button's label,
/// with no default shortcut, and a shortcut runs what a click on the button runs.
pub const COMMANDS: [(&str, Button); 15] = [
("align.horizontalLeft", Button::Align("horizontal", "left")),
("align.horizontalCenter", Button::Align("horizontal", "center")),
("align.horizontalRight", Button::Align("horizontal", "right")),
("align.horizontalVerticalCenter", Button::AlignCenters),
("align.verticalTop", Button::Align("vertical", "top")),
("align.verticalCenter", Button::Align("vertical", "center")),
("align.verticalBottom", Button::Align("vertical", "bottom")),
("align.distributeVerticalTop", Button::Distribute("vertical", "top")),
("align.distributeVerticalCenter", Button::Distribute("vertical", "center")),
("align.distributeVerticalBottom", Button::Distribute("vertical", "bottom")),
("align.distributeHorizontalLeft", Button::Distribute("horizontal", "left")),
("align.distributeHorizontalCenter", Button::Distribute("horizontal", "center")),
("align.distributeHorizontalRight", Button::Distribute("horizontal", "right")),
("align.distributeVerticalSpace", Button::Spacing("vertical")),
("align.distributeHorizontalSpace", Button::Spacing("horizontal")),
];
/// The button of Align panel command `id` ([`COMMANDS`]), which [`run_button`] runs and which is
/// enabled while the engine command it runs is. `None`: `id` is not one of them.
pub(crate) fn button(id: &str) -> Option<Button> {
COMMANDS.iter().find(|c| c.0 == id).map(|c| c.1)
}
/// Runs what a click on button `b` runs. An Align Objects button sends the `to` of
/// [`with_align_to`]. A Distribute Objects or Distribute Spacing button sends the key while there
/// is one, else the Align To choice, and the Distribute Spacing value while there is a key.
pub fn run_button(app: &mut VectorcraftApp, b: Button) -> Result<Value, String> {
let to = align_to(app).param();
let p = match b {
Button::Align(axis, v) => with_align_to(app, json!({axis: v})),
Button::AlignCenters => with_align_to(app, json!({"horizontal": "center", "vertical": "center"})),
Button::Distribute(axis, v) => {
let mut p = json!({axis: v, "to": to});
if let Some(g) = spacing_sent(app) {
p["spacing"] = json!(g);
}
p
}
Button::Spacing(axis) => json!({"axis": axis, "spacing": spacing_sent(app), "to": to}),
};
app.run(b.command(), p)
}
/// The Distribute Spacing value ([`crate::UiState::align_spacing`]) while there is a key object
/// or a key guide (#2143), which the Distribute Objects and Distribute Spacing buttons then send;
/// else none.
fn spacing_sent(app: &VectorcraftApp) -> Option<f64> {
(align_to(app) == AlignTo::Key).then_some(app.ui.align_spacing)
}
/// The six align buttons of the Control bar and the Properties panel, `size` points square.
/// They align to what the Align panel's Align To says (the key object while there is one), as
/// its own buttons do (#789).
@@ -133,10 +213,10 @@ fn topmost(app: &VectorcraftApp) -> Option<vectorcraft_doc::NodeId> {
app.session.active().and_then(|st| st.selection.in_paint_order(&st.doc).last().copied())
}
/// What Align aligns to: the key object while there is one (a click on an object of the
/// selection makes it the key, and Align to Key Object makes the topmost one the key), else the
/// stored Align To choice, Selection or Artboard. The Distribute Objects and Distribute Spacing
/// buttons send it as their `to`.
/// What Align aligns to: the key object or the key guide (#2143) while there is one (a click on
/// an object of the selection makes it the key, and Align to Key Object makes the topmost one the
/// key, or a guide selected with a single object), else the stored Align To choice, Selection or
/// Artboard. The Distribute Objects and Distribute Spacing buttons send it as their `to`.
fn align_to(app: &VectorcraftApp) -> AlignTo {
if has_key(app) { AlignTo::Key } else { app.ui.align_to }
}
@@ -146,9 +226,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let to = align_to(app);
// `spacing` is the Distribute Spacing value. The Distribute Objects and Distribute Spacing
// buttons send it while there is a key object or a key guide (#2143).
let spacing: f64 = pstate::<Option<f64>>(ui.ctx(), "align-spacing").unwrap_or(0.0);
let key = to == AlignTo::Key;
let sp = if key { Some(spacing) } else { None };
let spacing = app.ui.align_spacing;
let key = spacing_sent(app).is_some();
// Two or more objects and ruler guides (#1597), or anchor points, which may all be on one path.
let distributes = n + selected_guides(app) >= 2 || has_anchors(app);
widgets::subheader(ui, tl!("Align Objects:"));
@@ -158,12 +237,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
if i == 3 {
ui.add_space(6.0);
if widgets::icon_button_enabled(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, n >= 1, 32.0).clicked() {
app.run("object.align", with_align_to(app, json!({"horizontal": "center", "vertical": "center"}))).ok();
run_button(app, Button::AlignCenters).ok();
}
ui.add_space(6.0);
}
if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 1, 32.0).clicked() {
app.run("object.align", with_align_to(app, json!({*axis: v}))).ok();
run_button(app, Button::Align(axis, v)).ok();
}
}
});
@@ -176,11 +255,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.add_space(6.0);
}
if widgets::icon_button_enabled(ui, icon, tl!(tip), false, distributes, 32.0).clicked() {
let mut p = json!({*axis: v, "to": to.param()});
if let Some(g) = sp {
p["spacing"] = json!(g);
}
app.run("object.distribute", p).ok();
run_button(app, Button::Distribute(axis, v)).ok();
}
}
});
@@ -194,14 +269,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.horizontal(|ui| {
let on = distributes;
if widgets::icon_button_enabled(ui, "dc-dist-vspace", tl!("Vertical Distribute Space"), false, on, 28.0).clicked() {
app.run("object.distributeSpacing", json!({"axis": "vertical", "spacing": sp, "to": to.param()})).ok();
run_button(app, Button::Spacing("vertical")).ok();
}
if widgets::icon_button_enabled(ui, "dc-dist-hspace", tl!("Horizontal Distribute Space"), false, on, 28.0).clicked() {
app.run("object.distributeSpacing", json!({"axis": "horizontal", "spacing": sp, "to": to.param()})).ok();
run_button(app, Button::Spacing("horizontal")).ok();
}
ui.add_enabled_ui(key, |ui| {
if let Some(v) = widgets::spin_field(ui, "align-spacing", Some(spacing), app.session.general_unit(), 70.0, 1.0, 0.0, &[]) {
set_pstate(ui.ctx(), "align-spacing", Some(v));
app.ui.align_spacing = v;
}
});
});
@@ -550,6 +625,144 @@ mod tests {
assert_eq!((x1(&a), x1(&b)), (200.0, 200.0), "both to the artboard's right edge");
}
/// The Align panel's buttons, in the panel's order: (command id, label).
const BUTTON_ENTRIES: [(&str, &str); 15] = [
("align.horizontalLeft", "Horizontal Align Left"),
("align.horizontalCenter", "Horizontal Align Center"),
("align.horizontalRight", "Horizontal Align Right"),
("align.horizontalVerticalCenter", "Horizontal & Vertical Align Center"),
("align.verticalTop", "Vertical Align Top"),
("align.verticalCenter", "Vertical Align Center"),
("align.verticalBottom", "Vertical Align Bottom"),
("align.distributeVerticalTop", "Vertical Distribute Top"),
("align.distributeVerticalCenter", "Vertical Distribute Center"),
("align.distributeVerticalBottom", "Vertical Distribute Bottom"),
("align.distributeHorizontalLeft", "Horizontal Distribute Left"),
("align.distributeHorizontalCenter", "Horizontal Distribute Center"),
("align.distributeHorizontalRight", "Horizontal Distribute Right"),
("align.distributeVerticalSpace", "Vertical Distribute Space"),
("align.distributeHorizontalSpace", "Horizontal Distribute Space"),
];
/// The chord the tests below give an Align panel button.
const CHORD: &str = "Cmd+Alt+Shift+F2";
/// One frame pressing [`CHORD`] through the shortcut handler.
fn press_chord(app: &mut VectorcraftApp) {
let ctx = egui::Context::default();
let modifiers = egui::Modifiers::COMMAND | egui::Modifiers::ALT | egui::Modifiers::SHIFT;
let ev = egui::Event::Key { key: egui::Key::F2, 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();
}
/// Edit › Keyboard Shortcuts lists each of the Align panel's Align Objects, Distribute Objects
/// and Distribute Spacing buttons under Align, with the button's label and no default shortcut,
/// and each can be given one.
#[test]
fn keyboard_shortcuts_lists_each_align_panel_button() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
let panel = {
let (mut app, _, _) = back_and_front();
tree(|ui| show(&mut app, ui))
};
let list = app.run("shortcuts.list", json!({})).unwrap();
for (id, label) in BUTTON_ENTRIES {
assert_eq!(role_and_state(&panel, label), (Role::Button, None), "the panel's {label} button");
let e = list.as_array().unwrap().iter().find(|e| e["id"] == id).unwrap_or_else(|| panic!("{id} is not listed"));
assert_eq!(
(e["label"].as_str(), e["group"].as_str(), &e["default"], &e["shortcut"]),
(Some(label), Some("Align"), &Value::Null, &Value::Null),
"{id}"
);
let set = app.run("shortcuts.set", json!({"id": id, "shortcut": CHORD, "force": true})).unwrap();
assert_eq!(set["shortcut"], CHORD, "{id}");
}
}
/// A shortcut on an Align panel button's entry runs what a click on the button runs: Horizontal
/// Align Right aligns to the selection, to the active artboard with Align to Artboard, and to
/// the key object while there is one. Horizontal Distribute Left sends the key and the
/// Distribute Spacing value, and Horizontal Distribute Space makes the gaps equal.
#[test]
fn a_shortcut_runs_its_align_panel_button() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let ids: Vec<Value> = [(10, 20), (50, 40), (120, 20)]
.into_iter()
.map(|(x, w)| app.run("shape.rectangle", json!({"x": x, "y": 10, "width": w, "height": 10})).unwrap()["id"].clone())
.collect();
app.run("select.set", json!({"ids": ids})).unwrap();
let edges = |app: &VectorcraftApp, right: bool| {
let st = app.session.active().unwrap();
ids.iter()
.map(|id| {
let b = st.doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap();
if right { b.x1 } else { b.x0 }
})
.collect::<Vec<_>>()
};
let sent = |app: &VectorcraftApp| {
let (id, p) = app.session.journal.last().unwrap();
(id.clone(), p["to"].clone(), p.get("spacing").cloned())
};
app.run("shortcuts.set", json!({"id": "align.horizontalRight", "shortcut": CHORD})).unwrap();
press_chord(&mut app);
assert_eq!(edges(&app, true), [140.0; 3], "Align to Selection: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
app.ui.align_to = AlignTo::Artboard;
press_chord(&mut app);
assert_eq!(edges(&app, true), [200.0; 3], "Align to Artboard: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
app.run("select.key", json!({"id": ids[1]})).unwrap();
press_chord(&mut app);
assert_eq!(edges(&app, true), [90.0; 3], "the key object's right edge: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
// Distribute Objects with the key object and a Distribute Spacing value of 15 pt.
app.run("shortcuts.set", json!({"id": "align.distributeHorizontalLeft", "shortcut": CHORD, "force": true})).unwrap();
app.ui.align_spacing = 15.0;
press_chord(&mut app);
assert_eq!(sent(&app), ("object.distribute".to_string(), json!("key"), Some(json!(15.0))));
assert_eq!(edges(&app, false), [35.0, 50.0, 65.0], "15 pt apart: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
// Distribute Spacing with Align to Selection: the outer objects stay, and the gaps are equal.
app.run("select.key", json!({})).unwrap();
app.ui.align_to = AlignTo::Selection;
app.run("shortcuts.set", json!({"id": "align.distributeHorizontalSpace", "shortcut": CHORD, "force": true})).unwrap();
press_chord(&mut app);
assert_eq!(sent(&app), ("object.distributeSpacing".to_string(), json!("selection"), Some(Value::Null)));
assert_eq!(edges(&app, false), [10.0, 55.0, 120.0], "25 pt gaps: {}", app.ui.status);
}
/// A shortcut on an Align panel button's entry is kept in the set Export… writes, and Import…
/// of that set gives it back after a reset; it is kept in the saved preferences too.
#[test]
fn an_align_panel_button_shortcut_is_kept_in_a_saved_set() {
let (mut app, back, front) = back_and_front();
let written: std::rc::Rc<std::cell::RefCell<Vec<u8>>> = Default::default();
let w = written.clone();
app.services.write = Some(Box::new(move |_: &str, b: &[u8]| {
*w.borrow_mut() = b.to_vec();
Ok(())
}));
let top = |app: &VectorcraftApp, id: &Value| {
app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap().y0
};
app.run("shortcuts.set", json!({"id": "align.verticalTop", "shortcut": CHORD})).unwrap();
app.run("shortcuts.export", json!({"path": "/sets/Align.json"})).unwrap();
// The preferences save the UI state, and the next launch reads it back.
let saved = serde_json::to_value(&app.ui).unwrap();
let loaded: crate::UiState = serde_json::from_value(saved).unwrap();
assert_eq!(serde_json::to_value(&loaded).unwrap()["shortcut_overrides"]["align.verticalTop"], CHORD, "the saved preferences");
app.run("shortcuts.reset", json!({})).unwrap();
press_chord(&mut app);
assert_eq!(top(&app, &front), 50.0, "no shortcut after the reset: {}", app.ui.status);
let set: Value = serde_json::from_slice(&written.borrow()).unwrap();
app.run("shortcuts.import", json!({"data": set})).unwrap();
press_chord(&mut app);
assert_eq!((top(&app, &back), top(&app, &front)), (10.0, 10.0), "the imported shortcut: {}", app.ui.status);
}
/// One frame of the Align panel → its Horizontal Distribute Left and Horizontal Distribute
/// Space buttons.
fn distribute_buttons(app: &mut VectorcraftApp, ctx: &egui::Context) -> (egui::Rect, egui::Rect) {
@@ -628,7 +841,7 @@ mod tests {
assert_eq!(sent(&app), ("object.distribute".to_string(), json!("key"), Some(json!(0.0))));
assert_eq!(lefts(&app), [50.0; 3], "on the key's left edge: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
set_pstate(&ctx, "align-spacing", Some(15.0));
app.ui.align_spacing = 15.0;
click(&ctx, &mut app, left.center(), show);
assert_eq!(lefts(&app), [35.0, 50.0, 65.0], "15 pt apart: {}", app.ui.status);
}
@@ -658,7 +871,7 @@ mod tests {
assert_eq!(sent(&app), ("object.distribute".to_string(), json!("key"), Some(json!(0.0))));
assert_eq!(lefts(&app), [100.0; 3], "on the guide: {}", app.ui.status);
app.run("edit.undo", json!({})).unwrap();
set_pstate(&ctx, "align-spacing", Some(15.0));
app.ui.align_spacing = 15.0;
click(&ctx, &mut app, left.center(), show);
assert_eq!(lefts(&app), [70.0, 85.0, 115.0], "15 pt apart, outward from the guide: {}", app.ui.status);
assert_eq!(app.session.active().unwrap().doc.guides[0].pos, 100.0, "the guide stays put");
+6
View File
@@ -453,6 +453,11 @@ pub struct UiState {
/// buttons follow ([`crate::panels::align::with_align_to`]).
#[serde(default, deserialize_with = "crate::panels::align::deserialize_align_to")]
pub align_to: crate::panels::align::AlignTo,
/// Align panel: the Distribute Spacing value in points, which the Distribute Objects and
/// Distribute Spacing buttons and their commands send while there is a key object or a key
/// guide ([`crate::panels::align::run_button`]). Not saved.
#[serde(skip)]
pub align_spacing: f64,
/// Color panel: the mode picked last from its menu (`None`: never picked), in which the panel
/// shows the defaults for new art ([`crate::panels::color::deserialize_mode`]).
#[serde(default, deserialize_with = "crate::panels::color::deserialize_mode")]
@@ -825,6 +830,7 @@ impl Default for UiState {
flattener_preview: Default::default(),
color_guide_limit: String::new(),
align_to: Default::default(),
align_spacing: 0.0,
color_mode: None,
color_hide_options: false,
svg_options: Value::Null,
+12
View File
@@ -2312,6 +2312,18 @@ included. With `spacing: 0`, `object.distribute` puts every line on the guide. A
everything where it is, as `object.align` does. `to: "selection"` or `"artboard"` distributes without the key guide
as the key, as with `object.align`, and without `spacing` a key guide is spaced as any selected guide.
Each button of the Align panel's Align Objects, Distribute Objects and Distribute Spacing rows is also a UI command
that takes no params and runs what a click on the button runs: `align.horizontalLeft`, `align.horizontalCenter`,
`align.horizontalRight`, `align.horizontalVerticalCenter`, `align.verticalTop`, `align.verticalCenter`,
`align.verticalBottom`, `align.distributeVerticalTop`, `align.distributeVerticalCenter`,
`align.distributeVerticalBottom`, `align.distributeHorizontalLeft`, `align.distributeHorizontalCenter`,
`align.distributeHorizontalRight`, `align.distributeVerticalSpace` and `align.distributeHorizontalSpace`. The Align
commands send `to: "key"` while the selection has a key, else `"artboard"` for one selected object without selected
anchor points, else the Align To choice. The Distribute commands send `to: "key"` while the selection has a key, else
the Align To choice, and while there is a key object or a key guide they send the panel's Distribute Spacing value as
`spacing`. Edit › Keyboard Shortcuts lists them under Align, with no default shortcut, and
`shortcuts.set {id, shortcut}` gives one a shortcut.
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
+1
View File
@@ -542,6 +542,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
A key object needs another selected object outside it. A Shift-click or any other change that leaves one object selected clears the key, and `select.key` sets none on one object or on a group selected with only its own members. With one object selected, the Align panel, the Control bar and the Properties panel align it to the artboard whatever Align To is set to.
With two or more objects selected and no key object, Align to Key Object makes the topmost of them the key, whatever order they were selected in, and the Align panel, the Control bar and the Properties panel align to it. Align To keeps its Selection or Artboard choice and shows it again once the key is cleared.
Object › Align aligns objects as the align buttons do: it aligns one object to the active artboard whatever Align To is set to, and otherwise it aligns to the key object while there is one, else to the Align To choice, which is saved with the preferences (`ui.inspect` → `ui.align_to`). With direct-selected anchor points, Object › Align aligns the points to their own bounds. While one object is selected, Align To shows Artboard.
Edit › Keyboard Shortcuts lists each of the Align panel's Align Objects, Distribute Objects and Distribute Spacing buttons under Align, with no default shortcut, as the reference app's [release notes](https://web.archive.org/web/20261006170350/https://helpx.adobe.com/illustrator/desktop/new-features/release-notes.html) describe for its Align and Distribute commands. A shortcut set there runs what a click on the button runs, with the Align To choice, the key object and the Distribute Spacing value (`align.horizontalLeft` and the other `align.*` UI commands).
The command palette lists `select.key` as Cancel Key Object, and a pick cancels the key object (#541).
Agent selection commands validate every object id before changing the selection and report the resulting count and ids (#784).
Agents read the key object: `select.key` returns it, and `document.inspect` and the `ui.pointer` and `pointer_gesture` replies report it as `key`. `select.key` with an id that isn't a selected top-level object returns an error and leaves the key unchanged.
+3 -1
View File
@@ -201,7 +201,9 @@ selected objects, as the Appearance panel's Add New Fill and Add New Stroke butt
against Illustrator's published default shortcut list; estimated ~70% of it. Remaining: the panel shortcuts,
the type shortcuts (tracking/kerning/leading/baseline steps with Alt/Shift), and selection shortcuts in the
Layers panel. The shortcut editor also lists the commands that are in no menu but run without parameters (Edit › Duplicate,
panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1539). The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1539). It lists each button of the
Align panel's Align Objects, Distribute Objects and Distribute Spacing rows under Align, with no default shortcut, and a
shortcut set there runs what a click on the button runs, with the panel's Align To choice and Distribute Spacing value. The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
Cmd/Ctrl+Alt+N (`file.newWithoutDialog`, New Without Dialog in the shortcut editor) makes a document with the
settings the New Document dialog opens on (the first Recent entry, or the Print category's Letter while Recent is