From c35ab0f4f4e61c442803114e639db8113ab1fdac Mon Sep 17 00:00:00 2001 From: Hector Villarreal Date: Tue, 3 Nov 2026 01:41:55 -0600 Subject: [PATCH] #1482: every Paste Special option has its Excel access key Paste Special only knew the letters F, V, T, C and E. Excel gives every option one: A (All), N (Validation), X (All except borders), W (Column widths), R (Formulas and number formats), U (Values and number formats); O, D, S, M, I for the operations; B for Skip blanks; and L for the Paste Link button, which pastes at once. One table now maps each letter to what it does; Enter still pastes with the chosen options. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/paste.rs | 75 ++++++++++++++----- .../ui-egui/tests/it/paste_special_dialog.rs | 47 ++++++++++++ docs/ui-parity.md | 3 +- 3 files changed, 104 insertions(+), 21 deletions(-) diff --git a/crates/ui-egui/src/dialogs/paste.rs b/crates/ui-egui/src/dialogs/paste.rs index 9ac0bc579..253729b2a 100644 --- a/crates/ui-egui/src/dialogs/paste.rs +++ b/crates/ui-egui/src/dialogs/paste.rs @@ -5,6 +5,8 @@ use super::{Dialog, Field, Json, Key, SheetApp, Stroke, json, theme, vec2}; /// come from the dialog's `fields`, so the letter shortcuts and the control channel see the same. pub(super) fn paste_special(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) { let lang = app.ui.language; + // L, the Paste Link button's access key, pastes at once as the button does. + let link_now = d.values.remove(PASTE_LINK_NOW).is_some_and(|v| v == json!(true)); let t = theme::Tokens::get(ui.ctx()); type Choice = (&'static str, &'static str, Vec<(&'static str, &'static str)>); let choices: Vec = d @@ -56,7 +58,7 @@ pub(super) fn paste_special(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialo ui.separator(); ui.horizontal(|ui| { // Excel's Paste Link pastes references to the copied cells straight away. - if ui.button(lang.tr("Paste Link")).on_hover_text(lang.tr("Paste formulas that refer to the copied cells")).clicked() { + if ui.button(lang.tr("Paste Link")).on_hover_text(lang.tr("Paste formulas that refer to the copied cells")).clicked() || link_now { d.values.insert("what".into(), json!("all")); d.values.insert("operation".into(), json!("none")); d.values.insert("link".into(), json!(true)); @@ -74,6 +76,45 @@ pub(super) fn paste_special(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialo }); } +/// Set by the Paste Link access key for the next frame of the dialog, which then pastes. +const PASTE_LINK_NOW: &str = "pasteLinkNow"; + +/// What one of Paste Special's access keys does: choose an option of a group, or flip a box. +#[derive(Clone, Copy)] +enum Mnemonic { + Set(&'static str, &'static str), + Toggle(&'static str), + /// The Paste Link button: pastes at once. + PasteLink, +} + +/// Excel's access keys in Paste Special (#1482): the Paste choices, the operations, the two check +/// boxes and the Paste Link button, each the letter Excel underlines. +fn mnemonic_action(key: Key) -> Option<(char, Mnemonic)> { + use Mnemonic::{Set, Toggle}; + Some(match key { + Key::A => ('a', Set("what", "all")), + Key::F => ('f', Set("what", "formulas")), + Key::V => ('v', Set("what", "values")), + Key::T => ('t', Set("what", "formats")), + Key::C => ('c', Set("what", "comments")), + Key::N => ('n', Set("what", "validation")), + Key::X => ('x', Set("what", "allExceptBorders")), + Key::W => ('w', Set("what", "columnWidths")), + Key::R => ('r', Set("what", "formulasAndNumberFormats")), + Key::U => ('u', Set("what", "valuesAndNumberFormats")), + Key::O => ('o', Set("operation", "none")), + Key::D => ('d', Set("operation", "add")), + Key::S => ('s', Set("operation", "subtract")), + Key::M => ('m', Set("operation", "multiply")), + Key::I => ('i', Set("operation", "divide")), + Key::B => ('b', Toggle("skipBlanks")), + Key::E => ('e', Toggle("transpose")), + Key::L => ('l', Mnemonic::PasteLink), + _ => return None, + }) +} + /// Paste Special's legacy mnemonics select options; Enter still performs the paste. pub(super) fn paste_special_shortcuts(d: &mut Dialog, ctx: &egui::Context) { if ctx.text_edit_focused() { @@ -88,14 +129,7 @@ pub(super) fn paste_special_shortcuts(d: &mut Dialog, ctx: &egui::Context) { }); let mac = ctx.os() == egui::os::OperatingSystem::Mac; let allowed = |m: egui::Modifiers| !m.ctrl && !m.command && !m.mac_cmd && !(m.alt && (mac || right_alt)); - let mnemonic = |key| match key { - Key::T => Some('t'), - Key::V => Some('v'), - Key::F => Some('f'), - Key::C => Some('c'), - Key::E => Some('e'), - _ => None, - }; + let mnemonic = |key| mnemonic_action(key).map(|(letter, _)| letter); let mut text_to_consume = Vec::new(); for event in ctx.input(|i| i.events.clone()) { let egui::Event::Key { key, pressed: true, repeat, modifiers, .. } = event else { continue }; @@ -107,17 +141,18 @@ pub(super) fn paste_special_shortcuts(d: &mut Dialog, ctx: &egui::Context) { if repeat { continue; } - if key == Key::E { - let transpose = d.values.get("transpose").and_then(Json::as_bool).unwrap_or(false); - d.values.insert("transpose".into(), json!(!transpose)); - } else { - let what = match key { - Key::T => "formats", - Key::V => "values", - Key::F => "formulas", - _ => "comments", - }; - d.values.insert("what".into(), json!(what)); + match mnemonic_action(key).map(|(_, action)| action) { + Some(Mnemonic::Set(field, value)) => { + d.values.insert(field.into(), json!(value)); + } + Some(Mnemonic::Toggle(field)) => { + let on = d.values.get(field).and_then(Json::as_bool).unwrap_or(false); + d.values.insert(field.into(), json!(!on)); + } + Some(Mnemonic::PasteLink) => { + d.values.insert(PASTE_LINK_NOW.into(), json!(true)); + } + None => {} } } // Browsers can deliver Text before or after Key. Act only on Key, then remove diff --git a/crates/ui-egui/tests/it/paste_special_dialog.rs b/crates/ui-egui/tests/it/paste_special_dialog.rs index 161d35292..822503747 100644 --- a/crates/ui-egui/tests/it/paste_special_dialog.rs +++ b/crates/ui-egui/tests/it/paste_special_dialog.rs @@ -54,3 +54,50 @@ fn paste_link_pastes_references_at_once() { assert_eq!(c1["value"], json!(2.0)); assert!(c1["formula"].as_str().is_some_and(|f| f.contains("A1")), "{c1}"); } + +fn press(h: &mut Harness, key: egui::Key) { + for pressed in [true, false] { + h.input_mut().events.push(egui::Event::Key { key, physical_key: Some(key), pressed, repeat: false, modifiers: egui::Modifiers::NONE }); + } + h.run_steps(2); +} + +fn value(h: &Harness, field: &str) -> serde_json::Value { + h.state().dialog.as_ref().and_then(|d| d.values.get(field).cloned()).unwrap_or_default() +} + +#[test] +fn every_option_has_its_excel_access_key() { + // #1482: only F, V, T, C and E worked. + let mut h = harness(); + for (key, field, want) in [ + (egui::Key::N, "what", json!("validation")), + (egui::Key::X, "what", json!("allExceptBorders")), + (egui::Key::W, "what", json!("columnWidths")), + (egui::Key::R, "what", json!("formulasAndNumberFormats")), + (egui::Key::U, "what", json!("valuesAndNumberFormats")), + (egui::Key::A, "what", json!("all")), + (egui::Key::D, "operation", json!("add")), + (egui::Key::S, "operation", json!("subtract")), + (egui::Key::I, "operation", json!("divide")), + (egui::Key::O, "operation", json!("none")), + (egui::Key::M, "operation", json!("multiply")), + (egui::Key::B, "skipBlanks", json!(true)), + ] { + press(&mut h, key); + assert_eq!(value(&h, field), want, "{key:?}"); + } + press(&mut h, egui::Key::B); + assert_eq!(value(&h, "skipBlanks"), json!(false), "B flips Skip blanks back"); + // V then Enter: values multiplied, as with the mouse. + press(&mut h, egui::Key::V); + press(&mut h, egui::Key::Enter); + assert!(h.state().dialog.is_none()); + assert_eq!(cell(&mut h, "C1")["value"], json!(20.0)); + // L is the Paste Link button: it pastes at once. + let mut h = harness(); + press(&mut h, egui::Key::L); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + assert!(cell(&mut h, "C1")["formula"].as_str().is_some_and(|f| f.contains("A1"))); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 125182d27..09f29b9a9 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (every Paste Special option has its access key; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -237,6 +237,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | Paste Special: every option has Excel's access key (A, F, V, T, C, N, X, W, R, U; O, D, S, M, I; B, E; L pastes the link at once), not only F, V, T, C and E (#1482) | | 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into | | 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused | | 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it |