engine: pageLayout.printTitles only touches the axes it is given

print_titles read both rows and cols as Options and assigned both
unconditionally, so {"rows":"2:2"} set title_cols = None and
{"cols":"A:A"} set title_rows = None: the repeated rows or columns
disappeared from every printed page with no message, and there was no
way to set or clear just one axis. The sibling pageLayout.pageSetup
already distinguishes "key absent" from "key present but empty"; read
both axes the same way — absent leaves the axis alone, empty/null clears
it, anything else that is not a range is an error rather than a silent
wipe.

Regression test: print_titles_keeps_the_axis_it_is_not_given — on main
the rows-only call clears the columns; with the fix each axis is touched
only when named, an explicit empty value still clears, and a bad range
errors.
This commit is contained in:
midasmatterhorn
2026-10-15 17:18:19 +03:00
parent e6b6b34970
commit 18fecf8e8c
2 changed files with 49 additions and 4 deletions
+22 -4
View File
@@ -232,13 +232,31 @@ fn print_area(s: &mut Session, p: &Json) -> Result<Json> {
}
fn print_titles(s: &mut Session, p: &Json) -> Result<Json> {
let rows = str_param(p, "rows").and_then(RangeRef::parse).map(|r| (r.start.row, r.end.row));
let cols = str_param(p, "cols").and_then(RangeRef::parse).map(|r| (r.start.col, r.end.col));
// An absent key leaves that axis alone, and an empty or null value clears it — the same
// reading `pageLayout.pageSetup` gives `titleRows`/`titleCols`. Assigning `None` for a
// key that was never sent used to wipe the other axis.
let axis = |key: &str, is_cols: bool| -> Result<Option<Option<(u32, u32)>>> {
match p.get(key) {
None => Ok(None),
Some(Json::Null) => Ok(Some(None)),
Some(Json::String(t)) if t.trim().is_empty() => Ok(Some(None)),
Some(Json::String(t)) => match RangeRef::parse(t) {
Some(r) => Ok(Some(Some(if is_cols { (r.start.col, r.end.col) } else { (r.start.row, r.end.row) }))),
None => Err(bad("pageLayout.printTitles", format!("`{key}` is not a range: `{t}`"))),
},
Some(_) => Err(bad("pageLayout.printTitles", format!("`{key}` must be text like \"1:2\" or \"A:A\""))),
}
};
let (rows, cols) = (axis("rows", false)?, axis("cols", true)?);
edit(s, |cx| {
let si = cx.sheet_index();
let pr = &mut cx.sheet_mut(si)?.print;
pr.title_rows = rows;
pr.title_cols = cols;
if let Some(t) = rows {
pr.title_rows = t;
}
if let Some(t) = cols {
pr.title_cols = t;
}
Ok(Json::Null)
})
}
+27
View File
@@ -2925,6 +2925,33 @@ fn notes_comments_and_links_survive_a_json_save() {
assert_eq!(after.cells, before.cells);
}
/// Page Layout ▸ Print Titles: naming one axis must not clear the other. `print_titles` read
/// both `rows` and `cols` into `Option`s and assigned both unconditionally, so
/// `{"rows":"2:2"}` silently dropped the repeated columns from every printed page.
#[test]
fn print_titles_keeps_the_axis_it_is_not_given() {
let mut s = s();
s.execute("pageLayout.printTitles", json!({"rows": "1:1", "cols": "A:A"})).unwrap();
let titles = |s: &Session| {
let d = s.doc().unwrap();
let sh = d.wb.active().unwrap();
(sh.print.title_rows, sh.print.title_cols)
};
assert_eq!(titles(&s), (Some((0, 0)), Some((0, 0))));
// Only rows: the columns stay.
s.execute("pageLayout.printTitles", json!({"rows": "2:2"})).unwrap();
assert_eq!(titles(&s), (Some((1, 1)), Some((0, 0))), "rows alone must not clear the columns");
// Only columns: the rows stay.
s.execute("pageLayout.printTitles", json!({"cols": "B:B"})).unwrap();
assert_eq!(titles(&s), (Some((1, 1)), Some((1, 1))), "columns alone must not clear the rows");
// An empty value clears that axis on purpose.
s.execute("pageLayout.printTitles", json!({"cols": ""})).unwrap();
assert_eq!(titles(&s), (Some((1, 1)), None));
// A bad range is an error, not a silent wipe.
assert!(s.execute("pageLayout.printTitles", json!({"rows": "not a range"})).is_err());
assert_eq!(titles(&s), (Some((1, 1)), None));
}
/// Insert › Link sorts a target as Excel's dialog does: a sheet reference, a defined name or `#…` is
/// a place in this document, anything else an existing file or web page. A file saved as a
/// `location` opens nothing in Excel (#800).