Merge pull request #301 from theironchef/fix/layers-alt-click-others

Layers: Alt-clicking an eye or lock toggles the other layers
This commit is contained in:
BFlat 2026-10-08 03:51:27 +07:00 committed by GitHub
commit de779cc820
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 5 deletions

View File

@ -116,7 +116,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, and a symbol dragged onto the canvas places an instance where it is dropped.
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options.
- **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies, the trash deletes), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting.
- **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies, the trash deletes), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting.
- **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes.
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard.
- **Geometry and effects:**

View File

@ -11,7 +11,9 @@
//! another row moves the selected art there (Alt copies it).
//! - The eye and lock columns toggle a row (`layer.setProps`); dragging down a column sets every
//! row it passes to the same state, in one undo step. Ctrl/Cmd-click an eye switches the layer
//! between Preview and Outline.
//! between Preview and Outline. Alt-click an eye hides the other layers (`layer.hideOthers`), or
//! shows every layer when they are hidden (`layer.showAll`); Alt-click a lock does the same with
//! `layer.lockOthers` and `layer.unlockAll`.
//! - Dragging rows moves them above, below or into a layer or group (`layer.move`, a drop line or
//! a box shows where); Alt copies them; dropped on the trash they are deleted.
//! - The triangle opens or closes a row; Alt-click does the same to everything inside it.
@ -543,6 +545,21 @@ fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expand
}
}
/// Alt-clicking row `n`'s eye (`eye`) or lock: Hide Others or Lock Others, every top-level layer
/// but the one holding the row; when they all are hidden (locked) already, Show All Layers (Unlock
/// All Layers). Each is one undo step.
fn others_action(doc: &Document, n: &Node, eye: bool) -> (String, Value) {
let own = doc.layer_of(n.id);
let mut others = doc.layers.iter().filter(|l| Some(l.id) != own);
let cmd = match (eye, if eye { others.any(|l| l.visible) } else { others.any(|l| !l.locked) }) {
(true, true) => "layer.hideOthers",
(true, false) => "layer.showAll",
(false, true) => "layer.lockOthers",
(false, false) => "layer.unlockAll",
};
(cmd.into(), json!({ "ids": [n.id.0] }))
}
/// The eye and lock columns of row `n` at `r`.
fn eye_and_lock(ui: &mut Ui, view: &View, n: &Node, r: egui::Rect, out: &mut Out) {
let t = &view.t;
@ -574,14 +591,18 @@ fn eye_and_lock(ui: &mut Ui, view: &View, n: &Node, r: egui::Rect, out: &mut Out
}
let m = ui.input(|i| i.modifiers);
if er.clicked() {
if m.command && n.is_layer() {
if m.alt {
out.actions.push(others_action(view.doc, n, true));
} else if m.command && n.is_layer() {
// Ctrl/Cmd-click: Preview ↔ Outline for this layer.
out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "preview": !preview})));
} else {
out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "visible": !n.visible})));
}
}
if lr.clicked() {
if lr.clicked() && m.alt {
out.actions.push(others_action(view.doc, n, false));
} else if lr.clicked() {
out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "locked": !n.locked})));
}
// Dragging down a column gives every row it passes this row's new state.
@ -993,6 +1014,55 @@ mod tests {
}
}
#[test]
fn alt_clicking_an_eye_or_lock_toggles_the_other_layers() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap();
run(&mut app, "file.new", json!({"width": 200, "height": 200}));
run(&mut app, "layer.new", json!({}));
run(&mut app, "layer.new", json!({}));
let ctx = egui::Context::default();
// Rows top first: Layers 3, 2, 1.
let (circles, _) = frame(&mut app, &ctx, vec![], false);
assert_eq!(circles.len(), 3);
let state =
|app: &VectorcraftApp| -> Vec<(bool, bool)> { app.session.active().unwrap().doc.layers.iter().map(|l| (l.visible, l.locked)).collect() };
let click = |app: &mut VectorcraftApp, at: egui::Pos2, alt: bool| {
frame(app, &ctx, vec![egui::Event::PointerMoved(at)], alt);
frame(app, &ctx, vec![button(at, true)], alt);
frame(app, &ctx, vec![button(at, false)], alt);
};
let (eye, lock) = (egui::pos2(12.0, circles[1].y), egui::pos2(37.0, circles[1].y));
let undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
let before = undo(&app);
// Alt-click Layer 2's eye: Layers 1 and 3 hide in one step; again, they show.
click(&mut app, eye, true);
assert_eq!(state(&app), [(false, false), (true, false), (false, false)]);
assert_eq!(undo(&app), before + 1);
click(&mut app, eye, true);
assert_eq!(state(&app), [(true, false); 3]);
// Alt-click its lock: the others lock; again, every layer unlocks.
click(&mut app, lock, true);
assert_eq!(state(&app), [(true, true), (true, false), (true, true)]);
assert_eq!(app.session.active().unwrap().history.undo.last().map(|u| u.label.as_str()), Some("Lock Others"));
// A plain click still toggles only that layer.
click(&mut app, eye, false);
assert_eq!(state(&app), [(true, true), (false, false), (true, true)]);
click(&mut app, lock, true);
assert_eq!(state(&app), [(true, false), (false, false), (true, false)]);
// A sublayer's row stands for its top-level layer: the other layers hide.
click(&mut app, eye, false);
let layer2 = app.session.active().unwrap().doc.layers[1].id.0;
let sub = run(&mut app, "layer.newSublayer", json!({"parent": layer2}));
// Rows: Layer 3, Layer 2, its sublayer, Layer 1.
let (circles, _) = frame(&mut app, &ctx, vec![], false);
assert_eq!(circles.len(), 4);
click(&mut app, egui::pos2(12.0, circles[2].y), true);
assert_eq!(state(&app), [(false, false), (true, false), (false, false)]);
let doc = &app.session.active().unwrap().doc;
assert!(doc.node(NodeId(sub["id"].as_u64().unwrap())).unwrap().visible);
}
/// A document with two rectangles on one layer → (app, layer, [bottom, top]).
fn two_rects() -> (VectorcraftApp, NodeId, [NodeId; 2]) {
let mut app = VectorcraftApp::new(Session::new(), Default::default());

View File

@ -752,7 +752,8 @@ and `layerRows`, the rows highlighted in the panel.
templates stay), `layer.collectInNew {ids?}`, `layer.releaseToLayers {id?, build?}` and
`layer.releaseToLayersBuild` (each object of a layer or group in a sublayer of its own; Build adds up copies),
`layer.reverse {ids?}`, `layer.template {ids?, on?}`, `layer.hideOthers` / `layer.showAll`,
`layer.outlineOthers` / `layer.previewAll`, `layer.lockOthers` / `layer.unlockAll`, `layer.pasteRemembersLayers`,
`layer.outlineOthers` / `layer.previewAll`, `layer.lockOthers` / `layer.unlockAll` (Alt-clicking an eye or a lock
runs Hide or Lock Others for the row's layer, or Show or Unlock All when the others already are), `layer.pasteRemembersLayers`,
`object.isolate {id}` / `object.exitIsolation`.
- **Dialogs and panel state** (control channel and the app's MCP): `ui.layerOptions {ids?}` and `ui.newLayer
{sublayer?}` open Layer Options (dialog `layerOptions`: name, color, template, locked, visible, printable, preview,