diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 9c6d89eeb..662c87ef1 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -2452,14 +2452,25 @@ pub fn shortcut(app: &mut SheetApp, key: Key, m: Modifiers) { Key::Num9 => Some(("home.hideRows", json!({}))), Key::Num0 if shift => Some(("home.unhideColumns", json!({}))), Key::Num0 => Some(("home.hideColumns", json!({}))), - Key::Minus => { - app.open_dialog("deleteCells", json!({})); - None - } - Key::Plus | Key::Equals if shift => { - app.open_dialog("insertCells", json!({})); - None - } + // As in Excel: with whole rows or whole columns selected, Ctrl+Shift+= / Ctrl+- insert or + // delete them at once; any other selection asks which way to shift. + Key::Minus => match whole_lines(app) { + Some(true) => Some(("home.deleteRows", json!({}))), + Some(false) => Some(("home.deleteColumns", json!({}))), + None => { + app.open_dialog("deleteCells", json!({})); + None + } + }, + // Insert works on one area at a time, so several areas still go through the dialog. + Key::Plus | Key::Equals if shift => match whole_lines(app).filter(|_| single_area(app)) { + Some(true) => Some(("home.insertRows", json!({}))), + Some(false) => Some(("home.insertColumns", json!({}))), + None => { + app.open_dialog("insertCells", json!({})); + None + } + }, Key::Semicolon => { // Insert today's date. let text = app @@ -2514,3 +2525,23 @@ mod checkbox_on_tests { assert!(!checkbox_on(false, true)); } } + +/// Whether the selection is a single area. +fn single_area(app: &SheetApp) -> bool { + app.session.active().is_some_and(|d| d.selection.ranges.len() == 1) +} + +/// `Some(true)` when every selected area is whole rows, `Some(false)` when every one is whole +/// columns, `None` otherwise (including the whole sheet). +fn whole_lines(app: &SheetApp) -> Option { + let sel = &app.session.active()?.selection; + let rows = sel.ranges.iter().all(|r| r.is_full_rows() && !r.is_full_cols()); + let cols = sel.ranges.iter().all(|r| r.is_full_cols() && !r.is_full_rows()); + if rows { + Some(true) + } else if cols { + Some(false) + } else { + None + } +} diff --git a/crates/ui-egui/tests/whole_line_shortcuts.rs b/crates/ui-egui/tests/whole_line_shortcuts.rs new file mode 100644 index 000000000..2bbcf3adf --- /dev/null +++ b/crates/ui-egui/tests/whole_line_shortcuts.rs @@ -0,0 +1,65 @@ +//! Ctrl+Shift+= and Ctrl+- on whole rows or columns insert or delete them at once, as in Excel; +//! any other selection opens the Insert or Delete dialog. + +use egui::{Key, Modifiers}; +use gridcraft_engine::Session; +use gridcraft_engine::core::{CellRef, Value}; +use gridcraft_ui_egui::SheetApp; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(selection: &str) -> Harness { + let mut session = Session::new(); + session.new_workbook(); + session.execute("range.setValues", json!({"range": "A1", "values": [["a1", "b1"], ["a2", "b2"], ["a3", "b3"]]})).unwrap(); + session.execute("selection.set", json!({"range": selection})).unwrap(); + 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 value(h: &Harness, a1: &str) -> Value { + h.state().session.active().unwrap().wb.active().unwrap().value(CellRef::parse(a1).unwrap()) +} + +fn press(h: &mut Harness, key: Key, shift: bool) { + let modifiers = Modifiers { shift, ..Modifiers::COMMAND }; + h.key_press_modifiers(modifiers, key); + h.run_steps(3); +} + +#[test] +fn whole_rows_are_inserted_and_deleted_at_once() { + let mut h = harness("2:2"); + press(&mut h, Key::Equals, true); + assert!(h.state().dialog.is_none(), "no dialog for whole rows"); + assert!(value(&h, "A2").is_empty(), "a row was inserted above row 2"); + assert_eq!(value(&h, "A3"), Value::from("a2")); + press(&mut h, Key::Minus, false); + assert_eq!(value(&h, "A2"), Value::from("a2"), "and deleted again"); +} + +#[test] +fn whole_columns_are_inserted_and_deleted_at_once() { + let mut h = harness("A:A"); + press(&mut h, Key::Equals, true); + assert!(h.state().dialog.is_none(), "no dialog for whole columns"); + assert_eq!(value(&h, "B1"), Value::from("a1")); + press(&mut h, Key::Minus, false); + assert_eq!(value(&h, "A1"), Value::from("a1")); +} + +#[test] +fn other_selections_still_ask_which_way_to_shift() { + let mut h = harness("A2:B2"); + press(&mut h, Key::Minus, false); + assert_eq!(h.state().dialog.as_ref().map(|d| d.name().to_string()).as_deref(), Some("deleteCells")); +}