mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 11:46:49 -06:00
Status bar: a right-click opens a statistics menu, with Numerical Count, Minimum and Maximum added
The status bar always showed Average, Count and Sum for a selection of several cells and had no menu. A right-click on it now opens a menu with Average, Count, Numerical Count, Minimum, Maximum and Sum, as in Excel. Average, Count and Sum are on by default; the other three are off. The text on the status bar can no longer be selected, so a right-click on the mode text or a statistic opens the menu as well. The zoom and view controls handle their own clicks as before. The choice is a preference set with the new view.statusBar command and kept in prefs.json. A prefs.json without it keeps the defaults.
This commit is contained in:
@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |
|
||||
| 2026-10-23 | Accounting negatives in text: `$(1,234)`, `$ (5)` and `(5)%` read as negative numbers when typed, coerced by arithmetic or passed to VALUE, like Excel 365; `(-5)`, `((5))`, `$5%` and dates or times in parentheses stay text |
|
||||
| 2026-10-23 | Calc: `INDEX(A1:C3,2)` (one index on a reference of several rows and columns) is `#REF!` as in Excel, instead of the whole row; `INDEX(A1:C3,2,)` still gives the row |
|
||||
| 2026-10-22 | Formula hardening: digits too many for a finite number are text, so `"1 <400 digits>/<400 digits>"` no longer coerces to NaN; a NaN column index in VLOOKUP, HLOOKUP and the D-functions is `#VALUE!` instead of an index underflow; REGEXREPLACE stops building a result as soon as it can only be too long (a worst case went from 1 GB to 7 MB) (#2163, #2162) |
|
||||
|
||||
@@ -44,6 +44,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
s.prefs.formula_bar = bool_param(p, "on").unwrap_or(!s.prefs.formula_bar);
|
||||
Ok(json!({"on": s.prefs.formula_bar}))
|
||||
}),
|
||||
cmd!(
|
||||
query "view.statusBar",
|
||||
"Customize Status Bar",
|
||||
[],
|
||||
None,
|
||||
"{average?, count?, numericalCount?, minimum?, maximum?, sum?: bool (the statistics shown for a selection; returns all six)}",
|
||||
always,
|
||||
status_bar
|
||||
),
|
||||
cmd!("view.zoomToSelection", "Zoom to Selection", ["View", "Zoom"], None, "{viewWidth?, viewHeight?}", has_doc, zoom_selection),
|
||||
cmd!("pageLayout.orientation", "Orientation", ["Page Layout", "Page Setup"], None, "{orientation: portrait|landscape}", has_doc, orientation),
|
||||
cmd!(
|
||||
@@ -187,6 +196,30 @@ fn show_objects(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
})
|
||||
}
|
||||
|
||||
fn status_bar(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let st = &mut s.prefs.status_bar;
|
||||
for (key, flag) in [
|
||||
("average", &mut st.average),
|
||||
("count", &mut st.count),
|
||||
("numericalCount", &mut st.numerical_count),
|
||||
("minimum", &mut st.minimum),
|
||||
("maximum", &mut st.maximum),
|
||||
("sum", &mut st.sum),
|
||||
] {
|
||||
if let Some(on) = bool_param(p, key) {
|
||||
*flag = on;
|
||||
}
|
||||
}
|
||||
Ok(json!({
|
||||
"average": st.average,
|
||||
"count": st.count,
|
||||
"numericalCount": st.numerical_count,
|
||||
"minimum": st.minimum,
|
||||
"maximum": st.maximum,
|
||||
"sum": st.sum,
|
||||
}))
|
||||
}
|
||||
|
||||
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()?;
|
||||
@@ -478,4 +511,22 @@ mod tests {
|
||||
let old: crate::Prefs = serde_json::from_str(r#"{"userName": "Ann"}"#).unwrap();
|
||||
assert!(old.formula_bar);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_bar_statistics_default_to_average_count_and_sum_and_turn_on_one_at_a_time() {
|
||||
let mut s = Session::new();
|
||||
let all = json!({"average": true, "count": true, "numericalCount": false, "minimum": false, "maximum": false, "sum": true});
|
||||
assert_eq!(s.execute("view.statusBar", json!({})).unwrap(), all);
|
||||
let r = s.execute("view.statusBar", json!({"minimum": true, "sum": false, "count": "yes"})).unwrap();
|
||||
assert_eq!(r, json!({"average": true, "count": true, "numericalCount": false, "minimum": true, "maximum": false, "sum": false}));
|
||||
assert!(s.prefs.status_bar.minimum && !s.prefs.status_bar.sum);
|
||||
// A prefs.json saved before the option existed, or with only some of the six, keeps the defaults for the rest.
|
||||
let old: crate::Prefs = serde_json::from_str(r#"{"userName": "Ann"}"#).unwrap();
|
||||
assert_eq!(old.status_bar, crate::StatusBarStats::default());
|
||||
let part: crate::Prefs = serde_json::from_str(r#"{"statusBar": {"maximum": true}}"#).unwrap();
|
||||
assert!(part.status_bar.maximum && part.status_bar.average && part.status_bar.sum && !part.status_bar.minimum);
|
||||
let saved = serde_json::to_value(&s.prefs).unwrap();
|
||||
assert_eq!(saved["statusBar"]["minimum"], true);
|
||||
assert_eq!(saved["statusBar"]["numericalCount"], false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +347,27 @@ pub struct Prefs {
|
||||
pub user_dictionary: Vec<String>,
|
||||
/// View › Show › Formula Bar (an application option in Excel, not saved in the file).
|
||||
pub formula_bar: bool,
|
||||
/// The statistics the status bar shows for a selection (right-click the status bar).
|
||||
pub status_bar: StatusBarStats,
|
||||
}
|
||||
|
||||
/// The selection statistics shown on the status bar, set with `view.statusBar`. Excel turns on
|
||||
/// Average, Count and Sum by default.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct StatusBarStats {
|
||||
pub average: bool,
|
||||
pub count: bool,
|
||||
pub numerical_count: bool,
|
||||
pub minimum: bool,
|
||||
pub maximum: bool,
|
||||
pub sum: bool,
|
||||
}
|
||||
|
||||
impl Default for StatusBarStats {
|
||||
fn default() -> Self {
|
||||
StatusBarStats { average: true, count: true, numerical_count: false, minimum: false, maximum: false, sum: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
@@ -361,6 +382,7 @@ impl Default for Prefs {
|
||||
r1c1: false,
|
||||
user_dictionary: vec![],
|
||||
formula_bar: true,
|
||||
status_bar: StatusBarStats::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+99
-33
@@ -262,6 +262,13 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
egui::Panel::bottom("status_bar").exact_size(26.0).frame(egui::Frame::NONE.fill(t.status_bar).inner_margin(egui::Margin::symmetric(12, 0))).show(
|
||||
ui,
|
||||
|ui| {
|
||||
// A right-click anywhere on the status bar opens the statistics menu, the text included, so its labels are not
|
||||
// selectable. The zoom and view controls sense clicks and handle their own.
|
||||
let background = ui.interact(ui.max_rect(), egui::Id::new("gridcraft.status_bar"), Sense::click());
|
||||
background.context_menu(|ui| status_bar_menu(app, ui));
|
||||
let text = |ui: &mut Ui, rich: egui::RichText| {
|
||||
ui.add(egui::Label::new(rich).selectable(false));
|
||||
};
|
||||
ui.horizontal_centered(|ui| {
|
||||
let mode = match app.session.mode {
|
||||
gridcraft_engine::Mode::Ready => "Ready",
|
||||
@@ -269,25 +276,26 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
gridcraft_engine::Mode::Edit => "Edit",
|
||||
gridcraft_engine::Mode::Point => "Point",
|
||||
};
|
||||
ui.label(egui::RichText::new(app.ui.language.tr(mode)).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
text(ui, egui::RichText::new(app.ui.language.tr(mode)).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
if let Some(d) = app.session.active() {
|
||||
if d.wb.vba_project.is_some() {
|
||||
ui.add_space(12.0);
|
||||
ui.label(egui::RichText::new("Macros preserved (not executed)").font(theme::ui_font(12.5)).color(t.danger));
|
||||
text(ui, egui::RichText::new("Macros preserved (not executed)").font(theme::ui_font(12.5)).color(t.danger));
|
||||
}
|
||||
if d.wb.calc.mode == gridcraft_engine::model::CalcMode::Manual {
|
||||
ui.add_space(12.0);
|
||||
ui.label(egui::RichText::new(app.ui.language.tr("Calculate")).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
text(ui, egui::RichText::new(app.ui.language.tr("Calculate")).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
}
|
||||
if !d.calc.circular.is_empty() {
|
||||
ui.add_space(12.0);
|
||||
let (si, c) = d.calc.circular[0];
|
||||
let name = d.wb.sheet(si).map(|s| s.name.clone()).unwrap_or_default();
|
||||
ui.label(egui::RichText::new(format!("Circular References: {name}!{}", c.a1())).font(theme::ui_font(12.5)).color(t.danger));
|
||||
text(ui, egui::RichText::new(format!("Circular References: {name}!{}", c.a1())).font(theme::ui_font(12.5)).color(t.danger));
|
||||
}
|
||||
if app.session.clipboard.is_some() && app.editor.is_none() {
|
||||
ui.add_space(12.0);
|
||||
ui.label(
|
||||
text(
|
||||
ui,
|
||||
egui::RichText::new(app.ui.language.tr("Select destination and press Enter or choose Paste"))
|
||||
.font(theme::ui_font(12.5))
|
||||
.color(t.text_dim),
|
||||
@@ -336,34 +344,10 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
// Average / Count / Sum.
|
||||
if let Some(d) = app.session.active()
|
||||
&& let Some(sh) = d.wb.active()
|
||||
&& !d.selection.is_single_cell()
|
||||
{
|
||||
let st = gridcraft_engine::display::stats(sh, &d.selection.ranges);
|
||||
let fmt = |v: f64| {
|
||||
let code = d.wb.styles.get(sh.style_id(d.selection.active)).num_fmt.as_str().to_string();
|
||||
let code = if code == "General" { "#,##0.##########".to_string() } else { code };
|
||||
gridcraft_engine::display::format(&gridcraft_engine::core::Value::Number(v), &code, &d.wb)
|
||||
.text
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
if st.numeric > 0 {
|
||||
parts.push(format!("Sum: {}", fmt(st.sum)));
|
||||
}
|
||||
if st.count > 0 {
|
||||
parts.push(format!("Count: {}", st.count));
|
||||
}
|
||||
if let Some(a) = st.average {
|
||||
parts.push(format!("Average: {}", fmt(a)));
|
||||
}
|
||||
for p in parts {
|
||||
ui.label(egui::RichText::new(p).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
// The layout runs right to left, so the last statistic is drawn first.
|
||||
for p in selection_statistics(app).into_iter().rev() {
|
||||
text(ui, egui::RichText::new(p).font(theme::ui_font(12.5)).color(t.text_dim));
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -371,6 +355,64 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
);
|
||||
}
|
||||
|
||||
/// The statistics the status bar shows for a selection of more than one cell, left to right: Average, Count,
|
||||
/// Numerical Count, Minimum, Maximum, Sum. Each one shows when it is turned on (`view.statusBar`)
|
||||
/// and the selection has a value for it.
|
||||
pub(crate) fn selection_statistics(app: &SheetApp) -> Vec<String> {
|
||||
let Some(d) = app.session.active() else { return vec![] };
|
||||
let Some(sh) = d.wb.active() else { return vec![] };
|
||||
if d.selection.is_single_cell() {
|
||||
return vec![];
|
||||
}
|
||||
let on = app.session.prefs.status_bar;
|
||||
let st = gridcraft_engine::display::stats(sh, &d.selection.ranges);
|
||||
let fmt = |v: f64| {
|
||||
let code = d.wb.styles.get(sh.style_id(d.selection.active)).num_fmt.as_str().to_string();
|
||||
let code = if code == "General" { "#,##0.##########".to_string() } else { code };
|
||||
gridcraft_engine::display::format(&gridcraft_engine::core::Value::Number(v), &code, &d.wb).text.trim_end_matches('.').to_string()
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
if on.average
|
||||
&& let Some(a) = st.average
|
||||
{
|
||||
parts.push(format!("Average: {}", fmt(a)));
|
||||
}
|
||||
if on.count && st.count > 0 {
|
||||
parts.push(format!("Count: {}", st.count));
|
||||
}
|
||||
if on.numerical_count && st.count > 0 {
|
||||
parts.push(format!("Numerical Count: {}", st.numeric));
|
||||
}
|
||||
if on.minimum && st.numeric > 0 {
|
||||
parts.push(format!("Minimum: {}", fmt(st.min)));
|
||||
}
|
||||
if on.maximum && st.numeric > 0 {
|
||||
parts.push(format!("Maximum: {}", fmt(st.max)));
|
||||
}
|
||||
if on.sum && st.numeric > 0 {
|
||||
parts.push(format!("Sum: {}", fmt(st.sum)));
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
/// The status bar's context menu: a check item for each statistic, each one running `view.statusBar`.
|
||||
fn status_bar_menu(app: &mut SheetApp, ui: &mut Ui) {
|
||||
ui.set_min_width(180.0);
|
||||
let st = app.session.prefs.status_bar;
|
||||
for (key, label, mut on) in [
|
||||
("average", "Average", st.average),
|
||||
("count", "Count", st.count),
|
||||
("numericalCount", "Numerical Count", st.numerical_count),
|
||||
("minimum", "Minimum", st.minimum),
|
||||
("maximum", "Maximum", st.maximum),
|
||||
("sum", "Sum", st.sum),
|
||||
] {
|
||||
if ui.checkbox(&mut on, app.ui.language.tr(label)).changed() {
|
||||
app.run_or_alert("view.statusBar", json!({ key: on }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{activate_sheet, drop_copies_sheet, navigator_items, rename_needs_focus, tab_drop};
|
||||
@@ -396,6 +438,30 @@ mod tests {
|
||||
app.session.active().unwrap().wb.sheets.iter().map(|s| s.name.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_bar_shows_the_statistics_that_are_turned_on_in_order() {
|
||||
let mut session = gridcraft_engine::Session::default();
|
||||
session.new_workbook();
|
||||
let mut app = SheetApp::new(session, Default::default());
|
||||
for (cell, input) in [("A1", "4"), ("A2", "-1"), ("A3", "x"), ("A4", "9")] {
|
||||
app.run("cell.set", json!({"cell": cell, "input": input})).unwrap();
|
||||
}
|
||||
app.run("selection.set", json!({"range": "A1:A5"})).unwrap();
|
||||
assert_eq!(super::selection_statistics(&app), ["Average: 4", "Count: 4", "Sum: 12"]);
|
||||
app.run("view.statusBar", json!({"numericalCount": true, "minimum": true, "maximum": true})).unwrap();
|
||||
assert_eq!(super::selection_statistics(&app), ["Average: 4", "Count: 4", "Numerical Count: 3", "Minimum: -1", "Maximum: 9", "Sum: 12"]);
|
||||
app.run("view.statusBar", json!({"average": false, "count": false, "sum": false})).unwrap();
|
||||
assert_eq!(super::selection_statistics(&app), ["Numerical Count: 3", "Minimum: -1", "Maximum: 9"]);
|
||||
// Text only: Count and Numerical Count, nothing that needs a number.
|
||||
app.run("view.statusBar", json!({"average": true, "count": true, "sum": true})).unwrap();
|
||||
app.run("cell.set", json!({"cell": "B1", "input": "a"})).unwrap();
|
||||
app.run("cell.set", json!({"cell": "B2", "input": "b"})).unwrap();
|
||||
app.run("selection.set", json!({"range": "B1:B2"})).unwrap();
|
||||
assert_eq!(super::selection_statistics(&app), ["Count: 2", "Numerical Count: 0"]);
|
||||
app.run("selection.set", json!({"range": "A1"})).unwrap();
|
||||
assert!(super::selection_statistics(&app).is_empty(), "one cell shows no statistics");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_drop_modifier_is_ctrl_or_cmd_or_option_on_mac() {
|
||||
// egui reports Ctrl as `command` too off macOS, and Cmd as `mac_cmd` and `command` on macOS.
|
||||
|
||||
@@ -77,6 +77,7 @@ mod sheet_tabs;
|
||||
mod show_objects;
|
||||
mod shrink_to_fit;
|
||||
mod sort_dialog_columns;
|
||||
mod status_bar_menu;
|
||||
mod symbol_glyphs;
|
||||
mod system_theme;
|
||||
mod table_headings;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Right-clicking the status bar opens a menu that turns its selection statistics on and off.
|
||||
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, pos2, vec2};
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness() -> Harness {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
for (cell, input) in [("A1", "4"), ("A2", "-1"), ("A3", "9")] {
|
||||
session.execute("cell.set", json!({"cell": cell, "input": input})).unwrap();
|
||||
}
|
||||
session.execute("selection.set", json!({"range": "A1:A3"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn right_click(h: &mut Harness, pos: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(pos));
|
||||
h.run_steps(1);
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_clicking_the_status_bar_turns_on_minimum() {
|
||||
let mut h = harness();
|
||||
assert!(h.query_by_label("Sum: 12").is_some(), "Sum is on by default");
|
||||
assert!(h.query_by_label("Minimum: -1").is_none(), "Minimum is off by default");
|
||||
// An empty stretch of the 26-point status bar, between the mode text and the statistics.
|
||||
right_click(&mut h, pos2(300.0, 787.0));
|
||||
h.get_by_label("Minimum").click();
|
||||
h.run_steps(4);
|
||||
assert!(h.state().session.prefs.status_bar.minimum);
|
||||
assert!(h.query_by_label("Minimum: -1").is_some(), "the status bar shows the minimum of the selection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_clicking_the_status_bar_text_opens_the_menu() {
|
||||
for text in ["Sum: 12", "Ready"] {
|
||||
let mut h = harness();
|
||||
let pos = h.get_by_label(text).rect().center();
|
||||
right_click(&mut h, pos);
|
||||
assert!(h.query_by_label("Numerical Count").is_some(), "a right-click on {text:?} opens the statistics menu");
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (New Formatting Rule's formula box takes references clicked or dragged on the grid, #2348; Page Up and Ctrl+Up into frozen rows, Ctrl+Left and Alt+Page Up into frozen columns bring the scrolled area back; Auto Fill Options button after a left-drag of the fill handle (#1403); Show Outline Symbols, Ctrl+8; a too-wide number in a repeat-fill format such as Accounting shows #### instead of losing its leading digits; Esc and Enter close a data validation alert over the cell editor; Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Number Stored as Text indicator and Convert to Number) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (right-clicking the status bar opens a menu of its statistics, with Numerical Count, Minimum and Maximum added; New Formatting Rule's formula box takes references clicked or dragged on the grid, #2348; Page Up and Ctrl+Up into frozen rows, Ctrl+Left and Alt+Page Up into frozen columns bring the scrolled area back; Auto Fill Options button after a left-drag of the fill handle (#1403); Show Outline Symbols, Ctrl+8; a too-wide number in a repeat-fill format such as Accounting shows #### instead of losing its leading digits; Esc and Enter close a data validation alert over the cell editor; Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Number Stored as Text indicator and Convert to Number) · **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
|
||||
@@ -26,7 +26,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes | done; dynamic-array spill children show the source formula muted and read-only, using current spill ownership |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); New sheet (+) right after the last tab, the strip capped at 60% of the bar (#943); manual wheel/trackpad browsing preserved; next/previous skips Hidden and VeryHidden sheets and retains the current sheet and selection when no visible destination exists; rename focus bug on macOS (#145) |
|
||||
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes | customizable statistics partial |
|
||||
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes; a right-click on the status bar opens a menu that turns the statistics on and off (Average, Count, Numerical Count, Minimum, Maximum, Sum; `view.statusBar`) | done for the statistics; the other status bar options are not customizable |
|
||||
| Task panes (Format Chart, Comments, Watch, Selection, PivotTable fields) | yes | yes; PivotTable source-field list scrollbar sits at the right edge of its pane | element-level Format pane for shapes and pictures missing |
|
||||
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | Options dialog and template gallery missing |
|
||||
| Dark mode, system appearance | yes | yes | done (10-09); follows the system on first run, title-bar switch and File › Display theme (#194, #239) |
|
||||
@@ -178,6 +178,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-24 | minor | Right-clicking the status bar opens a menu of its statistics, as in Excel ("If you want to customize the status bar, right-click it, and then click the options that you want"): Average, Count and Sum are on by default, and Numerical Count, Minimum and Maximum are new and off by default. The choice is a preference (`view.statusBar`), kept in `prefs.json` |
|
||||
| 2026-10-23 | minor | Conditional Formatting ▸ New Rule, formula: clicking a cell on the grid writes its absolute reference at the formula box's caret (`=$B$2`, an empty box starts with `=`), a drag writes a range (`$B$2:$C$4`) and a second click replaces the reference just written, as in Excel ("Selecting cells on the worksheet inserts absolute cell references"). The selection the rule applies to doesn't move, and the box keeps the keyboard. After an operand (`=A1>5`) the click does nothing (#2348) |
|
||||
| 2026-10-23 | minor | Frozen panes: Page Up, Ctrl+Up and Shift variants that move from the scrolled rows into the frozen ones bring the scrolled rows back to the top, so the rows in between are seen again (Page Up from far down used to end on row 1 with the view still a page down); Ctrl+Left and Alt+Page Up do the same for frozen columns. Other ways of selecting a frozen cell (click, Find, Go To, Undo, formula pointing) leave the view where it is |
|
||||
| 2026-10-23 | minor | After a left-drag of the fill handle an Auto Fill Options button sits below the filled cells; its menu redoes the fill as Copy Cells, Fill Series, Fill Formatting Only or Fill Without Formatting (undoes the drag's fill, then `edit.autoFill` in that mode). It goes away when the workbook changes or the selection moves (#1403) |
|
||||
|
||||
Reference in New Issue
Block a user