From efc8171609c7c2351868cf41f1da613b07e38ae3 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 16 Oct 2026 07:03:58 +0200 Subject: [PATCH] Keyboard: Ctrl+6 hides and shows objects, saved as workbookPr showObjects --- crates/engine/src/cmd/view.rs | 11 +++ crates/model/src/lib.rs | 2 +- crates/model/src/workbook.rs | 18 ++++ crates/ui-egui/src/chartview.rs | 15 +++- crates/ui-egui/src/grid.rs | 6 +- crates/ui-egui/src/keymap.rs | 2 + crates/ui-egui/tests/keyboard_shortcuts.rs | 14 ++++ crates/ui-egui/tests/show_objects.rs | 96 ++++++++++++++++++++++ crates/xlsx/src/read.rs | 15 ++-- crates/xlsx/src/tests/roundtrip.rs | 45 ++++++++++ crates/xlsx/src/write.rs | 13 ++- docs/file-format-parity.md | 9 +- docs/ui-parity.md | 6 +- 13 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 crates/ui-egui/tests/show_objects.rs diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index edcacf57d..d21c2cab9 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -26,6 +26,7 @@ pub fn specs() -> Vec { cmd!("view.gridlines", "Gridlines", ["View", "Show"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "gridlines")), cmd!("view.headings", "Headings", ["View", "Show"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "headings")), cmd!("view.showZeros", "Show Zeros", [], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "zeros")), + cmd!("view.showObjects", "Show Objects", [], Some("Ctrl+6"), "{on?}", has_doc, show_objects), cmd!("view.rightToLeft", "Sheet Right-to-Left", ["Page Layout", "Sheet Options"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "rtl")), cmd!("view.zoom", "Zoom", ["View", "Zoom"], None, "{percent: 10..400}", has_doc, zoom), cmd!("view.zoom100", "100%", ["View", "Zoom"], None, "{}", has_doc, |s, _| zoom(s, &json!({"percent": 100}))), @@ -152,6 +153,16 @@ fn sheet_flag(s: &mut Session, p: &Json, which: &str) -> Result { }) } +/// Ctrl+6: hides every object, or shows them again. Placeholders (from a file) count as shown. +fn show_objects(s: &mut Session, p: &Json) -> Result { + use gridcraft_model::ShowObjects; + edit(s, |cx| { + let on = bool_param(p, "on").unwrap_or(cx.wb.show_objects == ShowObjects::None); + cx.wb.show_objects = if on { ShowObjects::All } else { ShowObjects::None }; + Ok(json!({"on": on})) + }) +} + fn zoom(s: &mut Session, p: &Json) -> Result { let z = f64_param(p, "percent").ok_or_else(|| bad("view.zoom", "missing `percent`"))?.clamp(10.0, 400.0) as u16; let d = s.doc_mut()?; diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index a76f30fc3..23fa4f50b 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -25,7 +25,7 @@ pub use style::{ Alignment, BorderLine, BorderStyle, Borders, Color, Fill, Font, HAlign, NumFmt, PatternType, Style, StyleId, StyleTable, Theme, Underline, VAlign, VertAlign, }; -pub use workbook::{CalcMode, CalcSettings, DefinedName, DocProps, Workbook}; +pub use workbook::{CalcMode, CalcSettings, DefinedName, DocProps, ShowObjects, Workbook}; /// Font used for new workbooks. Only a name: renderers substitute an available open font /// (e.g. the metric-compatible Carlito) when it isn't installed. diff --git a/crates/model/src/workbook.rs b/crates/model/src/workbook.rs index 828ca692e..c9662bacd 100644 --- a/crates/model/src/workbook.rs +++ b/crates/model/src/workbook.rs @@ -18,6 +18,17 @@ pub enum CalcMode { Manual, } +/// Options › Advanced › "For objects, show" (``, ECMA-376 `ST_Objects`): +/// whether charts, pictures, shapes and slicers are drawn. Ctrl+6 switches between `All` and +/// `None`; `Placeholders` only comes from files and is kept as it is. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ShowObjects { + #[default] + All, + Placeholders, + None, +} + /// File › Options › Formulas › Calculation options, stored per workbook (``). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CalcSettings { @@ -93,6 +104,8 @@ pub struct Workbook { pub theme: Theme, pub date_system: DateSystem, pub calc: CalcSettings, + #[serde(default, skip_serializing_if = "is_all")] + pub show_objects: ShowObjects, pub active_sheet: usize, pub props: DocProps, /// Workbook structure protection. @@ -122,6 +135,10 @@ pub struct Workbook { pub source_dir: Option, } +fn is_all(s: &ShowObjects) -> bool { + *s == ShowObjects::All +} + impl Default for Workbook { fn default() -> Self { Workbook::new() @@ -138,6 +155,7 @@ impl Workbook { theme: Theme::default(), date_system: DateSystem::D1900, calc: CalcSettings::default(), + show_objects: ShowObjects::All, active_sheet: 0, props: DocProps::default(), protected_structure: false, diff --git a/crates/ui-egui/src/chartview.rs b/crates/ui-egui/src/chartview.rs index 180f15c21..f54e609cd 100644 --- a/crates/ui-egui/src/chartview.rs +++ b/crates/ui-egui/src/chartview.rs @@ -258,8 +258,17 @@ pub(crate) struct PaintedObject { pub rect: Rect, } +/// Ctrl+6 (Options › Advanced › "For objects, show: Nothing"): objects are neither drawn nor +/// clickable. Placeholders, which Excel no longer offers, show the objects. +pub(crate) fn objects_hidden(wb: &Workbook) -> bool { + wb.show_objects == gridcraft_engine::model::ShowObjects::None +} + pub(crate) fn paint_objects(app: &SheetApp, p: &Painter, geo: &Geo, wb: &Workbook, si: usize, sh: &Sheet) -> Vec { let mut painted = Vec::new(); + if objects_hidden(wb) { + return painted; + } let mut done = |start: egui::layers::ShapeIdx, rect: Rect| painted.push(PaintedObject { shapes: start.0..p.add(egui::Shape::Noop).0, rect }); for sp in &sh.shapes { let r = anchor_rect(geo, sh, &sp.anchor); @@ -472,7 +481,7 @@ pub enum SlicerAction { /// A click on a slicer's value tile or clear button, if any. A click elsewhere in a slicer returns /// `None` so the caller can treat it as a select/drag on the object. pub fn slicer_click(geo: &Geo, sh: &Sheet, wb: &Workbook, si: usize, p: Pos2) -> Option<(u32, SlicerAction)> { - if !geo.cells.contains(p) { + if !geo.cells.contains(p) || objects_hidden(wb) { return None; } for sl in sh.slicers.iter().rev() { @@ -506,7 +515,9 @@ fn selection_frame(p: &Painter, r: Rect) { /// The topmost object under `p`: (kind, id, rect). pub fn hit(app: &SheetApp, geo: &Geo, sh: &Sheet, p: Pos2) -> Option<(&'static str, u32, Rect)> { - let _ = app; + if app.session.active().is_some_and(|d| objects_hidden(&d.wb)) { + return None; + } for sl in sh.slicers.iter().rev() { let r = anchor_rect(geo, sh, &sl.anchor); if r.contains(p) && geo.cells.contains(p) { diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 00e0c18be..5cf95cadf 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -726,7 +726,11 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) { } else { paint_panes(app, &painter, &geo, &wb, si, sh, &sel, outline, &mut cf, &t) }; - // Objects above cells. + // Objects above cells. A hidden object can't stay selected: Delete or the Format pane would act + // on something the user can't see. + if crate::chartview::objects_hidden(&wb) { + app.selected_chart = None; + } let objects = crate::chartview::paint_objects(app, &painter.with_clip_rect(geo.cells), &geo, &wb, si, sh); paint_overlays(app, ui, &painter.with_clip_rect(geo.cells), &geo, &wb, si, sh, &sel); if sh.view == gridcraft_engine::model::SheetViewMode::PageBreakPreview { diff --git a/crates/ui-egui/src/keymap.rs b/crates/ui-egui/src/keymap.rs index 032db1a2d..358db5a5f 100644 --- a/crates/ui-egui/src/keymap.rs +++ b/crates/ui-egui/src/keymap.rs @@ -92,6 +92,8 @@ pub fn lookup(key: Key, physical: Option, m: Modifiers, mac: bool) -> Optio Num3 if ctrl && !mac => run("home.italic"), Num4 if ctrl && !mac => run("home.underline"), Num5 if ctrl && !mac => run("home.strikethrough"), + // Ctrl+6 hides and shows objects (Windows, and Control+6 in Excel for Mac). + Num6 if ctrl && !s => run("view.showObjects"), // Letters that differ between Windows and Mac. L if (cmd || ctrl) && s => run("data.filter"), L if cmd && mac => run("home.alignLeft"), diff --git a/crates/ui-egui/tests/keyboard_shortcuts.rs b/crates/ui-egui/tests/keyboard_shortcuts.rs index c0a1307b3..3e2829a66 100644 --- a/crates/ui-egui/tests/keyboard_shortcuts.rs +++ b/crates/ui-egui/tests/keyboard_shortcuts.rs @@ -214,3 +214,17 @@ fn alt_down_only_opens_enabled_table_filter_buttons() { assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "B1"); } } + +#[test] +fn ctrl_6_is_show_objects_and_nothing_else() { + let show = Some(Shortcut::Run("view.showObjects", json!({}))); + let mac_control = Modifiers::CTRL; + assert_eq!(lookup(Key::Num6, Some(Key::Num6), CTRL, false), show); + assert_eq!(lookup(Key::Num6, Some(Key::Num6), mac_control, true), show); + // Its neighbours keep theirs: Ctrl+Shift+6 (Ctrl+^) is Scientific, Cmd+6 on a Mac is free. + assert_eq!(lookup(Key::Num6, Some(Key::Num6), CTRL_SHIFT, false), Some(Shortcut::Run("home.numberFormat", json!({"code": "0.00E+00"})))); + assert_eq!(lookup(Key::Num6, Some(Key::Num6), Modifiers::MAC_CMD, true), None); + // No other command claims Ctrl+6. + let claims: Vec<&str> = Session::new().commands().iter().filter(|c| c.shortcut == Some("Ctrl+6")).map(|c| c.id).collect(); + assert_eq!(claims, ["view.showObjects"]); +} diff --git a/crates/ui-egui/tests/show_objects.rs b/crates/ui-egui/tests/show_objects.rs new file mode 100644 index 000000000..9dcac7ce8 --- /dev/null +++ b/crates/ui-egui/tests/show_objects.rs @@ -0,0 +1,96 @@ +//! Ctrl+6 hides and shows every object (Options › Advanced › "For objects, show"), through real input. + +use egui::{Color32, Event, Key, Modifiers, PointerButton, epaint::Shape, vec2}; +use gridcraft_engine::Session; +use gridcraft_engine::core::CellRef; +use gridcraft_engine::model::ShowObjects; +use gridcraft_ui_egui::{SheetApp, grid::Geo}; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +const FILL: Color32 = Color32::from_rgb(0x12, 0x34, 0x56); +/// Ctrl on Windows and Linux sets both `ctrl` and `command`. +const CTRL: Modifiers = Modifiers { alt: false, ctrl: true, shift: false, mac_cmd: false, command: true }; + +/// A rectangle at D3, 144 × 96 points, filled with a colour nothing else on screen uses. +fn fixture() -> (Harness, u32) { + let mut session = Session::new(); + session.new_workbook(); + let shape = session.execute("insert.shape", json!({"kind": "rectangle", "at": "D3", "fill": "123456"})).unwrap(); + let id = shape["shape"].as_u64().unwrap() as u32; + session.execute("selection.set", json!({"range": "A1"})).unwrap(); + let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + (h, id) +} + +fn ctrl_6(h: &mut Harness) { + h.input_mut().events.push(Event::Key { key: Key::Num6, physical_key: Some(Key::Num6), pressed: true, repeat: false, modifiers: CTRL }); + h.input_mut().events.push(Event::Key { key: Key::Num6, physical_key: Some(Key::Num6), pressed: false, repeat: false, modifiers: CTRL }); + h.run_steps(2); +} + +fn show_objects(h: &Harness) -> ShowObjects { + h.state().session.active().unwrap().wb.show_objects +} + +/// Whether the last frame painted the rectangle's fill. +fn painted(h: &Harness) -> bool { + h.output().shapes.iter().any(|c| matches!(&c.shape, Shape::Rect(r) if r.fill == FILL)) +} + +/// Inside the rectangle, in the cell E4 (D3 is 64 points wide and 20 high at 100%). +fn inside(h: &Harness) -> egui::Pos2 { + let app = h.state(); + let sh = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sh, app.grid.rect.unwrap(), app.view().scroll); + geo.cell_rect(sh, CellRef::parse("E4").unwrap()).center() +} + +fn click(h: &mut Harness, pos: egui::Pos2) { + h.input_mut().events.push(Event::PointerMoved(pos)); + h.run_steps(2); + for pressed in [true, false] { + h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + h.run_steps(2); + } +} + +fn active_cell(h: &Harness) -> CellRef { + h.state().session.active().unwrap().selection.active +} + +#[test] +fn ctrl_6_hides_objects_and_a_second_ctrl_6_shows_them() { + let (mut h, _) = fixture(); + assert_eq!(show_objects(&h), ShowObjects::All); + assert!(painted(&h), "the rectangle is drawn to begin with"); + ctrl_6(&mut h); + assert_eq!(show_objects(&h), ShowObjects::None); + assert!(!painted(&h), "hidden objects are not drawn"); + ctrl_6(&mut h); + assert_eq!(show_objects(&h), ShowObjects::All); + assert!(painted(&h), "a second Ctrl+6 draws them again"); +} + +#[test] +fn hidden_objects_do_not_take_clicks() { + let (mut h, id) = fixture(); + let at = inside(&h); + click(&mut h, at); + assert_eq!(h.state().selected_chart, Some(id), "the click lands on the rectangle while it shows"); + assert_eq!(active_cell(&h), CellRef::parse("A1").unwrap()); + ctrl_6(&mut h); + assert_eq!(h.state().selected_chart, None, "hiding objects drops the selection on one"); + click(&mut h, at); + assert_eq!(h.state().selected_chart, None); + assert_eq!(active_cell(&h), CellRef::parse("E4").unwrap(), "the click selects the cell under the hidden rectangle"); +} diff --git a/crates/xlsx/src/read.rs b/crates/xlsx/src/read.rs index 1a479081c..4dfb18a28 100644 --- a/crates/xlsx/src/read.rs +++ b/crates/xlsx/src/read.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use gridcraft_core::{DateSystem, RangeRef}; -use gridcraft_model::{CalcMode, CellPicture, DefinedName, Sheet, Style, StyleId, StyleTable, Visibility, Workbook}; +use gridcraft_model::{CalcMode, CellPicture, DefinedName, Sheet, ShowObjects, Style, StyleId, StyleTable, Visibility, Workbook}; use crate::package::{Package, Rel}; use crate::styles::{StylesIn, read_styles}; @@ -169,10 +169,15 @@ pub fn read_xlsx(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> { } // Workbook properties. - if let Some(p) = wb_xml.child("workbookPr") - && p.flag("date1904", false) - { - cx.date_system = DateSystem::D1904; + if let Some(p) = wb_xml.child("workbookPr") { + if p.flag("date1904", false) { + cx.date_system = DateSystem::D1904; + } + wb.show_objects = match p.attr("showObjects") { + Some("none") => ShowObjects::None, + Some("placeholders") => ShowObjects::Placeholders, + _ => ShowObjects::All, + }; } wb.date_system = cx.date_system; if let Some(c) = wb_xml.child("calcPr") { diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs index 901802905..945c3da53 100644 --- a/crates/xlsx/src/tests/roundtrip.rs +++ b/crates/xlsx/src/tests/roundtrip.rs @@ -679,6 +679,51 @@ fn iterative_calculation_settings_roundtrip() { } } +#[test] +fn show_objects_roundtrip() { + let saved = |wb: &Workbook| { + let bytes = write_xlsx(wb).unwrap(); + let mut pkg = Package::open(&bytes).unwrap(); + let book = pkg.read_xml("xl/workbook.xml").unwrap().unwrap(); + let attr = book.child("workbookPr").and_then(|p| p.attr("showObjects")).map(str::to_string); + (attr, read_xlsx(&bytes).unwrap().0.show_objects) + }; + // Default: nothing written, All read back. + let mut wb = Workbook::new(); + assert_eq!(saved(&wb), (None, ShowObjects::All)); + // Beside date1904, which shares the element. + wb.date_system = DateSystem::D1904; + for (show, attr) in [(ShowObjects::None, "none"), (ShowObjects::Placeholders, "placeholders")] { + wb.show_objects = show; + assert_eq!(saved(&wb), (Some(attr.to_string()), show)); + assert_eq!(read_xlsx(&write_xlsx(&wb).unwrap()).unwrap().0.date_system, DateSystem::D1904); + } + // A file written elsewhere: the attribute among the others Excel writes, or absent. + let book = |pr: &str| { + let wbx = format!( + r#"{pr}"# + ); + let rels = r#""#; + let ws = r#""#; + let bytes = super::make_zip(&[ + ("[Content_Types].xml", super::CT.as_bytes()), + ("_rels/.rels", super::ROOT_RELS.as_bytes()), + ("xl/workbook.xml", wbx.as_bytes()), + ("xl/_rels/workbook.xml.rels", rels.as_bytes()), + ("xl/worksheets/sheet1.xml", ws.as_bytes()), + ]); + read_xlsx(&bytes).unwrap().0 + }; + let hidden = book(r#""#); + assert_eq!(hidden.show_objects, ShowObjects::None); + assert_eq!(saved(&hidden).0.as_deref(), Some("none")); + for pr in [r#""#, "", r#""#] { + let wb = book(pr); + assert_eq!(wb.show_objects, ShowObjects::All, "{pr}"); + assert_eq!(saved(&wb).0, None, "{pr}: a file without the attribute stays without it"); + } +} + #[test] fn empty_workbook_roundtrip() { let wb = Workbook::new(); diff --git a/crates/xlsx/src/write.rs b/crates/xlsx/src/write.rs index 06761f01d..01655f931 100644 --- a/crates/xlsx/src/write.rs +++ b/crates/xlsx/src/write.rs @@ -5,7 +5,7 @@ use std::fmt::Write as _; use gridcraft_core::RangeRef; use gridcraft_formula::quote_sheet; -use gridcraft_model::{CalcMode, CalcSettings, Style, Visibility, Workbook}; +use gridcraft_model::{CalcMode, CalcSettings, ShowObjects, Style, Visibility, Workbook}; use crate::package::ZipOut; use crate::xml::{esc, esc_attr}; @@ -315,7 +315,16 @@ pub fn write_xlsx(wb: &Workbook, kind: XlsxKind) -> Result, IoError> { let mut w = format!("{XML_DECL}"); w.push_str(&crate::passthrough::workbook_elements(wb, "fileVersion")); - w.push_str(if wb.date_system == gridcraft_core::DateSystem::D1904 { "" } else { "" }); + w.push_str(" {} + ShowObjects::Placeholders => w.push_str(" showObjects=\"placeholders\""), + ShowObjects::None => w.push_str(" showObjects=\"none\""), + } + if wb.date_system == gridcraft_core::DateSystem::D1904 { + w.push_str(" date1904=\"1\""); + } + w.push_str("/>"); if wb.protected_structure { let pw = wb.structure_password.as_ref().map(|h| crate::sheet_write::password_attrs(h, "workbook")).unwrap_or_default(); let _ = write!(w, ""); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 158149fb3..a6f9d2639 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** trivial (formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; previously trivial: XLSX chart sheets are kept on save (#727)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-16 · **Change:** trivial (XLSX keeps the workbook's show-objects setting, `workbookPr showObjects`; previously trivial: formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; previously trivial: XLSX chart sheets are kept on save (#727)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests. Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h** @@ -22,9 +22,9 @@ and repair-free opening remain unverified. | Format | Excel | GridCraft read | GridCraft write | Fidelity / notes | Tests | |---|---|---|---|---|---| -| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings. **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), form controls, ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | -| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, notes (with their shown/hidden state) and threaded comments with replies, resolved state and timestamps (`threadedComments` and `persons` parts, #300), hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings. **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | -| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, Allow Edit Ranges (`protectedRanges` with their password hashes, #307), calc settings. **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | +| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings, show objects (`workbookPr showObjects`). **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), form controls, ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | +| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, notes (with their shown/hidden state) and threaded comments with replies, resolved state and timestamps (`threadedComments` and `persons` parts, #300), hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings, show objects (`workbookPr showObjects`). **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | +| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, Allow Edit Ranges (`protectedRanges` with their password hashes, #307), calc settings, show objects (`workbookPr showObjects`). **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` | | `.xlsx` Strict Open XML | R/W | yes | no (writes transitional) | Excel also saves transitional by default | fixtures | | `.xlsm` Macro-Enabled Workbook | R/W | yes | yes, macros kept, not run | The VBA project is kept byte-for-byte and written back only into `.xlsm`/`.xltm` (#6); macros never run. Saved with the macro-enabled workbook content type | `roundtrip.rs` (`vba_project_roundtrip`, `package_kind_sets_workbook_content_type`), engine `io.rs` (`macros_are_saved_only_into_xlsm`) | | `.xltx` / `.xltm` templates | R/W | yes | partial | Saved with the template (and macro-enabled template) content type, chosen by extension; `.xltm` keeps a VBA project, `.xltx` drops it; "new from template" behaviour missing | `roundtrip.rs` (`package_kind_sets_workbook_content_type`), engine `io.rs` | @@ -85,6 +85,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | trivial | XLSX reads and writes the workbook's show-objects setting (`workbookPr showObjects`: all, placeholders, none), which Ctrl+6 switches; a file without it is saved without it | | 2026-10-14 | trivial | Formula parser: text longer than Excel's 8,192 characters, and postfix chains (`1%%%…`, `f(1)(1)…`) past the 256-level nesting limit, are parse errors (the cell keeps its formula text and cached value) instead of trees deep enough to overflow the stack when a hostile file is opened, saved or dropped | | 2026-10-12 | trivial | XLSX chart sheets are kept on save (#727) | | 2026-10-12 | trivial | CSV and TSV export is capped at 512 MiB and refuses a larger used range with an error (#528) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 22e221e69..8c0bd9e5a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Ctrl+6 hides and shows objects) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -103,6 +103,7 @@ otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, w | Ctrl+Shift+~ ! @ # $ % ^ | Control+Shift | General, Number, Time, Date, Currency, Percent, Scientific | **added** | | Ctrl+Shift+& / Ctrl+Shift+_ | Cmd+Option+0 / Cmd+Option+- (and Cmd+Option+arrows for one side) | outline border / remove borders | **added** | | Ctrl+2 / 3 / 4 / 5 | — | bold / italic / underline / strikethrough | **added** | +| Ctrl+6 | Control+6 | hide / show objects (charts, pictures, shapes, slicers), saved as `workbookPr showObjects` | **added** | | Ctrl+Shift+: | Cmd+; | current time | **added** | | Ctrl+' / Ctrl+Shift+" | Control | formula / value from the cell above | **added** | | Ctrl+L | Cmd+L is Align Left, Cmd+E Center, Control+E Flash Fill | create table | **added** | @@ -124,7 +125,7 @@ otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, w | F5, Shift+F5 (Mac), F7, Shift+F9, Ctrl+Alt+F9, F12, Ctrl+F4 | same | Go To, Find, Spelling, calculate sheet / all, Save As, close | **added** | | Shift+F10, Alt+Shift+F10, Ctrl+F1 | Shift+F10, Cmd+Option+R | context menu, Error Checking, ribbon | **added** | | F1, F3, F4 (repeat), F6 / Shift+F6, F8 / Shift+F8, F11, End mode | same | help, Paste Name, Repeat, pane cycling, extend/add mode, chart sheet | missing (no such feature yet) | -| Ctrl+6, Ctrl+8, Ctrl+Q, Ctrl+Shift+A, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | objects, outline symbols, Quick Analysis, argument names, Function Arguments, formula bar height, tooltips | missing | +| Ctrl+8, Ctrl+Q, Ctrl+Shift+A, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | outline symbols, Quick Analysis, argument names, Function Arguments, formula bar height, tooltips | missing | | Ctrl+Shift+Page Up / Down, Ctrl+Alt+5, Ctrl+Shift+F1, Ctrl+F5–F10, Alt+F8 / F11 / F12 | same | group sheets, shape cycling, full screen, window keys, macros, VBA, Power Query | missing | | — | Cmd+= (calculate), Cmd+T (references while editing; F4 works), Shift+F7, Cmd+Shift+L | Mac-only extras | missing (Cmd+= is egui's zoom key; no thesaurus or Modify Cell Style) | @@ -161,6 +162,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Ctrl+6 hides every object (charts, pictures, shapes, slicers) and shows them again; hidden objects are not drawn, take no clicks and drop their selection, and the setting is saved with the workbook (`workbookPr showObjects`) | | 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread | | 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) | | 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |