update tile-pasting to match Excel's behavior

This commit is contained in:
panoskentros
2026-07-05 18:22:31 +03:00
committed by Atanas Korchev
parent 483e1c0901
commit 0a44fb7815
4 changed files with 65 additions and 93 deletions

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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<CellRef>();
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()

View File

@@ -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.