Issue #348: copying multiple selections works when the areas line up, and explains when it can't
# Conflicts:
# crates/engine/src/tests.rs
# crates/ui-egui/tests/clipboard.rs
View: Formula Bar and Zoom 100% as engine commands (#289)
# Conflicts:
# ROADMAP.md
# crates/ui-egui/src/control.rs
# crates/ui-egui/src/lib.rs
# docs/target-app-parity.md
Protect Sheet and Unprotect Sheet were journaled with their `password`
param, so the password showed in plain text in the journal dialog and
`engine.journal`, and a recorded script (Automate > Record Actions)
saved it in the workbook's hidden `_sc_scripts` name.
Session::execute now journals the params without secret ones
(`password`, or a name ending in `Password`). A replayed entry runs
without that param: Protect Sheet protects with the same options but no
password, and the matching Unprotect Sheet still succeeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copying a multi-area selection always failed in the engine, and the UI
dropped the error, so Ctrl+C did nothing and Ctrl+V had nothing to
paste. Now, as in Excel:
- Areas that cover the same rows (or the same columns) and don't
overlap are copied as one block, slid together left to right (top to
bottom). The clipboard keeps a snapshot with the block packed at the
first area's place; formulas travel as their values, and column
widths or row heights, comments, pictures and merges come along.
- Other layouts, Cut of several areas and Paste Link of a multi-area
copy are refused with "This action won't work on multiple
selections."; Copy and Cut from the keyboard, ribbon and context menus
now show that message instead of failing silently.
- The marching ants go around each copied area.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a header boundary inside a selection of whole columns (rows)
sized only the current area, so with B, D and F Ctrl-selected only the
last-selected one changed. The drag now targets every whole-column
(whole-row) area of the selection, as Excel does; lines outside the
selection, and partial-cell areas, keep their size.
home.columnWidth / home.rowHeight accept a comma-separated list of
areas ("B:B,D:F") so the drag stays one command and one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View › Page Layout (and the status-bar button) now shows the sheet as printed
pages on a desk instead of overlaying page outlines on the grid.
- Model/XLSX: per-sheet view mode (normal / pageLayout / pageBreakPreview),
saved as <sheetView view="…"> and read back. view.* commands set it per
sheet ({sheet?}) without an undo step or dirtying a clean workbook.
- Engine: paper::PageGrid paginates the sheet from A1 with the print
pagination (print::split_bands, shared with PDF export and printing) and
maps cells to pages and back; header/footer section helpers
(split/join, &[Page] field names) shared with the PDF header code.
pageLayout.headerFooter takes {part, section, text} for one section and a
sheet; header/footer strings are capped at 255 characters.
view.ruler toggles the rulers.
- UI: Geo carries the page grid, so painting, hit-testing, selection,
editing, objects and scrolling work through the page transform; frozen
panes are ignored in this view. Pages with margins and gaps, "Click to add
data" pages, rulers in inches (English) or centimetres, headers/footers
drawn on printed pages, click a margin section to edit it in place; a
click elsewhere commits (one undo step), Escape cancels. Header & Footer
contextual tab: presets, Page Number, Number of Pages, Current Date/Time,
File Path, File Name, Sheet Name, Go to Header/Footer. View › Ruler.
- New strings in every catalog; new code-drawn icons; desk colour token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass-through of package content GridCraft does not model: on read the raw
parts are kept with their content types and relationship graph per owner
(package root, workbook, sheet); on write they get fresh part names where
needed and new relationship ids, and the r:ids in kept XML are rewritten.
Kept: custom document properties, ribbon customisation, add-in task panes,
custom XML, links to external workbooks with <externalReferences> (never
followed), connections, sheet query tables, printer settings with the
<pageSetup r:id> (#324), form and ActiveX controls, OLE objects and sheet
backgrounds with their VML shapes (merged with GridCraft's note shapes).
Content whose graph reaches parts GridCraft rewrites is dropped with a
warning; sheet content goes with its sheet and is not copied by Copy Sheet.
Fixes#324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean-room import of Excel 97-2003 workbooks from the public [MS-CFB] and
[MS-XLS] specifications: compound-file streams (FAT, mini FAT, directory
tree), the BIFF8 Workbook stream with Continue records, shared strings,
all cell records, formulas decoded from Ptg tokens (shared and array
formulas, 3-D refs, names, add-in/newer functions by name) with cached
results, number formats, the 1904 date system, merges, column widths, row
heights, defined names and print areas/titles. Encrypted (FILEPASS) and
BIFF5 files are refused with a clear message; everything else is skipped
with a warning. .xls opens as a new unsaved workbook (Save As .xlsx).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking the File tab now opens a full-window Backstage view in place of the
ribbon, formula bar, grid and sheet tabs, like Excel's. Esc or the back arrow
return to the sheet. Pages: Home (new + recent), New (blank + samples), Open
(recent + Browse), Info (location, properties, Protect Workbook), Save, Save As,
Print (page preview, scope, fit to page, Page Setup), Export (PDF, CSV, web
page), Close and Options (theme, language, formula bar, AutoSave, calculation).
Every action runs an existing command or dialog.
Adds `file.options` (reports the workbook's calculation settings and asks the
interface for the Options page) and the UI command `ui.backstage {page}`.
Keyboard shortcuts and the macOS native menu are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data.solver maximises, minimises or targets a value in an objective cell by
changing variable cells under <=, >=, =, int and bin constraints. Simplex LP
linearises the model through recalculation, checks linearity and solves it
with a two-phase simplex; integers use branch and bound. The GRG Nonlinear
choice runs an augmented-Lagrangian projected-gradient method. The model is
saved as hidden solver_* names; Solver Parameters and Solver Results dialogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New sheet before the active one with the sorted history, a FORECAST.ETS
forecast column, FORECAST.ETS.CONFINT lower/upper bounds, an optional
FORECAST.ETS.STAT table, an Excel table over the data and a line or column
chart. Parameters: timeline/values (default: the selection's first two
columns), end or periods, confidence interval, seasonality, missing-point
fill, duplicate aggregation, statistics, chart kind. Data ribbon button
opens a form dialog; programmatic calls never do. gridcraft-functions
exports Timeline to continue a timeline (numeric step or calendar months).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data.analysisToolPak runs Descriptive Statistics, Histogram, Correlation,
Covariance, Regression, Moving Average, Exponential Smoothing, Rank and
Percentile, the three t-Tests, F-Test and Anova: Single Factor, writing a
static report to an output range or a new sheet. Non-empty output cells are
refused unless overwrite is set; the dialog asks instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audited the grid, ribbon and editor key handling against Microsoft's
published Windows and Mac shortcut lists. New keymap.rs binds the ones
whose feature already exists: Ctrl+Shift+~!@#$%^&_ number formats and
borders, Ctrl+2-5, Ctrl+Shift+: time, Ctrl+' / Ctrl+Shift+" from the cell
above, Ctrl+L, Ctrl+Shift+L, Ctrl+P, Ctrl+Alt+V, Alt+=, Alt+Down,
Alt+Page Down/Up, Ctrl+Shift+Home/End, Shift+Home, the F-key family
(Shift+F2/F3/F9/F10/F11, F5, F7, F12, Ctrl+F1/F3/F4, Alt+F1, Ctrl+Alt+F9)
and the Excel for Mac variants (Cmd+L/E alignment, Control+E Flash Fill,
Cmd+Option borders, Cmd+Shift+K/J/H/R). Number-format keys match the
physical key; AltGr text never triggers a Ctrl+Alt shortcut. Fixes the
command catalog's wrong shortcut labels and documents the audit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chart linked to a PivotTable: series are built from the report
(innermost row items x column items/value fields, totals excluded) and
rebuilt on every refresh or layout change; c:pivotSource round trip;
chart series accept union references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Model: Sheet.edit_ranges (title, ranges, optional password hash,
security descriptor) and gridcraft_model::password: the ECMA-376 legacy
16-bit hash and the salted, iterated SHA-2 hash (SHA-512/384/256; other
algorithms, damaged hashes and spin counts over 10,000,000 are refused).
New passwords use SHA-512, spin count 100000. Adds the sha2 crate.
- XLSX: <protectedRanges> read and written; an unreadable password keeps
the range locked.
- review.allowEditRanges {action: list|add|modify|delete|unlock, ...}: empty
params list; ranges can't change while the sheet is protected; unlock
verifies the password for the session. Not journaled (passwords).
- Locked-cell checks (commit guard, cell.set, picture in cell) let edits
through in open or unlocked ranges; ranges move with structural edits.
- Ribbon button and a form dialog (menu invocation only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value field can be a calculated field whose formula is evaluated on
the sums of the fields it names for each report cell (Excel's documented
semantics). Command to add/change/delete, dialog from the field pane,
XLSX cacheField formula round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
insert.checkbox existed but had no ribbon button, replaced existing values
with FALSE and was written to XLSX as an invalid number format. Now:
Insert > Checkbox (Cell Controls); empty cells start FALSE, other values
stay and display as General; toggling an emptied box checks it and only
checkbox cells toggle; Delete unchecks first, then removes the boxes; XLSX
reads and writes the checkbox as Excel's feature-property-bag cell control
([MS-XLSX] 2.1.22, 2.3.9, 2.4.99).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line chart from numeric X values (years) plotted them as a series and
there was no way to set the axis labels. Now:
- a blank top-left cell over a header row makes the first column the
category labels even when it holds numbers (the usual convention);
- chart.selectData takes categories (Horizontal (Category) Axis Labels,
"" clears) and removeSeries, and range is optional;
- Chart Design gets a Select Data button opening a Select Data Source
dialog: chart data range, legend entries with Remove, axis labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Writer: one xl/threadedComments/threadedCommentN.xml per sheet with
comments (thread, replies via parentId, done for resolved, dT when known)
and xl/persons/person.xml (one person per author), with the [MS-XLSX]
relationship and content types. Each thread keeps its legacy note, linked
by the tc={id} author and xr:uid; its body is the text plus replies.
Ids are GUID-shaped and deterministic.
- Reader: keeps dT timestamps (Comment.dates, JSON-compatible), drops
replies whose thread is missing, and reads note visibility from the VML
<x:Visible/> (best effort, broken VML skipped silently).
- Invalid dates are never written. Round-trip and hostile-input tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ribbon button inserted a column chart straight away. It now opens an
Insert Chart dialog: a Recommended tab (insert.chartRecommendations, a
heuristic over the data's shape: series, points, labels, negatives) and an
All Charts tab, each with a live preview of the chart for the selection.
OK inserts the chosen type and selects the chart.
insert.recommendedCharts stays dialog-free for programmatic calls: without
a type it inserts the first recommendation; type/subtype pick one.
insert_chart is split into build_chart (also used for previews).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evaluator special form returning the range (or array) without its blank
outer rows and columns; trim codes 0-3 as documented; written as
_xlfn.TRIMRANGE. The trim-reference operators are not parsed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- view.zoom100 sets the active sheet's zoom to 100% (the ribbon's 100%
button runs it).
- view.formulaBar {on?} toggles a formulaBar preference (Prefs, saved in
prefs.json on desktop; an application option, not in the file). The UI
shows the formula bar from it; the ribbon checkbox, native menu and
ui.set {formulaBar} go through the command.
- The UI-only interceptions and their entries in engine.commands are gone,
so the CLI and MCP can run both. Parity checklist regenerated (260 / 290).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dialogs::show put the Name Manager back into app.dialog after its body
had opened the New Name dialog, so New… did nothing. A dialog opened from
another one now takes over and the opener waits behind it (back), coming
back when the child closes; Go To still hands over to Go To Special.
The Name Manager gets Excel's New…/Edit…/Delete buttons over a selectable
list. formulas.defineName takes oldName/oldScope to edit a name in place;
a renamed name is renamed in cell formulas and other names' definitions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New command home.insertCopiedCells: makes room at the active cell and
pastes the clipboard there, as one undo step. Whole copied rows or columns
insert whole rows or columns; a copied block shifts the cells down or right
(Insert Paste dialog from the menu). A cut of whole rows or columns moves
them and closes the gap (Insert Cut Cells). The cell and header context
menus show the entry while the clipboard holds a copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- review.previousComment / review.nextComment select the previous/next
threaded comment in reading order (row by row), across visible sheets in
tab order, wrapping around; disabled when the workbook has no comments.
- review.showComments {on?} toggles the Comments pane as session state, so
the CLI, MCP and control channel can drive it; the UI pane follows it.
- review.showAllNotes {on?} displays every note (session display setting,
not saved in the file).
- review.convertToComments turns every note in the workbook into a threaded
comment with the same author and text, as one undoable edit.
- Ribbon: Previous/Next and a Show Comments toggle in the Comments group;
Show All Notes and Convert to Comments in the Notes menu. Strings in all
catalogs. Parity checklist regenerated (263 / 290).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spreadsheets put an HTML table next to the tab-separated text; Excel
(x:num) and LibreOffice Calc (sdval) record each cell's number there.
Paste now reads the HTML flavour (new paste_html host service, arboard on
desktop) and edit.paste takes an optional html param: a field that would
paste as text becomes the recorded number when the HTML cell shows the
same text, with an inferred format ("Rp"#,##0.00) that renders it back
identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain drag copies a lone number and extends every other series; Ctrl+drag
(Cmd on macOS) now does the opposite, as in Excel: a lone number counts up
by 1, and numbers, dates, lists and `Item 1`-style text are copied.
edit.autoFill gains mode "alternate" for this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turning the filter on with a multi-cell selection used the selection as the
filter range. With only the header row selected, the filter had no data rows:
the dropdown list was empty and filtering hid nothing. A one-row selection
now extends down over the current region below it. An explicit `range`
parameter is still taken as given.
The filter menu gets a search box, as asked for in the issue. It narrows
the value list; Select All then reads "(Select All Search Results)" and
covers only the matches, and Apply keeps only the checked matches. The box
takes focus when the menu opens, so typing no longer lands in the active
cell.
generated with Claude Code
An icon-set rule without stored thresholds (every rule made by
home.conditionalFormat, and the sales sample) was written as an empty
<iconSet>, which CT_IconSet forbids (cfvo minOccurs 2); openpyxl drops it.
The writer now always emits one cfvo per icon: the stored thresholds when
there is one per icon, the lower bound prepended when it is missing, else
Excel's defaults (percent 0/33/67, 0/25/50/75, 0/20/40/60/80).
Evaluation read the thresholds as one fewer than the icons, but files
carry one per icon (the first is the lower bound), so thresholds read from
a file were ignored. Both now share icon_count and default_icon_thresholds
in gridcraft-model; icon_count is clamped to 3..=5 so a set name like
"0Foo" can't underflow k - 1.
generated with Claude Code
Excel decides duplicates by what a cell displays, not the value stored:
the same date shown as 3/8/2006 and as Mar 8, 2006 is two values, while
1.01 and 1.02 both shown as 1.0 are duplicates. Remove Duplicates
compared stored values, so it did the opposite in both cases.
With Case sensitive checked, Sort compared text by code point, so every
uppercase word came before every lowercase one (A, B, a, b). Excel
compares ignoring case first and only then puts lowercase before
uppercase: a, A, b, B.
home.conditionalFormat took any operator it didn't know as "greater",
so a Cell Value rule given Excel's own names (lessThan, greaterThan,
lessThanOrEqual, greaterThanOrEqual, as in the file format and VBA)
silently became "greater than". Those names are now accepted, as data
validation already does, and anything else is an error.
Excel compares a blank cell as 0 (or empty text) in Cell Value rules,
so "less than 5" or "equal to 0" also formats blank cells, and a
cell holding an error never matches (the comparison is an error).
GridCraft skipped blank cells and matched errors as if they sorted
above every number ("greater than 5" formatted #N/A).
The No Blanks rule also counted a cell holding only spaces as not
blank, while the Blanks rule (like Excel's LEN(TRIM())) counted it as
blank, so such a cell matched both.
Only the first matching rule's format was used, so with a light red
fill for >300 and a red border for >500, 510 showed only the border.
Excel applies every matching rule; where two rules set the same part
(fill, font colour, a border edge…), the rule with the higher priority
wins, and Stop If True still ends the list.
cell_param returned None for any cell argument it could not parse, so every
command that falls back to the selection (cell.set, view.freezePanes,
formulas.trace*, insert.* and others) silently used the active cell instead:
set_cell with cell "ZZZZ0" wrote A1 and reported success. It also dropped a
sheet prefix, so "Other!B2" was applied to the active sheet.
cell_param now returns Result<Option<CellRef>>. An absent or null value is still
None, so the selection fallback is unchanged; an unparsable or non-string value
is an error; a sheet prefix naming a sheet other than the one the command works
on is an error.
cell_param_on is for the commands that resolve their own sheet (cell.get, links,
comments, notes, checkboxes, chart placement, paste): they accept a prefix naming
that sheet. cell.set resolves its own target sheet and still writes to another
existing sheet.
cell.set treated a missing input as an empty string, so a call that forgot the
input (or sent it under the wrong key) cleared the cell and reported success.
It now returns an error when neither `input` nor `value` is given. An explicit
empty string still clears the cell.
Add static pictures inside cells with XLSX roundtrip
Rebased onto main (conflicts with #83/#98 and main resolved by maintainer); the contributor's branch does not allow maintainer edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implements the two slicer commands from the parity catalog as first-class
floating objects that filter a table column.
- model: Slicer struct on Sheet (#[serde(default)]), included in
Workbook::next_object_id; slicer anchors use AnchorMode::MoveOnly.
- engine: cmd/slicer.rs with insert.slicer, table.insertSlicer, slicer.toggle
(single + multi), slicer.selectAll, slicer.clear, slicer.delete. Selection is
written as a FilterCriterion::Values into the sheet AutoFilter and applied via
the existing apply_filter. object.move / object.delete handle "slicer".
- ui-egui: slicers rendered, hit-tested, selected and dragged like charts; value
tiles toggle on click (Ctrl/Cmd = multi), funnel button clears. Ribbon buttons
under Insert > Filters and Table Design > Tools.
- tests: slicer_filters_table; the every_command_survives_empty_params fuzz
covers the new commands.
Native-only for now; .xlsx slicer-cache round-trip is a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#101/#100 added tests that name temp dirs with SystemTime::now(); #125's
disallowed-methods lint (wasm has no clock) landed alongside them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-anchor loop zips objects with their measured corners instead of indexing; a sort moves
only objects anchored inside the sorted block on the other axis too (a picture at Z3 stays
put when A1:B4 is sorted). insert.picture defaults to Move but don't size (Excel's
editAs="oneCell", which the writer emits for it), and object.setAnchorMode errors on an
unknown or empty kind. Tests for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edit.copy and edit.cut build and return the HTML table only with
{"html": true}; the UI asks for it when the host can publish HTML, so
MCP and control-channel responses are not bloated by up to 4 MB. Tests
cover the default, html:false and html:true.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every legacy .xls/.doc is a CFB/OLE2 file, so the signature alone isn't encryption. Walk the
compound file's directory (bounds-checked, bounded loops; raw UTF-16 scan when the structure
is unreadable) and report Encrypted only for an EncryptedPackage/EncryptionInfo stream;
other compound files are LegacyBinary with a "save as .xlsx" message. Tests build CFB bytes
in code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shape.setText refuses edits on a protected sheet (drawing objects are
locked; there is no edit-objects allowance yet) and text longer than
32,767 characters, like a cell. The editor doesn't open on a protected
sheet and its TextEdit carries the same character limit. Tests for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blank cell and row repeats that run past 16384 columns / 1048576 rows
(padding from writers with larger sheets) are clipped instead of
rejecting the file; any data or merge past the edge is still an error.
convert() builds its Cell with ..Cell::value(value) so new Cell fields
take their defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smooth_run gives every segment the same number of sub-segments (cap / n, 1..100) so long
series are smoothed end to end within the point cap, and clamps the Catmull-Rom curve to
the plot box. List "smooth" in chart.addElement's params doc, drop the smooth_demo
example, and use the test sheet's own name in series refs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the shared integration points for both formats: combined sniff,
file.open spec, file filters, MCP description, docs. open_bytes now lets
sniffed content (ODS or XLSB) win over the extension.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clip a file chart's series block to the sheet's used range and refuse blocks over the
insert.chart cell cap, so whole-column references can't build ~1M series.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>