mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 22:26:57 -06:00
UI: AutoComplete for cell values can be turned off, Backspace and Escape drop the proposal (fixes #842)
This commit is contained in:
@@ -824,6 +824,12 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Editing");
|
||||
let mut autocomplete = app.ui.autocomplete;
|
||||
if ui.checkbox(&mut autocomplete, lang.tr("Enable AutoComplete for cell values")).changed() {
|
||||
app.run_or_alert("options.autoComplete", json!({"on": autocomplete}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Save");
|
||||
let mut autosave = app.grid.autosave();
|
||||
if ui.checkbox(&mut autosave, lang.tr("Save .xlsx workbooks automatically after every change")).changed() {
|
||||
|
||||
@@ -181,6 +181,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
|
||||
("view.darkMode", "Dark Mode", ""),
|
||||
("view.theme", "Display Theme", ""),
|
||||
("view.animations", "Animations", "{on?: bool} (toggles without it) → {on}"),
|
||||
("options.autoComplete", "Enable AutoComplete for cell values", "{on?: bool} (toggles without it) → {on}"),
|
||||
("view.interfaceScale", "Interface Size", "{percent: 75–200 | \"auto\"} (100 = auto; clamped)"),
|
||||
("ui.dialog", "Open Dialog", ""),
|
||||
("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|close}"),
|
||||
|
||||
@@ -173,6 +173,10 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
|
||||
let enter = i.consume_key(egui::Modifiers::NONE, Key::Enter) || i.consume_key(egui::Modifiers::SHIFT, Key::Enter);
|
||||
let tab = i.consume_key(egui::Modifiers::NONE, Key::Tab) || i.consume_key(egui::Modifiers::SHIFT, Key::Tab);
|
||||
let esc = i.consume_key(egui::Modifiers::NONE, Key::Escape);
|
||||
// Backspace takes back the AutoComplete proposal (Excel's selected rest), not a typed character.
|
||||
if ed.completion.is_some() && i.consume_key(egui::Modifiers::NONE, Key::Backspace) {
|
||||
ed.completion = None;
|
||||
}
|
||||
(enter, tab, esc, alt_enter, cmd_enter, ctrl_shift_enter, m.shift)
|
||||
});
|
||||
if alt_enter {
|
||||
@@ -198,6 +202,8 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
|
||||
if esc {
|
||||
if !ed.autocomplete.is_empty() {
|
||||
ed.autocomplete.clear();
|
||||
} else if ed.completion.is_some() {
|
||||
ed.completion = None;
|
||||
} else {
|
||||
cancel = true;
|
||||
}
|
||||
@@ -305,7 +311,8 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
|
||||
}
|
||||
if resp.changed() {
|
||||
ed.completion = None;
|
||||
if !ed.is_formula()
|
||||
if app.ui.autocomplete
|
||||
&& !ed.is_formula()
|
||||
&& ed.caret == ed.text.chars().count()
|
||||
&& let Some(sh) = app.session.active().and_then(|d| d.wb.sheet(ed.sheet))
|
||||
{
|
||||
|
||||
@@ -57,6 +57,9 @@ pub struct UiState {
|
||||
pub status_bar: bool,
|
||||
/// Motion in the UI: the selection outline glides to a new selection (View ▸ Animations).
|
||||
pub animations: bool,
|
||||
/// Enable AutoComplete for cell values (File ▸ Options ▸ Editing): typed text is completed
|
||||
/// from the one matching entry in the same column.
|
||||
pub autocomplete: bool,
|
||||
pub recent: Vec<String>,
|
||||
/// One-shot keytip request for a ribbon control: set when a keytip action resolves to
|
||||
/// [`keytips::Action::Control`], consumed (and cleared) by the control's render site.
|
||||
@@ -99,6 +102,7 @@ impl Default for UiState {
|
||||
formula_bar_expanded: false,
|
||||
status_bar: true,
|
||||
animations: true,
|
||||
autocomplete: true,
|
||||
recent: vec![],
|
||||
keytip_invoke: None,
|
||||
language: i18n::Language::En,
|
||||
@@ -497,6 +501,10 @@ impl SheetApp {
|
||||
self.ui.animations = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.animations);
|
||||
Ok(json!({"on": self.ui.animations}))
|
||||
}
|
||||
"options.autoComplete" => {
|
||||
self.ui.autocomplete = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.autocomplete);
|
||||
Ok(json!({"on": self.ui.autocomplete}))
|
||||
}
|
||||
"view.interfaceScale" => {
|
||||
// `{"percent": 125}` or `{"percent": "auto"}`; out-of-range values are clamped.
|
||||
let percent = match p.get("percent") {
|
||||
@@ -780,7 +788,7 @@ impl SheetApp {
|
||||
};
|
||||
let mut ed = editor::EditState::new(d.wb.active_sheet, at, text, replace, from_formula_bar);
|
||||
ed.locale = locale;
|
||||
if replace && !ed.is_formula() {
|
||||
if replace && self.ui.autocomplete && !ed.is_formula() {
|
||||
ed.completion = editor::column_completion(sh, at, &ed.text);
|
||||
}
|
||||
self.editor = Some(ed);
|
||||
|
||||
@@ -485,6 +485,79 @@ fn column_autocomplete_completes_on_enter() {
|
||||
assert_eq!(value(&h, "A5"), Value::from("Ea"));
|
||||
}
|
||||
|
||||
fn completion(h: &egui_kittest::Harness<'static, SheetApp>) -> (String, Option<String>) {
|
||||
let ed = h.state().editor.as_ref().expect("still editing");
|
||||
(ed.text.clone(), ed.completion.clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_autocomplete_backspace_and_escape_drop_only_the_proposal() {
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["Apple"], ["Banana"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A3"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
text(&mut h, "ap");
|
||||
assert_eq!(completion(&h), ("ap".into(), Some("Apple".into())), "\"ple\" is proposed after the caret");
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A3"), Value::from("Apple"), "Enter accepts it, with the entry's own case");
|
||||
text(&mut h, "ba");
|
||||
key(&mut h, Key::Tab, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A4"), Value::from("Banana"), "Tab accepts it too");
|
||||
|
||||
// Backspace removes the proposal, not the typed "a".
|
||||
h.state_mut().run("selection.set", json!({"cell": "A5"})).unwrap();
|
||||
h.run_steps(2);
|
||||
text(&mut h, "ba");
|
||||
key(&mut h, Key::Backspace, Modifiers::NONE);
|
||||
assert_eq!(completion(&h), ("ba".into(), None));
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A5"), Value::from("ba"));
|
||||
|
||||
// Escape dismisses the proposal and keeps the edit; a second Escape cancels it.
|
||||
text(&mut h, "ap");
|
||||
key(&mut h, Key::Escape, Modifiers::NONE);
|
||||
assert_eq!(completion(&h), ("ap".into(), None));
|
||||
key(&mut h, Key::Escape, Modifiers::NONE);
|
||||
assert!(h.state().editor.is_none());
|
||||
assert_eq!(value(&h, "A6"), Value::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_autocomplete_skips_ambiguous_prefixes_and_numbers() {
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["Apricot"], ["Apple"], ["12 Main St"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A4"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
text(&mut h, "ap");
|
||||
assert_eq!(completion(&h), ("ap".into(), None), "Apricot and Apple both match");
|
||||
text(&mut h, "p");
|
||||
assert_eq!(completion(&h), ("app".into(), Some("Apple".into())), "one more letter settles it");
|
||||
h.state_mut().cancel_edit();
|
||||
text(&mut h, "12");
|
||||
assert_eq!(completion(&h), ("12".into(), None), "a number is not completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_autocomplete_can_be_turned_off() {
|
||||
assert!(gridcraft_ui_egui::UiState::default().autocomplete);
|
||||
let old: gridcraft_ui_egui::UiState = serde_json::from_str(r#"{"dark": true}"#).unwrap();
|
||||
assert!(old.autocomplete, "a ui.json written before the setting existed keeps it on");
|
||||
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["North"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A2"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
assert_eq!(h.state_mut().run("options.autoComplete", json!({"on": false})), Ok(json!({"on": false})));
|
||||
assert!(h.state().dialog.is_none());
|
||||
text(&mut h, "No");
|
||||
assert_eq!(completion(&h), ("No".into(), None));
|
||||
text(&mut h, "r");
|
||||
assert_eq!(completion(&h), ("Nor".into(), None), "typing on doesn't bring it back");
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A2"), Value::from("Nor"));
|
||||
assert_eq!(h.state_mut().run("options.autoComplete", json!({})), Ok(json!({"on": true})), "no parameters: it toggles");
|
||||
}
|
||||
|
||||
/// An .xlsm carrying a VBA project, saved to bytes.
|
||||
fn macro_workbook_bytes() -> Vec<u8> {
|
||||
let mut wb = gridcraft_engine::model::Workbook::new();
|
||||
|
||||
Reference in New Issue
Block a user