From 14dee9155dd7e26ba99004bbcd2046b9227ef800 Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:35:36 -0400 Subject: [PATCH] Form controls: the object commands leave a locked control on a protected sheet alone control.format and control.assign refused to change a locked control on a protected sheet, but object.move, object.delete, object.setAnchorMode, arrange.bringForward, arrange.sendBackward and arrange.align changed it all the same. They now refuse it with the same message; an unlocked control still moves, and other kinds of objects are unchanged. --- crates/engine/src/cmd/controls.rs | 18 +++++++++++++++++- crates/engine/src/cmd/extra/objects.rs | 6 ++++++ crates/engine/src/cmd/insert.rs | 2 ++ crates/engine/src/tests/controls.rs | 25 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/controls.rs b/crates/engine/src/cmd/controls.rs index d2cf17f01..f68effc9e 100644 --- a/crates/engine/src/cmd/controls.rs +++ b/crates/engine/src/cmd/controls.rs @@ -8,7 +8,9 @@ //! else made asks before it runs anything. use gridcraft_core::CellRef; -use gridcraft_model::{Anchor, AnchorMode, CONTROL_MAX, ControlAction, ControlKind, FormControl, MAX_MACRO_NAME, MacroInput, Sheet, Workbook}; +use gridcraft_model::{ + Anchor, AnchorMode, CONTROL_MAX, ControlAction, ControlKind, FormControl, MAX_MACRO_NAME, MacroInput, ObjKind, Sheet, Workbook, +}; use serde_json::{Value as Json, json}; use super::{ @@ -298,6 +300,20 @@ fn find(s: &Session, cmd: &str, p: &Json) -> Result<(usize, usize, FormControl)> } /// Drawing objects on a protected sheet can't be changed while they are locked. +/// For the object commands (`object.move`, `object.delete`, `object.setAnchorMode`, `arrange.*`, +/// `arrange.align`): refuses to change form controls `ids` on `sheet` when one is locked and the +/// sheet is protected, as `control.format` and `control.assign` do. Other kinds pass. +pub(crate) fn check_objects_unlocked(s: &Session, sheet: usize, kind: ObjKind, ids: &[u32]) -> Result<()> { + if kind != ObjKind::Control { + return Ok(()); + } + let Some(sh) = s.doc()?.wb.sheet(sheet) else { return Ok(()) }; + if sh.is_protected() && sh.controls.iter().any(|c| c.locked && ids.contains(&c.id)) { + return Err(EngineError::Other(PROTECTED.into())); + } + Ok(()) +} + fn check_unlocked(s: &Session, sheet: usize, c: &FormControl) -> Result<()> { if c.locked && s.doc()?.wb.sheet(sheet).is_some_and(Sheet::is_protected) { return Err(EngineError::Other(PROTECTED.into())); diff --git a/crates/engine/src/cmd/extra/objects.rs b/crates/engine/src/cmd/extra/objects.rs index 67286f268..91952c219 100644 --- a/crates/engine/src/cmd/extra/objects.rs +++ b/crates/engine/src/cmd/extra/objects.rs @@ -36,6 +36,9 @@ pub(super) fn arrange(s: &mut Session, p: &Json, forward: bool) -> Result let kind = str_param(p, "kind").unwrap_or("shape").to_string(); let extreme = bool_param(p, "toFront").or(bool_param(p, "toBack")).unwrap_or(false); let si = s.doc()?.wb.active_sheet(); + if let Some(k) = gridcraft_model::ObjKind::parse(&kind) { + crate::cmd::controls::check_objects_unlocked(s, si, k, &[id])?; + } fn reorder(v: &mut Vec, i: usize, forward: bool, extreme: bool) { if i >= v.len() { return; @@ -125,6 +128,9 @@ pub(super) fn align(s: &mut Session, p: &Json) -> Result { p.get("ids").and_then(Json::as_array).map(|a| a.iter().filter_map(|v| v.as_u64().map(|x| x as u32)).collect()).unwrap_or_default(); let how = str_param(p, "align").unwrap_or("left").to_string(); let si = s.doc()?.wb.active_sheet(); + if let Some(k) = gridcraft_model::ObjKind::parse(&kind) { + crate::cmd::controls::check_objects_unlocked(s, si, k, &ids)?; + } edit(s, |cx| { let sheet = cx.sheet_mut(si)?; let geo: Vec<(u32, f64, f64, f32, f32)> = { diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 0806f2183..3c95e3388 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -1187,6 +1187,8 @@ fn object_param(s: &Session, p: &Json, cmd: &str) -> Result<(usize, ObjRef)> { if !d.wb.sheet(sheet).is_some_and(|sh| sh.has_object(o)) { return Err(bad(cmd, "no such object")); } + // A locked form control on a protected sheet stays as it is. + super::controls::check_objects_unlocked(s, sheet, kind, &[id])?; Ok((sheet, o)) } diff --git a/crates/engine/src/tests/controls.rs b/crates/engine/src/tests/controls.rs index 98b2404ae..8f0741d49 100644 --- a/crates/engine/src/tests/controls.rs +++ b/crates/engine/src/tests/controls.rs @@ -335,6 +335,31 @@ fn links_follow_inserts_deletes_renames_and_cut_moves() { assert_eq!((link(&s, 0, local), link(&s, 0, remote)), ("$G$9".to_string(), "Inputs!$A$1".to_string())); } +#[test] +fn the_object_commands_leave_a_locked_control_on_a_protected_sheet_alone() { + let mut s = s(); + let locked = id(&s.execute("insert.slider", json!({"at": "D2", "link": "B1"})).unwrap()); + let other = id(&s.execute("insert.spinButton", json!({"at": "D8"})).unwrap()); + let free = id(&s.execute("insert.button", json!({"at": "D12"})).unwrap()); + s.execute("control.format", json!({"id": free, "locked": false})).unwrap(); + s.execute("review.protectSheet", json!({})).unwrap(); + let before = s.doc().unwrap().wb.clone(); + for (cmd, p) in [ + ("object.move", json!({"kind": "control", "id": locked, "at": "H9"})), + ("object.delete", json!({"kind": "control", "id": locked})), + ("object.setAnchorMode", json!({"kind": "control", "id": locked, "mode": "absolute"})), + ("arrange.bringForward", json!({"kind": "control", "id": locked, "toFront": true})), + ("arrange.sendBackward", json!({"kind": "control", "id": locked})), + ("arrange.align", json!({"kind": "control", "ids": [locked, other], "align": "left"})), + ] { + assert!(err(s.execute(cmd, p)).contains("protected"), "{cmd}"); + } + assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().wb, &before), "nothing changed"); + // An unlocked control still moves, as in Excel. + s.execute("object.move", json!({"kind": "control", "id": free, "at": "H12"})).unwrap(); + assert_eq!(control(&s, free).anchor.cell.a1(), "H12"); +} + #[test] fn a_locked_control_on_a_protected_sheet_cannot_be_changed() { let mut s = s();