diff --git a/README.md b/README.md
index 71c077f1..3db5b9a0 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
A simple datasheet component for editing tabular data.
-![image](https://user-images.githubusercontent.com/34253568/197425287-690a747a-24f5-4e0d-afcf-e2a09efbaba2.png)
+
#### Features
- Data editing
@@ -10,10 +10,10 @@ A simple datasheet component for editing tabular data.
- Add custom editors for any data type
- Conditional formatting
- Data validation
-- Build datasheet from an object definition
+- Formula
- Keyboard navigation
- Copy and paste from tabulated data
-- Virtualization via Blazor Virtualization - handles many cells at once.
+- Virtualization - _handles_ many cells at once in both rows & cols.
Demo: https://anmcgrath.github.io/BlazorDatasheet/
@@ -43,3 +43,87 @@ The following code displays an empty 3 x 3 data grid.
```
The default editor is the text editor, but can be changed by defining the Type property of each cell.
+
+### Setting & getting cell values
+
+Cell values can be set in a few ways:
+
+```csharp
+sheet.Cells[0, 0].Value = "Test"
+sheet.Range("A1").Value = "Test";
+sheet.Cells.SetValue(0, 0, "Test");
+sheet.Commands.ExecuteCommand(new SetCellValueCommand(0, 0, "Test"));
+```
+
+In this example, the first two methods set the value but cannot be undone. The last two methods can be undone.
+
+### Formula
+
+Formula can be applied to cells. When the cells or ranges that the formula cells reference change, the cell value is re-calculated.
+
+Currently, the whole sheet is calculated if any referenced cell or range changes.
+
+```csharp
+sheet.Cells[0, 0].Formula = "=10+A2"
+```
+
+### Formatting
+
+Cell formats can be set in the following ways:
+
+```csharp
+sheet.Range("A1:A2").Format = new CellFormat() { BackgroundColor = "red" };
+sheet.Commands.ExecuteCommand(
+ new SetFormatCommand(new RowRegion(10, 12), new CellFormat() { ForegroundColor = "blue" }));
+sheet.SetFormat(sheet.Range(new ColumnRegion(5)), new CellFormat() { FontWeight = "bold" });
+sheet.Cells[0, 0].Format = new CellFormat() { TextAlign = "center" };
+```
+
+When a cell format is set, it will be merged into any existing cell formats in the region that it is applied to. Any non-null format paremeters will be merged:
+
+```csharp
+sheet.Range("A1").Format = new CellFormat() { BackgroundColor = "red" };
+sheet.Range("A1:A2").Format = new CellFormat() { ForegroundColor = "blue" };
+var format = sheet.Cells[0, 0].Format; // backroundColor = "red", foreground = "blue"
+var format2 = sheet.Cells[1, 0].Format; // foreground = "blue"
+```
+
+### Cell types
+The cell type specifies which renderer and editor are used for the cell.
+
+```csharp
+sheet.Range("A1:B5").Type = "boolean"; // renders a checkbox
+```
+
+Custom editors and renderers can be defined. See the examples for more information.
+
+### Validation
+Data validation can be set on cells/ranges. There are two modes of validation: strict and non-strict. When a validator is strict, the cell value will not be set by the editor if it fails validation.
+
+If validation is not strict, the value can be set during editing but will show a validation error when rendered.
+
+Although a strict validation may be set on a cell, the value can be changed programmatically, but it will display as a validation error.
+
+```csharp
+sheet.Validators.Add(new ColumnRegion(0), new NumberValidator(isStrict: true));
+```
+
+### Regions and ranges
+
+A region is a geometric construct, for example:
+
+```csharp
+var region = new Region(0, 5, 0, 5); // r0 to r5, c0 to c5
+var cellRegion = new Region(0, 0); // cell A1
+var colRegion = new ColumnRegion(0, 4); // col region spanning A to D
+var rowRegion = new RowRegion(0, 3); // row region spanning 1 to 4
+```
+
+A range is a collection of regions that also knows about the sheet. Ranges can be used to set certain parts of the sheet.
+
+```csharp
+var range = sheet.Range("A1:C5, B2:B7");
+var range = sheet.Range(new ColumnRegion(0));
+var range = sheet.Range(regions);
+var range = sheet.Range(0, 0, 4, 5);
+```
diff --git a/src/.run/dotnet watch run.run.xml b/src/.run/dotnet watch run.run.xml
index 03e3ef1b..39e17391 100644
--- a/src/.run/dotnet watch run.run.xml
+++ b/src/.run/dotnet watch run.run.xml
@@ -1,8 +1,8 @@
-
+
-
+
diff --git a/src/BlazorDatasheet.Core/BlazorDatasheet.Core.csproj b/src/BlazorDatasheet.Core/BlazorDatasheet.Core.csproj
new file mode 100644
index 00000000..e1e4ce00
--- /dev/null
+++ b/src/BlazorDatasheet.Core/BlazorDatasheet.Core.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/src/BlazorDatasheet.Core/Color/ColorConverter.cs b/src/BlazorDatasheet.Core/Color/ColorConverter.cs
new file mode 100644
index 00000000..54e1f4fd
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Color/ColorConverter.cs
@@ -0,0 +1,116 @@
+using BlazorDatasheet.DataStructures.Util;
+
+namespace BlazorDatasheet.Core.Color;
+
+public class ColorConverter
+{
+ public static System.Drawing.Color HSVToRGB(double H, double S, double V)
+ {
+ double r = 0, g = 0, b = 0;
+
+ if (S == 0)
+ {
+ r = V;
+ g = V;
+ b = V;
+ }
+ else
+ {
+ int i;
+ double f, p, q, t;
+
+ if (H == 360)
+ H = 0;
+ else
+ H = H / 60;
+
+ i = (int)Math.Truncate(H);
+ f = H - i;
+
+ p = V * (1.0 - S);
+ q = V * (1.0 - (S * f));
+ t = V * (1.0 - (S * (1.0 - f)));
+
+ switch (i)
+ {
+ case 0:
+ r = V;
+ g = t;
+ b = p;
+ break;
+
+ case 1:
+ r = q;
+ g = V;
+ b = p;
+ break;
+
+ case 2:
+ r = p;
+ g = V;
+ b = t;
+ break;
+
+ case 3:
+ r = p;
+ g = q;
+ b = V;
+ break;
+
+ case 4:
+ r = t;
+ g = p;
+ b = V;
+ break;
+
+ default:
+ r = V;
+ g = p;
+ b = q;
+ break;
+ }
+ }
+
+ return System.Drawing.Color.FromArgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255));
+ }
+
+ public static (double h, double s, double v) RGBToHSV(System.Drawing.Color color)
+ {
+ float cmax = Math.Max(color.R, Math.Max(color.G, color.B));
+ float cmin = Math.Min(color.R, Math.Min(color.G, color.B));
+ float delta = cmax - cmin;
+
+ float hue = 0;
+ float saturation = 0;
+
+ if (cmax == color.R)
+ {
+ hue = 60 * (((color.G - color.B) / delta) % 6);
+ }
+ else if (cmax == color.G)
+ {
+ hue = 60 * ((color.B - color.R) / delta + 2);
+ }
+ else if (cmax == color.B)
+ {
+ hue = 60 * ((color.R - color.G) / delta + 4);
+ }
+
+ if (cmax > 0)
+ {
+ saturation = delta / cmax;
+ }
+
+ return (hue, saturation, cmax);
+ }
+
+
+ public static (double h, double s, double v) HsvInterp((double h, double s, double v) c0, (double h, double s,
+ double v) c1, double t)
+ {
+ double h = ((1 - t) * c0.h + t * c1.h) % 360;
+ double s = SheetMath.ClampDouble(0, 1, ((1 - t) * c0.s + t * c1.s));
+ double v = SheetMath.ClampDouble(0, 1, ((1 - t) * c0.v + t * c1.v));
+ return (h, s, v);
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/ClearCellsCommand.cs b/src/BlazorDatasheet.Core/Commands/ClearCellsCommand.cs
new file mode 100644
index 00000000..65c12588
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/ClearCellsCommand.cs
@@ -0,0 +1,31 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Formula.Core;
+
+namespace BlazorDatasheet.Core.Commands;
+
+///
+/// Clears cell values in the given ranges
+///
+public class ClearCellsCommand : IUndoableCommand
+{
+ private readonly SheetRange _range;
+ private CellStoreRestoreData _restoreData;
+
+ public ClearCellsCommand(SheetRange range)
+ {
+ _range = range.Clone();
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Cells.ClearCellsImpl(_range.Regions);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Cells.Restore(_restoreData);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet/Commands/CommandGroup.cs b/src/BlazorDatasheet.Core/Commands/CommandGroup.cs
similarity index 52%
rename from src/BlazorDatasheet/Commands/CommandGroup.cs
rename to src/BlazorDatasheet.Core/Commands/CommandGroup.cs
index fc210117..e1feb6b4 100644
--- a/src/BlazorDatasheet/Commands/CommandGroup.cs
+++ b/src/BlazorDatasheet.Core/Commands/CommandGroup.cs
@@ -1,26 +1,32 @@
-using BlazorDatasheet.Data;
+using BlazorDatasheet.Core.Data;
-namespace BlazorDatasheet.Commands;
+namespace BlazorDatasheet.Core.Commands;
public class CommandGroup : IUndoableCommand
{
- private IEnumerable _commands;
- private List _successfulCommands;
+ private readonly List _commands;
+ private readonly List _successfulCommands;
///
/// Runs a series of commands sequentially, but stops if any fails.
///
///
- public CommandGroup(params IUndoableCommand[] commands)
+ public CommandGroup(params ICommand[] commands)
{
- _commands = commands;
- _successfulCommands = new List();
+ _commands = commands.ToList();
+ _successfulCommands = new List();
+ }
+
+ public void AddCommand(ICommand command)
+ {
+ _commands.Add(command);
}
public bool Execute(Sheet sheet)
{
_successfulCommands.Clear();
+ sheet.BatchUpdates();
foreach (var command in _commands)
{
var run = command.Execute(sheet);
@@ -34,16 +40,22 @@ public bool Execute(Sheet sheet)
_successfulCommands.Add(command);
}
+ sheet.EndBatchUpdates();
+
return true;
}
public bool Undo(Sheet sheet)
{
var undo = true;
- foreach (var command in _successfulCommands)
+ var undoCommands = _successfulCommands.Where(cmd => cmd is IUndoableCommand).Cast().ToList();
+ undoCommands.Reverse();
+ sheet.BatchUpdates();
+ foreach (var command in undoCommands)
{
undo &= command.Undo(sheet);
}
+ sheet.EndBatchUpdates();
return undo;
}
diff --git a/src/BlazorDatasheet.Core/Commands/CommandManager.cs b/src/BlazorDatasheet.Core/Commands/CommandManager.cs
new file mode 100644
index 00000000..36fa0cea
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/CommandManager.cs
@@ -0,0 +1,205 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Selecting;
+using BlazorDatasheet.Core.Util;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class CommandManager
+{
+ private readonly MaxStack _history;
+ private readonly MaxStack _redos;
+ private Sheet _sheet;
+ private CommandGroup? _currentCommandGroup;
+
+ ///
+ /// Whether the commands executed are being collected in a group.
+ ///
+ private bool _isCollectingCommands;
+
+ ///
+ /// If history is paused, commands are no longer added to the undo/redo stack.
+ ///
+ public bool HistoryPaused { get; private set; }
+
+ public CommandManager(Sheet sheet, int maxHistorySize = 50)
+ {
+ _sheet = sheet;
+ _history = new MaxStack(maxHistorySize);
+ _redos = new MaxStack(maxHistorySize);
+ }
+
+ ///
+ /// Executes a command and, if it is an undoable command, adds it to the undo stack.
+ ///
+ ///
+ ///
+ public bool ExecuteCommand(ICommand command)
+ {
+ if (_isCollectingCommands)
+ {
+ _currentCommandGroup!.AddCommand(command);
+ return true;
+ }
+
+ var result = command.Execute(_sheet);
+ if (result)
+ {
+ if (!HistoryPaused && command is IUndoableCommand undoCommand)
+ {
+ _history.Push(new UndoCommandData()
+ {
+ Command = undoCommand,
+ Selection = _sheet.Selection.Clone()
+ });
+ }
+ }
+
+ // Clear the redo stack because otherwise we will be redoing changes to the sheet with a changed
+ // model from the original time the commands were run.
+ _redos.Clear();
+
+ return result;
+ }
+
+ internal void SetSheet(Sheet sheet)
+ {
+ _sheet = sheet;
+ }
+
+ ///
+ /// Returns all the undo commands in the undo stack
+ ///
+ ///
+ public IEnumerable GetUndoCommands()
+ {
+ return _history.GetAllItems().Select(x => x.Command);
+ }
+
+ ///
+ /// Returns all the redo commands in the redo stack.
+ ///
+ ///
+ public IEnumerable GetRedoCommands()
+ {
+ return _redos.GetAllItems();
+ }
+
+ ///
+ /// Executes the undo function of the last undo-able command run.
+ ///
+ ///
+ public bool Undo()
+ {
+ if (_isCollectingCommands)
+ return false;
+
+ if (_history.Peek() == null)
+ return false;
+
+ var cmd = _history.Pop()!;
+ var result = cmd.Command.Undo(_sheet);
+ if (cmd.Selection != null)
+ _sheet.Selection.Set(cmd.Selection);
+
+ if (!HistoryPaused && result)
+ {
+ _redos.Push(cmd.Command);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Execute the last command that was un-done.
+ ///
+ ///
+ public bool Redo()
+ {
+ if (_isCollectingCommands)
+ return false;
+
+ if (_redos.Peek() == null)
+ return false;
+
+ var cmd = _redos.Pop()!;
+ var result = cmd.Execute(_sheet);
+
+ if (result)
+ {
+ if (!HistoryPaused && cmd is IUndoableCommand undoCommand)
+ {
+ _history.Push(new UndoCommandData()
+ {
+ Command = undoCommand,
+ Selection = _sheet.Selection.Clone()
+ });
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Removes all commands from the history, clearing the undo/redo functionality for those commands.
+ ///
+ public void ClearHistory()
+ {
+ _history.Clear();
+ }
+
+ ///
+ /// Stop commands to be added to the undo/redo stack.
+ ///
+ public void PauseHistory()
+ {
+ HistoryPaused = true;
+ }
+
+ ///
+ /// Allow commands to be added to the undo/redo stack.
+ ///
+ public void ResumeHistory()
+ {
+ HistoryPaused = false;
+ }
+
+ ///
+ /// Starts collecting commands in a group. When collecting is finished, via EndCommandGroup()
+ /// the commands are executed. When the commands are then undone/redone, they are undone/redone together.
+ ///
+ public void BeginCommandGroup()
+ {
+ _isCollectingCommands = true;
+ _currentCommandGroup = new CommandGroup();
+ }
+
+ ///
+ /// Finishes collecting commands in a group and executes the commands in the group.
+ ///
+ public bool EndCommandGroup()
+ {
+ _isCollectingCommands = false;
+ if (_currentCommandGroup != null)
+ {
+ var res = this.ExecuteCommand(_currentCommandGroup);
+ _currentCommandGroup = null;
+ return res;
+ }
+
+ _currentCommandGroup = null;
+ return false;
+ }
+}
+
+internal class UndoCommandData
+{
+ ///
+ /// The command to undo.
+ ///
+ public IUndoableCommand Command { get; init; }
+
+ ///
+ /// The selected range at the time the command was run.
+ ///
+ public SheetRange? Selection { get; init; }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet/Commands/ICommand.cs b/src/BlazorDatasheet.Core/Commands/ICommand.cs
similarity index 65%
rename from src/BlazorDatasheet/Commands/ICommand.cs
rename to src/BlazorDatasheet.Core/Commands/ICommand.cs
index fe839c82..2533ad44 100644
--- a/src/BlazorDatasheet/Commands/ICommand.cs
+++ b/src/BlazorDatasheet.Core/Commands/ICommand.cs
@@ -1,6 +1,6 @@
-using BlazorDatasheet.Data;
+using BlazorDatasheet.Core.Data;
-namespace BlazorDatasheet.Commands;
+namespace BlazorDatasheet.Core.Commands;
///
/// A command that can be executed on the sheet
diff --git a/src/BlazorDatasheet/Commands/IUndoableCommand.cs b/src/BlazorDatasheet.Core/Commands/IUndoableCommand.cs
similarity index 78%
rename from src/BlazorDatasheet/Commands/IUndoableCommand.cs
rename to src/BlazorDatasheet.Core/Commands/IUndoableCommand.cs
index 6f42aae4..1e43ea4e 100644
--- a/src/BlazorDatasheet/Commands/IUndoableCommand.cs
+++ b/src/BlazorDatasheet.Core/Commands/IUndoableCommand.cs
@@ -1,6 +1,6 @@
-using BlazorDatasheet.Data;
+using BlazorDatasheet.Core.Data;
-namespace BlazorDatasheet.Commands;
+namespace BlazorDatasheet.Core.Commands;
///
/// A command that can be un-done/re-run
diff --git a/src/BlazorDatasheet.Core/Commands/InsertColAtCommand.cs b/src/BlazorDatasheet.Core/Commands/InsertColAtCommand.cs
new file mode 100644
index 00000000..28704c45
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/InsertColAtCommand.cs
@@ -0,0 +1,40 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+///
+/// Command for inserting a column in to the sheet
+///
+public class InsertColAtCommand : IUndoableCommand
+{
+ private readonly int _colIndex;
+ private readonly int _nCols;
+
+ ///
+ /// Command for inserting a column into the sheet.
+ ///
+ /// The index that the column will be inserted at.
+ public InsertColAtCommand(int colIndex, int nCols = 1)
+ {
+ _colIndex = colIndex;
+ _nCols = nCols;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ sheet.Validators.Store.InsertCols(_colIndex, _nCols);
+ sheet.Cells.InsertColAt(_colIndex, _nCols);
+ sheet.InsertColAtImpl(_colIndex, _nCols);
+ sheet.Columns.InsertImpl(_colIndex, _nCols);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Validators.Store.RemoveCols(_colIndex, _colIndex + _nCols - 1);
+ sheet.RemoveColImpl(_colIndex, _nCols);
+ sheet.Cells.RemoveColAt(_colIndex, _nCols);
+ sheet.Columns.RemoveColumnsImpl(_colIndex, _colIndex + _nCols - 1);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/InsertRowsAtCommand.cs b/src/BlazorDatasheet.Core/Commands/InsertRowsAtCommand.cs
new file mode 100644
index 00000000..a8f9ccbd
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/InsertRowsAtCommand.cs
@@ -0,0 +1,41 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+///
+/// Command for inserting a row into the sheet.
+///
+internal class InsertRowsAtCommand : IUndoableCommand
+{
+ private readonly int _index;
+ private readonly int _nRows;
+
+ ///
+ /// Command for inserting a row into the sheet.
+ ///
+ /// The index that the row will be inserted at.
+ /// The number of rows to insert
+ public InsertRowsAtCommand(int index, int nRows = 1)
+ {
+ _index = index;
+ _nRows = nRows;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ sheet.Validators.Store.InsertRows(_index, _nRows);
+ sheet.Cells.InsertRowAt(_index, _nRows);
+ sheet.InsertRowAtImpl(_index, _nRows);
+ sheet.Rows.InsertImpl(_index, _nRows);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Validators.Store.RemoveRows(_index, _index + _nRows - 1);
+ sheet.Cells.RemoveRowAt(_index, _nRows);
+ sheet.RemoveRowAtImpl(_index, _nRows);
+ sheet.Rows.RemoveRowsImpl(_index, _index + _nRows - 1);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet/Commands/MergeCellsCommand.cs b/src/BlazorDatasheet.Core/Commands/MergeCellsCommand.cs
similarity index 57%
rename from src/BlazorDatasheet/Commands/MergeCellsCommand.cs
rename to src/BlazorDatasheet.Core/Commands/MergeCellsCommand.cs
index ae19edce..8ff37f56 100644
--- a/src/BlazorDatasheet/Commands/MergeCellsCommand.cs
+++ b/src/BlazorDatasheet.Core/Commands/MergeCellsCommand.cs
@@ -1,21 +1,23 @@
-using BlazorDatasheet.Data;
-using BlazorDatasheet.Util;
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.DataStructures.Geometry;
+using BlazorDatasheet.Formula.Core;
-namespace BlazorDatasheet.Commands;
+namespace BlazorDatasheet.Core.Commands;
public class MergeCellsCommand : IUndoableCommand
{
- private readonly BRange _range;
+ private readonly SheetRange _range;
private readonly List _overridenMergedRegions = new();
private readonly List _mergesPerformed = new();
- private readonly List _changes = new();
+ private CellStoreRestoreData _restoreData;
///
/// Command that merges the cells in the range give.
/// Note that the value in the top LHS will be kept, while other cell values will be cleared.
///
/// The range in which to merge.
- public MergeCellsCommand(BRange range)
+ public MergeCellsCommand(SheetRange range)
{
_range = range.Clone();
}
@@ -24,57 +26,52 @@ public MergeCellsCommand(BRange range)
public bool Execute(Sheet sheet)
{
_overridenMergedRegions.Clear();
- _changes.Clear();
_mergesPerformed.Clear();
foreach (var region in _range.Regions)
{
- var envelope = region.ToEnvelope();
// Determine if there are any merged cells in the region
// We can only merge over merged cells if we entirely overlap them
- var existingMerges = sheet.MergedCells.Search(envelope);
- if (!existingMerges.All(x => region.Contains(x.Region)))
+ var existingMerges = sheet.Cells.GetMerges(region).ToList();
+ if (!existingMerges.All(x => region.Contains(x)))
continue;
// Clear all the cells that are not the top-left posn of merge and store their values for undo
- var cellsToClear = region
+ var regionsToClear = region
.Break(region.TopLeft)
- .SelectMany(sheet.GetNonEmptyCellPositions)
.ToList();
- _changes.AddRange(cellsToClear.Select(x => getValueChangeOnClear(x.row, x.col, sheet)));
- sheet.ClearCellsImpl(cellsToClear);
+ _restoreData = sheet.Cells.ClearCellsImpl(regionsToClear);
// Store the merges that we will have to re-instate on undo
// And remove any merges that are contained in the region
- _overridenMergedRegions.AddRange(existingMerges.Select(x => x.Region));
- sheet.UnMergeCellsImpl(new BRange(sheet, existingMerges.Select(x => x.Region)));
+ _overridenMergedRegions.AddRange(existingMerges);
+ sheet.Cells.UnMergeCellsImpl(new SheetRange(sheet, existingMerges));
// Store the merge that we are doing and perform the actual merge
_mergesPerformed.Add(region);
- sheet.MergeCellsImpl(region);
+ sheet.Cells.MergeImpl(region);
}
return true;
}
- private CellChange getValueChangeOnClear(int row, int col, Sheet sheet)
+ private CellValueChange getValueChangeOnClear(int row, int col, Sheet sheet)
{
- return new CellChange(row, col, sheet.GetValue(row, col));
+ return new CellValueChange(row, col, sheet.Cells.GetValue(row, col));
}
public bool Undo(Sheet sheet)
{
// Undo the merge we performed
foreach (var merge in _mergesPerformed)
- sheet.UnMergeCellsImpl(merge);
+ sheet.Cells.UnMergeCellsImpl(merge);
// Restore all the merges we removed
foreach (var removedMerge in _overridenMergedRegions)
- sheet.MergeCellsImpl(removedMerge);
-
- sheet.Selection.Set(_range);
+ sheet.Cells.MergeImpl(removedMerge);
+
// Restore all the cell values that were lost when merging
- sheet.SetCellValuesImpl(_changes);
+ sheet.Cells.Restore(_restoreData);
return true;
}
diff --git a/src/BlazorDatasheet.Core/Commands/RemoveColumnCommand.cs b/src/BlazorDatasheet.Core/Commands/RemoveColumnCommand.cs
new file mode 100644
index 00000000..0f97fe21
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/RemoveColumnCommand.cs
@@ -0,0 +1,74 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Core.Events;
+using BlazorDatasheet.Core.Formats;
+using BlazorDatasheet.DataStructures.Geometry;
+using BlazorDatasheet.DataStructures.Intervals;
+using BlazorDatasheet.DataStructures.Store;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class RemoveColumnCommand : IUndoableCommand
+{
+ private int _columnIndex;
+ private readonly int _nCols;
+
+ private RegionRestoreData _mergeRestoreData;
+ private RegionRestoreData _validatorRestoreData;
+ private CellStoreRestoreData _cellStoreRestoreData;
+ private List> _colFormatRestoreData;
+ private int _nColsRemoved;
+ private ColumnInfoRestoreData _columnInfoRestoreData;
+
+ ///
+ /// Command for removing a column at the index given.
+ ///
+ /// The column to remove.
+ public RemoveColumnCommand(int columnIndex, int nCols)
+ {
+ _columnIndex = columnIndex;
+ _nCols = nCols;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ if (_columnIndex >= sheet.NumCols)
+ return false;
+ if (_nCols <= 0)
+ return false;
+ _nColsRemoved = Math.Min(sheet.NumCols - _columnIndex + 1, _nCols);
+
+ if (_nColsRemoved == 0)
+ return false;
+
+ _cellStoreRestoreData = sheet.Cells.RemoveColAt(_columnIndex, _nColsRemoved);
+ _columnInfoRestoreData = sheet.Columns.RemoveColumnsImpl(_columnIndex, _columnIndex + _nColsRemoved - 1);
+ _validatorRestoreData = sheet.Validators.Store.RemoveCols(_columnIndex, _columnIndex + _nColsRemoved - 1);
+ return sheet.RemoveColImpl(_columnIndex, _nColsRemoved);
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ UndoValidation(sheet);
+
+ // Insert column back in and set all the values that we removed
+ sheet.InsertColAtImpl(_columnIndex, _nColsRemoved);
+
+ sheet.Cells.InsertColAt(_columnIndex, _nColsRemoved, false);
+ sheet.Cells.Restore(_cellStoreRestoreData);
+
+ sheet.Columns.InsertImpl(_columnIndex, _nColsRemoved);
+ sheet.Columns.Restore(_columnInfoRestoreData);
+
+ sheet.MarkDirty(new ColumnRegion(_columnIndex, sheet.NumCols));
+
+ return true;
+ }
+
+
+ private void UndoValidation(Sheet sheet)
+ {
+ sheet.Validators.Store.InsertCols(_columnIndex, _nColsRemoved, false);
+ sheet.Validators.Store.Restore(_validatorRestoreData);
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/RemoveRowsCommand.cs b/src/BlazorDatasheet.Core/Commands/RemoveRowsCommand.cs
new file mode 100644
index 00000000..7c3fe2ed
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/RemoveRowsCommand.cs
@@ -0,0 +1,66 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Core.Events;
+using BlazorDatasheet.Core.Formats;
+using BlazorDatasheet.DataStructures.Geometry;
+using BlazorDatasheet.DataStructures.Intervals;
+using BlazorDatasheet.DataStructures.Store;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class RemoveRowsCommand : IUndoableCommand
+{
+ private readonly int _rowIndex;
+ private readonly int _nRows;
+
+ private List _removedCellFormats;
+ private RegionRestoreData _mergeRestoreData;
+ private RegionRestoreData _validatorRestoreData;
+ private RowInfoStoreRestoreData _rowInfoStoreRestore;
+ private CellStoreRestoreData _cellStoreRestoreData;
+
+ // The actual number of rows removed (takes into account num of rows in sheet)
+ private int _nRowsRemoved;
+
+ ///
+ /// Command to remove the row at the index given.
+ ///
+ /// The row to remove.
+ /// The number of rows to remove
+ public RemoveRowsCommand(int rowIndex, int nRows = 1)
+ {
+ _rowIndex = rowIndex;
+ _nRows = nRows;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ if (_rowIndex >= sheet.NumRows)
+ return false;
+ if (_nRows <= 0)
+ return false;
+ _nRowsRemoved = Math.Min(sheet.NumRows - _rowIndex + 1, _nRows);
+
+ _cellStoreRestoreData = sheet.Cells.RemoveRowAt(_rowIndex, _nRowsRemoved);
+ _rowInfoStoreRestore = sheet.Rows.RemoveRowsImpl(_rowIndex, _rowIndex + _nRowsRemoved - 1);
+ _validatorRestoreData = sheet.Validators.Store.RemoveRows(_rowIndex, _rowIndex + _nRowsRemoved - 1);
+ return sheet.RemoveRowAtImpl(_rowIndex, _nRowsRemoved);
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Validators.Store.InsertRows(_rowIndex, _nRowsRemoved, false);
+ sheet.Validators.Store.Restore(_validatorRestoreData);
+
+ sheet.Cells.InsertRowAt(_rowIndex, _nRows, false);
+ sheet.Cells.Restore(_cellStoreRestoreData);
+
+ sheet.Rows.InsertImpl(_rowIndex, _nRowsRemoved);
+ sheet.Rows.Restore(_rowInfoStoreRestore);
+
+ sheet.InsertRowAtImpl(_rowIndex);
+
+ sheet.MarkDirty(new RowRegion(_rowIndex, sheet.NumRows));
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetCellValueCommand.cs b/src/BlazorDatasheet.Core/Commands/SetCellValueCommand.cs
new file mode 100644
index 00000000..bd0bbd82
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetCellValueCommand.cs
@@ -0,0 +1,32 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Formula.Core;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetCellValueCommand : IUndoableCommand
+{
+ private readonly int _row;
+ private readonly int _col;
+ private readonly object? _value;
+ private CellStoreRestoreData _restoreData;
+
+ public SetCellValueCommand(int row, int col, object? value)
+ {
+ _row = row;
+ _col = col;
+ _value = value;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Cells.SetValueImpl(_row, _col, _value);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Cells.Restore(_restoreData);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetColumnHeadingsCommand.cs b/src/BlazorDatasheet.Core/Commands/SetColumnHeadingsCommand.cs
new file mode 100644
index 00000000..d40b07fc
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetColumnHeadingsCommand.cs
@@ -0,0 +1,34 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetColumnHeadingsCommand: IUndoableCommand
+{
+ private readonly int _colStart;
+ private readonly int _colEnd;
+ private readonly string _heading;
+ private List<(int start, int end, string heading)> _restoreData;
+
+ public SetColumnHeadingsCommand(int colStart, int colEnd, string heading)
+ {
+ _colStart = colStart;
+ _colEnd = colEnd;
+ _heading = heading;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Columns.SetColumnHeadingsImpl(_colStart, _colEnd, _heading);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ foreach (var heading in _restoreData)
+ {
+ sheet.Columns.SetColumnHeadingsImpl(heading.start, heading.end, heading.heading);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetColumnWidthCommand.cs b/src/BlazorDatasheet.Core/Commands/SetColumnWidthCommand.cs
new file mode 100644
index 00000000..b5f9dd86
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetColumnWidthCommand.cs
@@ -0,0 +1,40 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetColumnWidthCommand : IUndoableCommand
+{
+ private readonly int _colStart;
+ private readonly int _colEnd;
+ private readonly double _width;
+ private List<(int start, int end, double width)> _oldWidths;
+
+ ///
+ /// Command that changes the column width to the specified amount
+ ///
+ /// The index of the column to change.
+ /// The end of the range to change
+ /// The new width of the column, in pixels
+ public SetColumnWidthCommand(int colStart, int colEnd, double width)
+ {
+ _colStart = colStart;
+ _colEnd = colEnd;
+ _width = width;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _oldWidths = sheet.Columns.SetColumnWidthsImpl(_colStart, _colEnd, _width);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ for (int i = 0; i < _oldWidths.Count; i++)
+ {
+ sheet.Columns.SetColumnWidthsImpl(_oldWidths[i].start, _oldWidths[i].end, _oldWidths[i].width);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetFormatCommand.cs b/src/BlazorDatasheet.Core/Commands/SetFormatCommand.cs
new file mode 100644
index 00000000..f22aad72
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetFormatCommand.cs
@@ -0,0 +1,64 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Core.Events;
+using BlazorDatasheet.Core.Formats;
+using BlazorDatasheet.DataStructures.Geometry;
+using BlazorDatasheet.DataStructures.Intervals;
+using BlazorDatasheet.DataStructures.Store;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetFormatCommand : IUndoableCommand
+{
+ private readonly CellFormat _cellFormat;
+ private readonly IRegion _region;
+
+ private RowColFormatRestoreData? _colFormatRestoreData;
+ private RowColFormatRestoreData? _rowFormatRestoreData;
+ private CellStoreRestoreData? _cellFormatRestoreData;
+
+ ///
+ /// Command to set the format of the range given. The cell format is merged into the existing format, so that
+ /// only properties that are specifically defined in cellFormat are changed.
+ ///
+ /// The range to set the format for. Can be a cell, column or row range.
+ /// The new cell format.
+ public SetFormatCommand(IRegion region, CellFormat cellFormat)
+ {
+ _cellFormat = cellFormat;
+ _region = region.Clone();
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ if (_region is ColumnRegion columnRegion)
+ _colFormatRestoreData = sheet.Columns.SetColumnFormatImpl(_cellFormat, columnRegion);
+ else if (_region is RowRegion rowRegion)
+ _rowFormatRestoreData = sheet.Rows.SetRowFormatImpl(_cellFormat, rowRegion);
+ else
+ _cellFormatRestoreData = sheet.Cells.MergeFormatImpl(_region, _cellFormat);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ if (_colFormatRestoreData != null)
+ Restore(sheet, _colFormatRestoreData, sheet.Columns.ColFormats);
+ if (_rowFormatRestoreData != null)
+ Restore(sheet, _rowFormatRestoreData, sheet.Rows.RowFormats);
+ if (_cellFormatRestoreData != null)
+ sheet.Cells.Restore(_cellFormatRestoreData);
+
+ sheet.MarkDirty(_region);
+ return true;
+ }
+
+ private void Restore(Sheet sheet, RowColFormatRestoreData restoreData, MergeableIntervalStore store)
+ {
+ foreach (var added in restoreData.IntervalsAdded)
+ store.Clear(added);
+ store.AddRange(restoreData.IntervalsRemoved.Where(x => x.Data != null));
+ foreach (var cellRestore in restoreData.CellFormatRestoreData)
+ sheet.Cells.Restore(cellRestore);
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet/Commands/SetMetaDataCommand.cs b/src/BlazorDatasheet.Core/Commands/SetMetaDataCommand.cs
similarity index 66%
rename from src/BlazorDatasheet/Commands/SetMetaDataCommand.cs
rename to src/BlazorDatasheet.Core/Commands/SetMetaDataCommand.cs
index b5e2b22b..e88da67b 100644
--- a/src/BlazorDatasheet/Commands/SetMetaDataCommand.cs
+++ b/src/BlazorDatasheet.Core/Commands/SetMetaDataCommand.cs
@@ -1,6 +1,6 @@
-using BlazorDatasheet.Data;
+using BlazorDatasheet.Core.Data;
-namespace BlazorDatasheet.Commands;
+namespace BlazorDatasheet.Core.Commands;
public class SetMetaDataCommand : IUndoableCommand
{
@@ -20,14 +20,14 @@ public SetMetaDataCommand(int row, int col, string name, object? value)
public bool Execute(Sheet sheet)
{
- _oldValue = sheet.GetMetaData(_row, _col, _name);
- sheet.SetMetaDataImpl(_row, _col, _name, _value);
+ _oldValue = sheet.Cells.GetMetaData(_row, _col, _name);
+ sheet.Cells.SetMetaDataImpl(_row, _col, _name, _value);
return true;
}
public bool Undo(Sheet sheet)
{
- sheet.SetMetaDataImpl(_row, _col, _name, _oldValue);
+ sheet.Cells.SetMetaDataImpl(_row, _col, _name, _oldValue);
return true;
}
}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetParsedFormulaCommand.cs b/src/BlazorDatasheet.Core/Commands/SetParsedFormulaCommand.cs
new file mode 100644
index 00000000..82acbf3d
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetParsedFormulaCommand.cs
@@ -0,0 +1,33 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.Formula.Core;
+
+namespace BlazorDatasheet.Core.Commands;
+
+internal class SetParsedFormulaCommand : IUndoableCommand
+{
+ private readonly int _row;
+ private readonly int _col;
+ private readonly CellFormula _formula;
+ private readonly bool _calculateSheetOnSet;
+ private CellStoreRestoreData _restoreData;
+
+ public SetParsedFormulaCommand(int row, int col, CellFormula formula)
+ {
+ _row = row;
+ _col = col;
+ _formula = formula;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Cells.SetFormulaImpl(_row, _col, _formula);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Cells.Restore(_restoreData);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetRowHeadingsCommand.cs b/src/BlazorDatasheet.Core/Commands/SetRowHeadingsCommand.cs
new file mode 100644
index 00000000..777e7a8c
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetRowHeadingsCommand.cs
@@ -0,0 +1,34 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetRowHeadingsCommand : IUndoableCommand
+{
+ private readonly int _rowStart;
+ private readonly int _rowEnd;
+ private readonly string _heading;
+ private List<(int start, int end, string heading)> _restoreData;
+
+ public SetRowHeadingsCommand(int rowStart, int rowEnd, string heading)
+ {
+ _rowStart = rowStart;
+ _rowEnd = rowEnd;
+ _heading = heading;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Rows.SetRowHeadingsImpl(_rowStart, _rowEnd, _heading);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ foreach (var heading in _restoreData)
+ {
+ sheet.Rows.SetRowHeadingsImpl(heading.start, heading.end, heading.heading);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetRowHeightCommand.cs b/src/BlazorDatasheet.Core/Commands/SetRowHeightCommand.cs
new file mode 100644
index 00000000..69869320
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetRowHeightCommand.cs
@@ -0,0 +1,33 @@
+using BlazorDatasheet.Core.Data;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetRowHeightCommand : IUndoableCommand
+{
+ private int RowStart { get; }
+ public int RowEnd { get; }
+ private double _height { get; }
+ private List<(int start, int end, double height)> _oldHeights;
+
+ public SetRowHeightCommand(int rowStart, int rowEnd, double height)
+ {
+ RowStart = rowStart;
+ RowEnd = rowEnd;
+ _height = height;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _oldHeights = sheet.Rows.SetRowHeightsImpl(RowStart, RowEnd, _height);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ foreach (var old in _oldHeights)
+ {
+ sheet.Rows.SetRowHeightsImpl(old.start, old.end, old.height);
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetTypeCommand.cs b/src/BlazorDatasheet.Core/Commands/SetTypeCommand.cs
new file mode 100644
index 00000000..310123c8
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetTypeCommand.cs
@@ -0,0 +1,34 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Data.Cells;
+using BlazorDatasheet.DataStructures.Geometry;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetTypeCommand : IUndoableCommand
+{
+ private readonly IRegion _region;
+ private readonly string _type;
+ private CellStoreRestoreData _restoreData;
+
+ public SetTypeCommand(int row, int col, string type) : this(new Region(row, row, col, col), type)
+ {
+ }
+
+ public SetTypeCommand(IRegion region, string type)
+ {
+ _region = region;
+ _type = type;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ _restoreData = sheet.Cells.SetCellTypeImpl(_region, _type);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Cells.Restore(_restoreData);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet.Core/Commands/SetValidatorCommand.cs b/src/BlazorDatasheet.Core/Commands/SetValidatorCommand.cs
new file mode 100644
index 00000000..0c4b5979
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Commands/SetValidatorCommand.cs
@@ -0,0 +1,31 @@
+using BlazorDatasheet.Core.Data;
+using BlazorDatasheet.Core.Interfaces;
+using BlazorDatasheet.DataStructures.Geometry;
+
+namespace BlazorDatasheet.Core.Commands;
+
+public class SetValidatorCommand : IUndoableCommand
+{
+ private readonly IRegion _region;
+ private readonly IDataValidator _validator;
+
+ public SetValidatorCommand(IRegion region, IDataValidator validator)
+ {
+ _region = region;
+ _validator = validator;
+ }
+
+ public bool Execute(Sheet sheet)
+ {
+ sheet.Validators.AddImpl(_validator, _region);
+ sheet.Cells.ValidateRegion(_region);
+ return true;
+ }
+
+ public bool Undo(Sheet sheet)
+ {
+ sheet.Validators.Clear(_validator, _region);
+ sheet.Cells.ValidateRegion(_region);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorDatasheet/Data/CellChange.cs b/src/BlazorDatasheet.Core/Data/CellValueChange.cs
similarity index 77%
rename from src/BlazorDatasheet/Data/CellChange.cs
rename to src/BlazorDatasheet.Core/Data/CellValueChange.cs
index 1b763703..c14d596e 100644
--- a/src/BlazorDatasheet/Data/CellChange.cs
+++ b/src/BlazorDatasheet.Core/Data/CellValueChange.cs
@@ -1,9 +1,9 @@
-namespace BlazorDatasheet.Data;
+namespace BlazorDatasheet.Core.Data;
///
/// Describes a change to a cell's value
///
-public class CellChange
+public class CellValueChange
{
///
/// The cell's row.
@@ -18,7 +18,7 @@ public class CellChange
///
public object? NewValue { get; }
- public CellChange(int row, int col, object? newValue)
+ public CellValueChange(int row, int col, object? newValue)
{
Row = row;
Col = col;
diff --git a/src/BlazorDatasheet.Core/Data/Cells/CellStore.Data.cs b/src/BlazorDatasheet.Core/Data/Cells/CellStore.Data.cs
new file mode 100644
index 00000000..1f154fdf
--- /dev/null
+++ b/src/BlazorDatasheet.Core/Data/Cells/CellStore.Data.cs
@@ -0,0 +1,87 @@
+using BlazorDatasheet.Core.Commands;
+using BlazorDatasheet.DataStructures.Store;
+
+namespace BlazorDatasheet.Core.Data.Cells;
+
+public partial class CellStore
+{
+ ///
+ /// The cell DATA
+ ///
+ private readonly IMatrixDataStore