mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-05 01:58:58 +00:00
Merge PR #3240
This commit is contained in:
commit
85f09f05d6
3 files changed
+104
-21
No files matched your search
@@ -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<Choice> = 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
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (the Draw tab's live stroke preview uses the pen's colour and thickness; earlier: copy quotes only cells with a tab or line break, as Excel does; earlier: Ctrl+. Next Corner of Selection; earlier: dragging the selection's border onto data asks before replacing it; 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
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (every Paste Special option has its access key; earlier: the Draw tab's live stroke preview uses the pen's colour and thickness; earlier: copy quotes only cells with a tab or line break, as Excel does; earlier: Ctrl+. Next Corner of Selection; earlier: dragging the selection's border onto data asks before replacing it; 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
|
||||
@@ -238,6 +238,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-03 | minor | Draw tab: the stroke drawn under the pointer while dragging a pen is the ink that will be committed, in the pen's colour and thickness (translucent for a highlighter), instead of always blue at 2 pt (#1786) |
|
||||
| 2026-11-03 | minor | Copy puts a cell that holds a quote on the clipboard as it is (`say "hi"`), quoting only text with a tab or line break, as Excel does; pasting text reads a field as quoted only when its closing quote ends it, so `"hi" there` pastes with its quotes (#2225) |
|
||||
| 2026-11-03 | minor | Ctrl+. (Control+. on a Mac): Next Corner of Selection, `selection.nextCorner`: the active cell moves clockwise to the next corner of the selected area, which stays selected, and becomes the anchor, as in Excel (#1442) |
|
||||
|
||||
Reference in new issue
Block a user