Skip to content

Commit 54ce307

Browse files
billdenneyclaude
andcommitted
Group the sheet view settings into xl_sheet_view()
xl_sheet() had grown to 23 arguments, nine of them the tab and opening-view settings added in Phase 4. xl_page_setup() already established the pattern for this kind of cluster, so the view settings now follow it: xl_sheet(view = xl_sheet_view(...)). The signature drops to 15 arguments. freeze deliberately stays on xl_sheet(). It is the single most common worksheet option -- keeping the header row visible -- and making the common case wordier in order to tidy the rare ones would be a poor trade. split, its rarely-used divider variant, does move, and the two remain mutually exclusive with an error that now names both spellings. Done before adding data validation, so that argument lands in a function that is not already overloaded. Nothing here has been released, so no user code breaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1b8295b commit 54ce307

23 files changed

Lines changed: 309 additions & 182 deletions

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ S3method(print,xl_properties)
1515
S3method(print,xl_rich_run)
1616
S3method(print,xl_rich_string)
1717
S3method(print,xl_sheet)
18+
S3method(print,xl_sheet_view)
1819
S3method(print,xl_workbook)
1920
S3method(rep,xl_cell_general)
2021
export(is_xl_comment)
@@ -43,6 +44,7 @@ export(xl_rich_run)
4344
export(xl_rich_string)
4445
export(xl_row_spec)
4546
export(xl_sheet)
47+
export(xl_sheet_view)
4648
export(xl_workbook)
4749
useDynLib(writexl,C_lxw_version)
4850
useDynLib(writexl,C_set_tempdir)

R/sheet_visibility.R

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121

2222
# One sheet's visibility settings, with absent ones as NA.
2323
.sheet_view_flags <- function(el) {
24-
if (!inherits(el, "xl_sheet"))
24+
vw <- .sheet_view_of(el)
25+
if (is.null(vw))
2526
return(list(active = NA, selected = NA, visible = NA, first_tab = NA))
26-
list(active = el$active,
27-
selected = el$selected,
28-
visible = el$visible,
29-
first_tab = el$first_tab)
27+
list(active = vw$active,
28+
selected = vw$selected,
29+
visible = vw$visible,
30+
first_tab = vw$first_tab)
3031
}
3132

3233
# A readable name for a sheet in an error message.

