From 0a44fb7815bb4da154670714fe60f86c7ec094d2 Mon Sep 17 00:00:00 2001 From: panoskentros Date: Sun, 5 Jul 2026 18:22:31 +0300 Subject: [PATCH] update tile-pasting to match Excel's behavior --- .../Spreadsheet/PasteCommandTests.cs | 33 +++++---- .../Spreadsheet/SpreadsheetClipboardTests.cs | 2 +- Radzen.Blazor/Spreadsheet/PasteCommand.cs | 69 ++----------------- .../Spreadsheet/SpreadsheetClipboard.cs | 54 +++++++++++---- 4 files changed, 65 insertions(+), 93 deletions(-) diff --git a/Radzen.Blazor.Tests/Spreadsheet/PasteCommandTests.cs b/Radzen.Blazor.Tests/Spreadsheet/PasteCommandTests.cs index e7b0a663e..1dfe4a7bc 100644 --- a/Radzen.Blazor.Tests/Spreadsheet/PasteCommandTests.cs +++ b/Radzen.Blazor.Tests/Spreadsheet/PasteCommandTests.cs @@ -36,23 +36,22 @@ public class PasteCommandTests sheet.Cells[1, 0].Value = 20d; sheet.Cells[2, 0].Value = 30d; sheet.Cells[10, 3].Formula = "=SUM(B1:B3)"; // depends on the paste destination - sheet.Cells[10, 3].Formula = "=SUM(B1:B3)"; var clipboard = new SpreadsheetClipboard(); - sheet.Selection.Select(new RangeRef(new CellRef(0, 0), new CellRef(2, 0))); + sheet.Selection.Select(new RangeRef(new CellRef(0, 0), new CellRef(2, 0))); // A1:A3 clipboard.Copy(sheet); var evals = 0; sheet.Cells[10, 3].Changed += _ => evals++; var stack = new UndoRedoStack(sheet); - stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 1), new CellRef(0, 1)), null)); + stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 1), new CellRef(0, 1)), null)); // paste to B1 Assert.Equal(60d, sheet.Cells[10, 3].Value); Assert.True(evals <= 2); stack.Undo(); - Assert.Equal(0d, sheet.Cells[10, 3].Value); + Assert.Equal(0d, sheet.Cells[10, 3].Value); // destination cleared -> SUM 0 } [Fact] @@ -67,7 +66,7 @@ public class PasteCommandTests clipboard.Cut(sheet); var stack = new UndoRedoStack(sheet); - stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 2), new CellRef(0, 2)), null)); + stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 2), new CellRef(0, 2)), null)); // -> C1 Assert.Equal("A", sheet.Cells[0, 2].Value); Assert.Equal("B", sheet.Cells[1, 2].Value); @@ -93,7 +92,7 @@ public class PasteCommandTests clipboard.Cut(sheet); var stack = new UndoRedoStack(sheet); - stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 2), new CellRef(0, 2)), null)); + stack.Execute(new PasteCommand(clipboard, sheet, new RangeRef(new CellRef(0, 2), new CellRef(0, 2)), null)); // move A1 -> C1 stack.Undo(); Assert.Equal("A", sheet.Cells[0, 0].Value); @@ -110,23 +109,27 @@ public class PasteCommandTests public void TiledTextPaste_FillsSelectedRange() { var sheet = new Worksheet(20, 5); + sheet.Cells[0, 0].Value = "X"; + var clipboard = new SpreadsheetClipboard(); - var stack = new UndoRedoStack(sheet); - - var destination = new RangeRef(new CellRef(0, 0), new CellRef(1, 1)); - - stack.Execute(new PasteCommand(clipboard, sheet, destination, "X")); + sheet.Selection.Select(new CellRef(0, 0)); + clipboard.Copy(sheet); + + var stack = new UndoRedoStack(sheet); + var destination = new RangeRef(new CellRef(0, 1), new CellRef(1, 2)); + + stack.Execute(new PasteCommand(clipboard, sheet, destination, null)); - Assert.Equal("X", sheet.Cells[0, 0].Value); Assert.Equal("X", sheet.Cells[0, 1].Value); - Assert.Equal("X", sheet.Cells[1, 0].Value); + Assert.Equal("X", sheet.Cells[0, 2].Value); Assert.Equal("X", sheet.Cells[1, 1].Value); + Assert.Equal("X", sheet.Cells[1, 2].Value); stack.Undo(); - Assert.Null(sheet.Cells[0, 0].Value); Assert.Null(sheet.Cells[0, 1].Value); - Assert.Null(sheet.Cells[1, 0].Value); + Assert.Null(sheet.Cells[0, 2].Value); Assert.Null(sheet.Cells[1, 1].Value); + Assert.Null(sheet.Cells[1, 2].Value); } } \ No newline at end of file diff --git a/Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs b/Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs index 09b161d29..7fbbf3173 100644 --- a/Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs +++ b/Radzen.Blazor.Tests/Spreadsheet/SpreadsheetClipboardTests.cs @@ -39,7 +39,7 @@ public class SpreadsheetClipboardTests sheet.Selection.Select(CellRef.Parse("A1")); var clipboard = new SpreadsheetClipboard(); clipboard.Cut(sheet); - clipboard.Paste(sheet, CellRef.Parse("B2")); + clipboard.Paste(sheet, RangeRef.Parse("B2")); Assert.Equal("=A1", sheet.Cells[1, 1].Formula); // not adjusted Assert.Null(sheet.Cells[0, 0].Formula); // source cleared Assert.Null(sheet.Cells[0, 0].Value); diff --git a/Radzen.Blazor/Spreadsheet/PasteCommand.cs b/Radzen.Blazor/Spreadsheet/PasteCommand.cs index c4410d18d..0881ba450 100644 --- a/Radzen.Blazor/Spreadsheet/PasteCommand.cs +++ b/Radzen.Blazor/Spreadsheet/PasteCommand.cs @@ -12,8 +12,6 @@ namespace Radzen.Blazor.Spreadsheet; // the live clipboard. Batching comes from the UndoRedoStack. class PasteCommand : RangeSnapshotCommandBase { - private static readonly string[] LineSeparators = ["\r\n", "\r", "\n"]; - private readonly SpreadsheetClipboard clipboard; private readonly RangeRef destination; private readonly string? text; @@ -38,13 +36,8 @@ class PasteCommand : RangeSnapshotCommandBase Restore(result); return true; } - - if (!string.IsNullOrEmpty(text) && !destination.Collapsed) - { - return ExecuteTiledTextPaste(); - } - - var destinationRange = clipboard.GetPasteRange(sheet, destination.Start, text); + + var destinationRange = clipboard.GetPasteRange(sheet, destination, text); if (destinationRange == RangeRef.Invalid) { @@ -60,70 +53,18 @@ class PasteCommand : RangeSnapshotCommandBase if (text is not null) { - clipboard.Paste(sheet, destination.Start, text); + clipboard.Paste(sheet, destination, text); } else { - clipboard.Paste(sheet, destination.Start); + clipboard.Paste(sheet, destination); } CaptureResult(); pasted = true; return true; } - - private bool ExecuteTiledTextPaste() - { - var rows = text!.Split(LineSeparators, StringSplitOptions.None); - var data = rows.Select(r => r.Split('\t')).ToArray(); - - int dataRowCount = data.Length; - if (dataRowCount > 0 && data[dataRowCount - 1].Length > 0 && string.IsNullOrEmpty(data[dataRowCount - 1][0])) - { - dataRowCount--; - } - - if (dataRowCount == 0) - { - return false; - } - - int dataColCount = data[0].Length; - int startRow = destination.Start.Row; - int startCol = destination.Start.Column; - int endRow = destination.End.Row; - int endCol = destination.End.Column; - - var cellsToCapture = new List(); - for (int r = startRow; r <= endRow; r++) - { - for (int c = startCol; c <= endCol; c++) - { - cellsToCapture.Add(new CellRef(r, c)); - } - } - - CaptureRange(cellsToCapture); - - for (int r = startRow; r <= endRow; r++) - { - for (int c = startCol; c <= endCol; c++) - { - var cellRef = new CellRef(r, c); - if (sheet.IsCellEditable(cellRef)) - { - int dataRowIndex = (r - startRow) % dataRowCount; - int dataColIndex = (c - startCol) % dataColCount; - sheet.Cells[r, c].Value = data[dataRowIndex][dataColIndex]; - } - } - } - - CaptureResult(); - pasted = true; - return true; - } - + public override void Unexecute() => RestoreSnapshot(); private void CaptureResult() diff --git a/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs b/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs index 5fd47a8c8..c6e78a448 100644 --- a/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs +++ b/Radzen.Blazor/Spreadsheet/SpreadsheetClipboard.cs @@ -35,26 +35,45 @@ class SpreadsheetClipboard csv = sheet.GetDelimitedString(range.Value); } - public void Paste(Worksheet targetSheet, CellRef destinationStart) + public void Paste(Worksheet targetSheet, RangeRef destination) { if (range.HasValue && sheet is not null) { var adjustment = operation == ClipboardOperation.Copy ? FormulaAdjustment.AdjustRelative : FormulaAdjustment.Preserve; - targetSheet.PasteRange(sheet, range.Value, destinationStart, adjustment); + var source = range.Value; + + if (operation == ClipboardOperation.Copy && + !destination.Collapsed && + destination.Rows % source.Rows == 0 && + destination.Columns % source.Columns == 0) + { + for (var r = 0; r < destination.Rows; r += source.Rows) + { + for (var c = 0; c < destination.Columns; c += source.Columns) + { + var tileStart = new CellRef(destination.Start.Row + r, destination.Start.Column + c); + targetSheet.PasteRange(sheet, source, tileStart, adjustment); + } + } + } + else + { + targetSheet.PasteRange(sheet, source, destination.Start, adjustment); + } if (operation == ClipboardOperation.Move) { - Clear(sheet, range.Value); + Clear(sheet, source); ClearInternal(); } } } - public bool TryPaste(Worksheet targetSheet, CellRef destinationStart, string pastedText) + public bool TryPaste(Worksheet targetSheet, RangeRef destination, string pastedText) { if (range.HasValue && sheet is not null && !string.IsNullOrEmpty(csv) && pastedText == csv) { - Paste(targetSheet, destinationStart); + Paste(targetSheet, destination); return true; } @@ -63,20 +82,29 @@ class SpreadsheetClipboard return false; } - public void Paste(Worksheet targetSheet, CellRef destinationStart, string pastedText) + public void Paste(Worksheet targetSheet, RangeRef destination, string pastedText) { - if (!TryPaste(targetSheet, destinationStart, pastedText)) + if (!TryPaste(targetSheet, destination, pastedText)) { - targetSheet.InsertDelimitedString(destinationStart, pastedText); + targetSheet.InsertDelimitedString(destination.Start, pastedText); } } - public RangeRef GetPasteRange(Worksheet targetSheet, CellRef destinationStart, string? pastedText) + public RangeRef GetPasteRange(Worksheet targetSheet, RangeRef destination, string? pastedText) { if (range.HasValue && sheet is not null && !string.IsNullOrEmpty(csv) && (pastedText is null || pastedText == csv)) { var source = range.Value; - return new RangeRef(destinationStart, new CellRef(destinationStart.Row + source.Rows - 1, destinationStart.Column + source.Columns - 1)); + + if (operation == ClipboardOperation.Copy && + !destination.Collapsed && + destination.Rows % source.Rows == 0 && + destination.Columns % source.Columns == 0) + { + return destination; + } + + return new RangeRef(destination.Start, new CellRef(destination.Start.Row + source.Rows - 1, destination.Start.Column + source.Columns - 1)); } if (string.IsNullOrEmpty(pastedText)) @@ -85,21 +113,21 @@ class SpreadsheetClipboard } var lines = pastedText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None); - var rowCount = Math.Min(lines.Length, targetSheet.RowCount - destinationStart.Row); + var rowCount = Math.Min(lines.Length, targetSheet.RowCount - destination.Start.Row); var columnCount = 0; foreach (var line in lines) { columnCount = Math.Max(columnCount, line.Split('\t').Length); } - columnCount = Math.Min(columnCount, targetSheet.ColumnCount - destinationStart.Column); + columnCount = Math.Min(columnCount, targetSheet.ColumnCount - destination.Start.Column); if (rowCount <= 0 || columnCount <= 0) { return RangeRef.Invalid; } - return new RangeRef(destinationStart, new CellRef(destinationStart.Row + rowCount - 1, destinationStart.Column + columnCount - 1)); + return new RangeRef(destination.Start, new CellRef(destination.Start.Row + rowCount - 1, destination.Start.Column + columnCount - 1)); } // Cross-sheet moves are not covered: per-sheet undo stacks can't snapshot a source on another sheet.