diff --git a/crates/doc/src/selection.rs b/crates/doc/src/selection.rs index 6400c3ad..a64b17aa 100644 --- a/crates/doc/src/selection.rs +++ b/crates/doc/src/selection.rs @@ -128,6 +128,20 @@ impl Selection { } self.target = Some(id); } + /// Shift-clicking `id`'s target circle: it (a layer: its art) joins the selection, or leaves it + /// when it is all selected already. Several objects are then selected, none targeted. + pub fn toggle_target(&mut self, doc: &Document, id: NodeId) { + let ids = match doc.node(id) { + Some(n) if n.is_layer() => n.layer_art(true), + _ => vec![id], + }; + if ids.iter().all(|i| self.contains(*i)) { + ids.iter().for_each(|i| self.remove(*i)); + } else { + ids.into_iter().for_each(|i| self.add(i)); + } + self.target = None; + } /// What appearance and transparency edits act on: the targeted object, group or layer, else /// the selected objects. pub fn subjects(&self) -> &[NodeId] { diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 2bae10ab..2d69434a 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -163,7 +163,7 @@ pub fn specs() -> Vec { "Target", [], None, - "{id} target a layer, group or object as clicking its target circle in the Layers panel does: a layer gets its visible, unlocked art (its sublayers' too) selected and becomes the current layer, and appearance.*, effect.*, transparency.* and the opacity-mask commands without `ids` then act on the layer itself; anything else is selected. Any other selection change ends the targeting (`document.inspect` → target) → {id, selected: [..]}", + "{id, add?: bool (Shift-click: the item joins the selection, or leaves it when all selected; nothing is targeted)} target a layer, group or object as clicking its target circle in the Layers panel does: a layer gets its visible, unlocked art (its sublayers' too) selected and becomes the current layer, and appearance.*, effect.*, transparency.* and the opacity-mask commands without `ids` then act on the layer itself; anything else is selected. Any other selection change ends the targeting (`document.inspect` → target) → {id, selected: [..]}", has_doc, target ), @@ -362,7 +362,11 @@ fn target(s: &mut Session, p: &Value) -> Result { } let layer = n.is_layer(); let rows = st.layer_rows.clone(); - s.select(|d, sel| sel.set_target(d, id))?; + if p.get("add").and_then(Value::as_bool).unwrap_or(false) { + s.select(|d, sel| sel.toggle_target(d, id))?; + } else { + s.select(|d, sel| sel.set_target(d, id))?; + } let st = s.doc_mut()?; // Targeting doesn't change which rows are highlighted. st.layer_rows = rows; diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 51a30339..87c6a0fc 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -664,7 +664,7 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { } let m = ui.input(|i| i.modifiers); if tresp.clicked() { - out.actions.push(("layer.target".into(), json!({"id": n.id.0}))); + out.actions.push(("layer.target".into(), json!({"id": n.id.0, "add": m.shift}))); } else if sq_resp.clicked() { out.actions.push(("layer.selectAll".into(), json!({"id": n.id.0, "add": m.shift}))); } else if resp.clicked() { @@ -1239,6 +1239,13 @@ mod tests { } let st = app.session.active().unwrap(); assert_eq!((st.selection.target, st.selection.objects.clone()), (Some(a), vec![a])); + // Shift-clicking another object's circle adds it to the selection (#901), and again takes + // it out; nothing is targeted meanwhile. + click_with(&mut app, &ctx, c[1], egui::Modifiers::SHIFT); + let st = app.session.active().unwrap(); + assert_eq!((st.selection.target, st.selection.objects.clone()), (None, vec![a, b])); + click_with(&mut app, &ctx, c[1], egui::Modifiers::SHIFT); + assert_eq!(app.session.active().unwrap().selection.objects, vec![a]); } #[test]