From 02f2dd8c5a31042844d8bb53f08cfa083fd1c9d7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 29 Oct 2026 11:08:57 +0700 Subject: [PATCH] Object: Unlock All and Show All with Shift+Alt act only within the selected object's group Squashed from #4439 onto main. --- crates/engine/src/cmd/object/align.rs | 4 +- crates/engine/src/cmd/object/arrange.rs | 42 +++++++++++++-- crates/engine/src/cmd/object/commands.rs | 20 +++++++- crates/engine/src/tests.rs | 65 ++++++++++++++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/menus/bar.rs | 4 +- crates/ui-egui/src/menus/invoke.rs | 10 ++++ crates/ui-egui/src/menus/tests.rs | 53 +++++++++++++++++++ docs/mcp.md | 8 +++ docs/target-app-parity.md | 1 + 33 files changed, 221 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/object/align.rs b/crates/engine/src/cmd/object/align.rs index 873dc08ba..5c494db55 100644 --- a/crates/engine/src/cmd/object/align.rs +++ b/crates/engine/src/cmd/object/align.rs @@ -26,8 +26,8 @@ fn preview_bounds(s: &Session, p: &Value, cmd: &str) -> Result { } } -/// Param `key` of the Align commands, Shear and `text.setFormat`'s lists: one of `values`, or -/// `None` when it is missing or null. Any other value is an error that lists `values`. +/// Param `key` of the Align commands, Shear, Unlock All, Show All and `text.setFormat`'s lists: one +/// of `values`, or `None` when it is missing or null. Any other value is an error that lists `values`. pub(crate) fn choice<'a>(p: &'a Value, cmd: &str, key: &str, values: &[&str]) -> Result> { let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(None) }; match v.as_str() { diff --git a/crates/engine/src/cmd/object/arrange.rs b/crates/engine/src/cmd/object/arrange.rs index 48061fbe3..b03105778 100644 --- a/crates/engine/src/cmd/object/arrange.rs +++ b/crates/engine/src/cmd/object/arrange.rs @@ -172,8 +172,42 @@ fn collect_flag(d: &Document, f: impl Fn(&Node) -> bool) -> Vec { v } -pub(super) fn unlock_all(s: &mut Session, _: &Value) -> Result { - let ids = collect_flag(&s.doc()?.doc, |n| n.locked); +/// The objects that `f` picks for `object.unlockAll` and `object.showAll` (`cmd`): in the whole +/// document, or with `scope: "group"` inside the groups that hold the selected objects. A selected +/// group is the group that holds its own members, and the objects of a group inside another such +/// group are taken once. With `scope: "group"` and no selected object in a group, it returns an +/// error. +fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) -> bool) -> Result> { + let st = s.doc()?; + let d = &st.doc; + if choice(p, cmd, "scope", &["document", "group"])? != Some("group") { + return Ok(collect_flag(d, f)); + } + let is_group = |id: &NodeId| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. })); + let mut groups: Vec = st.selection.objects.iter().filter_map(|id| d.ancestry(*id)?.into_iter().rev().find(is_group)).collect(); + groups.sort(); + groups.dedup(); + if groups.is_empty() { + return Err(EngineError::Other("nothing selected is in a group".into())); + } + let mut v = vec![]; + for g in &groups { + if d.ancestry(*g).is_some_and(|a| a.iter().any(|up| up != g && groups.contains(up))) { + continue; + } + for c in d.node(*g).and_then(Node::children).into_iter().flatten() { + c.walk(&mut |n| { + if !n.is_layer() && f(n) { + v.push(n.id) + } + }); + } + } + Ok(v) +} + +pub(super) fn unlock_all(s: &mut Session, p: &Value) -> Result { + let ids = collect_flag_in_scope(s, "object.unlockAll", p, |n| n.locked)?; // With nothing locked, the document, the selection and the undo history stay as they are. if ids.is_empty() { return Ok(json!({ "count": 0 })); @@ -211,8 +245,8 @@ pub(super) fn hide(s: &mut Session, _: &Value) -> Result { ok() } -pub(super) fn show_all(s: &mut Session, _: &Value) -> Result { - let ids = collect_flag(&s.doc()?.doc, |n| !n.visible); +pub(super) fn show_all(s: &mut Session, p: &Value) -> Result { + let ids = collect_flag_in_scope(s, "object.showAll", p, |n| !n.visible)?; // With nothing hidden, the document, the selection and the undo history stay as they are. if ids.is_empty() { return Ok(json!({ "count": 0 })); diff --git a/crates/engine/src/cmd/object/commands.rs b/crates/engine/src/cmd/object/commands.rs index b01380411..70e73d004 100644 --- a/crates/engine/src/cmd/object/commands.rs +++ b/crates/engine/src/cmd/object/commands.rs @@ -154,7 +154,15 @@ pub fn specs() -> Vec { has_selection_outside_isolation, lock ), - cmd!("object.unlockAll", "Unlock All", ["Object"], Some("Cmd+Alt+2"), "{}", outside_isolation, unlock_all), + cmd!( + "object.unlockAll", + "Unlock All", + ["Object"], + Some("Cmd+Alt+2"), + "{scope?: \"document\"|\"group\" (default document; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} unlock the locked objects and select them → {count}", + outside_isolation, + unlock_all + ), cmd!( "object.hide", "Selection", @@ -164,7 +172,15 @@ pub fn specs() -> Vec { has_selection_outside_isolation, hide ), - cmd!("object.showAll", "Show All", ["Object"], Some("Cmd+Alt+3"), "{}", outside_isolation, show_all), + cmd!( + "object.showAll", + "Show All", + ["Object"], + Some("Cmd+Alt+3"), + "{scope?: \"document\"|\"group\" (default document; group: only the objects inside the groups that hold the selected objects, a selected group holding its own; the in-window menu item chosen with Shift+Alt)} show the hidden objects and select them → {count}", + outside_isolation, + show_all + ), cmd!( "object.compoundPath.make", "Make", diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8b2fe6aba..eb36d7943 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1671,6 +1671,71 @@ fn lock_hide_unlock_all_and_show_all_with_nothing_to_do_record_no_undo_step() { } } +/// Unlock All and Show All with `scope: "group"` unlock or show only the objects inside the groups +/// that hold the selected objects: the group nearest above a selected object, or a selected group +/// itself. With no selected object in a group they are refused and change nothing. +#[test] +fn unlock_all_and_show_all_within_the_groups_of_the_selection() { + let mut s = session(); + let ids = |s: &mut Session, cmd: &str| NodeId(s.execute(cmd, &json!({})).unwrap()["id"].as_u64().unwrap()); + let [a, b, m, k, h, s1, s2, j, j2] = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0].map(|i| rect(&mut s, i * 30.0, 0.0, 20.0, 20.0)); + // `g` holds `m`, `k`, `h` and the group `sub`, which holds `s1` and `s2`; `other` holds `j` and `j2`. + s.execute("select.set", &json!({"ids": [s1.0, s2.0]})).unwrap(); + let sub = ids(&mut s, "object.group"); + s.execute("select.set", &json!({"ids": [m.0, k.0, h.0, sub.0]})).unwrap(); + let g = ids(&mut s, "object.group"); + s.execute("select.set", &json!({"ids": [j.0, j2.0]})).unwrap(); + let other = ids(&mut s, "object.group"); + for id in [a, k, s1, j] { + s.execute("layer.setProps", &json!({"id": id.0, "locked": true})).unwrap(); + } + s.execute("layer.setProps", &json!({"id": h.0, "visible": false})).unwrap(); + let locked = |s: &Session| [a, k, s1, j].map(|id| s.doc().unwrap().doc.node(id).unwrap().locked); + let group = json!({"scope": "group"}); + + // A member of `g`, or `g` itself, selected: `k` and `s1` inside `sub` are unlocked and selected. + for selected in [m, g] { + s.execute("select.set", &json!({"ids": [selected.0]})).unwrap(); + let before = history(&s); + assert_eq!(s.execute("object.unlockAll", &group).unwrap(), json!({"count": 2}), "{selected:?}"); + assert_eq!(locked(&s), [true, false, false, true], "{selected:?}"); + assert_eq!(s.doc().unwrap().selection.objects, [k, s1], "{selected:?}"); + assert_eq!(history(&s).0, before.0 + 1, "{selected:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + } + // `s2` selected: only `sub` counts. With `m` selected too, `sub`'s objects are taken once. + for (selected, count) in [(vec![s2.0], 1), (vec![m.0, s2.0], 2)] { + s.execute("select.set", &json!({"ids": selected})).unwrap(); + assert_eq!(s.execute("object.unlockAll", &group).unwrap(), json!({"count": count}), "{selected:?}"); + assert!(!s.doc().unwrap().doc.node(s1).unwrap().locked, "{selected:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + } + // Show All within `g` shows `h` and selects it; `other` holds nothing hidden. + s.execute("select.set", &json!({"ids": [m.0]})).unwrap(); + assert_eq!(s.execute("object.showAll", &group).unwrap(), json!({"count": 1})); + assert_eq!(s.doc().unwrap().selection.objects, [h]); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [other.0]})).unwrap(); + assert_eq!(s.execute("object.showAll", &group).unwrap(), json!({"count": 0})); + + // `b`, outside every group, or nothing selected: refused, and nothing changes. + for selected in [vec![b.0], vec![]] { + s.execute("select.set", &json!({"ids": selected})).unwrap(); + let (doc, before) = (s.doc().unwrap().doc.clone(), history(&s)); + for cmd in ["object.unlockAll", "object.showAll"] { + let e = s.execute(cmd, &group).unwrap_err(); + assert!(matches!(&e, EngineError::Other(msg) if msg == "nothing selected is in a group"), "{cmd}: {e:?}"); + } + assert!(s.doc().unwrap().doc == doc && history(&s) == before, "{selected:?}"); + } + // Any other scope is refused; `scope: "document"` and no scope take the whole document. + let e = s.execute("object.unlockAll", &json!({"scope": "layer"})).unwrap_err(); + assert!(matches!(&e, EngineError::BadParams { msg, .. } if msg == "`scope` must be one of document, group"), "{e:?}"); + assert_eq!(s.execute("object.unlockAll", &json!({"scope": "document"})).unwrap(), json!({"count": 4})); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.execute("object.showAll", &json!({})).unwrap(), json!({"count": 1})); +} + /// Set Bounds with the current position and size records no undo step on a rotated object, with /// and without Use Preview Bounds, and on an unrotated object with Use Preview Bounds. In these /// cases computing the transform can leave a rounding error in it. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8615552dc..30f53cce7 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4088,6 +4088,7 @@ axis Both И двете @msg not in pattern editing mode не е в режим на редактиране на мостра @msg nothing encoded нищо не е кодирано @msg nothing selected нищо не е избрано +@msg nothing selected is in a group нищо от избраното не е в група @msg nothing to align няма нищо за подравняване @msg nothing to distort няма нищо за изкривяване @msg nothing to export: the document has no visible art (or turn on Use Artboards) няма нищо за експортиране: документът няма видима графика (или включете „Използване на работни области“) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 2375fca0f..83ee8b579 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4488,6 +4488,7 @@ imageTrace Outline Obrys @msg not in pattern editing mode není aktivní režim úprav vzorku @msg nothing encoded nic nebylo zakódováno @msg nothing selected nic není vybráno +@msg nothing selected is in a group nic z výběru není ve skupině @msg nothing to align není co zarovnat @msg nothing to distort není co deformovat @msg nothing to export: the document has no visible art (or turn on Use Artboards) není co exportovat: dokument nemá žádnou viditelnou kresbu (nebo zapněte Použít pracovní plochy) diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 4ad040f26..5f1a015da 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4193,6 +4193,7 @@ imageTrace Outline Kontur @msg not in pattern editing mode ikke i mønsterredigeringstilstand @msg nothing encoded intet kodet @msg nothing selected intet markeret +@msg nothing selected is in a group intet af det markerede er i en gruppe @msg nothing to align intet at justere @msg nothing to distort intet at forvrænge @msg nothing to export: the document has no visible art (or turn on Use Artboards) intet at eksportere: dokumentet har ingen synlig grafik (eller aktivér Brug artboards) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 6ab344853..0f5e69309 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4111,6 +4111,7 @@ theme Appearance Erscheinungsbild @msg not in pattern editing mode nicht im Musterbearbeitungsmodus @msg nothing encoded nichts kodiert @msg nothing selected nichts ausgewählt +@msg nothing selected is in a group nichts Ausgewähltes befindet sich in einer Gruppe @msg nothing to align nichts auszurichten @msg nothing to distort nichts zu verzerren @msg nothing to export: the document has no visible art (or turn on Use Artboards) nichts zu exportieren: Das Dokument enthält keine sichtbare Grafik (oder aktivieren Sie „Zeichenflächen verwenden“) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 3540bc823..ddbb4aecd 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4150,6 +4150,7 @@ theme Appearance Εμφάνιση @msg not in pattern editing mode δεν βρίσκεστε σε λειτουργία επεξεργασίας μοτίβου @msg nothing encoded δεν κωδικοποιήθηκε τίποτα @msg nothing selected δεν έχει επιλεγεί τίποτα +@msg nothing selected is in a group τίποτα από τα επιλεγμένα δεν βρίσκεται σε ομάδα @msg nothing to align δεν υπάρχει τίποτα για στοίχιση @msg nothing to distort δεν υπάρχει τίποτα για παραμόρφωση @msg nothing to export: the document has no visible art (or turn on Use Artboards) δεν υπάρχει τίποτα για εξαγωγή: το έγγραφο δεν έχει ορατό σχέδιο (ή ενεργοποιήστε Χρήση πινάκων σχεδίασης) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 20277db77..befb83ebd 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4186,6 +4186,7 @@ axis Both Ambos @msg not in pattern editing mode no está en el modo de edición de motivos @msg nothing encoded no se codificó nada @msg nothing selected no hay nada seleccionado +@msg nothing selected is in a group nada de lo seleccionado está en un grupo @msg nothing to align no hay nada que alinear @msg nothing to distort no hay nada que distorsionar @msg nothing to export: the document has no visible art (or turn on Use Artboards) no hay nada que exportar: el documento no tiene ilustración visible (o activa Usar mesas de trabajo) diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index ad2b1f261..1bee5aea2 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4401,6 +4401,7 @@ imageTrace Outline Ääriviiva @msg not in pattern editing mode ei kuvion muokkaustilassa @msg nothing encoded mitään ei koodattu @msg nothing selected mitään ei ole valittu +@msg nothing selected is in a group mikään valituista ei ole ryhmässä @msg nothing to align ei tasattavaa @msg nothing to distort ei vääristettävää @msg nothing to export: the document has no visible art (or turn on Use Artboards) ei vietävää: asiakirjassa ei ole näkyvää kuvitusta (tai ota Käytä piirtoalustoja käyttöön) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a3b8a70b8..532c76c4d 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4183,6 +4183,7 @@ axis Both Les deux @msg not in pattern editing mode pas en mode d’édition de motif @msg nothing encoded rien n’a été encodé @msg nothing selected aucune sélection +@msg nothing selected is in a group aucun élément sélectionné ne se trouve dans un groupe @msg nothing to align rien à aligner @msg nothing to distort rien à déformer @msg nothing to export: the document has no visible art (or turn on Use Artboards) rien à exporter : le document n’a aucune illustration visible (ou activez Utiliser les plans de travail) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 9e4d5e586..cd45d3287 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4855,6 +4855,7 @@ imageTrace Outline Körvonal @msg not in pattern editing mode nem mintázatszerkesztő módban vagy @msg nothing encoded nincs kódolt adat @msg nothing selected nincs kijelölés +@msg nothing selected is in a group a kijelölésből semmi sincs csoportban @msg nothing to align nincs mit igazítani @msg nothing to distort nincs mit torzítani @msg nothing to export: the document has no visible art (or turn on Use Artboards) nincs mit exportálni: a dokumentumban nincs látható grafika (vagy kapcsold be a Rajztáblák használata beállítást) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index b780f3289..a4127b0ee 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4193,6 +4193,7 @@ imageTrace Outline Kontur @msg not in pattern editing mode tidak dalam mode pengeditan pola @msg nothing encoded tidak ada yang dikodekan @msg nothing selected tidak ada yang dipilih +@msg nothing selected is in a group tidak ada yang dipilih yang berada dalam grup @msg nothing to align tidak ada yang dapat disejajarkan @msg nothing to distort tidak ada yang dapat didistorsi @msg nothing to export: the document has no visible art (or turn on Use Artboards) tidak ada yang dapat diekspor: dokumen tidak memiliki karya seni yang terlihat (atau aktifkan Gunakan Artboard) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index bc12f151c..9d7965c26 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4226,6 +4226,7 @@ axis Both Entrambi @msg not in pattern editing mode non sei in modalità modifica motivo @msg nothing encoded nessun dato codificato @msg nothing selected nessuna selezione +@msg nothing selected is in a group nessun elemento selezionato si trova in un gruppo @msg nothing to align niente da allineare @msg nothing to distort niente da distorcere @msg nothing to export: the document has no visible art (or turn on Use Artboards) niente da esportare: il documento non ha grafica visibile (oppure attiva Usa tavole da disegno) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 4ac4b1df8..0868c5712 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4554,6 +4554,7 @@ swatch Bright Violet ビビッドバイオレット @msg Couldn't copy to the system clipboard: {e} システムのクリップボードにコピーできませんでした: {e} @msg Couldn't paste from the clipboard: {e} クリップボードから貼り付けられませんでした: {e} @msg nothing selected 何も選択されていません +@msg nothing selected is in a group 選択したオブジェクトはどれもグループ内にありません @msg the {tool} tool has no options {tool} ツールにはオプションがありません @msg unknown tool `{tool}` `{tool}` は不明なツールです @msg {tool} isn't in a toolbar group of several tools {tool} は複数のツールからなるツールグループに含まれていません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 035f11e37..9d14fc962 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3960,6 +3960,7 @@ font-technology Variable ცვლადი @msg not in pattern editing mode პატერნის რედაქტირების რეჟიმი არ არის ჩართული @msg nothing encoded არაფერია კოდირებული @msg nothing selected არაფერია მონიშნული +@msg nothing selected is in a group მონიშნულთაგან არაფერია ჯგუფში @msg nothing to align გასასწორებელი არაფერია @msg nothing to distort დასადეფორმირებელი არაფერია @msg nothing to export: the document has no visible art (or turn on Use Artboards) ექსპორტისთვის არაფერია: დოკუმენტს ხილული ნახატი არ აქვს (ან ჩართეთ სამუშაო დაფების გამოყენება) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index e16ffc530..31c0b24a5 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4486,6 +4486,7 @@ theme Appearance 외관 @msg Couldn't copy to the system clipboard: {e} 시스템 클립보드로 복사할 수 없습니다: {e} @msg Couldn't paste from the clipboard: {e} 클립보드에서 붙여넣을 수 없습니다: {e} @msg nothing selected 선택된 항목이 없습니다 +@msg nothing selected is in a group 선택한 항목 중 그룹에 속한 항목이 없습니다 @msg the {tool} tool has no options {tool} 도구에는 옵션이 없습니다 @msg unknown tool `{tool}` 알 수 없는 도구 `{tool}` @msg {tool} isn't in a toolbar group of several tools {tool}은(는) 여러 도구가 있는 도구 모음 그룹에 속하지 않습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b09f5c1b7..4efce16c4 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4205,6 +4205,7 @@ imageTrace Outline Contour @msg not in pattern editing mode niet in patroonbewerkingsmodus @msg nothing encoded niets gecodeerd @msg nothing selected niets geselecteerd +@msg nothing selected is in a group niets van de selectie zit in een groep @msg nothing to align niets om uit te lijnen @msg nothing to distort niets om te vervormen @msg nothing to export: the document has no visible art (or turn on Use Artboards) niets om te exporteren: het document heeft geen zichtbare illustratie (of schakel Tekengebieden gebruiken in) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 015763e57..d2a7d1ba4 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4314,6 +4314,7 @@ imageTrace Outline Kontur @msg not in pattern editing mode nie jest w trybie edycji wzorka @msg nothing encoded nic nie zakodowano @msg nothing selected nic nie zaznaczono +@msg nothing selected is in a group nic z zaznaczenia nie należy do grupy @msg nothing to align nie ma czego wyrównać @msg nothing to distort nie ma czego zniekształcić @msg nothing to export: the document has no visible art (or turn on Use Artboards) nie ma czego eksportować: dokument nie ma widocznej kompozycji (lub włącz Użyj obszarów kompozycji) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6f5fad182..5678f15ad 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3759,6 +3759,7 @@ axis Both Ambos @msg not in pattern editing mode não está no modo de edição de padrão @msg nothing encoded nada codificado @msg nothing selected nada selecionado +@msg nothing selected is in a group nada do que está selecionado está em um grupo @msg nothing to align nada para alinhar @msg nothing to distort nada para distorcer @msg nothing to export: the document has no visible art (or turn on Use Artboards) nada para exportar: o documento não tem arte visível (ou ative Usar pranchetas) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e222fd7c3..9f8e5212b 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4222,6 +4222,7 @@ axis Both Оба @msg not in pattern editing mode не в режиме редактирования узора @msg nothing encoded ничего не закодировано @msg nothing selected ничего не выделено +@msg nothing selected is in a group ничего из выделенного не находится в группе @msg nothing to align нечего выравнивать @msg nothing to distort нечего искажать @msg nothing to export: the document has no visible art (or turn on Use Artboards) нечего экспортировать: в документе нет видимого рисунка (или включите «Использовать монтажные области») diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 12ba977c0..8e464ba87 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4101,6 +4101,7 @@ axis Both Båda @msg not in pattern editing mode inte i läget för mönsterredigering @msg nothing encoded inget kodat @msg nothing selected inget markerat +@msg nothing selected is in a group inget av det markerade ligger i en grupp @msg nothing to align inget att justera @msg nothing to distort inget att förvränga @msg nothing to export: the document has no visible art (or turn on Use Artboards) inget att exportera: dokumentet har ingen synlig grafik (eller aktivera Använd ritytor) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 2c8b32c66..ed8b22978 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3959,6 +3959,7 @@ axis Both Her İkisi @msg not in pattern editing mode desen düzenleme modunda değil @msg nothing encoded kodlanmış hiçbir şey yok @msg nothing selected hiçbir şey seçilmedi +@msg nothing selected is in a group seçilenlerin hiçbiri bir grupta değil @msg nothing to align hizalanacak hiçbir şey yok @msg nothing to distort çarpıtılacak hiçbir şey yok @msg nothing to export: the document has no visible art (or turn on Use Artboards) dışa aktarılacak bir şey yok: belgede görünür resim yok (veya Çalışma Yüzeylerini Kullan'ı açın) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index fbe9134d9..2b1ebeb4a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4210,6 +4210,7 @@ axis Both Обидві @msg not in pattern editing mode не в режимі редагування візерунка @msg nothing encoded нічого не закодовано @msg nothing selected нічого не виділено +@msg nothing selected is in a group нічого з виділеного не перебуває в групі @msg nothing to align нічого вирівнювати @msg nothing to distort нічого спотворювати @msg nothing to export: the document has no visible art (or turn on Use Artboards) нема чого експортувати: у документі немає видимого малюнка (або увімкніть «Використовувати монтажні області») diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 92b3587e8..b6209e36e 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4506,6 +4506,7 @@ theme Appearance Giao diện @msg not in pattern editing mode không ở chế độ chỉnh sửa họa tiết @msg nothing encoded không có gì được mã hóa @msg nothing selected chưa chọn gì +@msg nothing selected is in a group không mục nào được chọn nằm trong nhóm @msg nothing to align không có gì để căn chỉnh @msg nothing to distort không có gì để làm méo @msg nothing to export: the document has no visible art (or turn on Use Artboards) không có gì để xuất: tài liệu không có tác phẩm hiển thị (hoặc bật Dùng bảng vẽ) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ee84e8f43..2caf91f4e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4278,6 +4278,7 @@ imageTrace Outline 轮廓 @msg Couldn't copy to the system clipboard: {e} 无法复制到系统剪贴板:{e} @msg Couldn't paste from the clipboard: {e} 无法从剪贴板粘贴:{e} @msg nothing selected 未选择任何对象 +@msg nothing selected is in a group 所选对象均不在编组中 @msg the {tool} tool has no options {tool} 工具没有选项 @msg unknown tool `{tool}` 未知工具 `{tool}` @msg {tool} isn't in a toolbar group of several tools {tool} 不在包含多个工具的工具栏组中 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index a6c14b778..1d86caf46 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2239,6 +2239,7 @@ file-filter Templates 範本 @msg Couldn't copy to the system clipboard: {e} 無法複製到系統剪貼簿:{e} @msg Couldn't paste from the clipboard: {e} 無法從剪貼簿貼上:{e} @msg nothing selected 未選取任何物件 +@msg nothing selected is in a group 選取的物件都不在群組中 @msg the {tool} tool has no options {tool} 工具沒有選項 @msg unknown tool `{tool}` 未知的工具 `{tool}` @msg {tool} isn't in a toolbar group of several tools {tool} 不在包含多個工具的工具列群組中 diff --git a/crates/ui-egui/src/menus/bar.rs b/crates/ui-egui/src/menus/bar.rs index 8ad27053a..d33f1ce38 100644 --- a/crates/ui-egui/src/menus/bar.rs +++ b/crates/ui-egui/src/menus/bar.rs @@ -1,5 +1,5 @@ use super::entries::{Row, entry, pretty_shortcut}; -use super::invoke::{field_event, focused_text_field, invoke, to_field}; +use super::invoke::{field_event, focused_text_field, invoke, to_field, with_held_keys}; use super::tree::menu_tree_named; use super::{Entry, Item, Tokens, Value, VectorcraftApp, widgets}; @@ -153,7 +153,7 @@ fn render_row(app: &VectorcraftApp, ui: &mut egui::Ui, row: &Row, checks: bool, None => ui.add_enabled(row.enabled, egui::Button::new(text).shortcut_text(sc)), }; if r.clicked() { - *clicked = row.target(); + *clicked = row.target().map(|target| with_held_keys(target, ui.input(|i| i.modifiers))); ui.close(); } } diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index ee8e4c593..fd8706863 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -13,6 +13,16 @@ pub fn click_target(label: &str, id: &str, p: &Value) -> (String, Value) { (id.to_string(), if p.is_null() { json!({}) } else { p.clone() }) } +/// What an in-window menu click on `target` (a [`click_target`]) runs with `held` held: Object › +/// Unlock All and Object › Show All chosen with Shift+Alt (Shift+Option) act only inside the groups +/// that hold the selected objects (`scope: "group"`). +pub(super) fn with_held_keys(target: (String, Value), held: egui::Modifiers) -> (String, Value) { + match target.0.as_str() { + "object.unlockAll" | "object.showAll" if held.shift && held.alt => (target.0, json!({"scope": "group"})), + _ => target, + } +} + /// The dialog of command `id` that shows the selection's current values (its query): its heading /// and the queried values it doesn't show. Type on a Path Options leaves the brackets where they /// are. diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index efee80d27..c53c459ca 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -758,6 +758,59 @@ fn long_menus_scroll_inside_a_short_window() { assert!(tall.bottom() < 3000.0); } +/// Object › Unlock All and Object › Show All clicked with Shift+Alt held run with +/// `scope: "group"`; clicked without them, or with only one of the two, they run on the whole +/// document as before. +#[test] +fn unlock_all_and_show_all_clicked_with_shift_alt_act_within_the_group() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let tree = menu_tree(); + let (_, object) = tree.iter().find(|(t, _)| *t == "Object").unwrap(); + let id = egui::Id::unique("test-object-menu"); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 3000.0)); + let frame = |events: Vec, modifiers: egui::Modifiers| { + let mut clicked = None; + let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect(); + let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + egui::Area::new(id).fixed_pos(egui::pos2(0.0, 0.0)).show(ui.ctx(), |ui| { + egui::containers::menu::menu_style(ui.style_mut()); + ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| menu_body(&app, ui, object, &mut clicked)); + }); + }); + out.textures_delta.clear(); + (clicked, out.shapes) + }; + // Where the menu draws `label`. + fn find(s: &egui::Shape, label: &str) -> Option { + match s { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos + t.galley.rect.center().to_vec2()), + egui::Shape::Vec(v) => v.iter().find_map(|s| find(s, label)), + _ => None, + } + } + let click = |label: &str, modifiers: egui::Modifiers| { + let (_, shapes) = frame(vec![], Default::default()); + let at = shapes.iter().find_map(|c| find(&c.shape, label)).unwrap_or_else(|| panic!("{label} is not in the Object menu")); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers }; + frame(vec![egui::Event::PointerMoved(at)], modifiers); + frame(vec![button(true)], modifiers); + frame(vec![button(false)], modifiers).0 + }; + frame(vec![], Default::default()); + let shift_alt = egui::Modifiers { shift: true, alt: true, ..Default::default() }; + let group = json!({"scope": "group"}); + for (label, command) in [("Unlock All", "object.unlockAll"), ("Show All", "object.showAll")] { + assert_eq!(click(label, shift_alt), Some((command.to_string(), group.clone())), "{label} with Shift+Alt"); + for held in [egui::Modifiers::NONE, egui::Modifiers::SHIFT, egui::Modifiers::ALT] { + assert_eq!(click(label, held), Some((command.to_string(), json!({}))), "{label} with {held:?}"); + } + } +} + /// #3724: Window › New Window is greyed out until VectorCraft can open a second window on a /// document, instead of being enabled and answering with an internal milestone note. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 27faa692e..5d2f2859a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1345,6 +1345,14 @@ that were already hidden or locked. The key object is cleared when it is one of them again with the key object. When the call changes nothing, it records no undo step and still deselects them. Other `object.setProps` calls leave the selection unchanged. +`object.unlockAll` and `object.showAll` unlock or show every locked or hidden object in the document and select +them → `{count}`. With `scope: "group"` they take only the objects inside the groups that hold the selected objects: +for each selected object, the group nearest above it, or the object itself when it is a group. The objects of a +group inside another such group are counted once. With no selected object in a group, the call returns the error +"nothing selected is in a group" and changes nothing. `scope: "document"` is the default, and any other value is +refused. Object › Unlock All and Object › Show All clicked in the in-window menu bar with Shift+Alt (Shift+Option) +held run with `scope: "group"`. + `layer.move`, `node.move`, `layer.merge`, `layer.flatten`, `layer.collectInNew`, `dataset.select`, `dataset.next` and `dataset.prev` also deselect every selected object that is hidden or locked after the call, such as objects moved into a hidden or locked layer or group, or objects that a data set hides. The key object is cleared when it diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 462e227bb..ab2507744 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -910,6 +910,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur 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. + 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. 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. All Artwork Above and Other Layers (Object › Lock and Object › Hide), Transform Each, Rearrange All Artboards, the width point and width profile commands, Liquify, Puppet Warp, Define Perspective Grid, Attach to Active Plane, Release with Perspective, moves and transforms in perspective, Move Plane and Move Plane to Match Object record no undo step when they change nothing. Move in Perspective (its copy, perpendicular and Snap to Grid choices), Transform in Perspective (its copy), Nudge in Perspective (its ×10 and copy), Draw in Perspective (Snap to Grid), Define Perspective Grid (its reproject choice), Show Perspective Grid, Width Point (Adjust Adjoining Width Points), Liquify (its pen pressure, Simplify and Affect choices) and Puppet Warp (its pins' rest shape) refuse a value other than true, false or null, such as "true", and leave the document and its undo history as they were.