mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-30 13:46:47 -06:00
Merge storytold/gridcraft#2501: Error Checking: its arrow opens a Circular References submenu listing a cell on each loop
This commit is contained in:
@@ -1132,7 +1132,12 @@ impl Calc {
|
||||
}
|
||||
let tw = prof_now();
|
||||
self.last_recalc_cells += results.len();
|
||||
self.circular = cycles;
|
||||
// The loops this round finds replace the ones listed for the formulas it evaluated.
|
||||
if !self.circular.is_empty() {
|
||||
let evaluated: HashSet<Key> = dirty.iter().copied().collect();
|
||||
self.circular.retain(|k| !evaluated.contains(k));
|
||||
}
|
||||
self.circular.extend(cycles);
|
||||
// Write back.
|
||||
let mut spill_changes: Vec<Key> = Vec::new();
|
||||
let mut freed: Vec<(usize, RangeRef)> = Vec::new();
|
||||
@@ -1245,6 +1250,13 @@ impl Calc {
|
||||
}
|
||||
dirty = self.dirty_closure(wb, &spill_changes);
|
||||
}
|
||||
// Loops among formulas the run did not evaluate stay listed while they are still formulas.
|
||||
// Iterative calculation reports none.
|
||||
if wb.calc.iterative {
|
||||
self.circular.clear();
|
||||
} else {
|
||||
self.circular.retain(|k| self.graph.nodes.contains_key(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,6 +384,32 @@ fn circular_references_without_iterative_calculation() {
|
||||
assert_eq!(t.get("A1"), Value::Error(CellError::Circ));
|
||||
}
|
||||
|
||||
/// The list of circular references keeps the loops that a recalculation of other cells does not
|
||||
/// reach, and drops a loop once a constant is typed over one of its cells.
|
||||
#[test]
|
||||
fn circular_references_stay_listed_until_the_loop_is_removed() {
|
||||
let mut t = T::new();
|
||||
t.set("A1", "=A1+1");
|
||||
t.set("B1", "=B2");
|
||||
t.set("B2", "=B1");
|
||||
assert!(t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
|
||||
t.set("D4", "7");
|
||||
t.set("D5", "=D4*2");
|
||||
assert!(t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
|
||||
assert!(t.calc.circular.iter().any(|k| *k == (0, c("B1")) || *k == (0, c("B2"))), "{:?}", t.calc.circular);
|
||||
t.set("A1", "5");
|
||||
assert!(!t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
|
||||
assert!(t.calc.circular.iter().any(|k| *k == (0, c("B1")) || *k == (0, c("B2"))), "{:?}", t.calc.circular);
|
||||
t.set("B2", "3");
|
||||
assert!(t.calc.circular.is_empty(), "{:?}", t.calc.circular);
|
||||
// In manual calculation only the edited formulas are evaluated; a constant typed over the listed cell removes it.
|
||||
t.wb.calc.mode = gridcraft_model::CalcMode::Manual;
|
||||
t.set("C1", "=C1+1");
|
||||
assert_eq!(t.calc.circular, vec![(0, c("C1"))]);
|
||||
t.set("C1", "1");
|
||||
assert!(t.calc.circular.is_empty(), "{:?}", t.calc.circular);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_text_coerces_in_the_workbook_date_system() {
|
||||
for (sys, serial) in [(gridcraft_core::DateSystem::D1900, 1462.0), (gridcraft_core::DateSystem::D1904, 0.0)] {
|
||||
|
||||
@@ -142,6 +142,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Odebrat šipky",
|
||||
"Show Formulas" => "Zobrazit vzorce",
|
||||
"Error Checking" => "Kontrola chyb",
|
||||
"Circular References" => "Cyklické odkazy",
|
||||
"Evaluate Formula" => "Vyhodnotit vzorec",
|
||||
"Watch Window" => "Okno sledování",
|
||||
"Calculation\nOptions" => "Možnosti\nvýpočtu",
|
||||
|
||||
@@ -146,6 +146,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Pfeile entfernen",
|
||||
"Show Formulas" => "Formeln anzeigen",
|
||||
"Error Checking" => "Fehlerüberprüfung",
|
||||
"Circular References" => "Zirkelbezüge",
|
||||
"Evaluate Formula" => "Formel auswerten",
|
||||
"Watch Window" => "Überwachungsfenster",
|
||||
"Calculation\nOptions" => "Berechnungs-\noptionen",
|
||||
|
||||
@@ -141,6 +141,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Κατάργηση βελών",
|
||||
"Show Formulas" => "Εμφάνιση τύπων",
|
||||
"Error Checking" => "Έλεγχος σφαλμάτων",
|
||||
"Circular References" => "Κυκλικές αναφορές",
|
||||
"Evaluate Formula" => "Αξιολόγηση τύπου",
|
||||
"Watch Window" => "Παράθυρο παρακολούθησης",
|
||||
"Calculation\nOptions" => "Επιλογές\nυπολογισμού",
|
||||
|
||||
@@ -145,6 +145,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Quitar flechas",
|
||||
"Show Formulas" => "Mostrar fórmulas",
|
||||
"Error Checking" => "Comprobación de errores",
|
||||
"Circular References" => "Referencias circulares",
|
||||
"Evaluate Formula" => "Evaluar fórmula",
|
||||
"Watch Window" => "Ventana Inspección",
|
||||
"Calculation\nOptions" => "Opciones\nde cálculo",
|
||||
|
||||
@@ -154,6 +154,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Supprimer les flèches",
|
||||
"Show Formulas" => "Afficher les formules",
|
||||
"Error Checking" => "Vérification des erreurs",
|
||||
"Circular References" => "Références circulaires",
|
||||
"Evaluate Formula" => "Évaluation de formule",
|
||||
"Watch Window" => "Fenêtre Espion",
|
||||
"Calculation\nOptions" => "Options de\ncalcul",
|
||||
|
||||
@@ -149,6 +149,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Rimuovi frecce",
|
||||
"Show Formulas" => "Mostra formule",
|
||||
"Error Checking" => "Controllo errori",
|
||||
"Circular References" => "Riferimenti circolari",
|
||||
"Evaluate Formula" => "Valuta formula",
|
||||
"Watch Window" => "Finestra controlli",
|
||||
"Calculation\nOptions" => "Opzioni di\ncalcolo",
|
||||
|
||||
@@ -154,6 +154,7 @@ pub(super) const CORE: &[(&str, &str)] = &[
|
||||
("Remove Arrows", "矢印の解除"),
|
||||
("Show Formulas", "数式の表示"),
|
||||
("Error Checking", "エラーチェック"),
|
||||
("Circular References", "循環参照"),
|
||||
("Evaluate Formula", "数式の検証"),
|
||||
("Watch Window", "ウォッチ ウィンドウ"),
|
||||
("Calculation\nOptions", "計算方法の設定"),
|
||||
|
||||
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "화살표 제거",
|
||||
"Show Formulas" => "수식 표시",
|
||||
"Error Checking" => "오류 검사",
|
||||
"Circular References" => "순환 참조",
|
||||
"Evaluate Formula" => "수식 계산",
|
||||
"Watch Window" => "감시 창",
|
||||
"Calculation\nOptions" => "계산\n옵션",
|
||||
|
||||
@@ -150,6 +150,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Pijlen verwijderen",
|
||||
"Show Formulas" => "Formules weergeven",
|
||||
"Error Checking" => "Foutopsporing",
|
||||
"Circular References" => "Kringverwijzingen",
|
||||
"Evaluate Formula" => "Formule evalueren",
|
||||
"Watch Window" => "Bewakingsvenster",
|
||||
"Calculation\nOptions" => "Berekenings-\nopties",
|
||||
|
||||
@@ -145,6 +145,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Usuń strzałki",
|
||||
"Show Formulas" => "Pokaż formuły",
|
||||
"Error Checking" => "Sprawdzanie błędów",
|
||||
"Circular References" => "Odwołania cykliczne",
|
||||
"Evaluate Formula" => "Szacuj formułę",
|
||||
"Watch Window" => "Okno czujki",
|
||||
"Calculation\nOptions" => "Opcje\nobliczania",
|
||||
|
||||
@@ -148,6 +148,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Remover Setas",
|
||||
"Show Formulas" => "Exibir Fórmulas",
|
||||
"Error Checking" => "Verificação de Erros",
|
||||
"Circular References" => "Referências Circulares",
|
||||
"Evaluate Formula" => "Avaliar Fórmula",
|
||||
"Watch Window" => "Janela de Inspeção",
|
||||
"Calculation\nOptions" => "Opções de\nCálculo",
|
||||
|
||||
@@ -152,6 +152,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Eliminare săgeți",
|
||||
"Show Formulas" => "Afișare formule",
|
||||
"Error Checking" => "Verificare erori",
|
||||
"Circular References" => "Referințe circulare",
|
||||
"Evaluate Formula" => "Evaluare formulă",
|
||||
"Watch Window" => "Fereastră de urmărire",
|
||||
"Calculation\nOptions" => "Opțiuni de\ncalcul",
|
||||
|
||||
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Убрать стрелки",
|
||||
"Show Formulas" => "Показать формулы",
|
||||
"Error Checking" => "Проверка ошибок",
|
||||
"Circular References" => "Циклические ссылки",
|
||||
"Evaluate Formula" => "Вычислить формулу",
|
||||
"Watch Window" => "Окно контрольных значений",
|
||||
"Calculation\nOptions" => "Параметры\nвычислений",
|
||||
|
||||
@@ -146,6 +146,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Ukloni strelice",
|
||||
"Show Formulas" => "Prikaži formule",
|
||||
"Error Checking" => "Provera grešaka",
|
||||
"Circular References" => "Kružne reference",
|
||||
"Evaluate Formula" => "Proceni formulu",
|
||||
"Watch Window" => "Prozor za praćenje",
|
||||
"Calculation\nOptions" => "Opcije\nizračunavanja",
|
||||
|
||||
@@ -144,6 +144,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Okları Kaldır",
|
||||
"Show Formulas" => "Formülleri Göster",
|
||||
"Error Checking" => "Hata Denetimi",
|
||||
"Circular References" => "Döngüsel Başvurular",
|
||||
"Evaluate Formula" => "Formülü Değerlendir",
|
||||
"Watch Window" => "Gözcü Penceresi",
|
||||
"Calculation\nOptions" => "Hesaplama\nSeçenekleri",
|
||||
|
||||
@@ -147,6 +147,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Прибрати стрілки",
|
||||
"Show Formulas" => "Показати формули",
|
||||
"Error Checking" => "Перевірка помилок",
|
||||
"Circular References" => "Циклічні посилання",
|
||||
"Evaluate Formula" => "Обчислити формулу",
|
||||
"Watch Window" => "Окно контрольних значень",
|
||||
"Calculation\nOptions" => "Параметри\nобчислень",
|
||||
|
||||
@@ -153,6 +153,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "Bỏ mũi tên",
|
||||
"Show Formulas" => "Hiện công thức",
|
||||
"Error Checking" => "Kiểm tra lỗi",
|
||||
"Circular References" => "Tham chiếu vòng",
|
||||
"Evaluate Formula" => "Đánh giá công thức",
|
||||
"Watch Window" => "Cửa sổ theo dõi",
|
||||
"Calculation\nOptions" => "Tùy chọn\ntính toán",
|
||||
|
||||
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Remove Arrows" => "删除箭头",
|
||||
"Show Formulas" => "显示公式",
|
||||
"Error Checking" => "错误检查",
|
||||
"Circular References" => "循环引用",
|
||||
"Evaluate Formula" => "公式求值",
|
||||
"Watch Window" => "监视窗口",
|
||||
"Calculation\nOptions" => "计算\n选项",
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::SheetApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::keytips::keytip_table;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::widgets::{big_button, color_palette, icon_button, small_button, split_button, toggle_button};
|
||||
use crate::widgets::{big_button, color_palette, icon_button, small_button, small_split_button, split_button, toggle_button};
|
||||
|
||||
pub const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"];
|
||||
|
||||
@@ -382,6 +382,23 @@ fn act(app: &mut SheetApp, id: &str, params: serde_json::Value) {
|
||||
app.run_or_alert(id, params);
|
||||
}
|
||||
|
||||
/// How many cells Formulas › Error Checking › Circular References lists.
|
||||
const CIRCULAR_MENU_LIMIT: usize = 100;
|
||||
|
||||
/// The cell listed for each circular reference, for the Circular References submenu: each once, in the order
|
||||
/// the calculation found them, as (sheet index, cell, "Sheet!A1").
|
||||
fn circular_references(app: &SheetApp) -> Vec<(usize, String, String)> {
|
||||
let Some(d) = app.session.active() else { return Vec::new() };
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
d.calc
|
||||
.circular
|
||||
.iter()
|
||||
.filter(|k| seen.insert(**k))
|
||||
.filter_map(|(si, c)| d.wb.sheet(*si).map(|sh| (*si, c.a1(), format!("{}!{}", sh.name, c.a1()))))
|
||||
.take(CIRCULAR_MENU_LIMIT)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Keytips are painted, not focusable widgets: the grid retains keyboard focus.
|
||||
fn keytip(app: &SheetApp, ui: &Ui, sequence: &str, at: egui::Pos2) {
|
||||
let Some(rest) = app.keytips.prefix().and_then(|prefix| sequence.strip_prefix(prefix)).filter(|s| !s.is_empty()) else { return };
|
||||
@@ -2009,11 +2026,30 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) {
|
||||
if __k.clicked() {
|
||||
act(app, "formulas.showFormulas", json!({}));
|
||||
}
|
||||
let __k = small_button(ui, Icon::Validation, "Error Checking", "Error Checking", false);
|
||||
control_keytip(app, ui, &__k, "dialog:errorChecking");
|
||||
if __k.clicked() {
|
||||
let (ec, ec_arrow) = small_split_button(ui, Icon::Validation, "Error Checking", "Error Checking");
|
||||
control_keytip(app, ui, &ec, "dialog:errorChecking");
|
||||
if ec.clicked() {
|
||||
app.open_dialog("errorChecking", json!({}));
|
||||
}
|
||||
egui::Popup::menu(&ec_arrow).show(|ui| {
|
||||
let lang = app.ui.language;
|
||||
if ui.button(format!("{}…", lang.tr("Error Checking"))).clicked() {
|
||||
app.open_dialog("errorChecking", json!({}));
|
||||
}
|
||||
let circular = circular_references(app);
|
||||
ui.add_enabled_ui(!circular.is_empty(), |ui| {
|
||||
ui.menu_button(lang.tr("Circular References"), |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("circular_references").max_height(400.0).show(ui, |ui| {
|
||||
for (sheet, cell, label) in circular {
|
||||
if ui.button(label).clicked() {
|
||||
act(app, "selection.set", json!({"range": cell, "sheet": sheet}));
|
||||
app.grid.ensure_visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
let __k = small_button(ui, Icon::Calc, "Evaluate Formula", "Evaluate Formula", false);
|
||||
control_keytip(app, ui, &__k, "dialog:evaluateFormula");
|
||||
if __k.clicked() {
|
||||
|
||||
@@ -160,6 +160,34 @@ pub fn split_button(ui: &mut Ui, icon: Icon, accent: Option<Color32>, tip: &str)
|
||||
(m.on_hover_text(tip_text(ui, tip)).clicked(), a)
|
||||
}
|
||||
|
||||
/// A small ribbon split button with a label (Error Checking): the icon and label run the default
|
||||
/// action, the arrow at the right opens a menu. Returns (main response, arrow response).
|
||||
pub fn small_split_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str) -> (Response, Response) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let font = theme::ui_font(12.5);
|
||||
let label = tr(ui, label);
|
||||
let tw = ui.painter().layout_no_wrap(label.to_string(), font.clone(), t.text).size().x + 6.0;
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(24.0 + tw + 14.0, 23.0), Sense::hover());
|
||||
let main = Rect::from_min_size(rect.min, vec2(24.0 + tw, rect.height()));
|
||||
let arrow = Rect::from_min_max(pos2(main.right(), rect.top()), rect.max);
|
||||
let m = ui.interact(main, ui.id().with((tip, "main")), Sense::click());
|
||||
let a = ui.interact(arrow, ui.id().with((tip, "arrow")), Sense::click());
|
||||
if m.hovered() || a.hovered() {
|
||||
ui.painter().rect_filled(rect, 4.0, t.hover);
|
||||
for (part, r) in [(&m, main), (&a, arrow)] {
|
||||
if part.is_pointer_button_down_on() {
|
||||
ui.painter().rect_filled(r, 4.0, t.pressed);
|
||||
}
|
||||
}
|
||||
ui.painter().line_segment([pos2(main.right(), rect.top() + 3.0), pos2(main.right(), rect.bottom() - 3.0)], Stroke::new(1.0, t.separator));
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.left() + 12.0, rect.center().y), vec2(16.0, 16.0)), icon, t.text);
|
||||
ui.painter().text(pos2(rect.left() + 24.0, rect.center().y), Align2::LEFT_CENTER, label, font, t.text);
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(arrow.center().x, rect.center().y + 1.0), vec2(10.0, 10.0)), Icon::Chevron, t.text_dim);
|
||||
let tip = tip_text(ui, tip);
|
||||
(m.on_hover_text(tip.clone()), a.on_hover_text(tip))
|
||||
}
|
||||
|
||||
/// A one-line text field for a value stored in the workbook, which callers reload every frame. The text being typed is
|
||||
/// kept in egui memory until the field loses focus, then returned to commit (`None` when Escape cancels the edit).
|
||||
pub fn committed_text(ui: &mut Ui, id: egui::Id, current: &str, width: Option<f32>) -> Option<String> {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Formulas › Error Checking is a split button, as in Excel: the button opens the Error Checking
|
||||
//! dialog, and its arrow opens a menu whose Circular References submenu lists a cell on each
|
||||
//! circular reference and selects the one picked, on its own sheet.
|
||||
|
||||
use egui::{Event, Shape, pos2, vec2};
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
/// The submenu item, with the arrow egui puts after a submenu's label.
|
||||
const CIRCULAR: &str = "Circular References ⏵";
|
||||
|
||||
fn harness(loops: bool) -> Harness {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("home.insertSheet", json!({"name": "Data"})).unwrap();
|
||||
if loops {
|
||||
s.execute("cell.set", json!({"cell": "B3", "input": "=B4"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "B4", "input": "=B3"})).unwrap();
|
||||
}
|
||||
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||
if loops {
|
||||
s.execute("cell.set", json!({"cell": "C2", "input": "=C2+1"})).unwrap();
|
||||
}
|
||||
s.execute("selection.set", json!({"range": "H20"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(1400.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.state_mut().ui.ribbon_tab = "Formulas".into();
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
/// The painted "Error Checking" ribbon label: its left edge, right edge and vertical center.
|
||||
fn label(h: &Harness) -> (f32, f32, f32) {
|
||||
h.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.find_map(|s| match &s.shape {
|
||||
Shape::Text(t) if t.galley.job.text == "Error Checking" => {
|
||||
Some((t.pos.x, t.pos.x + t.galley.size().x, t.pos.y + t.galley.size().y / 2.0))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("the Formulas tab shows Error Checking")
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness, at: egui::Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.run_steps(2);
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn open_menu(h: &mut Harness) {
|
||||
let (_, right, y) = label(h);
|
||||
// The arrow sits right of the label, past the 6 px gap after the text.
|
||||
click(h, pos2(right + 13.0, y));
|
||||
}
|
||||
|
||||
fn hover(h: &mut Harness, label: &str) {
|
||||
let at = h.get_by_label(label).rect().center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.run_steps(6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_checking_button_opens_the_dialog_and_its_arrow_opens_the_menu() {
|
||||
let mut h = harness(false);
|
||||
let (left, _, y) = label(&h);
|
||||
click(&mut h, pos2(left + 4.0, y));
|
||||
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the button opens Error Checking");
|
||||
h.state_mut().dialog = None;
|
||||
h.run_steps(2);
|
||||
open_menu(&mut h);
|
||||
assert!(h.state().dialog.is_none(), "the arrow opens a menu, not the dialog");
|
||||
h.get_by_label("Error Checking…").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the menu's first item opens Error Checking");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_references_is_unavailable_without_a_circular_reference() {
|
||||
let mut h = harness(false);
|
||||
open_menu(&mut h);
|
||||
assert!(h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_references_lists_the_loops_and_selects_the_cell_picked() {
|
||||
let mut h = harness(true);
|
||||
open_menu(&mut h);
|
||||
assert!(!h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
|
||||
hover(&mut h, CIRCULAR);
|
||||
assert!(h.query_by_label("Sheet1!C2").is_some(), "the loop on the active sheet is listed");
|
||||
let other = ["Data!B3", "Data!B4"].into_iter().find(|l| h.query_by_label(l).is_some()).expect("the loop on Data is listed");
|
||||
h.get_by_label(other).click();
|
||||
h.run_steps(4);
|
||||
let app = h.state();
|
||||
let d = app.session.active().unwrap();
|
||||
assert_eq!(d.wb.active_sheet, 1, "picking a cell on Data goes to Data");
|
||||
assert_eq!(format!("Data!{}", d.selection.active.a1()), other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_references_scrolls_to_the_cell_picked() {
|
||||
let mut h = harness(false);
|
||||
h.state_mut().run("cell.set", json!({"cell": "C500", "input": "=C500+1"})).unwrap();
|
||||
h.state_mut().run("selection.set", json!({"range": "H20"})).unwrap();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().view().scroll.y, 0.0, "setup: the view is at the top of the sheet");
|
||||
open_menu(&mut h);
|
||||
hover(&mut h, CIRCULAR);
|
||||
h.get_by_label("Sheet1!C500").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "C500");
|
||||
assert!(h.state().view().scroll.y > 0.0, "the grid scrolls to the cell picked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_references_scrolls_a_list_taller_than_the_window() {
|
||||
let mut h = harness(false);
|
||||
for row in 1..=100 {
|
||||
h.state_mut().run("cell.set", json!({"cell": format!("A{row}"), "input": format!("=A{row}+1")})).unwrap();
|
||||
}
|
||||
h.state_mut().run("selection.set", json!({"range": "H20"})).unwrap();
|
||||
h.run_steps(4);
|
||||
open_menu(&mut h);
|
||||
hover(&mut h, CIRCULAR);
|
||||
let window = h.ctx.content_rect();
|
||||
assert!(h.get_by_label("Sheet1!A100").rect().bottom() > window.bottom(), "setup: the last cell starts below the window");
|
||||
hover(&mut h, "Sheet1!A1");
|
||||
for _ in 0..20 {
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: vec2(0.0, -10.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
});
|
||||
h.run_steps(4);
|
||||
}
|
||||
let last = h.get_by_label("Sheet1!A100").rect();
|
||||
assert!(last.bottom() <= window.bottom(), "the wheel brings the last cell into the window: {last:?} in {window:?}");
|
||||
click(&mut h, last.center());
|
||||
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "A100");
|
||||
}
|
||||
@@ -24,6 +24,7 @@ mod dialog_confirm;
|
||||
mod dialog_focus;
|
||||
mod dialog_move;
|
||||
mod drag_cursor;
|
||||
mod error_checking_menu;
|
||||
mod file_menu;
|
||||
mod fill_handle_double_click;
|
||||
mod fill_handle_right_drag;
|
||||
|
||||
@@ -24,6 +24,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Formula bar with Name Box, expand, function hints | yes | yes | done |
|
||||
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | Ctrl-drag (Cmd or Option on macOS) drops a copy (#971); active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
|
||||
| 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 |
|
||||
| Formulas › Error Checking arrow: Circular References | yes | yes: the button opens Error Checking and its arrow opens a menu with Error Checking… and Circular References, which lists a cell on each circular reference as `Sheet!A1`, up to 100, and selects the one picked, on its own sheet (unavailable when there are none) | done; the Error Checking… item and the limit of 100 are GridCraft's own |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user