From fb7886ca9e8a852098e786b4c0f74c1ce1264507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Thu, 24 Jul 2025 19:40:15 +0200 Subject: [PATCH] UPDATE: Add keyboard shortcuts for move column/row Also clean up a bit of the keyboard shortcuts mess --- base/src/actions.rs | 2 +- base/src/user_model/ui.rs | 45 ++++++-- .../src/components/Workbook/Workbook.tsx | 11 ++ .../Workbook/useKeyboardNavigation.ts | 109 ++++++++++++------ webapp/IronCalc/src/locale/en_us.json | 8 +- 5 files changed, 125 insertions(+), 50 deletions(-) diff --git a/base/src/actions.rs b/base/src/actions.rs index aa82c17..30c4029 100644 --- a/base/src/actions.rs +++ b/base/src/actions.rs @@ -607,7 +607,7 @@ impl Model { .set_cell_style(target_row, c, style_idx)?; } - let worksheet = &mut self.workbook.worksheets[sheet as usize]; + let worksheet = &mut self.workbook.worksheet_mut(sheet)?; let mut new_rows = Vec::new(); for r in worksheet.rows.iter() { if r.r == row { diff --git a/base/src/user_model/ui.rs b/base/src/user_model/ui.rs index 1b9238c..fd29995 100644 --- a/base/src/user_model/ui.rs +++ b/base/src/user_model/ui.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ - constants::LAST_ROW, + constants::{LAST_COLUMN, LAST_ROW}, expressions::utils::{is_valid_column_number, is_valid_row}, worksheet::NavigationDirection, }; @@ -114,7 +114,7 @@ impl UserModel { Ok(()) } - /// Sets the selected range. Note that the selected cell must be in one of the corners. + /// Sets the selected range. Note that the selected cell must be in the selected range. pub fn set_selected_range( &mut self, start_row: i32, @@ -148,16 +148,32 @@ impl UserModel { if let Some(view) = worksheet.views.get_mut(&0) { let selected_row = view.row; let selected_column = view.column; - // The selected cells must be on one of the corners of the selected range: - if selected_row != start_row && selected_row != end_row { - return Err(format!( - "The selected cells is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'" + if start_row == 1 && end_row == LAST_ROW { + // full row selected. The cell must be at the top or the bottom of the range + if selected_column != start_column && selected_column != end_column { + return Err(format!( + "The selected cell is not the column edge. Column '{selected_column}' and column range '({start_column}, {end_column})'" + )); + } + } else if start_column == 1 && end_column == LAST_COLUMN { + // full column selected. The cell must be at the left or the right of the range + if selected_row != start_row && selected_row != end_row { + return Err(format!( + "The selected cell is not in the row edge. Row: '{selected_row}' and row range '({start_row}, {end_row})'" + )); + } + } else { + // The selected cells must be on one of the corners of the selected range: + if selected_row != start_row && selected_row != end_row { + return Err(format!( + "The selected cell is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'" )); - } - if selected_column != start_column && selected_column != end_column { - return Err(format!( - "The selected cells is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'" + } + if selected_column != start_column && selected_column != end_column { + return Err(format!( + "The selected cell is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'" )); + } } view.range = [start_row, start_column, end_row, end_column]; } @@ -194,6 +210,15 @@ impl UserModel { return Ok(()); }; let [row_start, column_start, row_end, column_end] = range; + if ["ArrowUp", "ArrowDown"].contains(&key) && row_start == 1 && row_end == LAST_ROW { + // full column selected, nothing to do + return Ok(()); + } + if ["ArrowRight", "ArrowLeft"].contains(&key) && column_start == 1 && column_end == LAST_COLUMN + { + // full row selected, nothing to do + return Ok(()); + } match key { "ArrowRight" => { diff --git a/webapp/IronCalc/src/components/Workbook/Workbook.tsx b/webapp/IronCalc/src/components/Workbook/Workbook.tsx index 24d0212..63e79ac 100644 --- a/webapp/IronCalc/src/components/Workbook/Workbook.tsx +++ b/webapp/IronCalc/src/components/Workbook/Workbook.tsx @@ -13,6 +13,7 @@ import Worksheet from "../Worksheet/Worksheet"; import { COLUMN_WIDTH_SCALE, LAST_COLUMN, + LAST_ROW, ROW_HEIGH_SCALE, } from "../WorksheetCanvas/constants"; import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas"; @@ -318,6 +319,16 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { workbookState.clearCutRange(); setRedrawId((id) => id + 1); }, + onSelectColumn: (): void => { + const { column } = model.getSelectedView(); + model.setSelectedRange(1, column, LAST_ROW, column); + setRedrawId((id) => id + 1); + }, + onSelectRow: (): void => { + const { row } = model.getSelectedView(); + model.setSelectedRange(row, 1, row, LAST_COLUMN); + setRedrawId((id) => id + 1); + }, root: rootRef, }); diff --git a/webapp/IronCalc/src/components/Workbook/useKeyboardNavigation.ts b/webapp/IronCalc/src/components/Workbook/useKeyboardNavigation.ts index dd8cbde..b4004d9 100644 --- a/webapp/IronCalc/src/components/Workbook/useKeyboardNavigation.ts +++ b/webapp/IronCalc/src/components/Workbook/useKeyboardNavigation.ts @@ -32,6 +32,8 @@ interface Options { onNextSheet: () => void; onPreviousSheet: () => void; onEscape: () => void; + onSelectColumn: () => void; + onSelectRow: () => void; root: RefObject; } @@ -51,6 +53,16 @@ interface Options { // * Ctrl+u/i/b: style // * Ctrl+z/y: undo/redo // * F2: start editing +// * Ctrl+Space: select column +// * Shift+Space: select row +// +// # Not implemented yet: +// * Ctrl+a: select all (continuous area around the selection, if it exists, +// otherwise select whole sheet) +// * Ctrl+Shift+Arrows: select to edge +// * Ctrl+Shift+Home/End: select to end +// * Ctrl+Shift++: (after selecting) insert row/column (also Alt+I, R or C) +// * Ctrl+-: (after selecting) delete row/column // References: // In Google Sheets: Ctrl+/ shows the list of keyboard shortcuts @@ -63,6 +75,7 @@ const useKeyboardNavigation = ( const onKeyDown = useCallback( (event: KeyboardEvent) => { const { key } = event; + const lowerKey = key.toLowerCase(); const { root } = options; // Silence the linter if (!root.current) { @@ -71,45 +84,40 @@ const useKeyboardNavigation = ( if (event.target !== root.current) { return; } - if (event.metaKey || event.ctrlKey) { - switch (key) { + const isCtrl = event.metaKey || event.ctrlKey; + const isShift = event.shiftKey; + const isAlt = event.altKey; + if (isCtrl && !isShift && !isAlt) { + // Ctrl+... + switch (lowerKey) { case "z": { - if (event.shiftKey) { - options.onRedo(); - } else { - options.onUndo(); - } + options.onUndo(); event.stopPropagation(); event.preventDefault(); - break; } case "y": { options.onRedo(); event.stopPropagation(); event.preventDefault(); - break; } case "b": { options.onBold(); event.stopPropagation(); event.preventDefault(); - break; } case "i": { options.onItalic(); event.stopPropagation(); event.preventDefault(); - break; } case "u": { options.onUnderline(); event.stopPropagation(); event.preventDefault(); - break; } case "a": { @@ -119,18 +127,61 @@ const useKeyboardNavigation = ( event.preventDefault(); break; } + case " ": { + options.onSelectColumn(); + event.stopPropagation(); + event.preventDefault(); + break; + } // No default } if (isNavigationKey(key)) { // Ctrl+Arrows, Ctrl+Home/End options.onNavigationToEdge(key); - // navigate_to_edge_in_direction event.stopPropagation(); event.preventDefault(); } return; } - if (event.altKey) { + if (isCtrl && isShift && !isAlt) { + // Ctrl+Shift+... + switch (lowerKey) { + case "z": { + options.onRedo(); + event.stopPropagation(); + event.preventDefault(); + break; + } + } + return; + } + if (isShift && !isAlt && !isCtrl) { + // Shift+... + switch (key) { + case " ": { + options.onSelectRow(); + event.stopPropagation(); + event.preventDefault(); + break; + } + case "ArrowRight": + case "ArrowLeft": + case "ArrowUp": + case "ArrowDown": { + options.onExpandAreaSelectedKeyboard(key); + break; + } + case "Tab": { + options.onArrowLeft(); + event.stopPropagation(); + event.preventDefault(); + break; + } + } + return; + } + if (isAlt && !isCtrl && !isShift) { + // Alt+... switch (key) { case "ArrowDown": { // select next sheet @@ -147,6 +198,12 @@ const useKeyboardNavigation = ( break; } } + return; + } + // At this point we know that no modifier keys are pressed + if (isCtrl || isShift || isAlt) { + // If any modifier key is pressed, we do not handle the key + return; } if (key === "F2") { options.onCellEditStart(); @@ -162,67 +219,43 @@ const useKeyboardNavigation = ( return; } // Worksheet Navigation - if (event.shiftKey) { - if ( - key === "ArrowRight" || - key === "ArrowLeft" || - key === "ArrowUp" || - key === "ArrowDown" - ) { - options.onExpandAreaSelectedKeyboard(key); - } else if (key === "Tab") { - options.onArrowLeft(); - event.stopPropagation(); - event.preventDefault(); - } - return; - } switch (key) { case "ArrowRight": case "Tab": { options.onArrowRight(); - break; } case "ArrowLeft": { options.onArrowLeft(); - break; } case "ArrowDown": case "Enter": { options.onArrowDown(); - break; } case "ArrowUp": { options.onArrowUp(); - break; } case "End": { options.onKeyEnd(); - break; } case "Home": { options.onKeyHome(); - break; } case "Delete": { options.onCellsDeleted(); - break; } case "PageDown": { options.onPageDown(); - break; } case "PageUp": { options.onPageUp(); - break; } case "Escape": { diff --git a/webapp/IronCalc/src/locale/en_us.json b/webapp/IronCalc/src/locale/en_us.json index db69a3a..ee4ffeb 100644 --- a/webapp/IronCalc/src/locale/en_us.json +++ b/webapp/IronCalc/src/locale/en_us.json @@ -118,7 +118,13 @@ "delete_column": "Delete column '{{column}}'", "freeze": "Freeze", "insert_row": "Insert row", - "insert_column": "Insert column" + "insert_column": "Insert column", + "move_row": "Move row", + "move_column": "Move column", + "move_row_up": "Move row up", + "move_row_down": "Move row down", + "move_column_left": "Move column left", + "move_column_right": "Move column right" }, "color_picker": { "apply": "Add color",