diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index bcf742ffe..757de37a7 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -29,7 +29,7 @@ pub fn specs() -> Vec { "New Layer…", ["Window", "Layers"], Some("Cmd+L"), - "{name?, top?: bool, color?, template?, locked?, visible?, printable?, preview?, dimImages?} a layer directly above the current layer, at its level (a sublayer beside a current sublayer); `top`: above all layers (Ctrl-click the button). Options as layer.setProps → {id}", + "{name?, top?: bool, below?: bool, color?, template?, locked?, visible?, printable?, preview?, dimImages?} a layer directly above the current layer, at its level (a sublayer beside a current sublayer); `top`: above all layers (Ctrl/Cmd-click the button); `below`: directly below the current layer, at its level (Ctrl/Cmd+Alt-click the button), not with `top`. Options as layer.setProps → {id}", has_doc, new_layer ), @@ -38,7 +38,7 @@ pub fn specs() -> Vec { "New Sublayer…", ["Window", "Layers"], None, - "{parent?: layer id (default: the highlighted row's layer, else the current layer), name?, …options as layer.new} a sublayer on top of the parent's contents → {id}", + "{parent?: layer id (default: the highlighted row's layer, else the current layer), bottom?: bool, name?, …options as layer.new} a sublayer on top of the parent's contents; `bottom`: under the parent's contents, above a clipping layer's clipping path (Ctrl/Cmd+Alt-click the button) → {id}", has_doc, new_sublayer ), @@ -485,7 +485,11 @@ fn add_layer(s: &mut Session, p: &Value, cmd: &str, label: &str, place: impl FnO check_options(cmd, p, xml_id)?; let name = str_param(p, "name").filter(|n| !n.trim().is_empty()).map(str::to_string); let id = s.edit(label, |d, _| { - let (parent, index) = place(d)?; + let (parent, mut index) = place(d)?; + // The clipping path stays at the bottom of a clipping layer. + if d.children(parent).is_some_and(|c| !c.is_empty()) && parent.and_then(|p| d.node(p)).is_some_and(Node::clips) { + index = index.max(1); + } let mut n = make_layer(d, name.as_deref()); apply_options(&mut n, p, xml_id); Ok(d.insert(parent, index, n)?) @@ -497,12 +501,17 @@ fn add_layer(s: &mut Session, p: &Value, cmd: &str, label: &str, place: impl FnO } fn new_layer(s: &mut Session, p: &Value) -> Result { + const C: &str = "layer.new"; + let below = switch_or(p, "below", false, C)?; + if below && bool_or(p, "top", false) { + return Err(bad(C, "`top` and `below` can't both be true")); + } let st = s.doc()?; - // Above the current layer, at its level; on top of everything with `top`. + // Above the current layer, at its level; below it with `below`; on top of everything with `top`. let cur = st.current_layer().filter(|_| !bool_or(p, "top", false)); - add_layer(s, p, "layer.new", "New Layer", |d| match cur.and_then(|c| d.position(c)) { - Some((parent, i, _)) => Ok((parent, i + 1)), - None => Ok((None, usize::MAX)), + add_layer(s, p, C, "New Layer", |d| match cur.and_then(|c| d.position(c)) { + Some((parent, i, _)) => Ok((parent, if below { i } else { i + 1 })), + None => Ok((None, if below { 0 } else { usize::MAX })), }) } @@ -514,7 +523,8 @@ fn new_sublayer(s: &mut Session, p: &Value) -> Result { if !st.doc.node(parent).is_some_and(Node::is_layer) { return Err(bad(C, "the parent must be a layer or sublayer")); } - add_layer(s, p, C, "New Sublayer", |_| Ok((Some(parent), usize::MAX))) + let index = if switch_or(p, "bottom", false, C)? { 0 } else { usize::MAX }; + add_layer(s, p, C, "New Sublayer", |_| Ok((Some(parent), index))) } fn delete_rows(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/tests/layers.rs b/crates/engine/src/tests/layers.rs index b86143fb6..4052bf553 100644 --- a/crates/engine/src/tests/layers.rs +++ b/crates/engine/src/tests/layers.rs @@ -403,6 +403,58 @@ fn new_layers_go_above_the_current_one_at_its_level() { assert_eq!(unique.len(), names.len(), "{names:?}"); } +/// `below` puts a new layer directly below the current layer, at its level, and `bottom` puts a +/// new sublayer under the parent's contents, each in one undo step. +#[test] +fn new_layers_go_below_the_current_one_and_sublayers_under_the_contents() { + let mut s = session(); + let (layer, sub, a, g, _, c) = tree(&mut s); + run(&mut s, "layer.setCurrent", json!({"id": sub.0})); + let before = undos(&s); + let n = id(&run(&mut s, "layer.new", json!({"below": true}))); + assert_eq!(children(&s, layer), vec![c, n, sub], "a sublayer directly below the current sublayer"); + assert_eq!(undos(&s), before + 1); + assert_eq!(s.doc().unwrap().highlighted_rows(), vec![n]); + assert_eq!(s.doc().unwrap().current_layer(), Some(n)); + run(&mut s, "edit.undo", json!({})); + assert_eq!(children(&s, layer), vec![c, sub]); + // At the top level: below the current layer, above the layers under it. + let top = id(&run(&mut s, "layer.new", json!({"top": true, "name": "Top"}))); + let middle = id(&run(&mut s, "layer.new", json!({"below": true, "name": "Middle", "locked": true}))); + let order = s.doc().unwrap().doc.layers.iter().map(|l| l.id).collect::>(); + assert_eq!(order, vec![layer, middle, top]); + assert!(node(&s, middle).locked, "options as without `below`"); + run(&mut s, "layer.setCurrent", json!({"id": layer.0})); + let bottom = id(&run(&mut s, "layer.new", json!({"below": true}))); + assert_eq!(s.doc().unwrap().doc.layers[0].id, bottom, "below the bottom layer"); + // Sublayers: on top of the parent's contents, or under them with `bottom`. + let before = undos(&s); + let under = id(&run(&mut s, "layer.newSublayer", json!({"parent": sub.0, "bottom": true}))); + assert_eq!(children(&s, sub), vec![under, a, g]); + assert_eq!(undos(&s), before + 1); + assert_eq!(s.doc().unwrap().highlighted_rows(), vec![under]); + let over = id(&run(&mut s, "layer.newSublayer", json!({"parent": sub.0, "bottom": false}))); + assert_eq!(children(&s, sub), vec![under, a, g, over]); + // Bad values change nothing. + let count = s.doc().unwrap().doc.node_count(); + for (cmd, p) in [ + ("layer.new", json!({"below": true, "top": true})), + ("layer.new", json!({"below": "yes"})), + ("layer.newSublayer", json!({"parent": sub.0, "bottom": 1})), + ] { + assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}"); + } + assert_eq!(s.doc().unwrap().doc.node_count(), count); + // In a clipping layer the clipping path stays at the bottom. + let mut s = session(); + let layer = s.doc().unwrap().doc.layers[0].id; + let (back, front) = (rect(&mut s, 0.0), rect(&mut s, 100.0)); + run(&mut s, "layer.clippingMask.toggle", json!({"id": layer.0})); + assert_eq!(children(&s, layer), vec![front, back], "the top object clips"); + let under = id(&run(&mut s, "layer.newSublayer", json!({"parent": layer.0, "bottom": true}))); + assert_eq!(children(&s, layer), vec![front, under, back]); +} + #[test] fn rows_move_above_below_and_into_layers_and_groups() { let mut s = session(); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 780087175..3d354adcf 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4225,6 +4225,7 @@ imageTrace Outline Obrys @msg `{key}` must be between {min} and {max} (got {x}) `{key}` musí být mezi {min} a {max} (zadáno {x}) @msg `{key}` must be one of {_1} `{key}` musí být jedno z {_1} @msg `{key}` must be true or false `{key}` musí být true nebo false +@msg `top` and `below` can't both be true `top` a `below` nemohou být zároveň true @msg `select` must be true or false `select` musí být true nebo false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` a `maxAnchors` musí být celá čísla @msg `{key}` must be a number not below 0 `{key}` musí být číslo nejméně 0 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index f9d565ff3..c0dc4648f 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3846,6 +3846,7 @@ theme Appearance Erscheinungsbild @msg `{key}` must be between {min} and {max} (got {x}) `{key}` muss zwischen {min} und {max} liegen (erhalten: {x}) @msg `{key}` must be one of {_1} `{key}` muss einer der folgenden Werte sein: {_1} @msg `{key}` must be true or false `{key}` muss true oder false sein +@msg `top` and `below` can't both be true `top` und `below` können nicht beide true sein @msg `select` must be true or false `select` muss true oder false sein @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` und `maxAnchors` müssen ganze Zahlen sein @msg `{key}` must be a number not below 0 `{key}` muss eine Zahl nicht kleiner als 0 sein diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8453a2da5..5a8799a43 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2172,6 +2172,7 @@ axis Both Και τα δύο @msg `{key}` must be between {min} and {max} (got {x}) Το `{key}` πρέπει να είναι μεταξύ {min} και {max} (δόθηκε {x}) @msg `{key}` must be one of {_1} Το `{key}` πρέπει να είναι ένα από τα {_1} @msg `{key}` must be true or false Το `{key}` πρέπει να είναι true ή false +@msg `top` and `below` can't both be true Τα `top` και `below` δεν μπορούν να είναι και τα δύο true @msg `select` must be true or false Το `select` πρέπει να είναι true ή false @msg `minAnchors` and `maxAnchors` must be whole numbers Τα `minAnchors` και `maxAnchors` πρέπει να είναι ακέραιοι αριθμοί @msg `{key}` must be a number not below 0 Το `{key}` πρέπει να είναι αριθμός όχι μικρότερος από 0 diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index d085b626e..a8b4cfb32 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3922,6 +3922,7 @@ axis Both Ambos @msg `{key}` must be between {min} and {max} (got {x}) `{key}` debe estar entre {min} y {max} (se recibió {x}) @msg `{key}` must be one of {_1} `{key}` debe ser uno de estos: {_1} @msg `{key}` must be true or false `{key}` debe ser true o false +@msg `top` and `below` can't both be true `top` y `below` no pueden ser true a la vez @msg `select` must be true or false `select` debe ser true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` y `maxAnchors` deben ser números enteros @msg `{key}` must be a number not below 0 `{key}` debe ser un número no inferior a 0 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index f04238476..51ca6523c 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4143,6 +4143,7 @@ imageTrace Outline Ääriviiva @msg `{key}` must be between {min} and {max} (got {x}) `{key}` on oltava väliltä {min}–{max} (saatiin {x}) @msg `{key}` must be one of {_1} `{key}` on oltava jokin seuraavista: {_1} @msg `{key}` must be true or false `{key}` on oltava true tai false +@msg `top` and `below` can't both be true `top` ja `below` eivät voi olla molemmat true @msg `select` must be true or false `select` on oltava true tai false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ja `maxAnchors` on oltava kokonaislukuja @msg `{key}` must be a number not below 0 `{key}` on oltava luku, joka on vähintään 0 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index b8ff87563..3272215f7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3919,6 +3919,7 @@ axis Both Les deux @msg `{key}` must be between {min} and {max} (got {x}) `{key}` doit être compris entre {min} et {max} (reçu : {x}) @msg `{key}` must be one of {_1} `{key}` doit valoir l’une de ces valeurs : {_1} @msg `{key}` must be true or false `{key}` doit valoir true ou false +@msg `top` and `below` can't both be true `top` et `below` ne peuvent pas valoir true tous les deux @msg `select` must be true or false `select` doit valoir true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` et `maxAnchors` doivent être des nombres entiers @msg `{key}` must be a number not below 0 `{key}` doit être un nombre supérieur ou égal à 0 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e57a92649..d5b79d72e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4454,6 +4454,7 @@ imageTrace Outline Körvonal @msg `{key}` must be between {min} and {max} (got {x}) `{key}`: {min} és {max} közötti érték kell (kapott érték: {x}) @msg `{key}` must be one of {_1} `{key}`: ezek egyike kell: {_1} @msg `{key}` must be true or false `{key}`: true vagy false kell +@msg `top` and `below` can't both be true `top` és `below` nem lehet egyszerre true @msg `select` must be true or false `select`: true vagy false kell @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` és `maxAnchors`: egész szám kell @msg `{key}` must be a number not below 0 `{key}`: legalább 0 értékű szám kell diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 98fe4cf71..4ef697112 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3962,6 +3962,7 @@ axis Both Entrambi @msg `{key}` must be between {min} and {max} (got {x}) `{key}` deve essere compreso tra {min} e {max} (ricevuto {x}) @msg `{key}` must be one of {_1} `{key}` deve essere uno dei seguenti: {_1} @msg `{key}` must be true or false `{key}` deve essere true o false +@msg `top` and `below` can't both be true `top` e `below` non possono essere entrambi true @msg `select` must be true or false `select` deve essere true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devono essere numeri interi @msg `{key}` must be a number not below 0 `{key}` deve essere un numero non inferiore a 0 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 5076fc889..22a9cd7db 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4461,6 +4461,7 @@ swatch Bright Violet ビビッドバイオレット @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` には、メニュー項目がダイアログを開くコマンドを指定してください (ui.menuDialog を参照) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` には grayscale、rgb、hsb、cmyk、websafe のいずれかを指定してください @msg `{key}` must be true or false `{key}` には true または false を指定してください +@msg `top` and `below` can't both be true `top` と `below` を同時に true にすることはできません @msg `select` must be true or false `select` には true または false を指定してください @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` と `maxAnchors` は整数にしてください @msg `{key}` must be a number not below 0 `{key}` には 0 以上の数値を指定してください diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 0d13a6a6d..21d7a6247 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3714,6 +3714,7 @@ font-technology Variable ცვლადი @msg `{key}` must be between {min} and {max} (got {x}) `{key}` უნდა იყოს {min}-სა და {max}-ს შორის (მიღებულია {x}) @msg `{key}` must be one of {_1} `{key}` უნდა იყოს ერთ-ერთი: {_1} @msg `{key}` must be true or false `{key}` უნდა იყოს true ან false +@msg `top` and `below` can't both be true `top` და `below` ერთდროულად ვერ იქნება true @msg `select` must be true or false `select` უნდა იყოს true ან false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` და `maxAnchors` უნდა იყოს მთელი რიცხვები @msg `{key}` must be a number not below 0 `{key}` უნდა იყოს რიცხვი, არანაკლებ 0 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index aa54dcc6c..46ee1f775 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4393,6 +4393,7 @@ theme Appearance 외관 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command`는 메뉴 항목이 대화상자를 여는 명령이어야 합니다(ui.menuDialog 참조) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode`은(는) grayscale, rgb, hsb, cmyk, websafe 중 하나여야 합니다 @msg `{key}` must be true or false `{key}`은(는) true 또는 false여야 합니다 +@msg `top` and `below` can't both be true `top`과 `below`를 동시에 true로 지정할 수 없습니다 @msg `select` must be true or false `select`은(는) true 또는 false여야 합니다 @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors`와 `maxAnchors`는 정수여야 합니다 @msg `{key}` must be a number not below 0 `{key}`은(는) 0 이상의 숫자여야 합니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 275f14e7d..ef814fcf5 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4047,6 +4047,7 @@ imageTrace Outline Kontur @msg `{key}` must be between {min} and {max} (got {x}) `{key}` musi mieścić się w zakresie od {min} do {max} (podano {x}) @msg `{key}` must be one of {_1} `{key}` musi mieć jedną z wartości: {_1} @msg `{key}` must be true or false `{key}` musi mieć wartość true lub false +@msg `top` and `below` can't both be true `top` i `below` nie mogą mieć jednocześnie wartości true @msg `select` must be true or false `select` musi mieć wartość true lub false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` i `maxAnchors` muszą być liczbami całkowitymi @msg `{key}` must be a number not below 0 `{key}` musi być liczbą nie mniejszą niż 0 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 1c6b7a218..a7cebfb79 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3503,6 +3503,7 @@ axis Both Ambos @msg `{key}` must be between {min} and {max} (got {x}) `{key}` deve estar entre {min} e {max} (recebido {x}) @msg `{key}` must be one of {_1} `{key}` deve ser um de {_1} @msg `{key}` must be true or false `{key}` deve ser true ou false +@msg `top` and `below` can't both be true `top` e `below` não podem ser true ao mesmo tempo @msg `select` must be true or false `select` deve ser true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devem ser números inteiros @msg `{key}` must be a number not below 0 `{key}` deve ser um número não inferior a 0 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d2b022d61..712bf3fe6 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3958,6 +3958,7 @@ axis Both Оба @msg `{key}` must be between {min} and {max} (got {x}) `{key}` должно быть от {min} до {max} (получено {x}) @msg `{key}` must be one of {_1} `{key}` должно быть одним из: {_1} @msg `{key}` must be true or false `{key}` должно быть true или false +@msg `top` and `below` can't both be true `top` и `below` не могут одновременно быть true @msg `select` must be true or false `select` должно быть true или false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` и `maxAnchors` должны быть целыми числами @msg `{key}` must be a number not below 0 `{key}` должно быть числом не меньше 0 diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index e3e35e4a3..9f25ce924 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3842,6 +3842,7 @@ axis Both Båda @msg `{key}` must be between {min} and {max} (got {x}) `{key}` måste vara mellan {min} och {max} (fick {x}) @msg `{key}` must be one of {_1} `{key}` måste vara något av {_1} @msg `{key}` must be true or false `{key}` måste vara true eller false +@msg `top` and `below` can't both be true `top` och `below` kan inte båda vara true @msg `select` must be true or false `select` måste vara true eller false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` och `maxAnchors` måste vara heltal @msg `{key}` must be a number not below 0 `{key}` måste vara ett tal som inte är under 0 diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e0c17f972..f2553aef2 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3725,6 +3725,7 @@ axis Both Her İkisi @msg `{key}` must be between {min} and {max} (got {x}) `{key}`, {min} ile {max} arasında olmalıdır ({x}'yi aldı) @msg `{key}` must be one of {_1} `{key}`, {_1}'den biri olmalıdır @msg `{key}` must be true or false "{key}" doğru veya yanlış olmalıdır +@msg `top` and `below` can't both be true `top` ve `below` aynı anda true olamaz @msg `select` must be true or false `select` true veya false olmalıdır @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ve `maxAnchors` tam sayı olmalıdır @msg `{key}` must be a number not below 0 `{key}` 0'dan küçük olmayan bir sayı olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 943105b17..c09c33934 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3946,6 +3946,7 @@ axis Both Обидві @msg `{key}` must be between {min} and {max} (got {x}) `{key}` має бути від {min} до {max} (отримано {x}) @msg `{key}` must be one of {_1} `{key}` має бути одним з: {_1} @msg `{key}` must be true or false `{key}` має бути true або false +@msg `top` and `below` can't both be true `top` і `below` не можуть одночасно бути true @msg `select` must be true or false `select` має бути true або false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` і `maxAnchors` мають бути цілими числами @msg `{key}` must be a number not below 0 `{key}` має бути числом, не меншим за 0 diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 36525be97..6eac7c3c3 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4102,6 +4102,7 @@ theme Appearance Giao diện @msg `{key}` must be between {min} and {max} (got {x}) `{key}` phải nằm trong khoảng {min} đến {max} (nhận được {x}) @msg `{key}` must be one of {_1} `{key}` phải là một trong {_1} @msg `{key}` must be true or false `{key}` phải là true hoặc false +@msg `top` and `below` can't both be true `top` và `below` không thể cùng là true @msg `select` must be true or false `select` phải là true hoặc false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` và `maxAnchors` phải là số nguyên @msg `{key}` must be a number not below 0 `{key}` phải là một số không nhỏ hơn 0 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 7d58e45f3..89eb9baba 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4189,6 +4189,7 @@ imageTrace Outline 轮廓 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` 必须是其菜单项会打开对话框的命令 (请参阅 ui.menuDialog) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` 必须是 grayscale、rgb、hsb、cmyk 或 websafe @msg `{key}` must be true or false `{key}` 必须是 true 或 false +@msg `top` and `below` can't both be true `top` 和 `below` 不能同时为 true @msg `select` must be true or false `select` 必须是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必须是整数 @msg `{key}` must be a number not below 0 `{key}` 必须是不小于 0 的数字 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 37f704007..ece9df22f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2150,6 +2150,7 @@ file-filter Templates 範本 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` 必須是選單項目會開啟對話框的指令 (請參閱 ui.menuDialog) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` 必須是 grayscale、rgb、hsb、cmyk 或 websafe @msg `{key}` must be true or false `{key}` 必須是 true 或 false +@msg `top` and `below` can't both be true `top` 和 `below` 不能同時為 true @msg `select` must be true or false `select` 必須是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必須是整數 @msg `{key}` must be a number not below 0 `{key}` 必須是不小於 0 的數字 diff --git a/crates/ui-egui/src/panels/layers/controls.rs b/crates/ui-egui/src/panels/layers/controls.rs index f911c66ad..b6b43c3a0 100644 --- a/crates/ui-egui/src/panels/layers/controls.rs +++ b/crates/ui-egui/src/panels/layers/controls.rs @@ -67,10 +67,13 @@ pub(super) fn bottom_bar(app: &mut VectorcraftApp, ui: &mut Ui, view: &View, doc actions.push(("layer.delete".into(), json!({}))); } } - // Create New Layer: Alt asks for its options first, Ctrl/Cmd puts it on top of every layer. + // Create New Layer: Alt asks for its options first, Ctrl/Cmd puts it on top of every layer, + // and Ctrl/Cmd+Alt puts it directly below the current layer. let new_layer = widgets::icon_button(&mut child, "file-plus", tl!("Create New Layer"), false, 24.0); if new_layer.clicked() { - if m.alt { + if m.alt && m.command { + actions.push(("layer.new".into(), json!({"below": true}))); + } else if m.alt { actions.push(("ui.newLayer".into(), json!({}))); } else { actions.push(("layer.new".into(), json!({"top": m.command}))); @@ -90,8 +93,14 @@ pub(super) fn bottom_bar(app: &mut VectorcraftApp, ui: &mut Ui, view: &View, doc child.painter().rect_stroke(button.rect, 3.0, Stroke::new(1.5, t.accent), StrokeKind::Inside); } } + // Create New Sublayer: Alt asks for its options first, and Ctrl/Cmd+Alt puts it under the + // layer's contents. if widgets::icon_button(&mut child, "plus", tl!("Create New Sublayer"), false, 24.0).clicked() { - actions.push(if m.alt { ("ui.newLayer".into(), json!({"sublayer": true})) } else { ("layer.newSublayer".into(), json!({})) }); + actions.push(match (m.alt, m.command) { + (true, true) => ("layer.newSublayer".into(), json!({"bottom": true})), + (true, false) => ("ui.newLayer".into(), json!({"sublayer": true})), + _ => ("layer.newSublayer".into(), json!({})), + }); } if widgets::icon_button(&mut child, "frame", tl!("Make/Release Clipping Mask"), false, 24.0).clicked() { actions.push(("layer.clippingMask.toggle".into(), json!({}))); diff --git a/crates/ui-egui/src/panels/layers/tests.rs b/crates/ui-egui/src/panels/layers/tests.rs index e46df1a06..b78e6acc5 100644 --- a/crates/ui-egui/src/panels/layers/tests.rs +++ b/crates/ui-egui/src/panels/layers/tests.rs @@ -1040,6 +1040,50 @@ fn cmd_alt_l_opens_new_layer_options_as_alt_clicking_new_layer_does() { assert_eq!(names(&keyed).len(), 4); } +/// Ctrl/Cmd+Alt-click on the New Layer button adds a layer directly below the current layer, and +/// on the New Sublayer button a sublayer under the current layer's contents, without a dialog and +/// in one undo step each. A plain click still adds the layer above the current layer and the +/// sublayer on top, and an Alt-click still opens Layer Options. +#[test] +fn cmd_alt_click_puts_a_new_layer_below_and_a_new_sublayer_at_the_bottom() { + let (mut app, first, [a, b]) = two_rects(); + app.session.execute("layer.new", &json!({"name": "Top"})).unwrap(); + app.session.execute("layer.setCurrent", &json!({"id": first.0})).unwrap(); + let ctx = egui::Context::default(); + let names = |app: &VectorcraftApp| app.session.active().unwrap().doc.layers.iter().map(|l| l.display_name()).collect::>(); + let undos = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len(); + let cmd_alt = egui::Modifiers::COMMAND | egui::Modifiers::ALT; + // Clicks the bottom bar's button `k` (1: New Layer, 2: New Sublayer) with `mods` held. + let click = |app: &mut VectorcraftApp, k: usize, mods: egui::Modifiers| { + let at = bar_buttons(app, &ctx)[k]; + click_with(app, &ctx, at, mods); + }; + let before = undos(&app); + click(&mut app, 1, cmd_alt); + assert!(app.ui.dialog.is_none(), "no dialog"); + assert_eq!(names(&app), ["Layer 3", "Layer 1", "Top"]); + assert_eq!(undos(&app), before + 1); + app.session.execute("layer.setCurrent", &json!({"id": first.0})).unwrap(); + click(&mut app, 2, cmd_alt); + assert!(app.ui.dialog.is_none(), "no dialog"); + let d = &app.session.active().unwrap().doc; + let kids: Vec = d.node(first).unwrap().children().unwrap().iter().map(|n| n.id).collect(); + assert_eq!(kids.len(), 3); + assert!(d.node(kids[0]).is_some_and(Node::is_layer), "the sublayer is under the rectangles"); + assert_eq!(kids[1..], [a, b]); + assert_eq!(undos(&app), before + 2); + // A plain click: above the current layer, and the sublayer on top. + app.session.execute("layer.setCurrent", &json!({"id": first.0})).unwrap(); + click(&mut app, 2, egui::Modifiers::NONE); + let d = &app.session.active().unwrap().doc; + assert!(d.node(first).unwrap().children().unwrap().last().is_some_and(|n| n.is_layer()), "a plain click puts it on top"); + app.session.execute("layer.setCurrent", &json!({"id": first.0})).unwrap(); + click(&mut app, 1, egui::Modifiers::NONE); + assert_eq!(names(&app), ["Layer 3", "Layer 1", "Layer 6", "Top"]); + click(&mut app, 1, egui::Modifiers::ALT); + assert!(app.ui.dialog.is_some(), "Alt-click opens Layer Options"); +} + #[test] fn the_selected_art_square_drags_the_selection_to_another_layer() { let (mut app, [_, sub, g, _, a, c]) = nested(); diff --git a/docs/mcp.md b/docs/mcp.md index 744eead16..4be7ac337 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2133,10 +2133,12 @@ and `layerRows`, the rows highlighted in the panel. inside it); a row never goes into itself or a row inside it, and a locked layer or group takes nothing. Dragging the selected-art square is `layer.move` with the selected objects. `node.move {id, parent?, index}` is the low-level form with the same nesting rules; unlike `layer.move`, it also moves a row into a locked layer or group. -- **Buttons**: `layer.new {name?, top?, …options}` (above the current layer at its level; `top` with Ctrl/Cmd), - `layer.newSublayer {parent?, name?, …options}` (on top of the parent's contents), `layer.delete {ids?}` (rows with - what they hold; the last layer stays), `layer.locate {id?}` (highlights the selected object's row; the panel opens - the rows around it) and `layer.clippingMask.toggle`. +- **Buttons**: `layer.new {name?, top?, below?, …options}` (above the current layer at its level; `top` with + Ctrl/Cmd; `below`, directly below the current layer at its level, with Ctrl/Cmd+Alt), + `layer.newSublayer {parent?, bottom?, name?, …options}` (on top of the parent's contents; `bottom`, under them but + above a clipping layer's clipping path, with Ctrl/Cmd+Alt), `layer.delete {ids?}` (rows with what they hold; the + last layer stays), `layer.locate {id?}` (highlights the selected object's row; the panel opens the rows around it) + and `layer.clippingMask.toggle`. - **Panel menu**: `layer.duplicate {ids?}`, `layer.merge {ids?}` (into the layer highlighted last, keeping the stacking order), `layer.flatten {id?}` (every other visible layer's art into one layer; hidden layers are deleted, templates stay), `layer.collectInNew {ids?}`, `layer.releaseToLayers {id?, build?}` and diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8735f90b7..fa2310c0b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -709,6 +709,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The panel menu's Save Selection submenu has Save Selection…, Update Selection and Edit Selection…, enabled and run as in the Select menu, and lists the document's saved selections: choosing one selects its objects again (`select.recall`). `object.setProps` with `visible: false` or `locked: true` deselects every selected object that is then hidden or locked, the key object included, as the eye and lock columns do. Ctrl/Cmd+Alt-clicking an eye switches the other layers to Outline and the row's layer to Preview, or switches all layers back to Preview when the others are in Outline (`layer.outlineOthers`, `layer.previewAll`). + Ctrl/Cmd+Alt-clicking New Layer adds a layer directly below the current layer, at its level (`layer.new {below: true}`), and Ctrl/Cmd+Alt-clicking New Sublayer adds a sublayer under the contents of the layer it goes into (`layer.newSublayer {bottom: true}`), each in one undo step. The reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists these clicks for placing a layer below the selected layer and a new sublayer at the bottom of the layer list. Alt-clicking a layer's or sublayer's name makes it the current layer, as a click does, and selects its visible, unlocked art, as its selection square does (`layer.selectAll`, then `layer.setCurrent`); on a hidden or locked layer it clears the selection. The reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists Alt-click on the layer name for selecting all objects on the layer. Object › Arrange › Send to Current Layer returns the error "the target layer is locked or hidden" and leaves the selected objects where they are when the current layer is hidden or locked, or is inside a hidden or locked layer. Selected objects are deselected, the key object included, when a row drag moves them into a hidden layer or group, when `node.move` moves them into a hidden or locked layer or group, when Merge Selected moves them into a hidden or locked layer, when Collect in New Layer moves them into a new layer inside a hidden or locked layer, when Flatten Artwork moves them into a locked layer, and when applying a data set hides them.