Merge PR #2093: Paste Special: the reference app's two-column layout, translated, Paste Link button

crates/ui-egui/src/dialogs.rs: window width match keeps both zoom and pasteSpecial arms
docs/ui-parity.md: status line combined, change-log rows both kept
This commit is contained in:
aiongg
2026-10-22 02:36:35 +00:00
4 changed files with 134 additions and 1 deletions
+75
View File
@@ -1107,6 +1107,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
"analysisToolPak" => 420.0,
"solver" => 460.0,
"zoom" => 220.0,
"pasteSpecial" => 480.0,
_ => 380.0,
};
let window = egui::Window::new(app.ui.language.tr(&d.title))
@@ -1140,6 +1141,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
"selectData" => select_data(app, ui, &mut d, &mut confirm, &mut open),
"commandSearch" => command_search(app, ui, &mut d, &mut confirm),
"goalSeek" => goal_seek(app, ui, &mut d, &mut confirm),
"pasteSpecial" => paste_special(app, ui, &mut d, &mut confirm, &mut open),
"analysisToolPak" => analysis_toolpak(app, ui, &mut d, &mut confirm, &mut open),
"solver" => solver(ui, &mut d, &mut confirm, &mut open),
"solverResults" => solver_results(app, ui, &mut d, &mut open),
@@ -1383,6 +1385,79 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
}
/// Paste Special's legacy mnemonics select options; Enter still performs the paste.
/// Paste Special as Excel lays it out: Paste and Operation each in two columns of options, Skip
/// blanks and Transpose side by side, and Paste Link as a button that pastes at once. The options
/// come from the dialog's `fields`, so the letter shortcuts and the control channel see the same.
fn paste_special(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) {
let lang = app.ui.language;
let t = theme::Tokens::get(ui.ctx());
let choices: Vec<(&'static str, &'static str, Vec<(&'static str, &'static str)>)> = d
.fields
.iter()
.filter_map(|f| match f {
Field::Choice { key, label, options } => Some((*key, *label, options.clone())),
_ => None,
})
.collect();
for (key, label, options) in choices {
ui.add_space(4.0);
ui.label(egui::RichText::new(lang.tr(label)).strong());
egui::Frame::new().stroke(Stroke::new(1.0, t.separator)).corner_radius(4.0).inner_margin(egui::Margin::symmetric(10, 6)).show(ui, |ui| {
ui.set_min_width(ui.available_width());
let cur = d.values.get(key).and_then(Json::as_str).unwrap_or("").to_string();
// Column by column, as Excel fills them.
let rows = options.len().div_ceil(2);
egui::Grid::new(("paste_special", key)).num_columns(2).min_col_width(200.0).spacing(vec2(16.0, 4.0)).show(ui, |ui| {
for r in 0..rows {
for c in 0..2 {
if let Some((value, text)) = options.get(c * rows + r) {
if ui.radio(cur == *value, lang.tr(text)).clicked() {
d.set_value(key, json!(value));
}
} else {
ui.label("");
}
}
ui.end_row();
}
});
});
}
ui.add_space(8.0);
// In the same columns as the options above.
egui::Frame::new().inner_margin(egui::Margin::symmetric(11, 0)).show(ui, |ui| {
egui::Grid::new("paste_special_checks").num_columns(2).min_col_width(200.0).spacing(vec2(16.0, 4.0)).show(ui, |ui| {
for (key, label) in [("skipBlanks", "Skip blanks"), ("transpose", "Transpose (E)")] {
let mut b = d.values.get(key).and_then(Json::as_bool).unwrap_or(false);
if ui.checkbox(&mut b, lang.tr(label)).changed() {
d.values.insert(key.into(), json!(b));
}
}
ui.end_row();
});
});
ui.add_space(8.0);
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() {
d.values.insert("what".into(), json!("all"));
d.values.insert("operation".into(), json!("none"));
d.values.insert("link".into(), json!(true));
*confirm = true;
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let ok = ui.add(egui::Button::new(egui::RichText::new(format!(" {} ", lang.tr("OK"))).color(Color32::WHITE)).fill(t.accent));
if ok.clicked() || ui.input(|i| i.key_pressed(Key::Enter)) {
*confirm = true;
}
if ui.button(format!(" {} ", lang.tr("Cancel"))).clicked() {
*open = false;
}
});
});
}
fn paste_special_shortcuts(d: &mut Dialog, ctx: &egui::Context) {
if ctx.text_edit_focused() {
return;
+1
View File
@@ -427,6 +427,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Select a chart first." => "Selecione um gráfico primeiro.",
"Text Box" => "Caixa de Texto",
"Select Data" => "Selecionar Dados",
"Paste formulas that refer to the copied cells" => "Colar fórmulas que se referem às células copiadas",
"Row Height" => "Altura da Linha",
"Column Width" => "Largura da Coluna",
"Standard Width" => "Largura Padrão",
@@ -0,0 +1,56 @@
//! Paste Special: options in two columns, translated, OK pastes with them and Paste Link links.
use egui_kittest::kittest::Queryable as _;
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn harness() -> Harness {
let mut s = Session::new();
s.new_workbook();
s.execute("range.setValues", json!({"range": "A1", "values": [[2], [3]]})).unwrap();
s.execute("range.setValues", json!({"range": "C1", "values": [[10], [10]]})).unwrap();
s.execute("edit.copy", json!({"range": "A1:A2"})).unwrap();
s.execute("selection.set", json!({"cell": "C1"})).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(s, Default::default()),
);
h.state_mut().open_dialog("pasteSpecial", json!({}));
h.run_steps(4);
h
}
fn cell(h: &mut Harness, a1: &str) -> serde_json::Value {
h.state_mut().session.execute("cell.get", json!({"cell": a1})).unwrap()
}
#[test]
fn an_operation_chosen_in_the_dialog_is_applied() {
let mut h = harness();
h.get_by_label("Values (V)").click();
h.get_by_label("Multiply").click();
h.run_steps(3);
h.get_by_label_contains("OK").click();
h.run_steps(3);
assert!(h.state().dialog.is_none());
assert_eq!(cell(&mut h, "C1")["value"], json!(20.0));
assert_eq!(cell(&mut h, "C2")["value"], json!(30.0));
}
#[test]
fn paste_link_pastes_references_at_once() {
let mut h = harness();
h.get_by_label("Paste Link").click();
h.run_steps(3);
assert!(h.state().dialog.is_none());
let c1 = cell(&mut h, "C1");
assert_eq!(c1["value"], json!(2.0));
assert!(c1["formula"].as_str().is_some_and(|f| f.contains("A1")), "{c1}");
}
+2 -1
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; dialogs move by their title bar, #1250; Advanced Filter dialog, #1622; Remove Duplicates range prefill, #1388) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; dialogs move by their title bar, #1250; Advanced Filter dialog, #1622; Remove Duplicates range prefill, #1388; Paste Special in Excel's two-column layout) · **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
@@ -164,6 +164,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | minor | Paste Special laid out as Excel's: Paste and Operation options each in two columns inside a frame, Skip blanks and Transpose side by side, Paste Link as a button that pastes references at once; labels translated (the generic form didn't translate them) and the window no longer runs past the screen |
| 2026-10-19 | minor | Ctrl+Enter (`range.fill`) gives each cell what typing the input there gives: a General cell gets the automatic number format (`5%`, `$5`, `3/4/2026`, `=TODAY()`) even when it already held a value, and a cell with its own format, a formatted row or column, or Text keeps it. Cells that held a value used to keep General (`5%` showed 0.05), and empty cells took the active cell's format |
| 2026-10-18 | minor | Keytip letters typed with left Alt held continue the sequence (Alt+H, O, A / Alt+H, O, I ran nothing before: Alt+letter cancelled keytips); other Alt combinations, AltGr and macOS Option are unchanged (#1846) |
| 2026-10-16 | minor | Advanced Filter ribbon dialog, optional criteria for unique-only extraction (#1622); safe copy-to output (#1031) |