From fc7ef3102fb4c1e10b00bbae4710d6da284788c1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 11:41:21 -0700 Subject: [PATCH] Object: Cmd/Ctrl+Alt+Shift+2 locks inside clip groups and isolation, with no menu item object.lock.unselected is now reached by its key and as a command only: the Object > Lock > Unselected Artwork menu item is removed, and the command stays in the registry, the shortcut editor and automation. In a partly selected clip group or compound path it now locks the unselected members, the clipping path included, and leaves the container and the selected members unlocked. It locks unselected guides while Lock Guides is on. Hidden art and art on a locked layer stay unlocked, and a selected layer's art counts as selected. With a selection in isolation mode the command is enabled and locks only the unselected art inside the isolated group. The other Lock and Hide items keep their isolation behavior. Lock and Hide still share one selector. A lock flag on it selects the clip group, compound path and guide cases above; Hide > Unselected Artwork keeps its behavior, and a test covers both directions. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/menucmds.rs | 31 ++++--- crates/engine/src/tests/menucmds.rs | 124 +++++++++++++++++++------- crates/ui-egui/src/menus/tests.rs | 5 ++ crates/ui-egui/src/menus/tree.rs | 7 +- crates/ui-egui/src/shortcuts/tests.rs | 15 ++-- docs/mcp.md | 12 +-- docs/target-app-parity.md | 4 +- 7 files changed, 139 insertions(+), 59 deletions(-) diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index a6deaa19b..cd8e0c27c 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -61,8 +61,8 @@ pub fn specs() -> Vec { "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, + "{} lock unselected artwork in one undo step, including unselected members of partly selected groups, clipping groups and compound paths, and guides while Lock Guides is on; hidden art and art on locked layers stay unlocked; a selected layer's art counts as selected; in isolation, only art inside the isolated object is affected; object.unlockAll unlocks locked art document-wide → {count}", + has_selection, |s, _| unselected_art(s, true) ), cmd!( @@ -330,15 +330,24 @@ fn other_layers(s: &mut Session, lock: bool) -> Result { Ok(json!({ "count": n })) } -/// The objects [`super::select::walk_selectable`] visits that are not among the selected objects `roots` and hold none of -/// them. The objects of a selected layer count as selected. Guide paths are left out while `guides_locked`, and so are -/// the objects that hold one. The walk goes into a plain group that holds a selected object or such a guide path; a -/// clip group, a Shaper group, a compound path and every other object that holds one is left out with its members. -fn unselected(d: &vectorcraft_doc::Document, iso: Option, selection: &[NodeId], roots: &[NodeId], guides_locked: bool) -> Vec { +/// Returns the objects [`super::select::walk_selectable`] visits that are not among the selected objects `roots` and +/// hold none of them. A selected layer's objects count as selected. The walk goes into a plain group, but not a Shaper +/// group, that holds a selected object. With `lock`, it also goes into a clip group or compound path that holds one, +/// and includes guide paths while `guides_locked`. Without `lock`, guide paths and the objects that hold one are left +/// out while `guides_locked`; a clip group, Shaper group, compound path and every other object that holds a selected +/// object or such a guide are left out with their members. +fn unselected( + d: &vectorcraft_doc::Document, + iso: Option, + selection: &[NodeId], + roots: &[NodeId], + guides_locked: bool, + lock: bool, +) -> Vec { let mut roots: HashSet = roots.iter().copied().collect(); roots.extend(selection.iter().filter_map(|id| d.node(*id)).filter(|n| n.is_layer()).flat_map(|l| l.layer_art(true))); let mut kept: Vec = roots.iter().copied().collect(); - if guides_locked { + if guides_locked && !lock { super::select::walk_selectable(d, iso, |n| { if matches!(n.kind, NodeKind::Path { guide: true, .. }) { kept.push(n.id); @@ -353,7 +362,9 @@ fn unselected(d: &vectorcraft_doc::Document, iso: Option, selection: &[N out.push(n.id); return false; } - !roots.contains(&n.id) && matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.extras().shaper.is_none() + !roots.contains(&n.id) + && (matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.extras().shaper.is_none() + || lock && (matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Compound { .. }))) }); out } @@ -362,7 +373,7 @@ fn unselected_art(s: &mut Session, lock: bool) -> Result { 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); + let ids = unselected(&st.doc, st.isolation, &st.selection.objects, &roots, guides_locked, lock); // With nothing to lock (Lock) or hide (Hide), the document and its undo history stay as they are. if !ids.is_empty() { s.edit(if lock { "Lock" } else { "Hide" }, |d, _| { diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index 7135f7cef..df6f7db08 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -518,8 +518,9 @@ fn lock_and_hide_with_nothing_to_lock_or_hide_record_no_undo_step() { /// Hide › Unselected Artwork hides, in one undo step, 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, a locked object, the art on a locked -/// layer and an object hidden before stay as they are, and so does the selection. Show All shows them again. +/// that group, the clip group that holds the selected `c2` with all its members, the compound path that holds the +/// selected `hole` with both paths, a guide under Lock Guides, a locked object, the art on a locked layer and an +/// object hidden before stay as they are, and so does the selection. Show All shows them again. #[test] fn hide_unselected_artwork_hides_every_object_that_is_not_selected() { let mut s = session(); @@ -534,6 +535,14 @@ fn hide_unselected_artwork_hides_every_object_that_is_not_selected() { 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 outer = rect(&mut s, 0.0, 200.0, 100.0, 100.0); + let hole = rect(&mut s, 40.0, 240.0, 20.0, 20.0); + sel(&mut s, &[outer, hole]); + let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap()); + let guide = rect(&mut s, 200.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 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); @@ -544,13 +553,13 @@ fn hide_unselected_artwork_hides_every_object_that_is_not_selected() { 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 shown = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).visible).collect::>(); - sel(&mut s, &[a, m, c2]); + sel(&mut s, &[a, m, c2, hole]); let steps = undo_len(&s); assert_eq!(s.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 3})); assert_eq!(shown(&s, &[b, n, p, h]), [false; 4]); - assert_eq!(shown(&s, &[a, m, g, c1, c2, c3, clip, k, q]), [true; 9]); + assert_eq!(shown(&s, &[a, m, g, c1, c2, c3, clip, compound, outer, hole, guide, k, q]), [true; 13]); assert!(node(&s, k).locked); - assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2])); + assert_eq!((undo_len(&s), selected(&s)), (steps + 1, vec![a, m, c2, hole])); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(shown(&s, &[b, n, p]), [true; 3]); s.execute("edit.redo", &json!({})).unwrap(); @@ -650,11 +659,10 @@ 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. +/// Lock Unselected Artwork locks `b`, `n` in the group that holds selected `m`, `p` on another layer, and the clip +/// group's unselected members `c1` and clipping path `c3` in one undo step labeled "Lock". The clip group container, +/// `c2`, the selected objects, group `g`, hidden object `h` and art `q` on a locked layer stay unlocked. `k` was locked +/// before. The selection stays as it is. Unlock All unlocks all six locked objects and selects them. #[test] fn lock_unselected_artwork_locks_every_object_that_is_not_selected() { let mut s = session(); @@ -681,29 +689,28 @@ fn lock_unselected_artwork_locks_every_object_that_is_not_selected() { let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).collect::>(); 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!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 5})); + assert_eq!(locked(&s, &[b, n, p, c1, c3, k]), [true; 6]); + assert_eq!(locked(&s, &[a, m, g, c2, clip, h, q]), [false; 7]); 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]); + assert_eq!(s.execute("object.unlockAll", &json!({})).unwrap(), json!({"count": 6})); + assert_eq!(locked(&s, &[b, n, p, c1, c3, k]), [false; 6]); let mut unlocked = selected(&s); unlocked.sort(); - let mut want = vec![b, n, p, k]; + let mut want = vec![b, n, p, c1, c3, 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. +/// Lock Unselected Artwork affects only the isolated group's other members. Lock Guides does not protect an unselected +/// guide from this command. With nothing left to lock it records no undo step. #[test] -fn lock_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode() { +fn lock_unselected_artwork_scopes_isolation_and_locks_guides() { let mut s = session(); let guide = rect(&mut s, 0.0, 0.0, 10.0, 10.0); sel(&mut s, &[guide]); @@ -718,26 +725,83 @@ fn lock_unselected_artwork_keeps_locked_guides_and_is_disabled_in_isolation_mode let locked = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| node(s, *id).locked).collect::>(); 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])); + assert!(info.enabled, "{info:?}"); + let before = undo_len(&s); + assert_eq!(lock(&mut s), json!({"count": 1})); + assert_eq!(locked(&s, &[m, n, out, guide]), [false, true, false, false]); + assert_eq!((undo_len(&s), selected(&s)), (before + 1, 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]); + assert_eq!(locked(&s, &[n, out, guide, g, m]), [true, true, true, 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]); +} + +/// Lock Unselected Artwork locks the unselected path of a partly selected compound path, but needs a selection. +#[test] +fn lock_unselected_artwork_descends_into_compound_and_needs_selection() { + let mut s = session(); + let outer = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let hole = rect(&mut s, 40.0, 40.0, 20.0, 20.0); + sel(&mut s, &[outer, hole]); + let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap()); + s.execute("select.none", &json!({})).unwrap(); + let before = history(&s); + assert!(!find_command("object.lock.unselected").unwrap().info(&s).enabled); + assert!(matches!(s.execute("object.lock.unselected", &json!({})), Err(EngineError::Disabled(..)))); + assert_eq!(history(&s), before); + sel(&mut s, &[hole]); + assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 1})); + assert!(node(&s, outer).locked); + assert!(!node(&s, hole).locked); + assert!(!node(&s, compound).locked); + assert_eq!(selected(&s), [hole]); +} + +/// Lock Unselected Artwork leaves all art on a selected layer unlocked and locks unselected art on another layer. +#[test] +fn lock_unselected_artwork_keeps_a_selected_layers_art() { + let mut s = session(); + let selected_layer = id_of(&s.execute("layer.new", &json!({})).unwrap()); + 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); + s.execute("layer.new", &json!({})).unwrap(); + let other = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + sel(&mut s, &[selected_layer]); + assert_eq!(s.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 1})); + assert_eq!([a, b, other].map(|id| node(&s, id).locked), [false, false, true]); + assert_eq!(selected(&s), [selected_layer]); +} + +/// In a plain group, Lock and Hide Unselected Artwork affect the same members of identical documents. +#[test] +fn lock_and_hide_unselected_artwork_choose_the_same_objects() { + let prepare = || { + 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()); + sel(&mut s, &[a, m]); + (s, [a, b, m, n, g]) + }; + let (mut locked, lock_ids) = prepare(); + let (mut hidden, hide_ids) = prepare(); + assert_eq!(lock_ids, hide_ids); + assert_eq!(locked.execute("object.lock.unselected", &json!({})).unwrap(), json!({"count": 2})); + assert_eq!(hidden.execute("object.hide.unselected", &json!({})).unwrap(), json!({"count": 2})); + let changed_by_lock: Vec<_> = lock_ids.into_iter().filter(|id| node(&locked, *id).locked).collect(); + let changed_by_hide: Vec<_> = hide_ids.into_iter().filter(|id| !node(&hidden, *id).visible).collect(); + assert_eq!(changed_by_lock, changed_by_hide); + assert_eq!(changed_by_lock, [lock_ids[1], lock_ids[3]]); } // ---------- Transform Each ---------- diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 7a23026d2..b0ee269ad 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -677,7 +677,11 @@ fn lock_and_hide_menus_are_greyed_out_in_isolation_mode() { app.run("file.new", json!({})).unwrap(); let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone(); let b = app.run("shape.rectangle", json!({"x": 80, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone(); + app.run("select.none", json!({})).unwrap(); + let lock_unselected = |app: &VectorcraftApp| crate::menus::enabled(app, "object.lock.unselected"); + assert!(!lock_unselected(&app), "nothing selected"); app.run("select.set", json!({"ids": [a, b.clone()]})).unwrap(); + assert!(lock_unselected(&app), "selection present"); let g = app.run("object.group", json!({})).unwrap()["id"].clone(); let lock_and_hide: Vec = menu_entries(&app) .into_iter() @@ -712,6 +716,7 @@ fn lock_and_hide_menus_are_greyed_out_in_isolation_mode() { app.run("object.isolate", json!({"id": g.clone()})).unwrap(); app.run("select.set", json!({"ids": [b]})).unwrap(); assert_eq!(shown(&app), [false, false, false, false, false, false, false, false, false, true], "in isolation mode"); + assert!(lock_unselected(&app), "shortcut command remains enabled in isolation"); app.run("object.exitIsolation", json!({})).unwrap(); app.run("select.set", json!({"ids": [g]})).unwrap(); assert_eq!(shown(&app), [true; 10], "after Exit Isolation Mode"); diff --git a/crates/ui-egui/src/menus/tree.rs b/crates/ui-egui/src/menus/tree.rs index 48def5dbc..a64821a01 100644 --- a/crates/ui-egui/src/menus/tree.rs +++ b/crates/ui-egui/src/menus/tree.rs @@ -165,12 +165,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { c("Ungroup", "object.ungroup"), sub( "Lock", - vec![ - c("Selection", "object.lock"), - c("All Artwork Above", "object.lock.above"), - c("Other Layers", "object.lock.otherLayers"), - c("Unselected Artwork", "object.lock.unselected"), - ], + vec![c("Selection", "object.lock"), c("All Artwork Above", "object.lock.above"), c("Other Layers", "object.lock.otherLayers")], ), c("Unlock All", "object.unlockAll"), sub( diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 227b0c636..b47a321bc 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -336,22 +336,25 @@ 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. +/// Cmd+Alt+Shift+2 locks unselected artwork, including inside an isolated group. #[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")); + assert_eq!(crate::menus::shortcut_of("object.hide.unselected"), Some("Cmd+Alt+Shift+3")); + assert!(!crate::menus::menu_tree_named(true).iter().any(|(_, items)| format!("{items:?}").contains("object.lock.unselected"))); 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)); + app.session.execute("select.none", &json!({})).unwrap(); + assert!(!crate::menus::enabled(&app, "object.lock.unselected"), "nothing selected"); 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(); + assert!(crate::menus::enabled(&app, "object.lock.unselected"), "selection present"); 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)]); @@ -359,15 +362,15 @@ fn cmd_alt_shift_2_locks_unselected_artwork() { 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. + // The shortcut uses the isolated group as its scope. 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")); + 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"); + assert_eq!(locked(&app), [false, true], "unselected member locks in isolation mode"); } /// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724). diff --git a/docs/mcp.md b/docs/mcp.md index 618c625ad..db0b6bfab 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1398,11 +1398,13 @@ 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. +`object.lock.unselected` (Cmd+Alt+Shift+2; shortcut and command, with no Object › Lock menu item) locks unselected +artwork in one undo step and returns `{count}`. In a partly selected group, clipping group, or compound path, it locks +the unselected members and leaves the container and selected members unlocked. It includes unselected guides even +while Lock Guides is on. Hidden art and art on locked layers stay individually unlocked; a selected layer's artwork +counts as selected. In ordinary isolation mode it locks only unselected art inside the isolated object. The selection +stays as it is. `object.unlockAll` unlocks and selects all locked artwork in the document, including art locked before +this command. With nothing to lock it records no undo step. 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` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 628534142..77cecd5a7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -962,8 +962,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Libraries panel holds gradients and patterns: its + button adds the selection's fill or stroke gradient or pattern, a gradient or pattern swatch dragged from the Swatches panel is added, and a click paints the selection through the active Fill/Stroke proxy, a pattern bringing its definition and swatch into the document. 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." + Cmd/Ctrl+Alt+Shift+2 (`object.lock.unselected`) locks unselected artwork in one undo step and has no Object › Lock menu item. It locks unselected members of partly selected groups, clipping groups and compound paths, and unselected guides while Lock Guides is on. In ordinary isolation mode it locks only unselected art inside the isolated object. Object › Unlock All unlocks all locked art in the document. The [public shortcut page](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) calls the action "Lock all deselected artwork". + In isolation mode, the visible 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 shortcut-only `object.lock.unselected` command remains enabled with a selection. The Contextual Task Bar's Lock button is greyed out too, the Properties panel's Quick Actions have no Lock, and the other Lock and Hide commands return an error. Pattern editing mode and opacity mask editing do not count as isolation mode for them. 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.