diff --git a/crates/engine/src/cmd/controls.rs b/crates/engine/src/cmd/controls.rs index 77193e091..94a4ef7ec 100644 --- a/crates/engine/src/cmd/controls.rs +++ b/crates/engine/src/cmd/controls.rs @@ -13,7 +13,7 @@ use serde_json::{Value as Json, json}; use super::{ CommandSpec, EngineError, PROTECTED, Result, Session, anchor_mode_param, bad, bool_param, cell_param, cmd, edit, f64_param, find_command, - has_doc, split_sheet, str_param, u32_param, + has_doc, split_sheet, stash, str_param, u32_param, }; use crate::macros::policy::{Policy, macro_policy}; @@ -530,3 +530,59 @@ pub(crate) fn sync_linked_values(wb: &mut Workbook) { } } } + +// ---------------------------------------------------------------- xlsx + +/// The hidden-data key of the button actions xlsx has no place for. +const ACTIONS_KEY: &str = "control_actions"; + +/// A button's action that xlsx carries itself: a macro on the selection (`macro="[0]!Name"`). +fn plain(a: &ControlAction) -> bool { + matches!(a, ControlAction::Macro { input: MacroInput::Selection, output: None, .. }) +} + +/// A copy of `wb` for xlsx with the button actions xlsx can't express (commands, fixed inputs, +/// output cells) in a hidden name, keyed by sheet name and the control's place on the sheet. +/// `None` when there is nothing to store. +pub(crate) fn with_action_names(wb: &Workbook) -> Result> { + let mut items = Vec::new(); + for sh in &wb.sheets { + for (i, c) in sh.controls.iter().enumerate() { + if let Some(a) = c.action.as_ref().filter(|a| !plain(a)) { + items.push(json!({"sheet": sh.name, "index": i, "action": a})); + } + } + } + let stale = stash::get(wb, ACTIONS_KEY).is_some(); + if items.is_empty() && !stale { + return Ok(None); + } + let mut out = wb.clone(); + stash::remove(&mut out, ACTIONS_KEY); + if !items.is_empty() { + stash::set_json(&mut out, ACTIONS_KEY, &Json::Array(items))?; + } + Ok(Some(out)) +} + +/// After a file is opened: gives buttons back the actions kept in the hidden name, and removes +/// it. Entries that don't match a button, or don't parse, are dropped. +pub(crate) fn load_action_names(wb: &mut Workbook) { + let Some(items) = stash::get_json(wb, ACTIONS_KEY) else { return }; + stash::remove(wb, ACTIONS_KEY); + for item in items.as_array().into_iter().flatten().take(100_000) { + let (Some(sheet), Some(index)) = (item.get("sheet").and_then(Json::as_str), item.get("index").and_then(Json::as_u64)) else { continue }; + let Ok(action) = serde_json::from_value::(item.get("action").cloned().unwrap_or(Json::Null)) else { continue }; + let valid = match &action { + ControlAction::Macro { name, .. } => !name.trim().is_empty() && name.chars().count() <= MAX_MACRO_NAME, + ControlAction::Command { id, params } => !id.is_empty() && id.len() <= 200 && params.is_object(), + }; + let Some(si) = wb.sheet_index(sheet) else { continue }; + if !valid || !wb.sheet(si).is_some_and(|sh| sh.controls.get(index as usize).is_some_and(|c| c.kind == ControlKind::Button)) { + continue; + } + if let Some(c) = wb.sheet_mut(si).and_then(|sh| sh.controls.get_mut(index as usize)) { + c.action = Some(action); + } + } +} diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 71060a4c9..8351bb3f4 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -118,10 +118,12 @@ fn decrypt_message(name: &str, e: gridcraft_xlsx::DecryptError) -> String { } } -/// Parses bytes that aren't encrypted, then takes the workbook's macros out of their hidden names. +/// Parses bytes that aren't encrypted, then takes the workbook's macros, and the button actions +/// xlsx has no place for, out of their hidden names. fn open_plain(name: &str, bytes: &[u8], csv_as_text: bool) -> Result<(Workbook, Vec)> { let (mut wb, mut warnings) = read_plain(name, bytes, csv_as_text)?; warnings.extend(crate::macros::load(&mut wb)); + crate::cmd::controls::load_action_names(&mut wb); Ok((wb, warnings)) } @@ -247,9 +249,13 @@ pub fn save_bytes_as(wb: &Workbook, path: &str, kind: FileKind) -> Result