diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index 411f2004a..6b658da0d 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -179,6 +179,21 @@ pub fn has_multi(s: &Session) -> std::result::Result<(), String> { let st = s.active().ok_or("no document open")?; if st.selection.len() < 2 { Err("select at least two objects".into()) } else { Ok(()) } } +/// A document is open and it is not in isolation mode: Object › Lock's and Object › Hide's items, +/// Unlock All and Show All are off in isolation mode. Pattern editing mode and opacity mask +/// editing isolate their art too, and do not count as isolation mode here while their own layer +/// is the isolated one. An object isolated inside them counts. +pub fn outside_isolation(s: &Session) -> std::result::Result<(), String> { + let st = s.active().ok_or("no document open")?; + let editing_layer = |l| st.doc.pattern_edit.as_ref().is_some_and(|e| e.layer == l) || st.doc.mask_edit.as_ref().is_some_and(|e| e.layer == l); + if st.isolation.is_some_and(|l| !editing_layer(l)) { Err("exit isolation mode first".into()) } else { Ok(()) } +} +/// Objects are selected ([`has_selection`]) and the document is not in isolation mode +/// ([`outside_isolation`]). +pub fn has_selection_outside_isolation(s: &Session) -> std::result::Result<(), String> { + outside_isolation(s)?; + has_selection(s) +} pub fn can_undo(s: &Session) -> std::result::Result<(), String> { s.active().filter(|d| !d.history.undo.is_empty()).map(|_| ()).ok_or_else(|| "nothing to undo".into()) } diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index 6d8f9fd40..e4e02128c 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -16,7 +16,7 @@ use vectorcraft_render::encode::{RasterExportOptions, RasterFormat}; use super::edit::{duplicate_in, selected_roots}; use super::{ CommandSpec, EngineError, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, fileio, find_command, has_doc, has_selection, - switch_or, + has_selection_outside_isolation, switch_or, }; /// Session-level (not saved) state owned by the menu commands. @@ -44,7 +44,7 @@ pub fn specs() -> Vec { ["Object", "Lock"], None, "{} lock objects in the same layer that are above and overlap the selection, deselecting every selected object left locked or hidden (the key object too) → {count}", - has_selection, + has_selection_outside_isolation, |s, _| above(s, true) ), cmd!( @@ -53,7 +53,7 @@ pub fn specs() -> Vec { ["Object", "Lock"], None, "{} lock every layer that holds no selected object → {count}", - has_selection, + has_selection_outside_isolation, |s, _| other_layers(s, true) ), cmd!( @@ -62,7 +62,7 @@ pub fn specs() -> Vec { ["Object", "Hide"], None, "{} hide objects in the same layer that are above and overlap the selection, deselecting every selected object left hidden or locked (the key object too) → {count}", - has_selection, + has_selection_outside_isolation, |s, _| above(s, false) ), cmd!( @@ -71,7 +71,7 @@ pub fn specs() -> Vec { ["Object", "Hide"], None, "{} hide every layer that holds no selected object → {count}", - has_selection, + has_selection_outside_isolation, |s, _| other_layers(s, false) ), cmd!( diff --git a/crates/engine/src/cmd/object/commands.rs b/crates/engine/src/cmd/object/commands.rs index 4bf072749..5cf7fbe8e 100644 --- a/crates/engine/src/cmd/object/commands.rs +++ b/crates/engine/src/cmd/object/commands.rs @@ -6,6 +6,7 @@ use super::props::set_props; use super::shape::{expand_shape, set_live_shape}; use super::transform::{move_cmd, nudge, reflect, rotate, scale, shear, transform, transform_again}; use super::{CommandSpec, cmd, has_doc, has_multi, has_selection, has_selection_or_guides}; +use crate::cmd::{has_selection_outside_isolation, outside_isolation}; pub fn specs() -> Vec { vec![ @@ -150,20 +151,20 @@ pub fn specs() -> Vec { ["Object", "Lock"], Some("Cmd+2"), "{ids?: [id…] (selected first; default: the selection)}", - has_selection, + has_selection_outside_isolation, lock ), - cmd!("object.unlockAll", "Unlock All", ["Object"], Some("Cmd+Alt+2"), "{}", has_doc, unlock_all), + cmd!("object.unlockAll", "Unlock All", ["Object"], Some("Cmd+Alt+2"), "{}", outside_isolation, unlock_all), cmd!( "object.hide", "Selection", ["Object", "Hide"], Some("Cmd+3"), "{ids?: [id…] (selected first; default: the selection)}", - has_selection, + has_selection_outside_isolation, hide ), - cmd!("object.showAll", "Show All", ["Object"], Some("Cmd+Alt+3"), "{}", has_doc, show_all), + cmd!("object.showAll", "Show All", ["Object"], Some("Cmd+Alt+3"), "{}", outside_isolation, show_all), cmd!( "object.compoundPath.make", "Make", diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index c02314b96..5c227c8aa 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -286,6 +286,145 @@ fn lock_and_hide_other_layers() { assert!(!s.doc().unwrap().doc.node(l2).unwrap().visible); } +/// Whether `id` is visible and whether it is locked. +fn shown_and_locked(s: &Session, id: NodeId) -> (bool, bool) { + let n = node(s, id); + (n.visible, n.locked) +} + +/// Each of `commands` is off in isolation mode: the command list reports it disabled, its error +/// names isolation mode, and so does the error of a call that gives the selection as `ids` (for a +/// command that takes them). +fn off_in_isolation(s: &mut Session, commands: &[&str]) { + let ids: Vec = selected(s).iter().map(|i| i.0).collect(); + for c in commands { + let spec = find_command(c).unwrap(); + assert_eq!(spec.info(s).disabled_reason.as_deref(), Some("exit isolation mode first"), "{c}"); + let given = cmd::params::takes_key(spec.params, "ids").then(|| json!({"ids": ids})); + for p in std::iter::once(json!({})).chain(given) { + let e = s.execute(c, &p).unwrap_err().to_string(); + assert!(e.contains("exit isolation mode first"), "{c} {p}: {e}"); + } + } +} + +/// In isolation mode, Lock › Selection and Hide › Selection are off and change nothing, in an +/// isolated group and in an isolated compound path with one of its paths selected. After Exit +/// Isolation Mode they lock and hide again. +#[test] +fn lock_and_hide_selection_are_off_in_isolation_mode() { + 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); + sel(&mut s, &[a, b]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + let outer = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + let hole = rect(&mut s, 120.0, 120.0, 20.0, 20.0); + sel(&mut s, &[outer, hole]); + let compound = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap()); + let ids = [a, b, g, outer, hole, compound]; + let commands = ["object.lock", "object.hide"]; + for (isolated, member) in [(g, a), (compound, hole)] { + s.execute("object.isolate", &json!({"id": isolated.0})).unwrap(); + sel(&mut s, &[member]); + off_in_isolation(&mut s, &commands); + for id in ids { + assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?} in isolation of {isolated:?}"); + } + assert_eq!(selected(&s), [member]); + s.execute("object.exitIsolation", &json!({})).unwrap(); + } + sel(&mut s, &[a]); + s.execute("object.lock", &json!({})).unwrap(); + sel(&mut s, &[outer]); + s.execute("object.hide", &json!({})).unwrap(); + assert_eq!(shown_and_locked(&s, a), (true, true)); + assert_eq!(shown_and_locked(&s, compound), (false, false)); +} + +/// In isolation mode, Unlock All and Show All are off: the locked and the hidden objects outside +/// and inside the isolated group stay as they are. Pattern editing mode and opacity mask editing +/// are not isolation mode for them. +#[test] +fn unlock_all_and_show_all_are_off_in_isolation_mode() { + 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 c = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + sel(&mut s, &[a, b, c]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + let locked = rect(&mut s, 100.0, 0.0, 10.0, 10.0); + let hidden = rect(&mut s, 120.0, 0.0, 10.0, 10.0); + s.execute("layer.setProps", &json!({"id": locked.0, "locked": true})).unwrap(); + s.execute("layer.setProps", &json!({"id": hidden.0, "visible": false})).unwrap(); + s.execute("layer.setProps", &json!({"id": b.0, "locked": true})).unwrap(); + s.execute("layer.setProps", &json!({"id": c.0, "visible": false})).unwrap(); + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + sel(&mut s, &[a]); + off_in_isolation(&mut s, &["object.unlockAll", "object.showAll"]); + for (id, want) in [(locked, (true, true)), (hidden, (false, false)), (b, (true, true)), (c, (false, false))] { + assert_eq!(shown_and_locked(&s, id), want, "{id:?}"); + } + s.execute("object.exitIsolation", &json!({})).unwrap(); + s.execute("object.unlockAll", &json!({})).unwrap(); + s.execute("object.showAll", &json!({})).unwrap(); + for id in [locked, hidden, b, c] { + assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?}"); + } + + // Pattern editing mode: the tile's art hides and shows again. + let r = rect(&mut s, 300.0, 300.0, 10.0, 10.0); + sel(&mut s, &[r]); + s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); + let tile = selected(&s); + s.execute("object.hide", &json!({})).unwrap(); + assert!(!node(&s, tile[0]).visible, "hidden in pattern editing mode"); + s.execute("object.showAll", &json!({})).unwrap(); + assert!(node(&s, tile[0]).visible, "shown in pattern editing mode"); + // An object isolated inside pattern editing mode is isolation mode again. + s.execute("object.isolate", &json!({"id": tile[0].0})).unwrap(); + sel(&mut s, &[tile[0]]); + off_in_isolation(&mut s, &["object.unlockAll", "object.showAll"]); + s.execute("object.pattern.cancel", &json!({})).unwrap(); + + // Opacity mask editing: the mask's art locks and unlocks again. + let obj = rect(&mut s, 0.0, 300.0, 200.0, 200.0); + let m = rect(&mut s, 0.0, 300.0, 100.0, 200.0); + sel(&mut s, &[obj, m]); + s.execute("transparency.makeOpacityMask", &json!({})).unwrap(); + s.execute("transparency.editOpacityMask", &json!({})).unwrap(); + let mask = selected(&s); + s.execute("object.lock", &json!({})).unwrap(); + assert!(node(&s, mask[0]).locked, "locked in opacity mask editing"); + s.execute("object.unlockAll", &json!({})).unwrap(); + assert!(!node(&s, mask[0]).locked, "unlocked in opacity mask editing"); +} + +/// In isolation mode, Lock › All Artwork Above, Lock › Other Layers, Hide › All Artwork Above and +/// Hide › Other Layers are off: the art above and the other layer stay as they are. +#[test] +fn lock_and_hide_above_and_other_layers_are_off_in_isolation_mode() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let b = rect(&mut s, 200.0, 0.0, 10.0, 10.0); + sel(&mut s, &[a, b]); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + let over = rect(&mut s, 50.0, 50.0, 100.0, 100.0); + let l2 = id_of(&s.execute("layer.new", &json!({})).unwrap()); + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + sel(&mut s, &[a]); + off_in_isolation(&mut s, &["object.lock.above", "object.lock.otherLayers", "object.hide.above", "object.hide.otherLayers"]); + for id in [a, b, g, over, l2] { + assert_eq!(shown_and_locked(&s, id), (true, false), "{id:?}"); + } + s.execute("object.exitIsolation", &json!({})).unwrap(); + sel(&mut s, &[g]); + assert_eq!(s.execute("object.lock.above", &json!({})).unwrap(), json!({"count": 1})); + s.execute("object.hide.otherLayers", &json!({})).unwrap(); + assert_eq!(shown_and_locked(&s, over), (true, true)); + assert_eq!(shown_and_locked(&s, l2), (false, false)); +} + /// How many steps there are to undo and to redo, and whether the document has unsaved changes. fn history(s: &Session) -> (usize, usize, bool) { let st = s.doc().unwrap(); diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index d7cc1c4df..a81d8307f 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -225,7 +225,9 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() { run = Some(("ui.recolorDialog", json!({}))); } - if widgets::icon_button(ui, "lock", tl!("Lock (⌘2)"), false, 30.0).clicked() { + // The Lock button is off in isolation mode, as Object › Lock › Selection is. + let lockable = crate::menus::enabled(app, "object.lock"); + if widgets::icon_button_enabled(ui, "lock", tl!("Lock (⌘2)"), false, lockable, 30.0).clicked() { run = Some(("object.lock", json!({}))); } } diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 55a1341ab..bb76684dc 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6102,6 +6102,43 @@ fn the_task_bar_recolors_coloured_art() { assert!(app.ui.dialog.is_none(), "no Recolor button without colours"); } +/// The Contextual Task Bar's Lock button is off in isolation mode, as Object › Lock › Selection +/// is: a click locks nothing and shows no error. Outside isolation mode it locks the selection. +#[test] +fn the_task_bar_lock_button_is_off_in_isolation_mode() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let a = app.session.execute("shape.rectangle", &json!({"x": 150, "y": 130, "width": 40, "height": 40})).unwrap()["id"].clone(); + let b = app.session.execute("shape.rectangle", &json!({"x": 210, "y": 130, "width": 40, "height": 40})).unwrap()["id"].clone(); + app.session.execute("select.set", &json!({"ids": [a.clone(), b]})).unwrap(); + let g = app.session.execute("object.group", &json!({})).unwrap()["id"].clone(); + app.select_tool("selection"); + let ctx = egui::Context::default(); + let click_lock = |app: &mut VectorcraftApp| { + frame(app, &ctx, vec![]); + frame(app, &ctx, vec![]); + let bar = task_bar_rect(&ctx).expect("the task bar shows"); + // Right to left: the frame's margin, More Options, then Lock. + let at = pos2(bar.right() - 8.0 - 30.0 - 6.0 - 15.0, bar.center().y); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, &ctx, vec![egui::Event::PointerMoved(at)]); + frame(app, &ctx, vec![button(true)]); + frame(app, &ctx, vec![button(false)]); + }; + let locked = |app: &VectorcraftApp, id: &serde_json::Value| { + app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().locked + }; + app.session.execute("object.isolate", &json!({"id": g.clone()})).unwrap(); + app.session.execute("select.set", &json!({"ids": [a.clone()]})).unwrap(); + click_lock(&mut app); + assert!(!locked(&app, &a), "nothing is locked in isolation mode"); + assert_eq!(app.ui.status, "", "no error"); + app.session.execute("object.exitIsolation", &json!({})).unwrap(); + app.session.execute("select.set", &json!({"ids": [g.clone()]})).unwrap(); + click_lock(&mut app); + assert!(locked(&app, &g), "the group is locked outside isolation mode"); +} + /// The Contextual Task Bar shows Image Trace first for a selected image: it traces the image /// with the Default preset and leaves the Image Trace panel as it was. For the Image Trace /// object it made, the first button is Expand, which keeps only the traced paths. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8041aa9a3..973bc63e3 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4155,6 +4155,7 @@ axis Both И двете @msg select an object in perspective изберете обект в перспектива @msg select artwork (or a colour group) to recolor изберете графика (или цветова група) за преоцветяване @msg select at least two objects изберете поне два обекта +@msg exit isolation mode first първо излезте от режима на изолация @msg select objects, not layers изберете обекти, а не слоеве @msg select one path, or give its `id` изберете един път или задайте неговия `id` @msg select paths to join изберете пътища за обединяване diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3d354adcf..2375fca0f 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4561,6 +4561,7 @@ imageTrace Outline Obrys @msg select an object in perspective vyberte objekt v perspektivě @msg select artwork (or a colour group) to recolor vyberte kresbu (nebo skupinu barev) k přebarvení @msg select at least two objects vyberte alespoň dva objekty +@msg exit isolation mode first nejprve ukončete izolovaný režim @msg select objects, not layers vyberte objekty, ne vrstvy @msg select one path, or give its `id` vyberte jednu cestu, nebo zadejte její `id` @msg select paths to join vyberte cesty ke spojení diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 3816a2b89..4ad040f26 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4258,6 +4258,7 @@ imageTrace Outline Kontur @msg select an object in perspective markér et objekt i perspektiv @msg select artwork (or a colour group) to recolor markér grafik (eller en farvegruppe) for at skifte farver @msg select at least two objects markér mindst to objekter +@msg exit isolation mode first afslut først isoleringstilstand @msg select objects, not layers markér objekter, ikke lag @msg select one path, or give its `id` markér én sti, eller angiv dens `id` @msg select paths to join markér stier, der skal forbindes diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c0dc4648f..6ab344853 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4184,6 +4184,7 @@ theme Appearance Erscheinungsbild @msg select an object in perspective wählen Sie ein Objekt in der Perspektive aus @msg select artwork (or a colour group) to recolor wählen Sie eine Grafik (oder eine Farbgruppe) zum Neufärben aus @msg select at least two objects wählen Sie mindestens zwei Objekte aus +@msg exit isolation mode first beenden Sie zuerst den Isolationsmodus @msg select objects, not layers wählen Sie Objekte aus, keine Ebenen @msg select one path, or give its `id` wählen Sie einen Pfad aus oder geben Sie seine `id` an @msg select paths to join wählen Sie Pfade zum Zusammenfügen aus diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 5a8799a43..3540bc823 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4217,6 +4217,7 @@ theme Appearance Εμφάνιση @msg select an object in perspective επιλέξτε αντικείμενο σε προοπτική @msg select artwork (or a colour group) to recolor επιλέξτε σχέδιο (ή ομάδα χρωμάτων) για επαναχρωματισμό @msg select at least two objects επιλέξτε τουλάχιστον δύο αντικείμενα +@msg exit isolation mode first βγείτε πρώτα από τη λειτουργία απομόνωσης @msg select objects, not layers επιλέξτε αντικείμενα, όχι επίπεδα @msg select one path, or give its `id` επιλέξτε μία διαδρομή ή δώστε το `id` της @msg select paths to join επιλέξτε διαδρομές για ένωση diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a8b4cfb32..20277db77 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4259,6 +4259,7 @@ axis Both Ambos @msg select an object in perspective selecciona un objeto en perspectiva @msg select artwork (or a colour group) to recolor selecciona la ilustración (o un grupo de colores) que quieres volver a colorear @msg select at least two objects selecciona al menos dos objetos +@msg exit isolation mode first sal primero del modo de aislamiento @msg select objects, not layers selecciona objetos, no capas @msg select one path, or give its `id` selecciona un trazado o indica su `id` @msg select paths to join selecciona los trazados que quieres unir diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 51ca6523c..ad2b1f261 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4468,6 +4468,7 @@ imageTrace Outline Ääriviiva @msg select an object in perspective valitse perspektiivissä oleva objekti @msg select artwork (or a colour group) to recolor valitse uudelleenväritettävä kuvitus (tai väriryhmä) @msg select at least two objects valitse vähintään kaksi objektia +@msg exit isolation mode first poistu ensin eristystilasta @msg select objects, not layers valitse objekteja, ei tasoja @msg select one path, or give its `id` valitse yksi polku tai anna sen `id` @msg select paths to join valitse liitettävät polut diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3272215f7..a3b8a70b8 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4256,6 +4256,7 @@ axis Both Les deux @msg select an object in perspective sélectionnez un objet en perspective @msg select artwork (or a colour group) to recolor sélectionnez l’illustration (ou un groupe de couleurs) dont redéfinir les couleurs @msg select at least two objects sélectionnez au moins deux objets +@msg exit isolation mode first quittez d’abord le mode Isolation @msg select objects, not layers sélectionnez des objets, pas des calques @msg select one path, or give its `id` sélectionnez un tracé ou indiquez son `id` @msg select paths to join sélectionnez les tracés à joindre diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index d5b79d72e..9e4d5e586 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4969,6 +4969,7 @@ imageTrace Outline Körvonal @msg select an object in perspective jelölj ki egy perspektivikus objektumot @msg select artwork (or a colour group) to recolor jelölj ki átszínezendő grafikát (vagy színcsoportot) @msg select at least two objects jelölj ki legalább két objektumot +@msg exit isolation mode first előbb lépj ki az elkülönítési módból @msg select objects, not layers objektumokat jelölj ki, rétegeket ne @msg select one path, or give its `id` jelölj ki egy útvonalat, vagy add meg az `id` értékét @msg select paths or anchor points jelölj ki útvonalakat vagy horgonypontokat diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index bda640804..71d894fbe 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4258,6 +4258,7 @@ imageTrace Outline Kontur @msg select an object in perspective pilih objek dalam perspektif @msg select artwork (or a colour group) to recolor pilih karya seni (atau grup warna) untuk diwarnai ulang @msg select at least two objects pilih setidaknya dua objek +@msg exit isolation mode first keluar dari mode isolasi terlebih dahulu @msg select objects, not layers pilih objek, bukan lapisan @msg select one path, or give its `id` pilih satu jalur, atau berikan `id`-nya @msg select paths to join pilih jalur yang akan digabungkan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 4ef697112..bc12f151c 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4299,6 +4299,7 @@ axis Both Entrambi @msg select an object in perspective seleziona un oggetto in prospettiva @msg select artwork (or a colour group) to recolor seleziona la grafica (o un gruppo colori) da ricolorare @msg select at least two objects seleziona almeno due oggetti +@msg exit isolation mode first esci prima dalla modalità isolamento @msg select objects, not layers seleziona oggetti, non livelli @msg select one path, or give its `id` seleziona un tracciato o specifica il suo `id` @msg select paths to join seleziona i tracciati da unire diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 22a9cd7db..4ac4b1df8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4092,6 +4092,7 @@ swatch Bright Violet ビビッドバイオレット @msg clipboard is empty クリップボードが空です @msg no anchor points selected アンカーポイントが選択されていません @msg select at least two objects オブジェクトを 2 つ以上選択してください +@msg exit isolation mode first 先に分離モードを終了してください @msg no saved presets 保存したプリセットはありません @msg compound paths can only contain paths 複合パスに含められるのはパスだけです @msg no bounds 境界がありません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 21d7a6247..035f11e37 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4022,6 +4022,7 @@ font-technology Variable ცვლადი @msg select an object in perspective მონიშნეთ ობიექტი პერსპექტივაში @msg select artwork (or a colour group) to recolor მონიშნეთ ხელახლა შესაფერადებელი ნახატი (ან ფერთა ჯგუფი) @msg select at least two objects მონიშნეთ ორი ობიექტი მაინც +@msg exit isolation mode first ჯერ გამოდით იზოლაციის რეჟიმიდან @msg select objects, not layers მონიშნეთ ობიექტები და არა შრეები @msg select one path, or give its `id` მონიშნეთ ერთი ტრაექტორია ან მიუთითეთ მისი `id` @msg select paths to join მონიშნეთ შესაერთებელი ტრაექტორიები diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 46ee1f775..e16ffc530 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4024,6 +4024,7 @@ theme Appearance 외관 @msg clipboard is empty 클립보드가 비어 있습니다 @msg no anchor points selected 선택한 앵커 포인트가 없습니다 @msg select at least two objects 개체를 두 개 이상 선택하세요 +@msg exit isolation mode first 먼저 격리 모드를 종료하세요 @msg no saved presets 저장된 프리셋이 없습니다 @msg compound paths can only contain paths 컴파운드 패스에는 패스만 넣을 수 있습니다 @msg no bounds 경계가 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 59d7280d8..b09f5c1b7 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4270,6 +4270,7 @@ imageTrace Outline Contour @msg select an object in perspective selecteer een object in perspectief @msg select artwork (or a colour group) to recolor selecteer een illustratie (of een kleurgroep) om te herkleuren @msg select at least two objects selecteer minstens twee objecten +@msg exit isolation mode first verlaat eerst de isolatiemodus @msg select objects, not layers selecteer objecten, geen lagen @msg select one path, or give its `id` selecteer één pad, of geef het `id` @msg select paths to join selecteer paden om te verbinden diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 74fe7890f..015763e57 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4388,6 +4388,7 @@ imageTrace Outline Kontur @msg select an object in perspective zaznacz obiekt w perspektywie @msg select artwork (or a colour group) to recolor zaznacz kompozycję (lub grupę kolorów), aby zmienić kolory @msg select at least two objects zaznacz co najmniej dwa obiekty +@msg exit isolation mode first najpierw wyjdź z trybu izolacji @msg select objects, not layers zaznacz obiekty, nie warstwy @msg select one path, or give its `id` zaznacz jedną ścieżkę lub podaj jej `id` @msg select paths to join zaznacz ścieżki do połączenia diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a7cebfb79..45c606940 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3831,6 +3831,7 @@ axis Both Ambos @msg select an object in perspective selecione um objeto em perspectiva @msg select artwork (or a colour group) to recolor selecione uma arte-final (ou um grupo de cores) para recolorir @msg select at least two objects selecione pelo menos dois objetos +@msg exit isolation mode first saia do modo de isolamento primeiro @msg select objects, not layers selecione objetos, não camadas @msg select one path, or give its `id` selecione um traçado ou informe seu `id` @msg select paths to join selecione traçados para unir diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 712bf3fe6..e222fd7c3 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4295,6 +4295,7 @@ axis Both Оба @msg select an object in perspective выделите объект в перспективе @msg select artwork (or a colour group) to recolor выделите рисунок (или цветовую группу) для перекрашивания @msg select at least two objects выделите как минимум два объекта +@msg exit isolation mode first сначала выйдите из режима изоляции @msg select objects, not layers выделите объекты, а не слои @msg select one path, or give its `id` выделите один контур или укажите его `id` @msg select paths to join выделите контуры для соединения diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 9f25ce924..12ba977c0 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4168,6 +4168,7 @@ axis Both Båda @msg select an object in perspective markera ett objekt i perspektiv @msg select artwork (or a colour group) to recolor markera grafik (eller en färggrupp) att färga om @msg select at least two objects markera minst två objekt +@msg exit isolation mode first avsluta isoleringsläget först @msg select objects, not layers markera objekt, inte lager @msg select one path, or give its `id` markera en bana eller ange dess `id` @msg select paths to join markera banor att foga samman diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f2553aef2..bf6958f3f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4024,6 +4024,7 @@ axis Both Her İkisi @msg select an object in perspective perspektifte bir nesne seç @msg select artwork (or a colour group) to recolor yeniden renklendirilecek resmi (veya bir renk grubunu) seçin @msg select at least two objects en az iki nesne seç +@msg exit isolation mode first önce izolasyon modundan çık @msg select objects, not layers katmanları değil nesneleri seçin @msg select one path, or give its `id` bir yol seçin veya "kimliğini" verin @msg select paths to join katılmak için yolları seçin diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c09c33934..fbe9134d9 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4283,6 +4283,7 @@ axis Both Обидві @msg select an object in perspective виділіть об’єкт у перспективі @msg select artwork (or a colour group) to recolor виділіть малюнок (або колірну групу) для перефарбовування @msg select at least two objects виділіть як мінімум два об’єкти +@msg exit isolation mode first спочатку вийдіть з режиму ізоляції @msg select objects, not layers виділіть об’єкти, а не шари @msg select one path, or give its `id` виділіть один контур або вкажіть його `id` @msg select paths to join виділіть контури для з’єднання diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index cde41165c..cd22afbdf 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4621,6 +4621,7 @@ theme Appearance Giao diện @msg select an object in perspective hãy chọn đối tượng trong phối cảnh @msg select artwork (or a colour group) to recolor hãy chọn tác phẩm (hoặc nhóm màu) để tô lại @msg select at least two objects hãy chọn ít nhất hai đối tượng +@msg exit isolation mode first hãy thoát chế độ cô lập trước @msg select objects, not layers hãy chọn đối tượng, không phải lớp @msg select one path, or give its `id` hãy chọn một đường dẫn hoặc cung cấp `id` của nó @msg select paths or anchor points chọn đường dẫn hoặc điểm neo diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 89eb9baba..ee84e8f43 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3829,6 +3829,7 @@ imageTrace Outline 轮廓 @msg clipboard is empty 剪贴板为空 @msg no anchor points selected 未选择锚点 @msg select at least two objects 请至少选择两个对象 +@msg exit isolation mode first 请先退出隔离模式 @msg no saved presets 没有已保存的预设 @msg compound paths can only contain paths 复合路径只能包含路径 @msg no bounds 没有边界 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ece9df22f..a6c14b778 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1790,6 +1790,7 @@ file-filter Templates 範本 @msg clipboard is empty 剪貼簿是空的 @msg no anchor points selected 未選取錨點 @msg select at least two objects 請選取至少兩個物件 +@msg exit isolation mode first 請先結束隔離模式 @msg no saved presets 沒有已儲存的預設集 @msg compound paths can only contain paths 複合路徑只能包含路徑 @msg no bounds 沒有邊界 diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index d22b73258..ed1fd2180 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -655,6 +655,53 @@ fn expand_appearance_is_an_object_menu_item_only() { assert_eq!(paths, [vec!["Object".to_string()]]); } +/// In isolation mode, every item of Object › Lock and Object › Hide, Unlock All and Show All are +/// greyed out, while Group stays on. After Exit Isolation Mode they are on again. +#[test] +fn lock_and_hide_menus_are_greyed_out_in_isolation_mode() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + 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.set", json!({"ids": [a, b.clone()]})).unwrap(); + let g = app.run("object.group", json!({})).unwrap()["id"].clone(); + let lock_and_hide: Vec = menu_entries(&app) + .into_iter() + .filter(|e| { + let path: Vec<&str> = e.path.iter().map(String::as_str).collect(); + matches!(path[..], ["Object", "Lock" | "Hide"]) || path == ["Object"] && matches!(e.label.as_str(), "Unlock All" | "Show All") + }) + .filter_map(|e| e.command) + .collect(); + assert_eq!( + lock_and_hide, + [ + "object.lock", + "object.lock.above", + "object.lock.otherLayers", + "object.unlockAll", + "object.hide", + "object.hide.above", + "object.hide.otherLayers", + "object.showAll" + ] + ); + let shown = |app: &VectorcraftApp| -> Vec { + let entries = menu_entries(app); + lock_and_hide + .iter() + .chain([&"object.group".to_string()]) + .map(|id| entries.iter().any(|e| e.command.as_ref() == Some(id) && e.enabled)) + .collect() + }; + 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, true], "in isolation mode"); + app.run("object.exitIsolation", json!({})).unwrap(); + app.run("select.set", json!({"ids": [g]})).unwrap(); + assert_eq!(shown(&app), [true; 9], "after Exit Isolation Mode"); +} + #[test] fn menus_are_as_wide_as_their_items() { // Items size the popup to the widest label and shortcut instead of the widest a menu may be. diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index e242086bb..5db382e74 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -146,7 +146,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { actions.push((tl!("Offset Path"), "object.path.offsetPath")); actions.push((tl!("Simplify"), "object.path.simplify")); actions.push((tl!("Arrange: Bring to Front"), "object.arrange.bringToFront")); - actions.push((tl!("Lock"), "object.lock")); + // Lock is listed only when Object › Lock › Selection is on, so not in isolation mode. + if crate::menus::enabled(app, "object.lock") { + actions.push((tl!("Lock"), "object.lock")); + } let w = (ui.available_width() - 6.0) / 2.0; for pair in actions.chunks(2) { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index edd778a89..4b6af376e 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -1286,6 +1286,27 @@ fn blend_options_is_a_quick_action_for_one_blend_with_the_blend_tool() { assert!(!shows(&tall(&mut app, &ctx, vec![]), "Blend Options"), "a rectangle"); } +/// Quick Actions have no Lock in isolation mode, where Object › Lock › Selection is off. Outside +/// isolation mode Lock is there and locks the selection. +#[test] +fn lock_is_a_quick_action_outside_isolation_mode_only() { + let mut app = new_app(); + let (a, b) = (rect(&mut app, 20.0, 20.0, 40.0, 40.0), rect(&mut app, 200.0, 20.0, 40.0, 40.0)); + app.run("select.set", json!({"ids": [a.clone(), b]})).unwrap(); + let g = app.run("object.group", json!({})).unwrap()["id"].clone(); + app.run("object.isolate", json!({"id": g.clone()})).unwrap(); + app.run("select.set", json!({"ids": [a]})).unwrap(); + let ctx = egui::Context::default(); + let texts = tall(&mut app, &ctx, vec![]); + assert!(shows(&texts, "Offset Path") && !shows(&texts, "Lock"), "no Lock in isolation mode: {texts:?}"); + app.run("object.exitIsolation", json!({})).unwrap(); + app.run("select.set", json!({"ids": [g.clone()]})).unwrap(); + let texts = tall(&mut app, &ctx, vec![]); + press(&mut app, &ctx, text_at(&texts, "Lock").center()); + let locked = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(g.as_u64().unwrap())).unwrap().locked; + assert!(locked, "Lock locks the group"); +} + /// A Live Paint group selected with a path has Merge Live Paint in Quick Actions, which adds /// the path to the group in one undo step. Not for the group alone, nor for plain paths, nor /// with type selected, which Live Paint refuses. diff --git a/docs/mcp.md b/docs/mcp.md index 6bf025ee3..e4cd6d637 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2161,6 +2161,11 @@ and `layerRows`, the rows highlighted in the panel. opens no rows for it and scrolls as with the option off. A change made with the panel's own rows, buttons or menu is not located, and a change made during a drag is located when the drag ends. A document opens with only its top-level layers open; the open rows are saved in the native file and reopen that way. +- **Lock and Hide in isolation mode**: in isolation mode, Object › Lock's and Object › Hide's items (`object.lock`, + `object.lock.above`, `object.lock.otherLayers`, `object.hide`, `object.hide.above`, `object.hide.otherLayers`), + `object.unlockAll` and `object.showAll` are off, and a call returns the error "command `object.hide` is not + available right now: exit isolation mode first", with the command's own id. Pattern editing mode and opacity mask + editing do not count as isolation mode for these commands. - **Search and filter** (control channel and the app's MCP): the Search All field keeps the rows whose names hold the text typed, and `ui.layersFilter {objects?, layers?}` sets the choices of the Filter button beside it. `objects` (By Object) takes `paths` (paths and compound paths), `shapes` (live rectangles, ellipses, polygons and lines), `text`, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index aab69f52e..5439f0edc 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -901,6 +901,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names. - **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize. Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included. + 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. 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. Lock Grid, Lock Station Point, Snap to Grid and Show Rulers (View › Perspective Grid) and Import Perspective Grid Presets (its choice to replace saved presets of the same names) refuse a value other than true, false or null, such as "true", and leave the grid and the saved presets as they were.