mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge PR #1626: Keyboard: Ctrl+6 hides and shows objects, saved as workbookPr showObjects
Conflicts: - docs/file-format-parity.md: header and the three .xlsx rows keep main's text plus the show-objects entry; both log rows kept - docs/ui-parity.md: header keeps main's text plus Ctrl+6; both log rows kept
This commit is contained in:
@@ -35,6 +35,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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}))),
|
||||
@@ -166,6 +167,16 @@ fn sheet_flag(s: &mut Session, p: &Json, which: &str) -> Result<Json> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<Json> {
|
||||
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<Json> {
|
||||
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()?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,6 +18,17 @@ pub enum CalcMode {
|
||||
Manual,
|
||||
}
|
||||
|
||||
/// Options › Advanced › "For objects, show" (`<workbookPr showObjects>`, 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 (`<calcPr>`).
|
||||
#[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.
|
||||
@@ -129,6 +142,10 @@ pub struct Workbook {
|
||||
pub source_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
fn is_all(s: &ShowObjects) -> bool {
|
||||
*s == ShowObjects::All
|
||||
}
|
||||
|
||||
impl Default for Workbook {
|
||||
fn default() -> Self {
|
||||
Workbook::new()
|
||||
@@ -145,6 +162,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,
|
||||
|
||||
@@ -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<PaintedObject> {
|
||||
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) {
|
||||
|
||||
@@ -745,7 +745,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 {
|
||||
|
||||
@@ -92,6 +92,8 @@ pub fn lookup(key: Key, physical: Option<Key>, 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"),
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
+10
-5
@@ -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") {
|
||||
|
||||
@@ -680,6 +680,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#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">{pr}<sheets><sheet name="A" sheetId="1" r:id="rId1"/></sheets></workbook>"#
|
||||
);
|
||||
let rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>"#;
|
||||
let ws = r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>"#;
|
||||
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#"<workbookPr showObjects="none" defaultThemeVersion="166925"/>"#);
|
||||
assert_eq!(hidden.show_objects, ShowObjects::None);
|
||||
assert_eq!(saved(&hidden).0.as_deref(), Some("none"));
|
||||
for pr in [r#"<workbookPr defaultThemeVersion="166925"/>"#, "", r#"<workbookPr showObjects="bogus"/>"#] {
|
||||
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();
|
||||
|
||||
@@ -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};
|
||||
@@ -329,7 +329,16 @@ pub fn write_xlsx(wb: &Workbook, kind: XlsxKind) -> Result<Vec<u8>, IoError> {
|
||||
|
||||
let mut w = format!("{XML_DECL}<workbook xmlns=\"{NS_MAIN}\" xmlns:r=\"{NS_REL}\">");
|
||||
w.push_str(&crate::passthrough::workbook_elements(wb, "fileVersion"));
|
||||
w.push_str(if wb.date_system == gridcraft_core::DateSystem::D1904 { "<workbookPr date1904=\"1\"/>" } else { "<workbookPr/>" });
|
||||
w.push_str("<workbookPr");
|
||||
match wb.show_objects {
|
||||
ShowObjects::All => {}
|
||||
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, "<workbookProtection{pw} lockStructure=\"1\"/>");
|
||||
|
||||
Reference in New Issue
Block a user