mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Object: Cmd/Ctrl+Alt+Shift+2 locks every unselected object
The reference app locks all deselected artwork on Cmd/Ctrl+Alt+Shift+2. VectorCraft had no command for it: Object > Lock held Selection, All Artwork Above and Other Layers. The new command, object.lock.unselected (Object > Lock > Unselected Artwork), locks in one undo step the objects that Hide > Unselected Artwork hides: every object that Select All takes and that is neither selected nor holds a selected object. It returns the count. Hidden objects stay unlocked. The selection stays as it is, Unlock All unlocks the objects again and selects them, and with nothing to lock the command records no undo step. Both commands now share one function, which locks or hides the objects that the existing unselected helper returns. In isolation mode the menu item is disabled, and the command returns the error "not available in isolation mode" that Hide > Unselected Artwork returns there, and changes nothing. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -56,6 +56,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection_outside_isolation,
|
||||
|s, _| other_layers(s, true)
|
||||
),
|
||||
cmd!(
|
||||
"object.lock.unselected",
|
||||
"Unselected Artwork",
|
||||
["Object", "Lock"],
|
||||
Some("Cmd+Alt+Shift+2"),
|
||||
"{} lock, in one undo step, every object select.all takes that is not selected and holds no selected object; in a group that holds one it locks the other members one by one, and a clip group, compound path or other object that holds one stays as it is; the objects of a selected layer count as selected; while Lock Guides is on, guide paths count as selected and stay as they are; object.unlockAll unlocks them again; disabled in isolation mode → {count}",
|
||||
has_selection_outside_isolation,
|
||||
|s, _| unselected_art(s, true)
|
||||
),
|
||||
cmd!(
|
||||
"object.hide.above",
|
||||
"All Artwork Above",
|
||||
@@ -81,7 +90,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Some("Cmd+Alt+Shift+3"),
|
||||
"{} hide, in one undo step, every object select.all takes that is not selected and holds no selected object; in a group that holds one it hides the other members one by one, and a clip group, compound path or other object that holds one stays as it is; the objects of a selected layer count as selected; while Lock Guides is on, guide paths count as selected and stay as they are; object.showAll shows them again; disabled in isolation mode → {count}",
|
||||
has_selection_outside_isolation,
|
||||
hide_unselected
|
||||
|s, _| unselected_art(s, false)
|
||||
),
|
||||
cmd!(
|
||||
"object.transformEach",
|
||||
@@ -349,19 +358,23 @@ fn unselected(d: &vectorcraft_doc::Document, iso: Option<NodeId>, selection: &[N
|
||||
out
|
||||
}
|
||||
|
||||
fn hide_unselected(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
fn unselected_art(s: &mut Session, lock: bool) -> Result<Value> {
|
||||
let roots = selected_roots_to_transform(s)?;
|
||||
let guides_locked = s.menu.guides_locked;
|
||||
let st = s.doc()?;
|
||||
let ids = unselected(&st.doc, st.isolation, &st.selection.objects, &roots, guides_locked);
|
||||
// With nothing to hide, the document and its undo history stay as they are.
|
||||
// With nothing to lock (Lock) or hide (Hide), the document and its undo history stay as they are.
|
||||
if !ids.is_empty() {
|
||||
s.edit("Hide", |d, _| {
|
||||
// Hiding moves no node in the tree, so the paths found up front stay right.
|
||||
s.edit(if lock { "Lock" } else { "Hide" }, |d, _| {
|
||||
// Locking and hiding move no node in the tree, so the paths found up front stay right.
|
||||
let paths = d.index_paths(ids.iter().copied());
|
||||
for id in &ids {
|
||||
if let Some(n) = paths.get(id).and_then(|p| d.node_mut_at(p)) {
|
||||
n.visible = false;
|
||||
if lock {
|
||||
n.locked = true;
|
||||
} else {
|
||||
n.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -650,6 +650,96 @@ fn hide_unselected_artwork_keeps_groups_with_locked_guides_and_the_selected_laye
|
||||
assert_eq!(selected(&s), [other]);
|
||||
}
|
||||
|
||||
/// Lock › Unselected Artwork locks, in one undo step labeled "Lock", each object that is not selected and holds no
|
||||
/// selected object: here `b`, the member `n` of the group that holds the selected `m`, and `p` on another layer. The
|
||||
/// selected objects, that group, the clip group that holds the selected `c2` with all its members, an object hidden
|
||||
/// before and the art on a locked layer stay unlocked, and the selection stays as it is. Unlock All unlocks them again
|
||||
/// with the object locked before, and selects them.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_locks_every_object_that_is_not_selected() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let m = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let n = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[m, n]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let c1 = rect(&mut s, 80.0, 0.0, 10.0, 10.0);
|
||||
let c2 = rect(&mut s, 85.0, 5.0, 10.0, 10.0);
|
||||
let c3 = rect(&mut s, 80.0, 0.0, 30.0, 30.0);
|
||||
sel(&mut s, &[c1, c2, c3]);
|
||||
let clip = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap());
|
||||
let k = rect(&mut s, 120.0, 0.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": k.0, "locked": true})).unwrap();
|
||||
let h = rect(&mut s, 140.0, 0.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": h.0, "visible": false})).unwrap();
|
||||
s.execute("layer.new", &json!({})).unwrap();
|
||||
let p = rect(&mut s, 0.0, 100.0, 10.0, 10.0);
|
||||
let locked_layer = id_of(&s.execute("layer.new", &json!({})).unwrap());
|
||||
let q = rect(&mut s, 20.0, 100.0, 10.0, 10.0);
|
||||
s.execute("layer.setProps", &json!({"id": locked_layer.0, "locked": true})).unwrap();
|
||||
let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).collect::<Vec<_>>();
|
||||
sel(&mut s, &[a, m, c2]);
|
||||
let steps = undo_len(&s);
|
||||
assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 3}));
|
||||
assert_eq!(locked(&s, &[b, n, p, k]), [true; 4]);
|
||||
assert_eq!(locked(&s, &[a, m, g, c1, c2, c3, clip, h, q]), [false; 9]);
|
||||
assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2]));
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Lock");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(locked(&s, &[b, n, p]), [false; 3]);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(locked(&s, &[b, n, p]), [true; 3]);
|
||||
assert_eq!(s.execute("object.unlockAll", &json!({})).unwrap(), json!({"count": 4}));
|
||||
assert_eq!(locked(&s, &[b, n, p, k]), [false; 4]);
|
||||
let mut unlocked = selected(&s);
|
||||
unlocked.sort();
|
||||
let mut want = vec![b, n, p, k];
|
||||
want.sort();
|
||||
assert_eq!(unlocked, want);
|
||||
}
|
||||
|
||||
/// In isolation mode Lock › Unselected Artwork is disabled: the menu item is off, and the command returns an error that
|
||||
/// names isolation mode and changes nothing. While Lock Guides is on it leaves the guides unlocked. With nothing left to
|
||||
/// lock it records no undo step and returns a count of 0. With Lock Guides off it locks a guide path like other art.
|
||||
#[test]
|
||||
fn lock_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode() {
|
||||
let mut s = session();
|
||||
let guide = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[guide]);
|
||||
s.execute("view.guides.make", &json!({})).unwrap();
|
||||
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
|
||||
let out = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
let m = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
let n = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[m, n]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let lock = |s: &mut Session| s.execute("object.lock.unselected", &json!({})).unwrap();
|
||||
let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).collect::<Vec<_>>();
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
select_with_a_step_to_redo(&mut s, &[m]);
|
||||
let before = history(&s);
|
||||
let info = find_command("object.lock.unselected").unwrap().info(&s);
|
||||
assert!(!info.enabled && info.disabled_reason.as_deref() == Some("exit isolation mode first"), "{info:?}");
|
||||
let e = s.execute("object.lock.unselected", &json!({})).unwrap_err();
|
||||
assert!(matches!(e, EngineError::Disabled(..)) && e.to_string().contains("isolation mode"), "{e}");
|
||||
assert_eq!(locked(&s, &[m, n, out, guide]), [false; 4]);
|
||||
assert_eq!((history(&s), selected(&s)), (before, vec![m]));
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
sel(&mut s, &[m]);
|
||||
assert!(find_command("object.lock.unselected").unwrap().info(&s).enabled);
|
||||
assert_eq!(lock(&mut s), json!({"count": 2}));
|
||||
assert_eq!(locked(&s, &[n, out, guide, g, m]), [true, true, false, false, false]);
|
||||
select_with_a_step_to_redo(&mut s, &[m]);
|
||||
let before = history(&s);
|
||||
assert_eq!(lock(&mut s), json!({"count": 0}));
|
||||
assert_eq!(history(&s), before);
|
||||
s.execute("view.guides.lock", &json!({"locked": false})).unwrap();
|
||||
sel(&mut s, &[m]);
|
||||
assert_eq!(lock(&mut s), json!({"count": 1}));
|
||||
assert_eq!(locked(&s, &[guide]), [true]);
|
||||
}
|
||||
|
||||
// ---------- Transform Each ----------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -165,7 +165,12 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Ungroup", "object.ungroup"),
|
||||
sub(
|
||||
"Lock",
|
||||
vec![c("Selection", "object.lock"), c("All Artwork Above", "object.lock.above"), c("Other Layers", "object.lock.otherLayers")],
|
||||
vec![
|
||||
c("Selection", "object.lock"),
|
||||
c("All Artwork Above", "object.lock.above"),
|
||||
c("Other Layers", "object.lock.otherLayers"),
|
||||
c("Unselected Artwork", "object.lock.unselected"),
|
||||
],
|
||||
),
|
||||
c("Unlock All", "object.unlockAll"),
|
||||
sub(
|
||||
|
||||
@@ -336,6 +336,40 @@ fn cmd_alt_shift_j_opens_average_and_join() {
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.str("to")).as_deref(), Some("smooth"), "the choice confirmed last");
|
||||
}
|
||||
|
||||
/// Cmd+Alt+Shift+2 is Object › Lock › Unselected Artwork: it locks the object that is not
|
||||
/// selected. Cmd+Alt+2 still unlocks all, and Cmd+2 still locks the selection. In isolation mode
|
||||
/// the item is disabled.
|
||||
#[test]
|
||||
fn cmd_alt_shift_2_locks_unselected_artwork() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
assert_eq!(crate::menus::shortcut_of("object.lock.unselected"), Some("Cmd+Alt+Shift+2"));
|
||||
let rect = |app: &mut VectorcraftApp, x: f64| {
|
||||
let r = app.session.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
vectorcraft_doc::NodeId(r["id"].as_u64().unwrap())
|
||||
};
|
||||
let (a, b) = (rect(&mut app, 10.0), rect(&mut app, 50.0));
|
||||
let locked = |app: &VectorcraftApp| [a, b].map(|id| app.session.doc().unwrap().doc.node(id).unwrap().locked);
|
||||
let two = |modifiers| egui::Event::Key { key: Key::Num2, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
app.session.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
frame(&mut app, vec![two(Modifiers::COMMAND | Modifiers::ALT | Modifiers::SHIFT)]);
|
||||
assert_eq!(locked(&app), [false, true]);
|
||||
frame(&mut app, vec![two(Modifiers::COMMAND | Modifiers::ALT)]);
|
||||
assert_eq!(locked(&app), [false, false], "Cmd+Alt+2 unlocks all");
|
||||
app.session.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
frame(&mut app, vec![two(Modifiers::COMMAND)]);
|
||||
assert_eq!(locked(&app), [true, false], "Cmd+2 locks the selection");
|
||||
// In isolation mode the menu item is off, and the shortcut locks nothing.
|
||||
app.session.execute("object.unlockAll", &json!({})).unwrap();
|
||||
app.session.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let g = app.session.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
app.session.execute("object.isolate", &json!({"id": g})).unwrap();
|
||||
app.session.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
assert!(!crate::menus::enabled(&app, "object.lock.unselected"));
|
||||
frame(&mut app, vec![two(Modifiers::COMMAND | Modifiers::ALT | Modifiers::SHIFT)]);
|
||||
assert_eq!(locked(&app), [false, false], "nothing is locked in isolation mode");
|
||||
}
|
||||
|
||||
/// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724).
|
||||
#[test]
|
||||
fn cmd_shift_b_while_typing_is_bold_not_the_bounding_box() {
|
||||
|
||||
@@ -1398,6 +1398,12 @@ selected. While Lock Guides is on, the guide paths count as selected and stay as
|
||||
hide it records no undo step. In isolation mode the menu item is disabled, and the command returns the error "exit
|
||||
isolation mode first" and changes nothing.
|
||||
|
||||
`object.lock.unselected` (Object › Lock › Unselected Artwork, Cmd+Alt+Shift+2) locks, in one undo step, the objects
|
||||
that `object.hide.unselected` hides, and returns `{count}`. Hidden objects stay unlocked. The selection stays as it
|
||||
is, and `object.unlockAll` unlocks the objects again and selects them. With nothing to lock it records no undo step.
|
||||
In isolation mode the menu item is disabled, and the command returns the error "exit isolation mode first" and
|
||||
changes nothing.
|
||||
|
||||
These Select menu commands select only the objects that `select.all` selects and the objects inside them: every
|
||||
`select.same.*` and `select.object.*` command except `select.object.slices`, `select.nextAbove`, `select.nextBelow`
|
||||
and `select.recall`. They leave out hidden and locked objects with the objects inside them, the objects on hidden,
|
||||
|
||||
@@ -963,6 +963,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included.
|
||||
Object › Hide › Unselected Artwork (Cmd/Ctrl+Alt+Shift+3, `object.hide.unselected`) hides, in one undo step, every object Select All takes that is not selected and holds no selected object; the members of a group that holds a selected object are hidden one by one, and Object › Show All shows them again. It is disabled in isolation mode. Public references: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), "Hide unselected artwork"; [How to lock, hide, and delete objects in Illustrator](https://web.archive.org/web/20220303070637/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html), which says the Show All and Hide menus are unavailable in isolation mode.
|
||||
In isolation mode, the items of Object › Lock and Object › Hide, Unlock All and Show All are greyed out ([lock, hide, and delete objects](https://web.archive.org/web/20220303070637/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html)); the Contextual Task Bar's Lock button is greyed out too, the Properties panel's Quick Actions have no Lock, and the commands return an error. Pattern editing mode and opacity mask editing do not count as isolation mode for them.
|
||||
Object › Lock › Unselected Artwork (Cmd/Ctrl+Alt+Shift+2, `object.lock.unselected`) locks, in one undo step, the objects that Hide › Unselected Artwork hides, and Object › Unlock All unlocks them again. It is disabled in isolation mode. Public references: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), "Lock all deselected artwork"; [How to lock, hide, and delete objects in Illustrator](https://web.archive.org/web/20220303070637/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html), "The Lock menus are disabled when you’re in isolation mode."
|
||||
Liquify (its brush Width, Height and Angle, Intensity, Detail, Simplify, Twirl Rate, Complexity, Horizontal and Vertical), Puppet Warp (its Expand and each pin's angle), Transform in Perspective (its depth), Nudge in Perspective (its direction) and Move Plane (its location or distance) refuse a value other than a number or null, such as "40", Draw in Perspective refuses a click point that is not two numbers, and Width Profile refuses a point that is not three numbers; each leaves the document and its undo history as they were.
|
||||
Object › Lock › All Artwork Above and Object › Hide › All Artwork Above go through each top-level layer once, checking each object that is not selected against the selected objects below it: with 100 tiles selected under 25,000 rectangles, Lock took 6.4 s and now takes under 0.1 s, and with all of 5,001 objects selected it took 8.8 s and now takes under 0.1 s.
|
||||
Object › Unlock All and Object › Show All clicked in the in-window menu bar with Shift+Alt (Shift+Option) held unlock or show only the objects inside the groups that hold the selected objects ([lock, hide, and delete objects](https://web.archive.org/web/20230623025912/https://helpx.adobe.com/illustrator/using/locking-hiding-deleting-objects.html)); `object.unlockAll` and `object.showAll` take `scope: "group"` for it.
|
||||
|
||||
Reference in New Issue
Block a user