mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 06:06:48 -06:00
Merge pull request #288 from vangel76/fix/insert-copied-cells
Clipboard: Insert Copied Cells and Insert Cut Cells (#21)
This commit is contained in:
@@ -616,6 +616,8 @@ fn copy_impl(s: &mut Session, p: &Json, cut: bool) -> Result<Json> {
|
||||
if ranges.len() > 1 && cut {
|
||||
return Err(multiple());
|
||||
}
|
||||
// The selection as made (untrimmed), for Insert Copied Cells.
|
||||
let selected = ranges.first().copied().unwrap_or_default();
|
||||
let d = s.doc()?;
|
||||
let Some(sh) = d.wb.sheet(sheet) else { return Err(EngineError::NoDocument) };
|
||||
let trim = |range: RangeRef| {
|
||||
@@ -637,7 +639,7 @@ fn copy_impl(s: &mut Session, p: &Json, cut: bool) -> Result<Json> {
|
||||
// The HTML rendering is for the host clipboard (UI); it can reach 4 MB, so agents and
|
||||
// scripts get it only when they ask for it.
|
||||
let html = if p.get("html").and_then(Json::as_bool) == Some(true) { crate::io::range_html(&wb, sheet, range, &text) } else { None };
|
||||
let clip = Clipboard { wb, sheet, range, cut, doc_uid: d.uid, text: text.clone(), areas };
|
||||
let clip = Clipboard { wb, sheet, range, cut, doc_uid: d.uid, text: text.clone(), areas, selected };
|
||||
s.clipboard = Some(clip);
|
||||
let mut result = json!({"range": range.a1(), "text": text});
|
||||
if let Some(html) = html {
|
||||
|
||||
@@ -36,6 +36,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Axis::Cols
|
||||
)),
|
||||
cmd!("home.insertCells", "Insert Cells…", ["Home", "Cells", "Insert"], None, "{range?, shift: right|down|row|column}", has_doc, insert_cells),
|
||||
cmd!(
|
||||
"home.insertCopiedCells",
|
||||
"Insert Copied Cells",
|
||||
["Home", "Cells", "Insert"],
|
||||
None,
|
||||
"{at?: \"B3\", shift?: down|right}: inserts the copied cells at `at` (the active cell) and pastes them there. Whole copied rows or columns insert whole rows or columns; other blocks shift the cells at `at` down (default) or right. A cut of whole rows or columns moves them (Insert Cut Cells)",
|
||||
has_clipboard,
|
||||
insert_copied_cells
|
||||
),
|
||||
cmd!("home.deleteCells", "Delete Cells…", ["Home", "Cells", "Delete"], None, "{range?, shift: left|up|row|column}", has_doc, delete_cells),
|
||||
cmd!("home.insertSheet", "Insert Sheet", ["Home", "Cells", "Insert"], Some("Shift+F11"), "{name?, before?: index}", has_doc, insert_sheet),
|
||||
cmd!("home.deleteSheet", "Delete Sheet", ["Home", "Cells", "Delete"], None, "{sheet?}", has_doc, delete_sheet),
|
||||
@@ -385,6 +394,128 @@ fn delete_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Insert Copied Cells / Insert Cut Cells: makes room at the destination, then pastes (or, for a
|
||||
/// cut, moves) the clipboard there, as one undo step.
|
||||
fn insert_copied_cells(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some(clip) = s.clipboard.clone() else { return Err(EngineError::Other("Nothing to paste.".into())) };
|
||||
let sheet = target_sheet(s, p)?;
|
||||
let d = s.doc()?;
|
||||
let at = match str_param(p, "at") {
|
||||
Some(t) => cell_param(p, "at").ok_or_else(|| bad("home.insertCopiedCells", format!("not a cell: {t}")))?,
|
||||
None => d.selection.current().start,
|
||||
};
|
||||
let shift = str_param(p, "shift").unwrap_or("down");
|
||||
if shift != "down" && shift != "right" {
|
||||
return Err(bad("home.insertCopiedCells", format!("unknown `shift`: {shift} (down or right)")));
|
||||
}
|
||||
let sel = clip.selected;
|
||||
let rows = sel.is_full_rows();
|
||||
let cols = sel.is_full_cols();
|
||||
if rows && cols {
|
||||
return Err(EngineError::Other("The copy area is the whole sheet; there is no room to insert it.".into()));
|
||||
}
|
||||
let same_doc = d.uid == clip.doc_uid;
|
||||
let cut = clip.cut && same_doc;
|
||||
if cut && !rows && !cols {
|
||||
return Err(EngineError::Other("Insert Cut Cells works on whole rows or columns; cut whole rows or columns, or use Paste.".into()));
|
||||
}
|
||||
// A moved block can't be inserted inside itself.
|
||||
if cut && clip.sheet == sheet {
|
||||
let inside = if rows { at.row > sel.start.row && at.row <= sel.end.row } else { at.col > sel.start.col && at.col <= sel.end.col };
|
||||
if inside {
|
||||
return Err(EngineError::Other("The cut rows or columns can't be inserted inside themselves.".into()));
|
||||
}
|
||||
}
|
||||
let revision = d.revision;
|
||||
let before = (d.wb.clone(), d.selection.clone());
|
||||
let result = insert_copied_steps(s, &clip, sheet, at, shift, cut);
|
||||
let d = s.doc_mut()?;
|
||||
// The nested steps collapse into this command's single undo step.
|
||||
for _ in 0..d.revision.saturating_sub(revision) {
|
||||
d.undo.pop();
|
||||
}
|
||||
match result {
|
||||
Ok((final_range, out)) => {
|
||||
d.selection = crate::Selection { active: final_range.start, anchor: final_range.start, ranges: vec![final_range] };
|
||||
// Like inserting cells, this ends copy mode (the copied block may have moved).
|
||||
cancel_copy_mode(s);
|
||||
Ok(out)
|
||||
}
|
||||
Err(e) => {
|
||||
d.wb = before.0;
|
||||
d.selection = before.1;
|
||||
d.calc.rebuild(&d.wb);
|
||||
s.clipboard = Some(clip);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_copied_steps(s: &mut Session, clip: &crate::Clipboard, sheet: usize, at: CellRef, shift: &str, cut: bool) -> Result<(RangeRef, Json)> {
|
||||
let sel = clip.selected;
|
||||
let rows = sel.is_full_rows();
|
||||
let cols = sel.is_full_cols();
|
||||
// Where the copied block's top-left lands, relative to the (untrimmed) copied lines.
|
||||
let (inserted, paste_at) = if rows {
|
||||
let h = sel.height();
|
||||
let start = at.row;
|
||||
let r = format!("{}:{}", start as u64 + 1, start as u64 + h as u64);
|
||||
s.execute("home.insertRows", json!({"rows": r, "sheet": sheet}))?;
|
||||
let dr = clip.range.start.row.saturating_sub(sel.start.row);
|
||||
(RangeRef::rows(start, start.saturating_add(h - 1).min(MAX_ROWS - 1)), CellRef::new(start.saturating_add(dr), clip.range.start.col))
|
||||
} else if cols {
|
||||
let w = sel.width();
|
||||
let start = at.col;
|
||||
let end = start.saturating_add(w - 1).min(MAX_COLS - 1);
|
||||
s.execute("home.insertColumns", json!({"cols": RangeRef::cols(start, end).a1(), "sheet": sheet}))?;
|
||||
let dc = clip.range.start.col.saturating_sub(sel.start.col);
|
||||
(RangeRef::cols(start, end), CellRef::new(clip.range.start.row, start.saturating_add(dc)))
|
||||
} else {
|
||||
let end = at
|
||||
.offset(clip.range.height() as i64 - 1, clip.range.width() as i64 - 1)
|
||||
.ok_or_else(|| EngineError::Other("The copied cells don't fit on the sheet at this position.".into()))?;
|
||||
let block = RangeRef::new(at, end);
|
||||
s.execute("home.insertCells", json!({"range": block.a1(), "shift": shift, "sheet": sheet}))?;
|
||||
(block, at)
|
||||
};
|
||||
// Inserting ended copy mode; bring the clipboard back for the paste. A cut reads the live
|
||||
// workbook, so it follows the source if the insert pushed it along.
|
||||
let mut clip = clip.clone();
|
||||
if cut {
|
||||
let pushed = clip.sheet == sheet && if rows { sel.start.row >= at.row } else { sel.start.col >= at.col };
|
||||
if pushed {
|
||||
let (dr, dc) = if rows { (inserted.height() as i64, 0) } else { (0, inserted.width() as i64) };
|
||||
clip.range = RangeRef::new(clip.range.start.offset_clamped(dr, dc), clip.range.end.offset_clamped(dr, dc));
|
||||
clip.selected = RangeRef::new(sel.start.offset_clamped(dr, dc), sel.end.offset_clamped(dr, dc));
|
||||
}
|
||||
clip.wb = s.doc()?.wb.clone();
|
||||
}
|
||||
let source_lines = clip.selected;
|
||||
s.clipboard = Some(clip.clone());
|
||||
let out = s.execute("edit.pasteSpecial", json!({"what": "all", "at": paste_at.a1(), "sheet": sheet}))?;
|
||||
if !cut {
|
||||
return Ok((inserted, out));
|
||||
}
|
||||
// Close the gap the moved lines left behind.
|
||||
let (removed, before_dest) = if rows {
|
||||
s.execute(
|
||||
"home.deleteRows",
|
||||
json!({"rows": format!("{}:{}", source_lines.start.row as u64 + 1, source_lines.end.row as u64 + 1), "sheet": clip.sheet}),
|
||||
)?;
|
||||
(source_lines.height(), clip.sheet == sheet && source_lines.start.row < inserted.start.row)
|
||||
} else {
|
||||
s.execute("home.deleteColumns", json!({"cols": RangeRef::cols(source_lines.start.col, source_lines.end.col).a1(), "sheet": clip.sheet}))?;
|
||||
(source_lines.width(), clip.sheet == sheet && source_lines.start.col < inserted.start.col)
|
||||
};
|
||||
let moved = if before_dest {
|
||||
let (dr, dc) = if rows { (-(removed as i64), 0) } else { (0, -(removed as i64)) };
|
||||
RangeRef::new(inserted.start.offset_clamped(dr, dc), inserted.end.offset_clamped(dr, dc))
|
||||
} else {
|
||||
inserted
|
||||
};
|
||||
Ok((moved, out))
|
||||
}
|
||||
|
||||
fn insert_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
if s.doc()?.wb.protected_structure {
|
||||
return Err(EngineError::Other("The workbook is protected and cannot be changed.".into()));
|
||||
|
||||
@@ -67,6 +67,9 @@ pub struct Clipboard {
|
||||
pub wb: Arc<Workbook>,
|
||||
pub sheet: usize,
|
||||
pub range: RangeRef,
|
||||
/// The range as copied, before whole rows/columns were trimmed to the used range
|
||||
/// (Insert Copied Cells inserts whole lines for a row or column copy).
|
||||
pub selected: RangeRef,
|
||||
pub cut: bool,
|
||||
/// Document uid of the source (a cut only moves within the same workbook).
|
||||
pub doc_uid: u64,
|
||||
|
||||
@@ -521,6 +521,71 @@ fn insert_delete_rows_adjust() {
|
||||
assert_eq!(v(&s, "A1"), Value::Number(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_copied_cells_inserts_rows_columns_and_blocks() {
|
||||
// Issue #21: Insert Copied Cells.
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4], [5]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "B2", "input": "=A2*10"})).unwrap();
|
||||
s.execute("edit.copy", json!({"range": "2:2"})).unwrap();
|
||||
s.execute("selection.set", json!({"range": "C4"})).unwrap();
|
||||
s.execute("home.insertCopiedCells", json!({})).unwrap();
|
||||
assert_eq!(v(&s, "A4"), Value::Number(2.0));
|
||||
assert_eq!(v(&s, "B4"), Value::Number(20.0), "the formula follows its new row");
|
||||
assert_eq!(v(&s, "A5"), Value::Number(4.0));
|
||||
assert_eq!(v(&s, "A6"), Value::Number(5.0));
|
||||
assert_eq!(s.doc().unwrap().selection.current(), gridcraft_core::RangeRef::rows(3, 3));
|
||||
// One undo step puts everything back.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(v(&s, "A4"), Value::Number(4.0));
|
||||
assert_eq!(v(&s, "A6"), Value::Empty);
|
||||
// Whole columns.
|
||||
s.execute("edit.copy", json!({"range": "A:A"})).unwrap();
|
||||
s.execute("home.insertCopiedCells", json!({"at": "A1"})).unwrap();
|
||||
assert_eq!(v(&s, "A3"), Value::Number(3.0));
|
||||
assert_eq!(v(&s, "B3"), Value::Number(3.0));
|
||||
assert_eq!(v(&s, "C2"), Value::Number(20.0), "the old column B moved right");
|
||||
// A block shifts cells right.
|
||||
s.execute("cell.set", json!({"cell": "F1", "input": "x"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "G1", "input": "y"})).unwrap();
|
||||
s.execute("edit.copy", json!({"range": "F1"})).unwrap();
|
||||
s.execute("home.insertCopiedCells", json!({"at": "G1", "shift": "right"})).unwrap();
|
||||
assert_eq!(v(&s, "G1"), Value::from("x"));
|
||||
assert_eq!(v(&s, "H1"), Value::from("y"));
|
||||
assert!(s.clipboard.is_none(), "inserting ends copy mode");
|
||||
s.execute("edit.copy", json!({"range": "F1"})).unwrap();
|
||||
assert!(s.execute("home.insertCopiedCells", json!({"shift": "up"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_cut_cells_moves_whole_rows() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4], [5]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": "=A5"})).unwrap();
|
||||
// Up: row 5 goes above row 2.
|
||||
s.execute("edit.cut", json!({"range": "5:5"})).unwrap();
|
||||
s.execute("home.insertCopiedCells", json!({"at": "A2"})).unwrap();
|
||||
let col: Vec<Value> = (1..=6).map(|r| v(&s, &format!("A{r}"))).collect();
|
||||
assert_eq!(col, [1.0, 5.0, 2.0, 3.0, 4.0].map(Value::Number).into_iter().chain([Value::Empty]).collect::<Vec<_>>());
|
||||
assert_eq!(v(&s, "C1"), Value::Number(5.0), "references follow the moved cells");
|
||||
assert!(s.clipboard.is_none());
|
||||
// Down: row 1 goes above row 4.
|
||||
s.execute("edit.cut", json!({"range": "1:1"})).unwrap();
|
||||
s.execute("home.insertCopiedCells", json!({"at": "A4"})).unwrap();
|
||||
let col: Vec<Value> = (1..=5).map(|r| v(&s, &format!("A{r}"))).collect();
|
||||
assert_eq!(col, [5.0, 2.0, 1.0, 3.0, 4.0].map(Value::Number).to_vec());
|
||||
assert_eq!(s.doc().unwrap().selection.current(), gridcraft_core::RangeRef::rows(2, 2));
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(v(&s, "A1"), Value::Number(1.0));
|
||||
// Inside itself, and a cut block, are refused without changing anything.
|
||||
s.execute("edit.cut", json!({"range": "2:4"})).unwrap();
|
||||
assert!(s.execute("home.insertCopiedCells", json!({"at": "A3"})).is_err());
|
||||
s.execute("edit.cut", json!({"range": "A1:A2"})).unwrap();
|
||||
assert!(s.execute("home.insertCopiedCells", json!({"at": "B5"})).is_err());
|
||||
assert_eq!(v(&s, "A1"), Value::Number(1.0));
|
||||
assert_eq!(v(&s, "B5"), Value::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserting_or_deleting_cells_cancels_copy_mode() {
|
||||
let mut s = s();
|
||||
|
||||
@@ -100,6 +100,13 @@ impl Dialog {
|
||||
}],
|
||||
json!({"shift": "down"}),
|
||||
),
|
||||
"insertPaste" => Dialog::form(
|
||||
"insertPaste",
|
||||
"Insert Paste",
|
||||
"home.insertCopiedCells",
|
||||
vec![Choice { key: "shift", label: "Insert", options: vec![("right", "Shift cells right"), ("down", "Shift cells down")] }],
|
||||
json!({"shift": "down"}),
|
||||
),
|
||||
"deleteCells" => Dialog::form(
|
||||
"deleteCells",
|
||||
"Delete",
|
||||
|
||||
@@ -2172,7 +2172,8 @@ fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
let resp = egui::Area::new(egui::Id::new("cell_context")).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(200.0);
|
||||
let items: &[(&str, &str)] = &[
|
||||
let insert_copied = insert_copied_item(app);
|
||||
let mut items: Vec<(&str, &str)> = vec![
|
||||
("Cut", "edit.cut"),
|
||||
("Copy", "edit.copy"),
|
||||
("Paste", "edit.paste"),
|
||||
@@ -2194,15 +2195,20 @@ fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
("Define Name…", "ui:defineName"),
|
||||
("Link…", "ui:insertLink"),
|
||||
];
|
||||
if let Some(item) = insert_copied {
|
||||
items.insert(6, item);
|
||||
}
|
||||
let lang = crate::i18n::current(ui.ctx());
|
||||
for (label, id) in items {
|
||||
if *label == "-" {
|
||||
if label == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
if ui.add(egui::Button::new(*label).frame(false).min_size(vec2(190.0, 20.0))).clicked() {
|
||||
if ui.add(egui::Button::new(lang.tr(label)).frame(false).min_size(vec2(190.0, 20.0))).clicked() {
|
||||
close = true;
|
||||
match *id {
|
||||
match id {
|
||||
"ui:pasteSpecial" => app.open_dialog("pasteSpecial", json!({})),
|
||||
"ui:insertPaste" => app.open_dialog("insertPaste", json!({})),
|
||||
"ui:insertCells" => app.open_dialog("insertCells", json!({})),
|
||||
"ui:deleteCells" => app.open_dialog("deleteCells", json!({})),
|
||||
"ui:formatCells" => app.open_dialog("formatCells", json!({})),
|
||||
@@ -2414,7 +2420,8 @@ fn header_menu(app: &mut SheetApp, ui: &mut egui::Ui, was_open: bool) {
|
||||
let response = egui::Area::new(egui::Id::new("header_menu")).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(190.0);
|
||||
let items: Vec<(&str, &str)> = if rows {
|
||||
let insert_copied = insert_copied_item(app);
|
||||
let mut items: Vec<(&str, &str)> = if rows {
|
||||
vec![
|
||||
("Cut", "edit.cut"),
|
||||
("Copy", "edit.copy"),
|
||||
@@ -2448,14 +2455,19 @@ fn header_menu(app: &mut SheetApp, ui: &mut egui::Ui, was_open: bool) {
|
||||
("Group", "data.group"),
|
||||
]
|
||||
};
|
||||
if let Some(item) = insert_copied {
|
||||
items.insert(5, item);
|
||||
}
|
||||
let lang = crate::i18n::current(ui.ctx());
|
||||
for (label, id) in items {
|
||||
if label == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
if ui.add(egui::Button::new(label).frame(false).min_size(vec2(180.0, 20.0))).clicked() {
|
||||
if ui.add(egui::Button::new(lang.tr(label)).frame(false).min_size(vec2(180.0, 20.0))).clicked() {
|
||||
close = true;
|
||||
match id {
|
||||
"ui:insertPaste" => app.open_dialog("insertPaste", json!({})),
|
||||
"ui:rowHeight" => app.open_dialog("rowHeight", json!({})),
|
||||
"ui:columnWidth" => app.open_dialog("columnWidth", json!({})),
|
||||
"edit.copy" | "edit.cut" => {
|
||||
@@ -2472,6 +2484,21 @@ fn header_menu(app: &mut SheetApp, ui: &mut egui::Ui, was_open: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The context-menu entry for Insert Copied Cells / Insert Cut Cells, when the clipboard holds a
|
||||
/// copy or cut of this app: whole rows or columns insert at once, a block asks which way to shift
|
||||
/// (Insert Paste). A cut block can only be pasted, so it gets no entry.
|
||||
fn insert_copied_item(app: &SheetApp) -> Option<(&'static str, &'static str)> {
|
||||
let clip = app.session.clipboard.as_ref()?;
|
||||
let lines = clip.selected.is_full_rows() != clip.selected.is_full_cols();
|
||||
let cut = clip.cut && app.session.active().is_some_and(|d| d.uid == clip.doc_uid);
|
||||
match (cut, lines) {
|
||||
(true, true) => Some(("Insert Cut Cells", "home.insertCopiedCells")),
|
||||
(true, false) => None,
|
||||
(false, true) => Some(("Insert Copied Cells", "home.insertCopiedCells")),
|
||||
(false, false) => Some(("Insert Copied Cells…", "ui:insertPaste")),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_checkbox(sh: &Sheet, app: &SheetApp) -> bool {
|
||||
let Some(d) = app.session.active() else { return false };
|
||||
d.wb.styles.get(sh.style_id(d.selection.active)).num_fmt.as_str() == "checkbox"
|
||||
|
||||
@@ -171,6 +171,10 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Transpose", "行列を入れ替え"),
|
||||
("Paste Link", "リンクの貼り付け"),
|
||||
("Paste Special…", "形式を選択して貼り付け…"),
|
||||
("Insert Copied Cells", "コピーしたセルの挿入"),
|
||||
("Insert Copied Cells…", "コピーしたセルの挿入…"),
|
||||
("Insert Cut Cells", "切り取ったセルの挿入"),
|
||||
("Insert Paste", "挿入貼り付け"),
|
||||
("Cut (⌘X)", "切り取り (⌘X)"),
|
||||
("Copy (⌘C)", "コピー (⌘C)"),
|
||||
("Format Painter (double-click to keep it on)", "書式のコピー/貼り付け (ダブルクリックで固定)"),
|
||||
|
||||
@@ -184,6 +184,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Mřížka",
|
||||
"Headings" => "Záhlaví",
|
||||
"Zoom" => "Lupa",
|
||||
"Insert Copied Cells" => "Vložit zkopírované buňky",
|
||||
"Insert Copied Cells…" => "Vložit zkopírované buňky…",
|
||||
"Insert Cut Cells" => "Vložit vyjmuté buňky",
|
||||
"Insert Paste" => "Vložit a posunout",
|
||||
"Previous" => "Předchozí",
|
||||
"Next" => "Další",
|
||||
"Previous Comment" => "Předchozí komentář",
|
||||
|
||||
@@ -184,6 +184,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Gitternetzlinien",
|
||||
"Headings" => "Überschriften",
|
||||
"Zoom" => "Zoom",
|
||||
"Insert Copied Cells" => "Kopierte Zellen einfügen",
|
||||
"Insert Copied Cells…" => "Kopierte Zellen einfügen…",
|
||||
"Insert Cut Cells" => "Ausgeschnittene Zellen einfügen",
|
||||
"Insert Paste" => "Einfügen und verschieben",
|
||||
"Previous" => "Zurück",
|
||||
"Next" => "Weiter",
|
||||
"Previous Comment" => "Vorheriger Kommentar",
|
||||
|
||||
@@ -185,6 +185,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Gridlines" => "Líneas de cuadrícula",
|
||||
"Headings" => "Encabezados",
|
||||
"Zoom" => "Zoom",
|
||||
"Insert Copied Cells" => "Insertar celdas copiadas",
|
||||
"Insert Copied Cells…" => "Insertar celdas copiadas…",
|
||||
"Insert Cut Cells" => "Insertar celdas cortadas",
|
||||
"Insert Paste" => "Insertar y pegar",
|
||||
"Previous" => "Anterior",
|
||||
"Next" => "Siguiente",
|
||||
"Previous Comment" => "Comentario anterior",
|
||||
|
||||
@@ -9,6 +9,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Transpose" => "행/열 바꾸기",
|
||||
"Paste Link" => "연결하여 붙여넣기",
|
||||
"Paste Special…" => "선택하여 붙여넣기…",
|
||||
"Insert Copied Cells" => "복사한 셀 삽입",
|
||||
"Insert Copied Cells…" => "복사한 셀 삽입…",
|
||||
"Insert Cut Cells" => "잘라낸 셀 삽입",
|
||||
"Insert Paste" => "삽입 붙여넣기",
|
||||
"Cut (⌘X)" => "잘라내기 (⌘X)",
|
||||
"Copy (⌘C)" => "복사 (⌘C)",
|
||||
"Format Painter (double-click to keep it on)" => "서식 복사 (두 번 클릭하여 유지)",
|
||||
|
||||
@@ -10,6 +10,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Transpose" => "Transpor",
|
||||
"Paste Link" => "Colar Vínculo",
|
||||
"Paste Special…" => "Colar Especial…",
|
||||
"Insert Copied Cells" => "Inserir Células Copiadas",
|
||||
"Insert Copied Cells…" => "Inserir Células Copiadas…",
|
||||
"Insert Cut Cells" => "Inserir Células Recortadas",
|
||||
"Insert Paste" => "Inserir Colagem",
|
||||
"Cut (⌘X)" => "Recortar (⌘X)",
|
||||
"Copy (⌘C)" => "Copiar (⌘C)",
|
||||
"Format Painter (double-click to keep it on)" => "Pincel de Formatação (clique duas vezes para mantê-lo ativo)",
|
||||
|
||||
@@ -9,6 +9,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Transpose" => "Транспонировать",
|
||||
"Paste Link" => "Вставить связь",
|
||||
"Paste Special…" => "Специальная вставка…",
|
||||
"Insert Copied Cells" => "Вставить скопированные ячейки",
|
||||
"Insert Copied Cells…" => "Вставить скопированные ячейки…",
|
||||
"Insert Cut Cells" => "Вставить вырезанные ячейки",
|
||||
"Insert Paste" => "Вставка со сдвигом",
|
||||
"Cut (⌘X)" => "Вырезать (⌘X)",
|
||||
"Copy (⌘C)" => "Копировать (⌘C)",
|
||||
"Format Painter (double-click to keep it on)" => "Формат по образцу (дважды щёлкните, чтобы закрепить)",
|
||||
|
||||
@@ -9,6 +9,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Transpose" => "转置",
|
||||
"Paste Link" => "粘贴链接",
|
||||
"Paste Special…" => "选择性粘贴…",
|
||||
"Insert Copied Cells" => "插入复制的单元格",
|
||||
"Insert Copied Cells…" => "插入复制的单元格…",
|
||||
"Insert Cut Cells" => "插入剪切的单元格",
|
||||
"Insert Paste" => "插入粘贴",
|
||||
"Cut (⌘X)" => "剪切 (⌘X)",
|
||||
"Copy (⌘C)" => "复制 (⌘C)",
|
||||
"Format Painter (double-click to keep it on)" => "格式刷(双击可持续使用)",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! The context menus offer Insert Copied Cells while the clipboard holds a copy (#21).
|
||||
|
||||
use egui::{Event, Modifiers, PointerButton, pos2};
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::{CellRef, Value};
|
||||
use gridcraft_ui_egui::{SheetApp, grid};
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness() -> Harness {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
session.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4]]})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn value(h: &Harness, cell: &str) -> Value {
|
||||
h.state().session.active().unwrap().wb.active().unwrap().value(CellRef::parse(cell).unwrap())
|
||||
}
|
||||
|
||||
fn right_click_row_header(h: &mut Harness, cell: &str) {
|
||||
let app = h.state();
|
||||
let sh = app.session.active().unwrap().wb.active().unwrap();
|
||||
let geo = grid::Geo::new(sh, app.grid.rect.unwrap(), app.view().scroll);
|
||||
let y = geo.cell_rect(sh, CellRef::parse(cell).unwrap()).center().y;
|
||||
let pos = pos2((geo.rect.left() + geo.cells.left()) / 2.0, y);
|
||||
h.input_mut().events.push(Event::PointerMoved(pos));
|
||||
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_header_menu_inserts_copied_rows() {
|
||||
let mut h = harness();
|
||||
// Nothing copied: no entry.
|
||||
right_click_row_header(&mut h, "A3");
|
||||
assert!(h.state().grid.header_menu.is_some());
|
||||
assert!(h.query_by_label("Insert Copied Cells").is_none());
|
||||
h.state_mut().grid.header_menu = None;
|
||||
h.run_steps(2);
|
||||
|
||||
h.state_mut().run("edit.copy", json!({"range": "1:1"})).unwrap();
|
||||
right_click_row_header(&mut h, "A3");
|
||||
h.get_by_label("Insert Copied Cells").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "A3"), Value::Number(1.0));
|
||||
assert_eq!(value(&h, "A4"), Value::Number(3.0));
|
||||
assert_eq!(value(&h, "A5"), Value::Number(4.0));
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
||||
- **Estimate:** 20–30 h for beta (25–40 h full).
|
||||
|
||||
### 9. Keyboard-first users: key tips, shortcuts, context menus (U)
|
||||
- **Evidence:** #33, #49 (key tips: initial Home-tab support landed in #43 on 10-10, the rest of the ribbon not yet), #110, #21 (Insert Copied Cells), #28 (selection),
|
||||
- **Evidence:** #33, #49 (key tips: initial Home-tab support landed in #43 on 10-10, the rest of the ribbon not yet), #110, #28 (selection),
|
||||
59 command shortcuts vs Excel's 200+.
|
||||
- **Estimate:** 12–20 h. [ui-parity.md](ui-parity.md)
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Header resize and double-click AutoFit | done (#42, #88 for header dividers) |
|
||||
| Freeze panes, split panes | freeze done; split to verify |
|
||||
| Copy as HTML table for other apps | done (#36, 10-10) |
|
||||
| Context menu on cells | 16 items (Cut, Copy, Paste, Paste Special, Insert, Delete, Clear Contents, Filter by value, Sort, New Comment, New Note, Format Cells, Pick From Drop-down List, Define Name, Link). Missing: Insert Copied/Cut Cells (#21), paste-option icons, Quick Analysis, Get Data from Table, Show Changes, Smart Lookup |
|
||||
| Context menu on cells | 16 items (Cut, Copy, Paste, Paste Special, Insert, Delete, Clear Contents, Filter by value, Sort, New Comment, New Note, Format Cells, Pick From Drop-down List, Define Name, Link). Insert Copied Cells (Insert Cut Cells for whole rows/columns) while the clipboard holds a copy (#21). Missing: Insert Cut Cells for a cell block, paste-option icons, Quick Analysis, Get Data from Table, Show Changes, Smart Lookup |
|
||||
| Move and resize charts, pictures and shapes | charts can't be resized (#190) |
|
||||
| Context menus on row/column headers, sheet tabs, charts, shapes | headers and tabs done; objects partial |
|
||||
| Quick Analysis (Ctrl+Q) | missing |
|
||||
|
||||
Reference in New Issue
Block a user