mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-06 02:21:03 +00:00
Merge PR #3237
This commit is contained in:
commit
502fac89fa
8 files changed
+184
-26
No files matched your search
@@ -194,7 +194,7 @@ pub(super) fn merged_edges(wb: &Workbook, si: usize, sh: &Sheet, geo: &Geo, area
|
||||
if sh.col_width(col) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (x0, x1) = (geo.x(sh, col), geo.x(sh, col.saturating_add(1)));
|
||||
let (x0, x1) = (geo.x(sh, col), geo.x_end(sh, col));
|
||||
let top = look(wb, si, sh, CellRef::new(m.start.row, col), cf).0.style.borders.top;
|
||||
let bottom = look(wb, si, sh, CellRef::new(m.end.row, col), cf).0.style.borders.bottom;
|
||||
out.push((top, [pos2(x0, rect.top()), pos2(x1, rect.top())]));
|
||||
@@ -205,7 +205,7 @@ pub(super) fn merged_edges(wb: &Workbook, si: usize, sh: &Sheet, geo: &Geo, area
|
||||
if sh.row_height(row) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (y0, y1) = (geo.y(sh, row), geo.y(sh, row.saturating_add(1)));
|
||||
let (y0, y1) = (geo.y(sh, row), geo.y_end(sh, row));
|
||||
let left = look(wb, si, sh, CellRef::new(row, m.start.col), cf).0.style.borders.left;
|
||||
let right = look(wb, si, sh, CellRef::new(row, m.end.col), cf).0.style.borders.right;
|
||||
out.push((left, [pos2(rect.left(), y0), pos2(rect.left(), y1)]));
|
||||
|
||||
@@ -158,11 +158,21 @@ impl Geo {
|
||||
self.cells.top() + self.frozen_h + (self.ri.start(row) as f32 - self.ri.start(self.fr) as f32 - self.scroll.y) * self.z
|
||||
}
|
||||
}
|
||||
/// Screen x of `col`'s right edge. Not `x(col + 1)`: across the freeze line (and between
|
||||
/// pages) the next column is laid out elsewhere (#2544).
|
||||
pub fn x_end(&self, sh: &Sheet, col: u32) -> f32 {
|
||||
self.x(sh, col) + sh.col_width(col) * self.z
|
||||
}
|
||||
/// Screen y of `row`'s bottom edge (see `x_end`).
|
||||
pub fn y_end(&self, sh: &Sheet, row: u32) -> f32 {
|
||||
self.y(sh, row) + sh.row_height(row) * self.z
|
||||
}
|
||||
/// Columns `a` and `b` are on the same side of the freeze line, so they scroll together.
|
||||
pub fn same_col_pane(&self, a: u32, b: u32) -> bool {
|
||||
(a < self.fc) == (b < self.fc)
|
||||
}
|
||||
pub fn cell_rect(&self, sh: &Sheet, c: CellRef) -> Rect {
|
||||
Rect::from_min_max(
|
||||
pos2(self.x(sh, c.col), self.y(sh, c.row)),
|
||||
pos2(self.x(sh, c.col) + sh.col_width(c.col) * self.z, self.y(sh, c.row) + sh.row_height(c.row) * self.z),
|
||||
)
|
||||
Rect::from_min_max(pos2(self.x(sh, c.col), self.y(sh, c.row)), pos2(self.x_end(sh, c.col), self.y_end(sh, c.row)))
|
||||
}
|
||||
pub fn range_rect(&self, sh: &Sheet, r: RangeRef) -> Rect {
|
||||
let a = self.cell_rect(sh, r.start);
|
||||
|
||||
@@ -902,7 +902,7 @@ fn freeze_boundary(geo: &Geo, sh: &Sheet, p: Pos2, cols: bool) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let c = geo.col_at(sh, p.x);
|
||||
let (l, r) = (geo.x(sh, c), geo.x(sh, c.saturating_add(1).min(MAX_COLS - 1)));
|
||||
let (l, r) = (geo.x(sh, c), geo.x_end(sh, c));
|
||||
let b = if p.x - l > r - p.x { c.saturating_add(1) } else { c };
|
||||
let room = geo.ci.at((((geo.cells.width() - 40.0) / geo.z).max(0.0)) as f64);
|
||||
b.min(room).min(MAX_COLS - 1)
|
||||
@@ -911,7 +911,7 @@ fn freeze_boundary(geo: &Geo, sh: &Sheet, p: Pos2, cols: bool) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let rr = geo.row_at(sh, p.y);
|
||||
let (t, b2) = (geo.y(sh, rr), geo.y(sh, rr.saturating_add(1).min(MAX_ROWS - 1)));
|
||||
let (t, b2) = (geo.y(sh, rr), geo.y_end(sh, rr));
|
||||
let b = if p.y - t > b2 - p.y { rr.saturating_add(1) } else { rr };
|
||||
let room = geo.ri.at((((geo.cells.height() - 40.0) / geo.z).max(0.0)) as f64);
|
||||
b.min(room).min(MAX_ROWS - 1)
|
||||
|
||||
@@ -142,21 +142,17 @@ fn paint_gridlines(q: &QuadrantPaint<'_>) {
|
||||
if sh.show_gridlines {
|
||||
let stroke = Stroke::new(1.0, t.gridline);
|
||||
let top = geo.y(sh, r0);
|
||||
let bottom = geo.y(sh, r1 + 1);
|
||||
let bottom = geo.y_end(sh, r1);
|
||||
let left = geo.x(sh, c0);
|
||||
let right = geo.x(sh, c1 + 1);
|
||||
for c in c0..=c1 + 1 {
|
||||
if c <= c1 && sh.col_width(c) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let x = geo.x(sh, c).round() - 0.5;
|
||||
let right = geo.x_end(sh, c1);
|
||||
let xs = (c0..=c1).filter(|&c| sh.col_width(c) > 0.0).map(|c| geo.x(sh, c)).chain([right]);
|
||||
for x in xs {
|
||||
let x = x.round() - 0.5;
|
||||
p.line_segment([pos2(x, top), pos2(x, bottom)], stroke);
|
||||
}
|
||||
for r in r0..=r1 + 1 {
|
||||
if r <= r1 && sh.row_height(r) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let y = geo.y(sh, r).round() - 0.5;
|
||||
let ys = (r0..=r1).filter(|&r| sh.row_height(r) > 0.0).map(|r| geo.y(sh, r)).chain([bottom]);
|
||||
for y in ys {
|
||||
let y = y.round() - 0.5;
|
||||
p.line_segment([pos2(left, y), pos2(right, y)], stroke);
|
||||
}
|
||||
}
|
||||
@@ -224,7 +220,6 @@ fn outside_text_cells(q: &mut QuadrantPaint<'_>, merges: &[RangeRef]) -> Vec<(Ce
|
||||
// Text cells just outside the quadrant whose text may overflow into it: the nearest non-empty
|
||||
// cell left of `c0` and right of `c1` in each row (within the same frozen / scrolled pane).
|
||||
let mut outside: Vec<(CellRef, Rect, Look, Option<CfLook>)> = Vec::new();
|
||||
let same_pane = |col: u32| (col < geo.fc) == (c0 < geo.fc);
|
||||
for r in r0..=r1 {
|
||||
if sh.row_height(r) == 0.0 {
|
||||
continue;
|
||||
@@ -232,7 +227,7 @@ fn outside_text_cells(q: &mut QuadrantPaint<'_>, merges: &[RangeRef]) -> Vec<(Ce
|
||||
let left = (c0.saturating_sub(20)..c0).rev().map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n));
|
||||
let right = (c1.saturating_add(1)..=c1.saturating_add(20).min(MAX_COLS - 1)).map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n));
|
||||
for n in [left, right].into_iter().flatten() {
|
||||
if same_pane(n.col) && sh.col_width(n.col) > 0.0 && in_merge(n).is_none() {
|
||||
if geo.same_col_pane(n.col, c0) && sh.col_width(n.col) > 0.0 && in_merge(n).is_none() {
|
||||
let (lk, cfl) = look(wb, si, sh, n, cf);
|
||||
outside.push((n, geo.cell_rect(sh, n), lk, cfl));
|
||||
}
|
||||
@@ -333,7 +328,9 @@ fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option<CfLo
|
||||
let z = geo.z;
|
||||
let (c0, c1) = q.cols;
|
||||
let in_merge = |cell| merges.iter().find(|m| m.contains(cell)).copied();
|
||||
let empty_ok = |cell| sh.value_ref(cell).is_none_or(Value::is_empty) && in_merge(cell).is_none();
|
||||
// Text overflows only into empty cells of its own pane: the frozen and scrolled columns move
|
||||
// apart when scrolling (#2544).
|
||||
let empty_ok = |cell: CellRef| geo.same_col_pane(cell.col, c.col) && sh.value_ref(cell).is_none_or(Value::is_empty) && in_merge(cell).is_none();
|
||||
if paint_cell_decorations(q, c, *rect, lk, cfl.as_ref()) {
|
||||
return;
|
||||
}
|
||||
@@ -454,12 +451,12 @@ fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option<CfLo
|
||||
let mut right = rect.right();
|
||||
let mut left = rect.left();
|
||||
if across_end > c.col {
|
||||
right = geo.x(sh, across_end + 1);
|
||||
right = geo.x_end(sh, across_end);
|
||||
}
|
||||
if matches!(halign, HAlign::Left | HAlign::Center | HAlign::Justify) {
|
||||
let mut cc = c.col + 1;
|
||||
while right - rect.left() < w + 2.0 * pad && cc < MAX_COLS && empty_ok(CellRef::new(c.row, cc)) && cc <= c1 + 20 {
|
||||
right = geo.x(sh, cc + 1);
|
||||
right = geo.x_end(sh, cc);
|
||||
cc += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,3 +160,40 @@ fn merged_range_draws_the_outline_from_its_edge_cells() {
|
||||
assert_eq!(ys.iter().filter(|y| (**y - max_y).abs() < 0.5).count(), 4, "bottom edge");
|
||||
assert!(max_x - min_x > 100.0 && max_y - min_y > 40.0, "the outline spans the whole merged area");
|
||||
}
|
||||
|
||||
/// #2544: a merged range in frozen columns keeps its whole outline while the others scroll: the
|
||||
/// bottom edge under its last column ends at that column's own right edge, the freeze line.
|
||||
#[test]
|
||||
fn frozen_merged_range_keeps_its_outline_when_scrolled() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("home.borders", json!({"range": "A2:B3", "preset": "thickOutside", "color": "#C81E1E"})).unwrap();
|
||||
h.state_mut().run("home.mergeCells", json!({"range": "A2:B3"})).unwrap();
|
||||
h.state_mut().run("view.freezePanes", json!({"cell": "C1"})).unwrap();
|
||||
h.state_mut().run("selection.set", json!({"range": "H10"})).unwrap();
|
||||
h.state_mut().view_mut().unwrap().scroll.x = 300.0;
|
||||
h.run_steps(6);
|
||||
let freeze_x = {
|
||||
let app = h.state();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
let geo = gridcraft_ui_egui::grid::Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
|
||||
geo.cells.left() + geo.frozen_w
|
||||
};
|
||||
let red = Color32::from_rgb(0xC8, 0x1E, 0x1E);
|
||||
let right_ends: Vec<f32> = h
|
||||
.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|shape| match &shape.shape {
|
||||
Shape::LineSegment { points, stroke } if stroke.color == red && (points[0].y - points[1].y).abs() < 0.5 => {
|
||||
Some(points[0].x.max(points[1].x))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(right_ends.len(), 4, "top and bottom edges, one segment per column: {right_ends:?}");
|
||||
assert_eq!(
|
||||
right_ends.iter().filter(|x| (**x - freeze_x).abs() < 1.0).count(),
|
||||
2,
|
||||
"B's segments end at the freeze line: {right_ends:?} vs {freeze_x}"
|
||||
);
|
||||
}
|
||||
@@ -66,3 +66,36 @@ fn dragging_the_handles_freezes_moves_and_unfreezes() {
|
||||
drag(&mut h, rows.center(), pos2(rows.center().x, g.cells.top() - 5.0));
|
||||
assert_eq!(freeze(&h), None);
|
||||
}
|
||||
|
||||
/// The last frozen column ends at the freeze line however far the other columns scroll, while
|
||||
/// the next column's left edge moves with the scroll (#2544).
|
||||
#[test]
|
||||
fn last_frozen_column_ends_at_the_freeze_line_when_scrolled() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("view.freezePanes", serde_json::json!({"cell": "C3"})).unwrap();
|
||||
h.state_mut().view_mut().unwrap().scroll = egui::vec2(300.0, 200.0);
|
||||
h.run_steps(2);
|
||||
let g = geo(&h);
|
||||
let sh = h.state().session.active().unwrap().wb.active().unwrap().clone();
|
||||
let split = g.cells.min + egui::vec2(g.frozen_w, g.frozen_h);
|
||||
assert!((g.x_end(&sh, 1) - split.x).abs() < 0.01 && (g.y_end(&sh, 1) - split.y).abs() < 0.01);
|
||||
assert!(g.x(&sh, 2) < split.x && g.y(&sh, 2) < split.y, "C and row 3 are scrolled under the frozen panes");
|
||||
assert!(g.same_col_pane(0, 1) && g.same_col_pane(2, 9) && !g.same_col_pane(1, 2));
|
||||
}
|
||||
|
||||
/// Dropping the column handle on the left part of the last frozen column while scrolled freezes
|
||||
/// up to its left edge: the column's right edge is the freeze line, not the next column's
|
||||
/// scrolled position (#2544).
|
||||
#[test]
|
||||
fn dragging_the_column_handle_when_scrolled_measures_the_last_frozen_column() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("view.freezePanes", serde_json::json!({"cell": "C1"})).unwrap();
|
||||
h.state_mut().view_mut().unwrap().scroll = egui::vec2(300.0, 0.0);
|
||||
h.run_steps(2);
|
||||
let g = geo(&h);
|
||||
let sh = h.state().session.active().unwrap().wb.active().unwrap().clone();
|
||||
let (cols, _) = freeze_handles(&g);
|
||||
assert!(g.x(&sh, 2) < g.x(&sh, 1), "C is scrolled left of B");
|
||||
drag(&mut h, cols.center(), pos2(g.x(&sh, 1) + 10.0, g.cells.top() + 40.0));
|
||||
assert_eq!(freeze(&h), Some((0, 1)));
|
||||
}
|
||||
@@ -53,6 +53,18 @@ fn text_clips(h: &Harness, text: &str) -> Vec<Rect> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Painted extents of the text shapes whose text is `text`, cut to their clip rects.
|
||||
fn text_visible_rects(h: &Harness, text: &str) -> Vec<Rect> {
|
||||
h.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|s| match &s.shape {
|
||||
Shape::Text(t) if t.galley.job.text == text => Some(t.visual_bounding_rect().intersect(s.clip_rect)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cell_rect(h: &Harness, cell: &str) -> Rect {
|
||||
let app = h.state();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
@@ -390,3 +402,71 @@ fn wrapped_text_clipping_survives_partial_vertical_scroll() {
|
||||
clip={clip:?}, expected={expected_bottom:.2}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #2544: text in a frozen column keeps its whole cell while the other columns scroll.
|
||||
#[test]
|
||||
fn frozen_column_text_keeps_its_cell_when_scrolled() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": LEFT})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": "v"})).unwrap();
|
||||
s.execute("view.freezeFirstColumn", json!({})).unwrap();
|
||||
});
|
||||
for col in [1, 2, 3, 8] {
|
||||
scroll_to_col(&mut h, col);
|
||||
let a1 = cell_rect(&h, "A1");
|
||||
let clips = text_clips(&h, LEFT);
|
||||
assert_eq!(clips.len(), 1, "A1 is drawn once: {clips:?}");
|
||||
assert!(
|
||||
(clips[0].left() - a1.left()).abs() <= 0.5 && (clips[0].right() - a1.right()).abs() <= 0.5,
|
||||
"A1's text spans A1 at column {col}: {clips:?} vs {a1:?}"
|
||||
);
|
||||
let visible = text_visible_rects(&h, LEFT);
|
||||
assert!(visible.len() == 1 && visible[0].is_positive(), "A1's text shows inside its clip at column {col}: {visible:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #2544: with two frozen columns, A1's text keeps overflowing across the whole of empty B1.
|
||||
#[test]
|
||||
fn frozen_text_overflows_to_the_freeze_line_when_scrolled() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": LEFT})).unwrap();
|
||||
s.execute("view.freezePanes", json!({"cell": "C1"})).unwrap();
|
||||
});
|
||||
for col in [2, 3, 4, 10] {
|
||||
scroll_to_col(&mut h, col);
|
||||
let (a1, b1) = (cell_rect(&h, "A1"), cell_rect(&h, "B1"));
|
||||
let clips = text_clips(&h, LEFT);
|
||||
assert_eq!(clips.len(), 1, "A1 is drawn once: {clips:?}");
|
||||
assert!(
|
||||
(clips[0].left() - a1.left()).abs() <= 0.5 && (clips[0].right() - b1.right()).abs() <= 0.5,
|
||||
"A1's text spans A1:B1 at column {col}: {clips:?} vs {a1:?} {b1:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #2544: the frozen columns' gridlines reach the freeze line, and none is drawn inside
|
||||
/// them at a scrolled column's edge.
|
||||
#[test]
|
||||
fn frozen_column_gridlines_stay_put_when_scrolled() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("view.freezePanes", json!({"cell": "C1"})).unwrap();
|
||||
});
|
||||
scroll_to_col(&mut h, 6);
|
||||
let (a5, b5) = (cell_rect(&h, "A5"), cell_rect(&h, "B5"));
|
||||
let frozen = |s: &egui::epaint::ClippedShape| s.clip_rect.right() <= b5.right() + 0.5 && s.clip_rect.left() <= a5.left();
|
||||
let segments: Vec<[egui::Pos2; 2]> = h
|
||||
.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|s| frozen(s))
|
||||
.filter_map(|s| match &s.shape {
|
||||
Shape::LineSegment { points, .. } => Some(*points),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let row5 = segments.iter().find(|p| (p[0].y - (a5.top() - 0.5).round()).abs() <= 1.0 && p[0].y == p[1].y);
|
||||
assert!(row5.is_some_and(|p| p[1].x >= b5.right() - 1.0), "row 5's gridline reaches the freeze line: {row5:?} vs {b5:?}");
|
||||
let verticals: Vec<f32> = segments.iter().filter(|p| p[0].x == p[1].x && p[0].y < b5.bottom()).map(|p| p[0].x).collect();
|
||||
let expected = [a5.left(), a5.right(), b5.right()].map(|x| x.round() - 0.5);
|
||||
assert!(verticals.iter().all(|x| expected.iter().any(|e| (x - e).abs() <= 0.5)), "only A and B's edges: {verticals:?} vs {expected:?}");
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (every Paste Special option has its access key; earlier: the Draw tab's live stroke preview uses the pen's colour and thickness; earlier: copy quotes only cells with a tab or line break, as Excel does; earlier: Ctrl+. Next Corner of Selection; earlier: dragging the selection's border onto data asks before replacing it; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (frozen columns keep their text, gridlines and merged borders whole while the sheet scrolls (#2544); earlier: every Paste Special option has its access key; earlier: the Draw tab's live stroke preview uses the pen's colour and thickness; earlier: copy quotes only cells with a tab or line break, as Excel does; earlier: Ctrl+. Next Corner of Selection; earlier: dragging the selection's border onto data asks before replacing it; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **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
|
||||
@@ -238,6 +238,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | minor | Frozen panes: text in frozen columns overflows only into its own pane and keeps its whole width while the sheet scrolls; frozen gridlines and merged-cell borders reach the freeze line, and the freeze handle measures the last frozen column itself (#2544). Page Layout view gridlines stop at each page's last column |
|
||||
| 2026-11-03 | minor | Excel 2003 menu access keys: 43 legacy sequences (Insert, Edit, Format, Data, View, Tools and File ▸ Page Setup, e.g. Alt+I, R inserts rows, Alt+E, A, A clears all, Alt+D, S sorts, Alt+T, O opens Options) run the existing commands and dialogs; Alt+I, D, V and T paint no badges, as in Excel (#941, #2311) |
|
||||
| 2026-11-03 | minor | Paste Special: every option has Excel's access key (A, F, V, T, C, N, X, W, R, U; O, D, S, M, I; B, E; L pastes the link at once), not only F, V, T, C and E (#1482) |
|
||||
| 2026-11-03 | minor | Draw tab: the stroke drawn under the pointer while dragging a pen is the ink that will be committed, in the pen's colour and thickness (translucent for a highlighter), instead of always blue at 2 pt (#1786) |
|
||||
|
||||
Reference in new issue
Block a user