mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge PR #2388
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
use egui::{Color32, Key, Sense, Stroke, StrokeKind, vec2};
|
use egui::{Color32, Key, Sense, Stroke, StrokeKind, vec2};
|
||||||
|
|
||||||
use crate::icons::{self, Icon};
|
use crate::icons::{self, Icon};
|
||||||
|
use gridcraft_engine::core::{CellRef, RangeRef};
|
||||||
use serde_json::{Map, Value as Json, json};
|
use serde_json::{Map, Value as Json, json};
|
||||||
|
|
||||||
use crate::SheetApp;
|
use crate::SheetApp;
|
||||||
@@ -44,6 +45,28 @@ pub struct Dialog {
|
|||||||
/// The dialog this one was opened from (Name Manager › New…): it comes back when this closes.
|
/// The dialog this one was opened from (Name Manager › New…): it comes back when this closes.
|
||||||
back: Option<Box<Dialog>>,
|
back: Option<Box<Dialog>>,
|
||||||
focus_initialized: bool,
|
focus_initialized: bool,
|
||||||
|
/// The formula box that takes references clicked or dragged on the grid (New Formatting Rule).
|
||||||
|
pick: Option<RefPick>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formula box in point mode, as in Excel: a click on the grid writes the cell's absolute
|
||||||
|
/// reference at the caret instead of selecting it, and a drag grows it into a range.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RefPick {
|
||||||
|
key: &'static str,
|
||||||
|
/// The sheet the dialog was opened on, by name: its index changes when sheets are inserted or
|
||||||
|
/// moved while the dialog is open. References to another sheet carry that sheet's name.
|
||||||
|
sheet: String,
|
||||||
|
/// Caret (char index), mirrored from the text box while it has the keyboard.
|
||||||
|
caret: usize,
|
||||||
|
/// The reference last inserted (byte span), replaced by the next click or the drag.
|
||||||
|
point: Option<(usize, usize)>,
|
||||||
|
anchor: CellRef,
|
||||||
|
/// Give the box the keyboard back, caret after the reference, on the next frame.
|
||||||
|
refocus: bool,
|
||||||
|
/// The selection the rule applies to, fixed when the dialog opened as Excel's "Applies to":
|
||||||
|
/// pointing on another sheet must not move the rule there.
|
||||||
|
applies_to: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
@@ -77,6 +100,46 @@ impl Dialog {
|
|||||||
self.values.insert(key.into(), value);
|
self.values.insert(key.into(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a click on the grid goes to a formula box of this dialog rather than the selection.
|
||||||
|
pub fn picks_references(&self) -> bool {
|
||||||
|
self.pick.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A click (`extend` false) or a drag reaching `cell`: writes the absolute reference at the
|
||||||
|
/// formula box's caret, replacing the reference the press inserted, as in the formula bar's
|
||||||
|
/// point mode. An empty box starts with `=`. After an operand there is no place for a
|
||||||
|
/// reference, so nothing changes. `active_sheet` names the sheet shown when it is not the
|
||||||
|
/// dialog's own.
|
||||||
|
pub fn pick_reference(&mut self, cell: CellRef, active_sheet: &str, extend: bool) {
|
||||||
|
let Some(pick) = self.pick.as_mut() else { return };
|
||||||
|
let text = self.values.get(pick.key).and_then(Json::as_str).unwrap_or("").to_string();
|
||||||
|
let mut ed = crate::editor::EditState::new(0, cell, text, true, false);
|
||||||
|
ed.caret = pick.caret.min(ed.text.chars().count());
|
||||||
|
ed.point = pick.point;
|
||||||
|
if ed.text.trim().is_empty() {
|
||||||
|
ed.text = "=".into();
|
||||||
|
ed.caret = 1;
|
||||||
|
ed.point = None;
|
||||||
|
}
|
||||||
|
if !ed.can_point() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !extend {
|
||||||
|
pick.anchor = cell;
|
||||||
|
}
|
||||||
|
let range = abs(&RangeRef::new(pick.anchor, cell));
|
||||||
|
let reference = if active_sheet.eq_ignore_ascii_case(&pick.sheet) {
|
||||||
|
range
|
||||||
|
} else {
|
||||||
|
format!("{}!{range}", gridcraft_engine::formula::quote_sheet(active_sheet))
|
||||||
|
};
|
||||||
|
ed.insert_ref(&reference);
|
||||||
|
pick.caret = ed.caret;
|
||||||
|
pick.point = ed.point;
|
||||||
|
pick.refocus = true;
|
||||||
|
self.values.insert(pick.key.into(), json!(ed.text));
|
||||||
|
}
|
||||||
|
|
||||||
fn form(name: &str, title: &str, command: &'static str, fields: Vec<Field>, defaults: Json) -> Dialog {
|
fn form(name: &str, title: &str, command: &'static str, fields: Vec<Field>, defaults: Json) -> Dialog {
|
||||||
Dialog {
|
Dialog {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
@@ -94,6 +157,7 @@ impl Dialog {
|
|||||||
picture_target: None,
|
picture_target: None,
|
||||||
back: None,
|
back: None,
|
||||||
focus_initialized: false,
|
focus_initialized: false,
|
||||||
|
pick: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +178,7 @@ impl Dialog {
|
|||||||
picture_target: None,
|
picture_target: None,
|
||||||
back: None,
|
back: None,
|
||||||
focus_initialized: false,
|
focus_initialized: false,
|
||||||
|
pick: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +755,19 @@ impl Dialog {
|
|||||||
fields,
|
fields,
|
||||||
json!({"preset": "lightRedFillDarkRedText", "period": "today"}),
|
json!({"preset": "lightRedFillDarkRedText", "period": "today"}),
|
||||||
);
|
);
|
||||||
|
if ty == "expression" {
|
||||||
|
let (sheet, applies_to) = app
|
||||||
|
.session
|
||||||
|
.active()
|
||||||
|
.map(|doc| {
|
||||||
|
(
|
||||||
|
doc.wb.active().map(|sh| sh.name.clone()).unwrap_or_default(),
|
||||||
|
doc.selection.ranges.iter().map(RangeRef::a1).collect::<Vec<_>>().join(","),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
d.pick = Some(RefPick { key: "formula", sheet, caret: 0, point: None, anchor: CellRef::new(0, 0), refocus: false, applies_to });
|
||||||
|
}
|
||||||
d.values.insert("type".into(), json!(ty));
|
d.values.insert("type".into(), json!(ty));
|
||||||
d.values.insert("operator".into(), p("operator"));
|
d.values.insert("operator".into(), p("operator"));
|
||||||
d
|
d
|
||||||
@@ -1115,6 +1193,10 @@ pub(crate) fn build_params(d: &Dialog) -> Json {
|
|||||||
rule.insert(k.clone(), val.clone());
|
rule.insert(k.clone(), val.clone());
|
||||||
}
|
}
|
||||||
v = json!({"rule": Json::Object(rule)});
|
v = json!({"rule": Json::Object(rule)});
|
||||||
|
if let Some(pick) = d.pick.as_ref().filter(|pick| !pick.applies_to.is_empty() && !pick.sheet.is_empty()) {
|
||||||
|
v["sheet"] = json!(pick.sheet.as_str());
|
||||||
|
v["range"] = json!(pick.applies_to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"series" => {
|
"series" => {
|
||||||
if let Some(s) = d.values.get("stop").and_then(Json::as_str).and_then(|s| s.parse::<f64>().ok()) {
|
if let Some(s) = d.values.get("stop").and_then(Json::as_str).and_then(|s| s.parse::<f64>().ok()) {
|
||||||
@@ -1634,6 +1716,25 @@ fn take_first_focus(d: &mut Dialog, r: &egui::Response) {
|
|||||||
d.focus_first_field = false;
|
d.focus_first_field = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps a picking formula box's caret, and gives it the keyboard back after a grid click put a
|
||||||
|
/// reference in it (the click took the focus), its caret after the reference.
|
||||||
|
fn track_pick(pick: &mut RefPick, r: &egui::Response, typed: bool) {
|
||||||
|
if typed {
|
||||||
|
pick.point = None;
|
||||||
|
}
|
||||||
|
if pick.refocus {
|
||||||
|
let mut state = egui::text_edit::TextEditState::load(&r.ctx, r.id).unwrap_or_default();
|
||||||
|
state.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(pick.caret))));
|
||||||
|
state.store(&r.ctx, r.id);
|
||||||
|
r.request_focus();
|
||||||
|
pick.refocus = false;
|
||||||
|
} else if r.has_focus()
|
||||||
|
&& let Some(range) = egui::text_edit::TextEditState::load(&r.ctx, r.id).and_then(|state| state.cursor.char_range())
|
||||||
|
{
|
||||||
|
pick.caret = range.primary.index.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draws a dialog's `fields`, editing its values.
|
/// Draws a dialog's `fields`, editing its values.
|
||||||
fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||||
// Field labels, choices and notes are English source strings, translated as they are drawn.
|
// Field labels, choices and notes are English source strings, translated as they are drawn.
|
||||||
@@ -1659,6 +1760,9 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
|||||||
if r.changed() {
|
if r.changed() {
|
||||||
d.values.insert(key.into(), json!(s));
|
d.values.insert(key.into(), json!(s));
|
||||||
}
|
}
|
||||||
|
if let Some(pick) = d.pick.as_mut().filter(|pick| pick.key == key) {
|
||||||
|
track_pick(pick, &r, r.changed());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Field::Password { key, label } => {
|
Field::Password { key, label } => {
|
||||||
|
|||||||
@@ -2336,6 +2336,19 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||||||
app.grid.filter_menu = Some((c.col, c.row, geo.mirror_pos(p)));
|
app.grid.filter_menu = Some((c.col, c.row, geo.mirror_pos(p)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A dialog's formula box in point mode (New Formatting Rule) takes the reference; the
|
||||||
|
// selection, which the dialog acts on, stays.
|
||||||
|
if let Some(d) = app.dialog.as_mut().filter(|d| d.picks_references()) {
|
||||||
|
d.pick_reference(c, &sh.name, false);
|
||||||
|
app.grid.drag = Drag::Select;
|
||||||
|
app.grid.drag_select = true;
|
||||||
|
if resp.drag_started()
|
||||||
|
&& let Some(p) = pos
|
||||||
|
{
|
||||||
|
select_to_pointer(app, geo, sh, wb, p);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Point mode while editing a formula: clicking inserts a reference.
|
// Point mode while editing a formula: clicking inserts a reference.
|
||||||
if let Some(ed) = app.editor.as_mut()
|
if let Some(ed) = app.editor.as_mut()
|
||||||
&& ed.can_point()
|
&& ed.can_point()
|
||||||
@@ -2658,7 +2671,10 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||||||
/// An armed Format Painter pastes onto the cells just clicked or dragged over (not while
|
/// An armed Format Painter pastes onto the cells just clicked or dragged over (not while
|
||||||
/// pointing a formula reference, which also drags a Select).
|
/// pointing a formula reference, which also drags a Select).
|
||||||
fn format_painter_apply(app: &mut SheetApp) {
|
fn format_painter_apply(app: &mut SheetApp) {
|
||||||
if app.session.format_painter.is_some() && !app.editor.as_ref().is_some_and(|ed| ed.can_point()) {
|
if app.session.format_painter.is_some()
|
||||||
|
&& !app.editor.as_ref().is_some_and(|ed| ed.can_point())
|
||||||
|
&& !app.dialog.as_ref().is_some_and(|d| d.picks_references())
|
||||||
|
{
|
||||||
let dest = app.session.active().map(|d| d.selection.current().a1());
|
let dest = app.session.active().map(|d| d.selection.current().a1());
|
||||||
if let Some(dest) = dest
|
if let Some(dest) = dest
|
||||||
&& let Err(e) = app.run("edit.formatPainter", json!({"apply": dest}))
|
&& let Err(e) = app.run("edit.formatPainter", json!({"apply": dest}))
|
||||||
@@ -2711,7 +2727,9 @@ fn select_to_pointer(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, p
|
|||||||
geo.row_at(sh, p.y.clamp(geo.cells.top() + 1.0, geo.cells.bottom() - 1.0)),
|
geo.row_at(sh, p.y.clamp(geo.cells.top() + 1.0, geo.cells.bottom() - 1.0)),
|
||||||
geo.col_at(sh, p.x.clamp(geo.cells.left() + 1.0, geo.cells.right() - 1.0)),
|
geo.col_at(sh, p.x.clamp(geo.cells.left() + 1.0, geo.cells.right() - 1.0)),
|
||||||
);
|
);
|
||||||
if let Some(ed) = app.editor.as_mut()
|
if let Some(d) = app.dialog.as_mut().filter(|d| d.picks_references()) {
|
||||||
|
d.pick_reference(cell, &sh.name, true);
|
||||||
|
} else if let Some(ed) = app.editor.as_mut()
|
||||||
&& ed.point.is_some()
|
&& ed.point.is_some()
|
||||||
{
|
{
|
||||||
if let Some(start) = ed.point_anchor.filter(|(sheet, _)| *sheet == wb.active_sheet).map(|(_, cell)| cell).or(ed.point_cell) {
|
if let Some(start) = ed.point_anchor.filter(|(sheet, _)| *sheet == wb.active_sheet).map(|(_, cell)| cell).or(ed.point_cell) {
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
//! New Formatting Rule ▸ formula: clicking or dragging on the grid writes the absolute reference
|
||||||
|
//! into the formula box at its caret, as in Excel, and leaves the selection the rule applies to alone.
|
||||||
|
|
||||||
|
use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2, vec2};
|
||||||
|
use gridcraft_engine::Session;
|
||||||
|
use gridcraft_ui_egui::{SheetApp, grid::Geo};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||||
|
|
||||||
|
fn harness() -> Harness {
|
||||||
|
let mut session = Session::new();
|
||||||
|
session.new_workbook();
|
||||||
|
session.execute("home.insertSheet", json!({})).unwrap();
|
||||||
|
session.execute("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||||
|
session.execute("selection.set", json!({"range": "F8:G9"})).unwrap();
|
||||||
|
let mut h = egui_kittest::Harness::builder().with_step_dt(1.0 / 60.0).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.state_mut().open_dialog("cfQuick", json!({"type": "expression", "title": "New Formatting Rule"}));
|
||||||
|
h.run_steps(6);
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formula(h: &Harness) -> String {
|
||||||
|
h.state().dialog.as_ref().and_then(|d| d.values.get("formula")).and_then(|v| v.as_str()).unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(h: &Harness) -> String {
|
||||||
|
h.state().session.active().unwrap().selection.current().a1()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_text(h: &mut Harness, text: &str) {
|
||||||
|
for c in text.chars() {
|
||||||
|
h.input_mut().events.push(Event::Text(c.to_string()));
|
||||||
|
h.run_steps(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_center(h: &Harness, a1: &str) -> Pos2 {
|
||||||
|
let app = h.state();
|
||||||
|
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||||
|
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
|
||||||
|
let c = gridcraft_engine::core::CellRef::parse(a1).unwrap();
|
||||||
|
pos2(geo.x(sheet, c.col) + sheet.col_width(c.col) * geo.z * 0.5, geo.y(sheet, c.row) + sheet.row_height(c.row) * geo.z * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn click(h: &mut Harness, a1: &str) {
|
||||||
|
let pos = cell_center(h, a1);
|
||||||
|
for pressed in [true, false] {
|
||||||
|
h.input_mut()
|
||||||
|
.events
|
||||||
|
.extend([Event::PointerMoved(pos), Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
|
||||||
|
h.step();
|
||||||
|
}
|
||||||
|
h.run_steps(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag(h: &mut Harness, from: &str, to: &str) {
|
||||||
|
let (a, b) = (cell_center(h, from), cell_center(h, to));
|
||||||
|
h.input_mut()
|
||||||
|
.events
|
||||||
|
.extend([Event::PointerMoved(a), Event::PointerButton { pos: a, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }]);
|
||||||
|
h.step();
|
||||||
|
for i in 1..=6 {
|
||||||
|
let p = a + (b - a) * (i as f32 / 6.0);
|
||||||
|
h.input_mut().events.push(Event::PointerMoved(p));
|
||||||
|
h.step();
|
||||||
|
}
|
||||||
|
h.input_mut().events.push(Event::PointerButton { pos: b, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||||
|
h.step();
|
||||||
|
h.run_steps(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clicking_a_cell_after_equals_inserts_its_absolute_reference() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
assert_eq!(formula(&h), "=", "the formula box had the keyboard");
|
||||||
|
click(&mut h, "B2");
|
||||||
|
assert_eq!(formula(&h), "=$B$2");
|
||||||
|
assert_eq!(selection(&h), "F8:G9", "the range the rule applies to is still selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clicking_a_cell_in_an_empty_box_starts_the_formula() {
|
||||||
|
let mut h = harness();
|
||||||
|
click(&mut h, "C3");
|
||||||
|
assert_eq!(formula(&h), "=$C$3");
|
||||||
|
assert_eq!(selection(&h), "F8:G9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_second_click_replaces_the_reference_just_inserted() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
click(&mut h, "B2");
|
||||||
|
click(&mut h, "D5");
|
||||||
|
assert_eq!(formula(&h), "=$D$5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dragging_inserts_a_range() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
drag(&mut h, "B2", "C4");
|
||||||
|
assert_eq!(formula(&h), "=$B$2:$C$4");
|
||||||
|
assert_eq!(selection(&h), "F8:G9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_reference_goes_at_the_caret_and_typing_continues_after_it() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=SUM()");
|
||||||
|
h.key_press(Key::ArrowLeft);
|
||||||
|
h.run_steps(2);
|
||||||
|
click(&mut h, "B2");
|
||||||
|
assert_eq!(formula(&h), "=SUM($B$2)");
|
||||||
|
type_text(&mut h, "+1");
|
||||||
|
assert_eq!(formula(&h), "=SUM($B$2+1)", "the box has the keyboard back, its caret after the reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_click_after_an_operand_changes_nothing() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=A1>5");
|
||||||
|
click(&mut h, "B2");
|
||||||
|
assert_eq!(formula(&h), "=A1>5");
|
||||||
|
assert_eq!(selection(&h), "F8:G9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ok_applies_the_picked_formula_to_the_selection() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
click(&mut h, "B2");
|
||||||
|
type_text(&mut h, ">0");
|
||||||
|
h.key_press(Key::Enter);
|
||||||
|
h.run_steps(4);
|
||||||
|
assert!(h.state().dialog.is_none(), "Enter confirmed the rule");
|
||||||
|
let sheet = h.state().session.active().unwrap().wb.active().unwrap();
|
||||||
|
let cf = sheet.cond_formats.last().expect("a rule was added");
|
||||||
|
assert_eq!(cf.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>(), ["F8:G9"]);
|
||||||
|
assert!(format!("{:?}", cf.rule).contains("$B$2>0"), "rule: {:?}", cf.rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_dialogs_leave_grid_clicks_alone() {
|
||||||
|
let mut h = harness();
|
||||||
|
h.state_mut().dialog = None;
|
||||||
|
h.state_mut().open_dialog("cfQuick", json!({"type": "containsText", "title": "Text that Contains"}));
|
||||||
|
h.run_steps(6);
|
||||||
|
click(&mut h, "B2");
|
||||||
|
assert_eq!(selection(&h), "B2", "a box that is not a formula does not take references");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointing_on_another_sheet_keeps_the_rule_on_the_sheet_it_was_opened_for() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": 1})).unwrap();
|
||||||
|
h.run_steps(3);
|
||||||
|
click(&mut h, "A1");
|
||||||
|
assert_eq!(formula(&h), "=Sheet2!$A$1");
|
||||||
|
h.key_press(Key::Enter);
|
||||||
|
h.run_steps(4);
|
||||||
|
assert!(h.state().dialog.is_none(), "Enter confirmed the rule");
|
||||||
|
let wb = &h.state().session.active().unwrap().wb;
|
||||||
|
assert!(wb.sheet(1).unwrap().cond_formats.is_empty(), "nothing lands on the sheet pointed at");
|
||||||
|
let cf = wb.sheet(0).unwrap().cond_formats.last().expect("the rule is on the sheet the dialog was opened on");
|
||||||
|
assert_eq!(cf.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>(), ["F8:G9"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_rule_follows_its_sheet_when_sheets_move_while_the_dialog_is_open() {
|
||||||
|
let mut h = harness();
|
||||||
|
type_text(&mut h, "=");
|
||||||
|
click(&mut h, "B2");
|
||||||
|
h.state_mut().session.execute("sheet.move", json!({"sheet": 0, "to": 1})).unwrap();
|
||||||
|
h.run_steps(3);
|
||||||
|
let names = |h: &Harness| h.state().session.active().unwrap().wb.sheets.iter().map(|s| s.name.clone()).collect::<Vec<_>>();
|
||||||
|
assert_eq!(names(&h), ["Sheet2", "Sheet1"], "Sheet1 now sits at index 1");
|
||||||
|
h.key_press(Key::Enter);
|
||||||
|
h.run_steps(4);
|
||||||
|
assert!(h.state().dialog.is_none(), "Enter confirmed the rule");
|
||||||
|
let wb = &h.state().session.active().unwrap().wb;
|
||||||
|
let sheet1 = wb.sheets.iter().find(|s| s.name == "Sheet1").unwrap();
|
||||||
|
let sheet2 = wb.sheets.iter().find(|s| s.name == "Sheet2").unwrap();
|
||||||
|
assert!(sheet2.cond_formats.is_empty(), "nothing lands on the sheet that took index 0");
|
||||||
|
let cf = sheet1.cond_formats.last().expect("the rule stays on Sheet1");
|
||||||
|
assert_eq!(cf.ranges.iter().map(|r| r.a1()).collect::<Vec<_>>(), ["F8:G9"]);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ mod autosave_persist;
|
|||||||
mod borders;
|
mod borders;
|
||||||
mod cell_editor_caret;
|
mod cell_editor_caret;
|
||||||
mod cell_pictures;
|
mod cell_pictures;
|
||||||
|
mod cf_rule_pick;
|
||||||
mod chart_resize;
|
mod chart_resize;
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
mod comments_pane;
|
mod comments_pane;
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
# UI parity
|
# UI parity
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (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-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
|
||||||
|
|
||||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
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
|
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||||
@@ -152,6 +152,9 @@ Seek, Scenario Manager, Data Analysis, Forecast Sheet and others exist. Missing
|
|||||||
Solver, Power Query editor, Insert Object, Equation editor, Protect/Allow Edit Ranges,
|
Solver, Power Query editor, Insert Object, Equation editor, Protect/Allow Edit Ranges,
|
||||||
Workbook Links, Accessibility checker. Like Excel's, every dialog opens centred and moves by its
|
Workbook Links, Accessibility checker. Like Excel's, every dialog opens centred and moves by its
|
||||||
title bar, keeping its place while it is open (#1250).
|
title bar, keeping its place while it is open (#1250).
|
||||||
|
New Formatting Rule's formula box points like the formula bar: a click on the grid writes the
|
||||||
|
cell's absolute reference at the caret (`=$B$2`) and a drag a range, leaving the selection the rule
|
||||||
|
applies to as it is (#2348). Other dialogs' reference fields don't take clicks yet.
|
||||||
|
|
||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
@@ -175,6 +178,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 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 | 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) |
|
| 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) |
|
||||||
| 2026-10-22 | minor | Show Outline Symbols (`view.showOutlineSymbols`, Ctrl+8): a sheet can hide the outline's level buttons and brackets while keeping its outline, as Excel saves with `showOutlineSymbols="0"` (read and written); the grid then gives the bands no room |
|
| 2026-10-22 | minor | Show Outline Symbols (`view.showOutlineSymbols`, Ctrl+8): a sheet can hide the outline's level buttons and brackets while keeping its outline, as Excel saves with `showOutlineSymbols="0"` (read and written); the grid then gives the bands no room |
|
||||||
|
|||||||
Reference in New Issue
Block a user