diff --git a/crates/engine/src/extra_cmds.rs b/crates/engine/src/extra_cmds.rs index 4e2a265ab9..c24f30e515 100644 --- a/crates/engine/src/extra_cmds.rs +++ b/crates/engine/src/extra_cmds.rs @@ -366,6 +366,13 @@ fn paste_into(s: &mut Session, p: &Value, outside: bool) -> Result { params["target"] = p.get("target").cloned().unwrap_or_default(); return crate::edit_cmds::paste_to_target(s, ¶ms, false, Some(&limit), label); } + // The pasted layer lands beside the active one, inside its group, and a Lock-All group refuses + // its layer mask: refuse before pasting rather than leave an unmasked paste behind. + let d = s.active().ok_or(EngineError::NoDocument)?; + let group = d.active_layer.and_then(|a| d.doc.path_of(a)).and_then(|p| d.doc.layer_at(p.split_last()?.1).map(|g| g.id)); + if let Some(g) = group { + crate::commands::check_all_unlocked(&d.doc, g)?; + } let key = step_key(s, "pasteInto"); params["coalesce"] = json!(key); params["target"] = json!("pixels"); diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs index cd44a7a946..874c4f5226 100644 --- a/crates/engine/tests/it/main.rs +++ b/crates/engine/tests/it/main.rs @@ -44,6 +44,7 @@ mod layer_mask_depth; mod layer_props_several; mod layer_range_panel_order; mod live_line_transform; +mod mask_lock_all; mod merge_transparent_bounds; mod missing_layer_error; mod nonfinite_style; diff --git a/crates/engine/tests/mask_lock_all.rs b/crates/engine/tests/it/mask_lock_all.rs similarity index 68% rename from crates/engine/tests/mask_lock_all.rs rename to crates/engine/tests/it/mask_lock_all.rs index f0334619f1..74ea71ff62 100644 --- a/crates/engine/tests/mask_lock_all.rs +++ b/crates/engine/tests/it/mask_lock_all.rs @@ -69,3 +69,24 @@ fn a_locked_layer_among_the_selected_layers_refuses_the_whole_mask() { assert!(s.execute("layer.layerMask.hideAll", json!({})).is_err()); assert!(s.active().unwrap().doc.walk().iter().all(|(_, _, l)| l.mask.is_none()), "no layer got a mask"); } + +#[test] +fn paste_into_a_lock_all_group_refuses_before_pasting() { + for cmd in ["edit.pasteSpecial.pasteInto", "edit.pasteSpecial.pasteOutside"] { + let mut s = Session::new(); + s.execute("file.new", json!({"width": 40, "height": 30})).unwrap(); + let child = s.execute("layer.new.layer", json!({"name": "Child"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("paint.stroke", json!({"points": [[5, 10], [20, 10]], "size": 8, "hardness": 1.0, "color": "#ff0000"})).unwrap(); + s.execute("select.all", json!({})).unwrap(); + s.execute("edit.copy", json!({})).unwrap(); + let group = s.execute("layer.groupLayers", json!({})).unwrap()["layer"].as_u64().unwrap(); + s.execute("layer.setProps", json!({"layer": group, "locks": {"all": true}})).unwrap(); + s.execute("layer.select", json!({"layer": child, "mode": "replace"})).unwrap(); + s.execute("select.rect", json!({"x": 2, "y": 2, "width": 20, "height": 15})).unwrap(); + let layers = s.active().unwrap().doc.walk().len(); + let undo = undo_depth(&s); + assert!(s.execute(cmd, json!({})).is_err(), "{cmd}"); + assert_eq!(s.active().unwrap().doc.walk().len(), layers, "{cmd} pasted no layer"); + assert_eq!(undo_depth(&s), undo, "{cmd} added no history step"); + } +}