Layers: Cmd/Ctrl+Alt-click places a new layer below the current one and a new sublayer at the bottom

The reference app places a new layer below the selected layer with
Ctrl+Alt-click (Command+Option-click) on the New Layer button, and a
new sublayer at the bottom of the layer list with the same click on
the New Sublayer button. In VectorCraft that click opened Layer
Options, and New Layer and New Sublayer always added the layer above
the current layer and the sublayer on top of the layer's contents.

layer.new takes below, which adds the layer directly below the current
layer at its level, and layer.newSublayer takes bottom, which adds the
sublayer under the parent's contents. In a clipping layer the new
sublayer goes above the clipping path. Each is one undo step, and
below is refused together with top. A Cmd/Ctrl+Alt-click on the
Layers panel's New Layer and New Sublayer buttons runs them. A plain
click, an Alt-click and a Cmd/Ctrl-click work as before.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 10:10:59 +07:00
committed by bflatastic
parent 12b7e0fc9c
commit 16d679fde7
26 changed files with 153 additions and 15 deletions
+18 -8
View File
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<CommandSpec> {
"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<Value> {
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<Value> {
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<Value> {
+52
View File
@@ -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::<Vec<_>>();
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();
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2070 and column 19.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 以上の数値を指定してください
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 이상의 숫자여야 합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 749 and column 40.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 4071 and column 10.
+1
View File
@@ -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 的数字
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 的數字
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12 -3
View File
@@ -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!({})));
+44
View File
@@ -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::<Vec<_>>();
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<NodeId> = 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();
+6 -4
View File
@@ -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
+1
View File
@@ -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.