Files
gridcraft/crates/engine/src/cmd/extra.rs
T

368 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! More ribbon commands: Chart Design and Format, object arrangement, workbook views, table
//! tools, Get Data, scripts (Automate), cell checkboxes, theme colours and fonts.
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::{
Anchor, Chart, Color, DefinedName, HAlign, LegendPos, NumFmt, Shape, Sheet, SheetViewMode, TableColumn, TotalsFn, Trendline, TrendlineKind,
Workbook,
};
use serde_json::{Value as Json, json};
use super::{
CommandSpec, EngineError, Result, Session, always, anchor_mode_name, bad, bool_param, cell_param, cell_param_on, cmd, edit, f64_param, has_doc,
ok, split_sheet, str_param, target_range, target_ranges, target_sheet, u32_param,
};
mod cells;
mod chart;
mod data;
mod objects;
mod scripts;
mod window;
pub(crate) use cells::is_checkbox_cell;
use cells::{background, checkbox, draw_border, new_cell_style, recently_used, theme_colors, theme_fonts, toggle_checkbox, use_in_formula};
use chart::{add_element, change_colors, format_series, move_chart, quick_layout, select_data};
use data::{from_csv, from_table_range, queries, table_resize};
use objects::{align, arrange, rotate, selection_pane, shape_fill, shape_outline};
pub(crate) use scripts::has_scripts;
use scripts::{all_scripts, delete_script, new_script, record, run_script, stop_action, trust_scripts};
use window::{arrange_all, custom_views, hide_window, new_window, ruler, set_view, split, switch_windows, unhide_window, watch_window};
pub(crate) use window::{keep_watch_list, window_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
// Chart Design / Format
cmd!(
"chart.addElement",
"Add Chart Element",
["Chart Design", "Chart Layouts"],
None,
"{chart?, element: title|legend|dataLabels|gridlines|axisTitles|smooth|trendline, on?: bool, position?: bottom|top|left|right; trendline: series?: index (default all), type?: linear|exponential|linearForecast|movingAverage|logarithmic|power|polynomial, order?: 2..6, period?: 2..255, forward?, backward?, displayEquation?, displayRSquared?}",
has_doc,
add_element
),
cmd!("chart.quickLayout", "Quick Layout", ["Chart Design", "Chart Layouts"], None, "{chart?, layout: 1..6}", has_doc, quick_layout),
cmd!(
"chart.changeColors",
"Change Colors",
["Chart Design", "Chart Styles"],
None,
"{chart?, palette: colorful|monochrome|accent1..accent6 | colors: [hex…]}",
has_doc,
change_colors
),
cmd!(
"chart.selectData",
"Select Data…",
["Chart Design", "Data"],
None,
"{chart?, range?: \"Sheet1!A1:D7\", byRows?: bool, categories?: \"Sheet1!A2:A7\" (Horizontal (Category) Axis Labels for every series; \"\" clears), removeSeries?: [index…]}",
has_doc,
select_data
),
cmd!("chart.changeType", "Change Chart Type…", ["Chart Design", "Type"], None, "{chart?, type, subtype?}", has_doc, |s, p| s
.execute("chart.set", p.clone())),
cmd!(
"chart.move",
"Move Chart…",
["Chart Design", "Location"],
None,
"{chart?, sheet?: name (moves to that sheet), at?: \"B2\"}",
has_doc,
move_chart
),
cmd!(
"chart.formatSelection",
"Format Selection",
["Format", "Current Selection"],
None,
"{chart?, series?: index, color?: hex}",
has_doc,
format_series
),
cmd!(
"chart.shapeFill",
"Shape Fill",
["Format", "Shape Styles"],
None,
"{chart? | kind: shape, id?, series?, color: hex}",
has_doc,
shape_fill
),
cmd!("chart.shapeOutline", "Shape Outline", ["Format", "Shape Styles"], None, "{kind: shape, id?, color: hex}", has_doc, shape_outline),
// Arrange
cmd!(
"arrange.bringForward",
"Bring Forward",
["Page Layout", "Arrange"],
None,
"{kind: chart|image|shape, id, toFront?: bool}",
has_doc,
|s, p| arrange(s, p, true)
),
cmd!(
"arrange.sendBackward",
"Send Backward",
["Page Layout", "Arrange"],
None,
"{kind: chart|image|shape, id, toBack?: bool}",
has_doc,
|s, p| arrange(s, p, false)
),
cmd!(query "arrange.selectionPane", "Selection Pane", ["Page Layout", "Arrange"], None, "{} → objects on the sheet in stacking order", has_doc, selection_pane),
cmd!(
"arrange.align",
"Align",
["Page Layout", "Arrange"],
None,
"{kind: chart|image|shape, ids: [..], align: left|center|right|top|middle|bottom}",
has_doc,
align
),
cmd!(
"arrange.group",
"Group",
["Page Layout", "Arrange"],
None,
"{kind, ids} (moves objects together: aligns their anchors to the first)",
has_doc,
|s, p| align(s, &super::format::merge_json(p.clone(), &json!({"align": "left"})))
),
cmd!(
"arrange.rotate",
"Rotate",
["Page Layout", "Arrange"],
None,
"{kind: shape, id} (swaps width/height for a 90° turn; `flip` is rejected: shapes have no flip state)",
has_doc,
rotate
),
// Views
cmd!(noundo "view.normal", "Normal", ["View", "Workbook Views"], None, "{sheet?} → {view} (per sheet, saved with the file)", has_doc, |s, p| set_view(
s,
p,
SheetViewMode::Normal
)),
cmd!(
noundo "view.pageLayout",
"Page Layout",
["View", "Workbook Views"],
None,
"{sheet?} → {view}: pages on paper with rulers; headers and footers are edited in place (per sheet, saved with the file)",
has_doc,
|s, p| set_view(s, p, SheetViewMode::PageLayout)
),
cmd!(
noundo "view.pageBreakPreview",
"Page Break Preview",
["View", "Workbook Views"],
None,
"{sheet?} → {view} (per sheet, saved with the file)",
has_doc,
|s, p| set_view(s, p, SheetViewMode::PageBreakPreview)
),
cmd!(noundo "view.customViews", "Custom Views…", ["View", "Workbook Views"], None, "{save?: name, show?: name, delete?: name} → list", has_doc, custom_views),
cmd!(noundo "view.split", "Split", ["View", "Window"], None, "{} (toggles a split at the active cell; shown as frozen panes in this version)", has_doc, split),
cmd!(noundo "view.newWindow", "New Window", ["View", "Window"], None, "{} → {window, doc, title}: opens another window on the active workbook (the same workbook: saving from either window saves it)", has_doc, new_window),
cmd!(
noundo "view.arrangeAll",
"Arrange All",
["View", "Window"],
None,
"{arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?: bool} → {arrange, windows}: lays out the visible windows (tiled by default; `activeWorkbook` only the active workbook's), the active window first",
has_doc,
arrange_all
),
cmd!(noundo "view.hideWindow", "Hide", ["View", "Window"], None, "{window?, doc?} (the active window by default)", has_doc, hide_window),
cmd!(noundo "view.unhideWindow", "Unhide…", ["View", "Window"], None, "{window?, doc?, index?} (the most recently hidden window by default)", has_doc, unhide_window),
cmd!(
noundo "view.switchWindows",
"Switch Windows",
["View", "Window"],
None,
"{window?, doc?} → open windows [{window, doc, index, title, dirty, active, hidden}]; with `window` (an id from the list), or `doc` (a workbook id: its most recent window), shows and activates that window first",
always,
switch_windows
),
cmd!(noundo "view.ruler", "Ruler", ["View", "Show"], None, "{on?} → {on}: rulers in Page Layout view (an application option)", has_doc, ruler),
cmd!(query "view.macros", "Macros", ["View", "Macros"], None, "{} → scripts", has_doc, all_scripts),
// Table tools
cmd!("table.resize", "Resize Table", ["Table Design", "Properties"], None, "{table?, range: \"A1:F20\"}", has_doc, table_resize),
cmd!("table.removeDuplicates", "Remove Duplicates", ["Table Design", "Tools"], None, "{table?, columns?}", has_doc, |s, p| s
.execute("data.removeDuplicates", p.clone())),
// Get & Transform
cmd!(
"data.fromTextCsv",
"From Text/CSV",
["Data", "Get & Transform Data"],
None,
"{path | text, delimiter?: \",\", sheet?: new sheet name} → imports into a new sheet as a table",
has_doc,
from_csv
),
cmd!(
"data.getData",
"Get Data",
["Data", "Get & Transform Data"],
None,
"{path | text} (CSV/TSV/JSON array of objects) → new sheet table",
has_doc,
from_csv
),
cmd!(
"data.fromTableRange",
"From Table/Range",
["Data", "Get & Transform Data"],
None,
"{range?} → copies the table/range to a new sheet as a table",
has_doc,
from_table_range
),
cmd!(query "data.queriesConnections", "Queries & Connections", ["Data", "Queries & Connections"], None, "{} → imported tables", has_doc, queries),
// Automate
cmd!(noundo "automate.recordActions", "Record Actions", ["Automate", "Scripting Tools"], None, "{stop?: bool, name?: script name when stopping}", has_doc, record),
cmd!(
"automate.newScript",
"New Script",
["Automate", "Scripting Tools"],
None,
"{name, commands: [{command, params}] }",
has_doc,
new_script
),
cmd!(query "automate.allScripts", "All Scripts", ["Automate", "Scripting Tools"], None, "{} → saved scripts", has_doc, all_scripts),
cmd!(
"automate.runScript",
"Run Script",
[],
None,
"{name, trusted?: bool (runs a script from an untrusted workbook this once)} — refuses scripts in a workbook opened from a file until it is trusted, and steps that reach outside the workbook (file.*, link.*, any `path` param)",
has_doc,
run_script
),
cmd!(
noundo "automate.trustScripts",
"Trust Scripts",
["Automate", "Scripting Tools"],
None,
"{trusted?: bool (default true)} → {trusted}: lets this workbook's scripts run for the rest of the session",
has_doc,
trust_scripts
),
cmd!("automate.deleteScript", "Delete Script", [], None, "{name}", has_doc, delete_script),
cmd!(
noundo "automate.stopAction",
"Stop Action",
[],
None,
"{} → {stopped: bool}: stops an action left running on the active workbook (an open undo group, such as a debugger's) and puts back the workbook and its undo history as they were when it began",
has_doc,
stop_action
),
// Cell controls, themes, borders, styles
cmd!(
"insert.checkbox",
"Checkbox",
["Insert", "Cell Controls"],
None,
"{range?} (TRUE/FALSE and empty cells draw as checkboxes, empty ones become FALSE, other values show as they are; click or Space toggles; Delete unchecks, then removes; Clear Formats removes and keeps the values)",
has_doc,
checkbox
),
cmd!("cell.toggleCheckbox", "Toggle Checkbox", [], Some("Space"), "{cell?}", has_doc, toggle_checkbox),
cmd!("pageLayout.themeColors", "Colors", ["Page Layout", "Themes"], None, "{name: theme name | colors: [12 hex]}", has_doc, theme_colors),
cmd!("pageLayout.themeFonts", "Fonts", ["Page Layout", "Themes"], None, "{major, minor}", has_doc, theme_fonts),
cmd!(
"pageLayout.themeEffects",
"Effects",
["Page Layout", "Themes"],
None,
"{name} (chart effects preset; stored with the theme name)",
has_doc,
|_, _| ok()
),
cmd!(
"pageLayout.background",
"Background",
["Page Layout", "Page Setup"],
None,
"{path? | clear?: bool} (sheet background picture)",
has_doc,
background
),
cmd!(
"home.drawBorder",
"Draw Border",
["Home", "Font"],
None,
"{range, edge: top|bottom|left|right|grid, style?, color?}",
has_doc,
draw_border
),
cmd!(
"home.newCellStyle",
"New Cell Style…",
["Home", "Styles"],
None,
"{name, fromCell?: \"A1\" (default: active cell)}",
has_doc,
new_cell_style
),
cmd!(query "home.addIns", "Add-ins", ["Home", "Add-ins"], None, "{} → GridCraft extends through MCP and scripts instead of add-ins", always, |_, _| Ok(json!({"addIns": [], "hint": "Use gridcraft-cli mcp or Automate › scripts"}))),
cmd!(query "formulas.recentlyUsed", "Recently Used", ["Formulas", "Function Library"], None, "{} → recently used functions in this workbook", has_doc, recently_used),
cmd!(query "formulas.financial", "Financial", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Financial"}))),
cmd!(query "formulas.logical", "Logical", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Logical"}))),
cmd!(query "formulas.text", "Text", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Text"}))),
cmd!(query "formulas.dateTime", "Date & Time", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "DateTime"}))),
cmd!(query "formulas.lookupReference", "Lookup & Reference", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Lookup"}))),
cmd!(query "formulas.mathTrig", "Math & Trig", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "MathTrig"}))),
cmd!(query "formulas.moreFunctions", "More Functions", ["Formulas", "Function Library"], None, "{category?: Statistical|Engineering|Information|Database|Compatibility|Web|Cube}", always, |s, p| s.execute("formulas.functions", json!({"category": str_param(p, "category").unwrap_or("Statistical")}))),
cmd!(
"formulas.useInFormula",
"Use in Formula",
["Formulas", "Defined Names"],
None,
"{name} (inserts the name into the cell being edited)",
has_doc,
use_in_formula
),
cmd!(query "formulas.watchWindow", "Watch Window", ["Formulas", "Formula Auditing"], None, "{add?: \"Sheet1!B5\", remove?: \"…\"} → watched cells with current values", has_doc, watch_window),
]
}
const VIEWS_NAME: &str = "_sc_custom_views";
const SCRIPTS_NAME: &str = "_sc_scripts";
const WATCH_NAME: &str = "_sc_watch";
/// Workbook-level JSON stored in a hidden defined name (round-trips through XLSX).
fn stash_get(wb: &Workbook, key: &str) -> Json {
wb.names
.iter()
.find(|n| n.name == key)
.and_then(|n| n.formula.strip_prefix('"').and_then(|t| t.strip_suffix('"')).map(|t| t.replace("\"\"", "\"")))
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or(json!({}))
}
/// Like `stash_get`, for the name-keyed lists (views, scripts): a stored value that is not a JSON object is treated as empty.
fn stash_map(wb: &Workbook, key: &str) -> Json {
let v = stash_get(wb, key);
if v.is_object() { v } else { json!({}) }
}
fn stash_set(wb: &mut Workbook, key: &str, v: &Json) {
let text = v.to_string();
let formula = format!("\"{}\"", text.replace('"', "\"\""));
wb.names.retain(|n| n.name != key);
wb.names.push(DefinedName { name: key.into(), scope: None, formula, comment: String::new(), hidden: true });
}
/// Number format that marks checkbox cells (TRUE/FALSE values drawn as boxes).
pub const CHECKBOX_FORMAT: &str = "\"☐\";\"☐\";\"☐\";[=1]\"☑\"";
/// Copy of the params that is always a JSON object, so keys can be assigned; any other JSON value counts as no params (like null).
fn object_params(p: &Json) -> Json {
Json::Object(p.as_object().cloned().unwrap_or_default())
}