R/xl_sheet.R

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -141,47 +141,9 @@ xl_row_spec <- function(rows, height = NA, hidden = NA, level = NA,
141141
#' per-comment `author` overrides it).
142142
#' @param show_comments If `TRUE`, all comments on the sheet are initially
143143
#' shown (individual comments can still be forced via `xl_comment(visible=)`).
144-
#' @param active Logical; make this the tab Excel opens on. At most one sheet
145-
#' in a workbook may be active.
146-
#' @param selected Logical; include this tab in the selected group. The active
147-
#' sheet is always selected.
148-
#' @param visible Logical; `FALSE` hides the sheet's tab. A hidden sheet cannot
149-
#' be active or selected, the first sheet cannot be hidden unless another is
150-
#' made active, and at least one sheet must stay visible or Excel will not
151-
#' open the file.
152-
#' @param first_tab Logical; make this the leftmost visible tab in the tab
153-
#' strip. This is independent of which sheet is active.
154-
#' @param hide_zero Logical; display zero values as blank cells.
155-
#' @param right_to_left Logical; order the columns right to left, for a sheet in
156-
#' a right-to-left language.
157-
#' @param selection The cell or range selected when the sheet opens, as an Excel
158-
#' reference (`"B2"`, `"B2:D10"`) or a `list(rows = , cols = )` spec.
159-
#'
160-
#' Excel also uses the order of a selection's corners to mark which cell in it
161-
#' is active; writexl does not expose that, because ranges are normalised by
162-
#' the shared range parser, which rejects an inverted range.
163-
#' @param top_left The cell scrolled to the top-left of the window when the
164-
#' sheet opens, as an Excel reference such as `"A5"`.
165-
#' @param merge One [xl_merge()], or a list of them, merging rectangles of cells
166-
#' into single cells. Merges are applied after the sheet's rows are written,
167-
#' so a merge over cells the data frame filled keeps only the merged text ---
168-
#' as merging in Excel does. Any merge turns off the memory-efficient
169-
#' row-streaming mode, since it writes back over rows already emitted.
170-
#' @param split Split the sheet into scrollable panes with a visible, movable
171-
#' divider, given as the cell reference the split sits above and to the left
172-
#' of --- `"B3"` splits above row 3 and left of column B. Mutually exclusive
173-
#' with `freeze`, which does the same thing without the divider.
174-
#'
175-
#' libxlsxwriter positions a split by distance, in row-height and
176-
#' column-width units, not by row and column number. writexl converts the
177-
#' cell reference using the sheet's actual row heights and column widths, so
178-
#' the split lands where you asked even after resizing. Pass
179-
#' `list(vertical = , horizontal = )` to give those units directly.
180-
#'
181-
#' Note that libxlsxwriter derives the pane's scroll anchor back from that
182-
#' distance assuming default row heights, so on a sheet with resized rows or
183-
#' columns the divider is placed correctly but the anchor cell may be a row or
184-
#' two out.
144+
#' @param view An [xl_sheet_view()] describing the sheet's tab state and
145+
#' opening view (active/selected/hidden tab, selection, scroll position,
146+
#' zero display, direction, split panes).
185147
#' @param page An [xl_page_setup()] describing how the sheet prints
186148
#' (orientation, paper size, margins, scaling, header and footer). Affects
187149
#' printing only, never the cell data.
@@ -203,10 +165,7 @@ xl_sheet <- function(data, cols = NULL, rows = NULL, freeze = NULL,
203165
default_row_height = NA, auto_colwidth = FALSE,
204166
autofilter = FALSE, protect = FALSE,
205167
comment_author = NA, show_comments = FALSE,
206-
page = NULL, active = NA, selected = NA, visible = NA,
207-
first_tab = NA, hide_zero = NA, right_to_left = NA,
208-
selection = NULL, top_left = NULL, split = NULL,
209-
merge = NULL) {
168+
page = NULL, view = NULL, merge = NULL) {
210169
if (!is.data.frame(data))
211170
stop("`data` must be a data frame", call. = FALSE)
212171
if (!is.logical(auto_colwidth) || length(auto_colwidth) != 1L || is.na(auto_colwidth))
@@ -234,15 +193,7 @@ xl_sheet <- function(data, cols = NULL, rows = NULL, freeze = NULL,
234193
comment_author = comment_author,
235194
show_comments = show_comments,
236195
page = page,
237-
active = .val_flag(active, "active"),
238-
selected = .val_flag(selected, "selected"),
239-
visible = .val_flag(visible, "visible"),
240-
first_tab = .val_flag(first_tab, "first_tab"),
241-
hide_zero = .val_flag(hide_zero, "hide_zero"),
242-
right_to_left = .val_flag(right_to_left, "right_to_left"),
243-
selection = selection,
244-
top_left = top_left,
245-
split = split,
196+
view = view,
246197
merge = merge
247198
),
248199
class = "xl_sheet"
@@ -472,15 +423,16 @@ print.xl_sheet <- function(x, ...) {
472423
overlay <- c(overlay, .as_overlay_list(el$overlay))
473424
protect <- .resolve_protect(el$protect)
474425
page_payload <- .page_setup_payload(el$page, df, header_offset)
426+
vw <- .sheet_view_of(el)
475427
for (k in c("active", "selected", "visible", "first_tab", "hide_zero",
476428
"right_to_left"))
477-
if (!is.null(el[[k]])) view[[k]] <- as.integer(isTRUE(el[[k]]))
478-
if (!is.null(el$selection))
479-
view$selection <- .xl_resolve_range(el$selection, arg = "selection",
429+
if (!is.null(vw[[k]])) view[[k]] <- as.integer(isTRUE(vw[[k]]))
430+
if (!is.null(vw$selection))
431+
view$selection <- .xl_resolve_range(vw$selection, arg = "selection",
480432
df = df, header_offset = header_offset,
481433
allow_cell = TRUE)
482-
if (!is.null(el$top_left))
483-
view$top_left <- .xl_resolve_range(el$top_left, arg = "top_left",
434+
if (!is.null(vw$top_left))
435+
view$top_left <- .xl_resolve_range(vw$top_left, arg = "top_left",
484436
df = df, header_offset = header_offset,
485437
allow_cell = TRUE)[1:2]
486438
comment_author <- el$comment_author
@@ -498,11 +450,13 @@ print.xl_sheet <- function(x, ...) {
498450
# The split is resolved last, because converting a cell reference into
499451
# libxlsxwriter's units needs the sheet's final row heights and column widths
500452
# (including any set by auto_colwidth just above).
501-
if (inherits(el, "xl_sheet") && !is.null(el$split)) {
453+
vw_split <- .sheet_view_of(el)$split
454+
if (!is.null(vw_split)) {
502455
if (!is.null(el$freeze) && !(length(el$freeze) == 1L && is.na(el$freeze)))
503-
stop("`split` and `freeze` cannot both be set: Excel supports frozen ",
504-
"panes or a split, not both", call. = FALSE)
505-
view$split <- .resolve_split(el$split, df, header_offset, props,
456+
stop("`xl_sheet_view(split =)` and `xl_sheet(freeze =)` cannot both be ",
457+
"set: Excel supports frozen panes or a split, not both",
458+
call. = FALSE)
459+
view$split <- .resolve_split(vw_split, df, header_offset, props,
506460
default_row_height, row_row, row_height,
507461
col_width)
508462
}

R/xl_sheet_view.R

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# =============================================================================
2+
# Sheet view: the tab strip and what the user sees when the sheet opens
3+
# =============================================================================
4+
#
5+
# Grouped into an object for the same reason xl_page_setup() is: these are a
6+
# cluster of rarely-used settings that would otherwise widen xl_sheet()'s
7+
# signature past the point of being readable.
8+
#
9+
# `freeze` deliberately stays on xl_sheet() rather than moving here. It is the
10+
# single most common worksheet option -- keeping the header row visible -- and
11+
# making the common case wordier to tidy the rare ones would be a poor trade.
12+
# `split`, its rarely-used divider variant, does live here; the two remain
13+
# mutually exclusive and the check spans both.
14+
# -----------------------------------------------------------------------------
15+
16+
#' How a worksheet appears when it opens
17+
#'
18+
#' @description
19+
#' `xl_sheet_view()` collects a worksheet's tab state and opening view: which
20+
#' tab is active, selected or hidden, where the sheet is scrolled and selected,
21+
#' and a few display options. Pass it as `xl_sheet(view = )`.
22+
#'
23+
#' None of these affect the cell data.
24+
#'
25+
#' @param active Logical; make this the tab Excel opens on. At most one sheet
26+
#' in a workbook may be active.
27+
#' @param selected Logical; include this tab in the selected group. The active
28+
#' sheet is always selected.
29+
#' @param visible Logical; `FALSE` hides the sheet's tab. A hidden sheet cannot
30+
#' be active or selected, the first sheet cannot be hidden unless another is
31+
#' made active, and at least one sheet must stay visible or Excel will not
32+
#' open the file. All four rules are checked before writing.
33+
#' @param first_tab Logical; make this the leftmost visible tab in the tab
34+
#' strip. Independent of which sheet is active.
35+
#' @param selection The cell or range selected when the sheet opens, as an Excel
36+
#' reference (`"B2"`, `"B2:D10"`) or a `list(rows = , cols = )` spec.
37+
#'
38+
#' Excel also uses the order of a selection's corners to mark which cell in it
39+
#' is active; writexl does not expose that, because ranges are normalised by
40+
#' the shared range parser, which rejects an inverted range.
41+
#' @param top_left The cell scrolled to the top-left of the window when the
42+
#' sheet opens, as an Excel reference such as `"A5"`.
43+
#' @param hide_zero Logical; display zero values as blank cells.
44+
#' @param right_to_left Logical; order the columns right to left, for a sheet in
45+
#' a right-to-left language.
46+
#' @param split Split the sheet into scrollable panes with a visible, movable
47+
#' divider, given as the cell reference the split sits above and to the left
48+
#' of --- `"B3"` splits above row 3 and left of column B. Mutually exclusive
49+
#' with `xl_sheet(freeze = )`, which does the same thing without the divider.
50+
#'
51+
#' libxlsxwriter positions a split by distance, in row-height and column-width
52+
#' units, not by row and column number. writexl converts the cell reference
53+
#' using the sheet's actual row heights and column widths, so the split lands
54+
#' where you asked even after resizing. Pass
55+
#' `list(vertical = , horizontal = )` to give those units directly.
56+
#'
57+
#' Note that libxlsxwriter derives the pane's scroll anchor back from that
58+
#' distance assuming default row heights, so on a sheet with resized rows or
59+
#' columns the divider is placed correctly but the anchor cell may be a row or
60+
#' two out.
61+
#' @return An `xl_sheet_view` object.
62+
#' @family writexl
63+
#' @seealso [xl_sheet], [xl_page_setup]
64+
#' @export
65+
#' @examples
66+
#' xl_sheet_view(active = TRUE, selection = "B2")
67+
#' xl_sheet_view(visible = FALSE)
68+
#'
69+
#' df <- data.frame(x = 1:3)
70+
#' tmp <- write_xlsx(list(
71+
#' Summary = xl_sheet(df, view = xl_sheet_view(active = TRUE)),
72+
#' Working = xl_sheet(df, view = xl_sheet_view(visible = FALSE))
73+
#' ))
74+
xl_sheet_view <- function(active = NA, selected = NA, visible = NA,
75+
first_tab = NA, selection = NULL, top_left = NULL,
76+
hide_zero = NA, right_to_left = NA, split = NULL) {
77+
structure(
78+
list(active = .val_flag(active, "active"),
79+
selected = .val_flag(selected, "selected"),
80+
visible = .val_flag(visible, "visible"),
81+
first_tab = .val_flag(first_tab, "first_tab"),
82+
selection = selection,
83+
top_left = top_left,
84+
hide_zero = .val_flag(hide_zero, "hide_zero"),
85+
right_to_left = .val_flag(right_to_left, "right_to_left"),
86+
split = split),
87+
class = "xl_sheet_view"
88+
)
89+
}
90+
91+
#' @export
92+
print.xl_sheet_view <- function(x, ...) {
93+
p <- .drop_null(unclass(x))
94+
cat(sprintf("<xl_sheet_view: %d setting%s>\n", length(p),
95+
if (length(p) == 1L) "" else "s"))
96+
for (k in names(p))
97+
cat(sprintf(" %s: %s\n", k, paste(format(unlist(p[[k]])), collapse = ", ")))
98+
invisible(x)
99+
}
100+
101+
# The view settings of a sheet element, with absent ones as NULL. Accepts a
102+
# sheet whose `view` is unset so callers need not special-case it.
103+
.sheet_view_of <- function(el) {
104+
if (!inherits(el, "xl_sheet") || is.null(el$view)) return(NULL)
105+
if (!inherits(el$view, "xl_sheet_view"))
106+
stop("`view` must be an xl_sheet_view object", call. = FALSE)
107+
unclass(el$view)
108+
}

man/is_xl_comment.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/is_xl_format.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/xl_cell_general.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/xl_color.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/xl_colrow_spec.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/xl_comment.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)