//! Excel keyboard shortcuts (crate::keymap) dispatched through real egui key events. use egui::{Event, Key, Modifiers}; use gridcraft_engine::Session; use gridcraft_engine::core::CellRef; use gridcraft_engine::model::BorderStyle; use gridcraft_ui_egui::SheetApp; use gridcraft_ui_egui::keymap::{Shortcut, lookup}; use serde_json::json; type Harness = egui_kittest::Harness<'static, SheetApp>; fn harness(session: Session) -> Harness { let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( |ui, app: &mut SheetApp| { let ctx = ui.ctx().clone(); app.logic(&ctx); app.ui(ui); }, SheetApp::new(session, Default::default()), ); h.run_steps(4); h } fn book(selected: &str) -> Session { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [[1.5, 2], [3, 4], [5, 6]]})).unwrap(); s.execute("cell.set", json!({"cell": "C1", "input": "=A1*2"})).unwrap(); s.execute("selection.set", json!({"range": selected})).unwrap(); s } /// A key press as a US keyboard reports it (the physical key is the unshifted one). fn key(h: &mut Harness, k: Key, m: Modifiers) { h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: true, repeat: false, modifiers: m }); h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: false, repeat: false, modifiers: m }); h.run_steps(2); } fn style(h: &Harness, a1: &str) -> gridcraft_engine::model::Style { let d = h.state().session.active().unwrap(); let sh = d.wb.active().unwrap(); d.wb.styles.get(sh.style_id(CellRef::parse(a1).unwrap())).clone() } /// Ctrl on Windows and Linux sets both `ctrl` and `command`. const CTRL: Modifiers = Modifiers { alt: false, ctrl: true, shift: false, mac_cmd: false, command: true }; const CTRL_SHIFT: Modifiers = Modifiers { shift: true, ..CTRL }; #[test] fn ctrl_shift_symbols_apply_number_formats_and_borders() { let mut h = harness(book("A1:B2")); key(&mut h, Key::Num4, CTRL_SHIFT); assert_eq!(style(&h, "B2").num_fmt.as_str(), "$#,##0.00_);($#,##0.00)"); key(&mut h, Key::Num5, CTRL_SHIFT); assert_eq!(style(&h, "A1").num_fmt.as_str(), "0%"); key(&mut h, Key::Backtick, CTRL_SHIFT); assert_eq!(style(&h, "A1").num_fmt.as_str(), "General"); key(&mut h, Key::Num7, CTRL_SHIFT); assert!(style(&h, "A1").borders.top.style != BorderStyle::None && style(&h, "B2").borders.right.style != BorderStyle::None); key(&mut h, Key::Minus, CTRL_SHIFT); assert!(style(&h, "A1").borders.top.style == BorderStyle::None); // Ctrl+Shift+End extends from the anchor to the last used cell. key(&mut h, Key::End, CTRL_SHIFT); assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "A1:C3"); } #[test] fn copy_formula_from_above_and_function_keys() { let mut h = harness(book("C2")); key(&mut h, Key::Quote, CTRL); assert_eq!(h.state().editor.as_ref().map(|e| e.text.clone()).as_deref(), Some("=A1*2")); key(&mut h, Key::Escape, Modifiers::NONE); assert!(h.state().editor.is_none()); let sheets = h.state().session.active().unwrap().wb.sheets.len(); key(&mut h, Key::F11, Modifiers::SHIFT); assert_eq!(h.state().session.active().unwrap().wb.sheets.len(), sheets + 1); // Back on the data: Alt+F1 charts the current region, Ctrl+Alt+= zooms in. h.state_mut().session.run("sheet.previous", json!({})).unwrap(); h.state_mut().session.run("selection.set", json!({"range": "A1:B3"})).unwrap(); key(&mut h, Key::F1, Modifiers::ALT); assert_eq!(h.state().session.active().unwrap().wb.sheets[0].charts.len(), 1); key(&mut h, Key::Equals, Modifiers { alt: true, ..CTRL }); assert_eq!(h.state().session.active().unwrap().wb.sheets[0].zoom, 110); } #[test] fn windows_and_mac_variants() { let cmd = CTRL; let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }; let ctrl = Modifiers::CTRL; let run = |id: &'static str| Some(Shortcut::Run(id, json!({}))); assert_eq!(lookup(Key::L, Some(Key::L), cmd, false), run("insert.table")); assert_eq!(lookup(Key::L, Some(Key::L), mac_cmd, true), run("home.alignLeft")); assert_eq!(lookup(Key::E, Some(Key::E), mac_cmd, true), run("home.alignCenter")); assert_eq!(lookup(Key::E, Some(Key::E), ctrl, true), run("edit.flashFill")); assert_eq!(lookup(Key::Equals, Some(Key::Equals), Modifiers::ALT, false), run("formulas.autoSum")); // The shifted character doesn't matter: Ctrl+Shift+1 is the Number format wherever `!` is. assert_eq!( lookup(Key::Exclamationmark, Some(Key::Num1), CTRL_SHIFT, false), Some(Shortcut::Run("home.numberFormat", json!({"code": "#,##0.00"}))) ); // Navigation keys go by what they are, not where: the numpad's arrows and Page Down (Num // Lock off) report Num6 / Num3 as their physical keys. let alt_shift = Modifiers { alt: true, shift: true, ..Modifiers::NONE }; assert_eq!(lookup(Key::ArrowRight, Some(Key::Num6), alt_shift, false), run("data.group")); assert_eq!(lookup(Key::ArrowLeft, Some(Key::Num4), alt_shift, false), run("data.ungroup")); assert_eq!(lookup(Key::PageDown, Some(Key::Num3), Modifiers::ALT, false), Some(Shortcut::ScreenRight)); assert_eq!(lookup(Key::PageDown, Some(Key::Num3), CTRL_SHIFT, false), None); // Plain keys stay with the grid (typing, navigation). assert_eq!(lookup(Key::A, Some(Key::A), Modifiers::NONE, false), None); assert_eq!(lookup(Key::F2, Some(Key::F2), Modifiers::NONE, false), None); } #[test] fn space_selection_shortcuts_do_not_start_an_edit() { let mut h = harness(book("B2")); // Shift+Space: the key press, then the space the OS types with it. let shift = Modifiers::SHIFT; h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: Some(Key::Space), pressed: true, repeat: false, modifiers: shift }); h.input_mut().events.push(Event::Text(" ".into())); h.run_steps(2); assert!(h.state().editor.is_none(), "Shift+Space started an edit"); assert_eq!(h.state().session.active().unwrap().selection.ranges[0].a1(), "2:2"); h.state_mut().session.run("selection.set", json!({"range": "B2"})).unwrap(); // Ctrl+Space selects the column, also without starting an edit. h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: Some(Key::Space), pressed: true, repeat: false, modifiers: CTRL }); h.input_mut().events.push(Event::Text(" ".into())); h.run_steps(2); assert!(h.state().editor.is_none()); assert_eq!(h.state().session.active().unwrap().selection.ranges[0].a1(), "B:B"); // Plain typing still edits. h.input_mut().events.push(Event::Text("x".into())); h.run_steps(2); assert!(h.state().editor.is_some()); } #[test] fn space_shortcuts_per_platform() { let run = |id: &'static str| Some(Shortcut::Run(id, json!({}))); let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }; let space = |m, mac| lookup(Key::Space, Some(Key::Space), m, mac); assert_eq!(space(Modifiers::SHIFT, false), run("selection.row")); assert_eq!(space(Modifiers::SHIFT, true), run("selection.row")); assert_eq!(space(CTRL, false), run("selection.column")); assert_eq!(space(Modifiers::CTRL, true), run("selection.column")); // Cmd+Space is Spotlight on macOS. assert_eq!(space(mac_cmd, true), None); }