Browse Source

Merge branch 'master' into pixelperfect

Krzysztof Krysiński 4 years ago
parent
commit
1feac2f236
41 changed files with 1582 additions and 586 deletions
  1. 81 9
      PixiEditor.sln
  2. 62 62
      PixiEditor/Helpers/Behaviours/MouseBehavior.cs
  3. 3 3
      PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs
  4. 230 226
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  5. 1 0
      PixiEditor/Models/Controllers/ClipboardController.cs
  6. 12 12
      PixiEditor/Models/Controllers/PixelChangesController.cs
  7. 45 11
      PixiEditor/Models/Controllers/UndoManager.cs
  8. 93 5
      PixiEditor/Models/DataHolders/Document.cs
  9. 7 7
      PixiEditor/Models/DataHolders/LayerChange.cs
  10. 3 2
      PixiEditor/Models/IO/Importer.cs
  11. 60 60
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  12. 2 0
      PixiEditor/Models/Layers/BasicLayer.cs
  13. 38 7
      PixiEditor/Models/Layers/Layer.cs
  14. 25 0
      PixiEditor/Models/Layers/LayerHelper.cs
  15. 5 4
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  16. 6 5
      PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs
  17. 19 15
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  18. 8 4
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  19. 126 97
      PixiEditor/Models/Undo/Change.cs
  20. 182 0
      PixiEditor/Models/Undo/StorageBasedChange.cs
  21. 52 0
      PixiEditor/Models/Undo/UndoLayer.cs
  22. 2 11
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  23. 21 1
      PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs
  24. 2 3
      PixiEditor/ViewModels/ViewModelMain.cs
  25. 1 1
      PixiEditor/Views/UserControls/DrawingViewPort.xaml
  26. 1 1
      PixiEditor/Views/UserControls/LayerItem.xaml
  27. 2 2
      PixiEditor/Views/UserControls/LayerItem.xaml.cs
  28. 5 4
      PixiEditor/Views/UserControls/NumberInput.xaml
  29. 5 5
      PixiEditor/Views/UserControls/NumberInput.xaml.cs
  30. 16 0
      PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml
  31. 50 0
      PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml.cs
  32. 7 0
      PixiEditorTests/ApplicationFixture.cs
  33. 1 0
      PixiEditorTests/ModelsTests/ControllersTests/BitmapManagerTests.cs
  34. 3 3
      PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs
  35. 19 13
      PixiEditorTests/ModelsTests/ControllersTests/PixelChangesControllerTests.cs
  36. 53 0
      PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs
  37. 138 0
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  38. 4 3
      PixiEditorTests/ModelsTests/ImageManipulationTests/BitmapUtilsTests.cs
  39. 1 10
      PixiEditorTests/ModelsTests/LayersTests/LayerTests.cs
  40. 22 0
      PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs
  41. 169 0
      PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs

+ 81 - 9
PixiEditor.sln

@@ -3,43 +3,115 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 16
 VisualStudioVersion = 16.0.28729.10
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor", "PixiEditor\PixiEditor.csproj", "{2CCDDE79-06CB-4771-AF85-7B25313EBA30}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor", "PixiEditor\PixiEditor.csproj", "{2CCDDE79-06CB-4771-AF85-7B25313EBA30}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UpdateInstaller", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj", "{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateInstaller", "PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj", "{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UpdateModule", "PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj", "{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.UpdateModule", "PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj", "{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditorTests", "PixiEditorTests\PixiEditorTests.csproj", "{5193C1C1-8362-40FD-802B-E097E8C88082}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorTests", "PixiEditorTests\PixiEditorTests.csproj", "{5193C1C1-8362-40FD-802B-E097E8C88082}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildConfiguration", "BuildConfiguration", "{9337E60D-8425-4E87-950C-F07A09518081}"
-ProjectSection(SolutionItems) = preProject
-	stylecop.json = stylecop.json
-	Custom.ruleset = Custom.ruleset
-	Directory.Build.props = Directory.Build.props
-EndProjectSection
+	ProjectSection(SolutionItems) = preProject
+		Custom.ruleset = Custom.ruleset
+		Directory.Build.props = Directory.Build.props
+		stylecop.json = stylecop.json
+	EndProjectSection
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
+		Debug|ARM = Debug|ARM
+		Debug|ARM64 = Debug|ARM64
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
 		Release|Any CPU = Release|Any CPU
+		Release|ARM = Release|ARM
+		Release|ARM64 = Release|ARM64
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|ARM.Build.0 = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x64.Build.0 = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.Build.0 = Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|ARM.ActiveCfg = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|ARM.Build.0 = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|ARM64.Build.0 = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|x64.ActiveCfg = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|x64.Build.0 = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|x86.ActiveCfg = Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Release|x86.Build.0 = Release|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM.Build.0 = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.Build.0 = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM64.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x86.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x86.Build.0 = Release|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|ARM.Build.0 = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.Build.0 = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.Build.0 = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|Any CPU.Build.0 = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM.Build.0 = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM64.Build.0 = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|x64.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|x64.Build.0 = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|x86.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|x86.Build.0 = Release|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|ARM.Build.0 = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x64.Build.0 = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.Build.0 = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|ARM.ActiveCfg = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|ARM.Build.0 = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|ARM64.Build.0 = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|x64.ActiveCfg = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|x64.Build.0 = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|x86.ActiveCfg = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 62 - 62
PixiEditor/Helpers/Behaviours/MouseBehaviour.cs → PixiEditor/Helpers/Behaviours/MouseBehavior.cs

@@ -1,63 +1,63 @@
-using System.Windows;
-using System.Windows.Input;
-using System.Windows.Interactivity;
-
-namespace PixiEditor.Helpers.Behaviours
-{
-    public class MouseBehaviour : Behavior<FrameworkElement>
-    {
-        public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register(
-            "MouseY", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));
-
-        public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register(
-            "MouseX", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));
-
-        // Using a DependencyProperty as the backing store for RelativeTo.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty RelativeToProperty =
-            DependencyProperty.Register(
-                "RelativeTo",
-                typeof(FrameworkElement),
-                typeof(MouseBehaviour),
-                new PropertyMetadata(default(FrameworkElement)));
-
-        public double MouseY
-        {
-            get => (double)GetValue(MouseYProperty);
-            set => SetValue(MouseYProperty, value);
-        }
-
-        public double MouseX
-        {
-            get => (double)GetValue(MouseXProperty);
-            set => SetValue(MouseXProperty, value);
-        }
-
-        public FrameworkElement RelativeTo
-        {
-            get => (FrameworkElement)GetValue(RelativeToProperty);
-            set => SetValue(RelativeToProperty, value);
-        }
-
-        protected override void OnAttached()
-        {
-            AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
-        }
-
-        protected override void OnDetaching()
-        {
-            AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
-        }
-
-        private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
-        {
-            if (RelativeTo == null)
-            {
-                RelativeTo = AssociatedObject;
-            }
-
-            Point pos = mouseEventArgs.GetPosition(RelativeTo);
-            MouseX = pos.X;
-            MouseY = pos.Y;
-        }
-    }
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Interactivity;
+
+namespace PixiEditor.Helpers.Behaviours
+{
+    public class MouseBehavior : Behavior<FrameworkElement>
+    {
+        public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register(
+            "MouseY", typeof(double), typeof(MouseBehavior), new PropertyMetadata(default(double)));
+
+        public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register(
+            "MouseX", typeof(double), typeof(MouseBehavior), new PropertyMetadata(default(double)));
+
+        // Using a DependencyProperty as the backing store for RelativeTo.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty RelativeToProperty =
+            DependencyProperty.Register(
+                "RelativeTo",
+                typeof(FrameworkElement),
+                typeof(MouseBehavior),
+                new PropertyMetadata(default(FrameworkElement)));
+
+        public double MouseY
+        {
+            get => (double)GetValue(MouseYProperty);
+            set => SetValue(MouseYProperty, value);
+        }
+
+        public double MouseX
+        {
+            get => (double)GetValue(MouseXProperty);
+            set => SetValue(MouseXProperty, value);
+        }
+
+        public FrameworkElement RelativeTo
+        {
+            get => (FrameworkElement)GetValue(RelativeToProperty);
+            set => SetValue(RelativeToProperty, value);
+        }
+
+        protected override void OnAttached()
+        {
+            AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
+        }
+
+        protected override void OnDetaching()
+        {
+            AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
+        }
+
+        private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
+        {
+            if (RelativeTo == null)
+            {
+                RelativeTo = AssociatedObject;
+            }
+
+            Point pos = mouseEventArgs.GetPosition(RelativeTo);
+            MouseX = pos.X;
+            MouseY = pos.Y;
+        }
+    }
 }

+ 3 - 3
PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs

@@ -5,17 +5,17 @@ namespace PixiEditor.Models.Controllers
 {
     public class BitmapChangedEventArgs : EventArgs
     {
-        public BitmapChangedEventArgs(BitmapPixelChanges pixelsChanged, BitmapPixelChanges oldPixelsValues, int changedLayerIndex)
+        public BitmapChangedEventArgs(BitmapPixelChanges pixelsChanged, BitmapPixelChanges oldPixelsValues, Guid changedLayerGuid)
         {
             PixelsChanged = pixelsChanged;
             OldPixelsValues = oldPixelsValues;
-            ChangedLayerIndex = changedLayerIndex;
+            ChangedLayerGuid = changedLayerGuid;
         }
 
         public BitmapPixelChanges PixelsChanged { get; set; }
 
         public BitmapPixelChanges OldPixelsValues { get; set; }
 
-        public int ChangedLayerIndex { get; set; }
+        public Guid ChangedLayerGuid{ get; set; }
     }
 }

+ 230 - 226
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -1,227 +1,231 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Helpers.Extensions;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class BitmapOperationsUtility
-    {
-        public List<LayerChange> PreviewLayerChanges => previewLayerChanges;
-
-        private List<LayerChange> previewLayerChanges;
-
-        private Coordinates lastMousePos;
-
-        public BitmapOperationsUtility(BitmapManager manager)
-        {
-            Manager = manager;
-        }
-
-        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
-
-        public BitmapManager Manager { get; set; }
-
-        public void DeletePixels(Layer[] layers, Coordinates[] pixels)
-        {
-            if (Manager.ActiveDocument == null)
-            {
-                return;
-            }
-
-            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, Color.FromArgb(0, 0, 0, 0));
-            Dictionary<Layer, Color[]> oldValues = BitmapUtils.GetPixelsForSelection(layers, pixels);
-            LayerChange[] old = new LayerChange[layers.Length];
-            LayerChange[] newChange = new LayerChange[layers.Length];
-            for (int i = 0; i < layers.Length; i++)
-            {
-                int indexOfLayer = Manager.ActiveDocument.Layers.IndexOf(layers[i]);
-                old[i] = new LayerChange(
-                    BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i]]), indexOfLayer);
-                newChange[i] = new LayerChange(changes, indexOfLayer);
-                layers[i].SetPixels(changes);
-            }
-
-            Manager.ActiveDocument.UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
-        }
-
-        /// <summary>
-        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!.
-        /// </summary>
-        /// <param name="newPos">Most recent coordinates.</param>
-        /// <param name="mouseMove">Last mouse movement coordinates.</param>
-        /// <param name="tool">Tool to execute.</param>
-        public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
-        {
-            if (Manager.ActiveDocument != null && tool != null)
-            {
-                if (Manager.ActiveDocument.Layers.Count == 0 || mouseMove.Count == 0)
-                {
-                    return;
-                }
-
-                mouseMove.Reverse();
-                UseTool(mouseMove, tool, Manager.PrimaryColor);
-
-                lastMousePos = newPos;
-            }
-        }
-
-        /// <summary>
-        ///     Applies pixels from preview layer to selected layer.
-        /// </summary>
-        public void ApplyPreviewLayer()
-        {
-            if (previewLayerChanges == null)
-            {
-                return;
-            }
-
-            foreach (var modifiedLayer in previewLayerChanges)
-            {
-                Layer layer = Manager.ActiveDocument.Layers[modifiedLayer.LayerIndex];
-
-                BitmapPixelChanges oldValues = ApplyToLayer(layer, modifiedLayer).PixelChanges;
-
-                BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                    modifiedLayer.PixelChanges,
-                    oldValues,
-                    modifiedLayer.LayerIndex));
-                Manager.ActiveDocument.GeneratePreviewLayer();
-            }
-
-            previewLayerChanges = null;
-        }
-
-        private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
-        {
-            if (Keyboard.IsKeyDown(Key.LeftShift) && !MouseCordsNotInLine(mouseMoveCords))
-            {
-                mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
-            }
-
-            if (!tool.RequiresPreviewLayer)
-            {
-                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords.ToArray(), color);
-                LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
-                for (int i = 0; i < modifiedLayers.Length; i++)
-                {
-                    Layer layer = Manager.ActiveDocument.Layers[modifiedLayers[i].LayerIndex];
-                    oldPixelsValues[i] = ApplyToLayer(layer, modifiedLayers[i]);
-
-                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        modifiedLayers[i].PixelChanges,
-                        oldPixelsValues[i].PixelChanges,
-                        modifiedLayers[i].LayerIndex));
-                }
-            }
-            else
-            {
-                UseToolOnPreviewLayer(mouseMoveCords, tool.ClearPreviewLayerOnEachIteration);
-            }
-        }
-
-        private LayerChange ApplyToLayer(Layer layer, LayerChange change)
-        {
-            layer.DynamicResize(change.PixelChanges);
-
-            LayerChange oldPixelsValues = new LayerChange(
-                GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
-                change.LayerIndex);
-
-            layer.SetPixels(change.PixelChanges, false);
-            return oldPixelsValues;
-        }
-
-        private bool MouseCordsNotInLine(List<Coordinates> cords)
-        {
-            return cords[0].X == cords[^1].X || cords[0].Y == cords[^1].Y;
-        }
-
-        /// <summary>
-        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
-        /// </summary>
-        private List<Coordinates> GetSquareCoordiantes(List<Coordinates> mouseMoveCords)
-        {
-            int xLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            int yLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            if (mouseMoveCords[^1].Y > mouseMoveCords[0].Y)
-            {
-                xLength *= -1;
-            }
-
-            if (mouseMoveCords[^1].X > mouseMoveCords[0].X)
-            {
-                xLength *= -1;
-            }
-
-            mouseMoveCords[0] = new Coordinates(mouseMoveCords[^1].X + xLength, mouseMoveCords[^1].Y + yLength);
-            return mouseMoveCords;
-        }
-
-        private BitmapPixelChanges GetOldPixelsValues(Coordinates[] coordinates)
-        {
-            Dictionary<Coordinates, Color> values = new Dictionary<Coordinates, Color>();
-            using (Manager.ActiveLayer.LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
-            {
-                Coordinates[] relativeCoords = Manager.ActiveLayer.ConvertToRelativeCoordinates(coordinates);
-                for (int i = 0; i < coordinates.Length; i++)
-                {
-                    values.Add(
-                        coordinates[i],
-                        Manager.ActiveLayer.GetPixel(relativeCoords[i].X, relativeCoords[i].Y));
-                }
-            }
-
-            return new BitmapPixelChanges(values);
-        }
-
-        private void UseToolOnPreviewLayer(List<Coordinates> mouseMove, bool clearPreviewLayer = true)
-        {
-            LayerChange[] modifiedLayers;
-            if (mouseMove.Count > 0 && mouseMove[0] != lastMousePos)
-            {
-                if (clearPreviewLayer || Manager.ActiveDocument.PreviewLayer == null)
-                {
-                    Manager.ActiveDocument.GeneratePreviewLayer();
-                }
-
-                modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
-                    Manager.ActiveDocument.ActiveLayer,
-                    mouseMove.ToArray(),
-                    Manager.PrimaryColor);
-
-                BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
-                Manager.ActiveDocument.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
-
-                if (clearPreviewLayer || previewLayerChanges == null)
-                {
-                    previewLayerChanges = new List<LayerChange>(modifiedLayers);
-                }
-                else
-                {
-                    InjectPreviewLayerChanges(modifiedLayers);
-                }
-            }
-        }
-
-        private void InjectPreviewLayerChanges(LayerChange[] modifiedLayers)
-        {
-            for (int i = 0; i < modifiedLayers.Length; i++)
-            {
-                var layer = previewLayerChanges.First(x => x.LayerIndex == modifiedLayers[i].LayerIndex);
-                layer.PixelChanges.ChangedPixels.AddRangeOverride(modifiedLayers[i].PixelChanges.ChangedPixels);
-                layer.PixelChanges = layer.PixelChanges.WithoutTransparentPixels();
-            }
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using PixiEditor.Models.Undo;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class BitmapOperationsUtility
+    {
+        public List<LayerChange> PreviewLayerChanges => previewLayerChanges;
+
+        private List<LayerChange> previewLayerChanges;
+
+        private Coordinates lastMousePos;
+
+        public BitmapOperationsUtility(BitmapManager manager)
+        {
+            Manager = manager;
+        }
+
+        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
+
+        public BitmapManager Manager { get; set; }
+
+        public void DeletePixels(Layer[] layers, Coordinates[] pixels)
+        {
+            if (Manager.ActiveDocument == null)
+            {
+                return;
+            }
+
+            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, Color.FromArgb(0, 0, 0, 0));
+            Dictionary<Guid, Color[]> oldValues = BitmapUtils.GetPixelsForSelection(layers, pixels);
+            LayerChange[] old = new LayerChange[layers.Length];
+            LayerChange[] newChange = new LayerChange[layers.Length];
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Guid guid = layers[i].LayerGuid;
+                old[i] = new LayerChange(
+                    BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i].LayerGuid]), guid);
+                newChange[i] = new LayerChange(changes, guid);
+                layers[i].SetPixels(changes);
+            }
+
+            Manager.ActiveDocument.UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
+        }
+
+        /// <summary>
+        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!.
+        /// </summary>
+        /// <param name="newPos">Most recent coordinates.</param>
+        /// <param name="mouseMove">Last mouse movement coordinates.</param>
+        /// <param name="tool">Tool to execute.</param>
+        public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
+        {
+            if (Manager.ActiveDocument != null && tool != null)
+            {
+                if (Manager.ActiveDocument.Layers.Count == 0 || mouseMove.Count == 0)
+                {
+                    return;
+                }
+
+                mouseMove.Reverse();
+                UseTool(mouseMove, tool, Manager.PrimaryColor);
+
+                lastMousePos = newPos;
+            }
+        }
+
+        /// <summary>
+        ///     Applies pixels from preview layer to selected layer.
+        /// </summary>
+        public void ApplyPreviewLayer()
+        {
+            if (previewLayerChanges == null)
+            {
+                return;
+            }
+
+            foreach (var modifiedLayer in previewLayerChanges)
+            {
+                Layer layer = Manager.ActiveDocument.Layers.FirstOrDefault(x => x.LayerGuid == lastModifiedLayers[i].LayerGuid);
+
+                if (layer != null)
+                {
+                    BitmapPixelChanges oldValues = ApplyToLayer(layer, lastModifiedLayers[i]).PixelChanges;
+
+                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
+                        lastModifiedLayers[i].PixelChanges,
+                        oldValues,
+                        lastModifiedLayers[i].LayerGuid));
+                    Manager.ActiveDocument.GeneratePreviewLayer();
+                }
+            }
+
+            previewLayerChanges = null;
+        }
+
+        private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
+        {
+            if (Keyboard.IsKeyDown(Key.LeftShift) && !MouseCordsNotInLine(mouseMoveCords))
+            {
+                mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
+            }
+
+            if (!tool.RequiresPreviewLayer)
+            {
+                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords.ToArray(), color);
+                LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
+                for (int i = 0; i < modifiedLayers.Length; i++)
+                {
+                    Layer layer = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
+                    oldPixelsValues[i] = ApplyToLayer(layer, modifiedLayers[i]);
+
+                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
+                        modifiedLayers[i].PixelChanges,
+                        oldPixelsValues[i].PixelChanges,
+                        modifiedLayers[i].LayerGuid));
+                }
+            }
+            else
+            {
+                UseToolOnPreviewLayer(mouseMoveCords, tool.ClearPreviewLayerOnEachIteration);
+            }
+        }
+
+        private LayerChange ApplyToLayer(Layer layer, LayerChange change)
+        {
+            layer.DynamicResize(change.PixelChanges);
+
+            LayerChange oldPixelsValues = new LayerChange(
+                GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
+                change.LayerGuid);
+
+            layer.SetPixels(change.PixelChanges, false);
+            return oldPixelsValues;
+        }
+
+        private bool MouseCordsNotInLine(List<Coordinates> cords)
+        {
+            return cords[0].X == cords[^1].X || cords[0].Y == cords[^1].Y;
+        }
+
+        /// <summary>
+        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
+        /// </summary>
+        private List<Coordinates> GetSquareCoordiantes(List<Coordinates> mouseMoveCords)
+        {
+            int xLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
+            int yLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
+            if (mouseMoveCords[^1].Y > mouseMoveCords[0].Y)
+            {
+                xLength *= -1;
+            }
+
+            if (mouseMoveCords[^1].X > mouseMoveCords[0].X)
+            {
+                xLength *= -1;
+            }
+
+            mouseMoveCords[0] = new Coordinates(mouseMoveCords[^1].X + xLength, mouseMoveCords[^1].Y + yLength);
+            return mouseMoveCords;
+        }
+
+        private BitmapPixelChanges GetOldPixelsValues(Coordinates[] coordinates)
+        {
+            Dictionary<Coordinates, Color> values = new Dictionary<Coordinates, Color>();
+            using (Manager.ActiveLayer.LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
+            {
+                Coordinates[] relativeCoords = Manager.ActiveLayer.ConvertToRelativeCoordinates(coordinates);
+                for (int i = 0; i < coordinates.Length; i++)
+                {
+                    values.Add(
+                        coordinates[i],
+                        Manager.ActiveLayer.GetPixel(relativeCoords[i].X, relativeCoords[i].Y));
+                }
+            }
+
+            return new BitmapPixelChanges(values);
+        }
+
+        private void UseToolOnPreviewLayer(List<Coordinates> mouseMove, bool clearPreviewLayer = true)
+        {
+            LayerChange[] modifiedLayers;
+            if (mouseMove.Count > 0 && mouseMove[0] != lastMousePos)
+            {
+                if (clearPreviewLayer || Manager.ActiveDocument.PreviewLayer == null)
+                {
+                    Manager.ActiveDocument.GeneratePreviewLayer();
+                }
+
+                modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
+                    Manager.ActiveDocument.ActiveLayer,
+                    mouseMove.ToArray(),
+                    Manager.PrimaryColor);
+
+                BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
+                Manager.ActiveDocument.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
+
+                if (clearPreviewLayer || previewLayerChanges == null)
+                {
+                    previewLayerChanges = new List<LayerChange>(modifiedLayers);
+                }
+                else
+                {
+                    InjectPreviewLayerChanges(modifiedLayers);
+                }
+            }
+        }
+
+        private void InjectPreviewLayerChanges(LayerChange[] modifiedLayers)
+        {
+            for (int i = 0; i < modifiedLayers.Length; i++)
+            {
+                var layer = previewLayerChanges.First(x => x.LayerIndex == modifiedLayers[i].LayerIndex);
+                layer.PixelChanges.ChangedPixels.AddRangeOverride(modifiedLayers[i].PixelChanges.ChangedPixels);
+                layer.PixelChanges = layer.PixelChanges.WithoutTransparentPixels();
+            }
+        }
+    }
 }

+ 1 - 0
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -6,6 +6,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers

+ 12 - 12
PixiEditor/Models/Controllers/PixelChangesController.cs

@@ -7,9 +7,9 @@ namespace PixiEditor.Models.Controllers
 {
     public class PixelChangesController
     {
-        private Dictionary<int, LayerChange> LastChanges { get; set; }
+        private Dictionary<Guid, LayerChange> LastChanges { get; set; }
 
-        private Dictionary<int, LayerChange> LastOldValues { get; set; }
+        private Dictionary<Guid, LayerChange> LastOldValues { get; set; }
 
         /// <summary>
         ///     Adds layer changes to controller.
@@ -22,10 +22,10 @@ namespace PixiEditor.Models.Controllers
             {
                 if (LastChanges == null)
                 {
-                    LastChanges = new Dictionary<int, LayerChange> { { changes.LayerIndex, changes } };
-                    LastOldValues = new Dictionary<int, LayerChange> { { oldValues.LayerIndex, oldValues } };
+                    LastChanges = new Dictionary<Guid, LayerChange> { { changes.LayerGuid, changes } };
+                    LastOldValues = new Dictionary<Guid, LayerChange> { { oldValues.LayerGuid, oldValues } };
                 }
-                else if (LastChanges.ContainsKey(changes.LayerIndex))
+                else if (LastChanges.ContainsKey(changes.LayerGuid))
                 {
                     AddToExistingLayerChange(changes, oldValues);
                 }
@@ -50,7 +50,7 @@ namespace PixiEditor.Models.Controllers
 
             Tuple<LayerChange, LayerChange>[] result = new Tuple<LayerChange, LayerChange>[LastChanges.Count];
             int i = 0;
-            foreach (KeyValuePair<int, LayerChange> change in LastChanges)
+            foreach (KeyValuePair<Guid, LayerChange> change in LastChanges)
             {
                 Dictionary<Position.Coordinates, System.Windows.Media.Color> pixelChanges =
                     change.Value.PixelChanges.ChangedPixels.ToDictionary(entry => entry.Key, entry => entry.Value);
@@ -71,33 +71,33 @@ namespace PixiEditor.Models.Controllers
 
         private void AddNewLayerChange(LayerChange changes, LayerChange oldValues)
         {
-            LastChanges[changes.LayerIndex] = changes;
-            LastOldValues[changes.LayerIndex] = oldValues;
+            LastChanges[changes.LayerGuid] = changes;
+            LastOldValues[changes.LayerGuid] = oldValues;
         }
 
         private void AddToExistingLayerChange(LayerChange layerChange, LayerChange oldValues)
         {
             foreach (KeyValuePair<Position.Coordinates, System.Windows.Media.Color> change in layerChange.PixelChanges.ChangedPixels)
             {
-                if (LastChanges[layerChange.LayerIndex].PixelChanges.ChangedPixels.ContainsKey(change.Key))
+                if (LastChanges[layerChange.LayerGuid].PixelChanges.ChangedPixels.ContainsKey(change.Key))
                 {
                     continue;
                 }
                 else
                 {
-                    LastChanges[layerChange.LayerIndex].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
+                    LastChanges[layerChange.LayerGuid].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
                 }
             }
 
             foreach (KeyValuePair<Position.Coordinates, System.Windows.Media.Color> change in oldValues.PixelChanges.ChangedPixels)
             {
-                if (LastOldValues[layerChange.LayerIndex].PixelChanges.ChangedPixels.ContainsKey(change.Key))
+                if (LastOldValues[layerChange.LayerGuid].PixelChanges.ChangedPixels.ContainsKey(change.Key))
                 {
                     continue;
                 }
                 else
                 {
-                    LastOldValues[layerChange.LayerIndex].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
+                    LastOldValues[layerChange.LayerGuid].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
                 }
             }
         }

+ 45 - 11
PixiEditor/Models/Controllers/UndoManager.cs

@@ -1,6 +1,8 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
-using PixiEditor.Models.DataHolders;
+using System.Reflection;
+using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers
@@ -9,6 +11,8 @@ namespace PixiEditor.Models.Controllers
     {
         private bool lastChangeWasUndo;
 
+        private PropertyInfo newUndoChangeBlockedProperty;
+
         public Stack<Change> UndoStack { get; set; } = new Stack<Change>();
 
         public Stack<Change> RedoStack { get; set; } = new Stack<Change>();
@@ -35,15 +39,21 @@ namespace PixiEditor.Models.Controllers
         /// <summary>
         ///     Adds property change to UndoStack.
         /// </summary>
-        public void AddUndoChange(Change change)
+        public void AddUndoChange(Change change, bool invokedInsideSetter = false)
         {
+            if (change.Property != null && (ChangeIsBlockedProperty(change) && invokedInsideSetter == true))
+            {
+                newUndoChangeBlockedProperty = null;
+                return;
+            }
+
             lastChangeWasUndo = false;
 
             // Clears RedoStack if last move wasn't redo or undo and if redo stack is greater than 0.
             if (lastChangeWasUndo == false && RedoStack.Count > 0)
-            {
-                RedoStack.Clear();
-            }
+                {
+                    RedoStack.Clear();
+                }
 
             change.Root ??= MainRoot;
             UndoStack.Push(change);
@@ -58,7 +68,7 @@ namespace PixiEditor.Models.Controllers
             Change change = UndoStack.Pop();
             if (change.ReverseProcess == null)
             {
-                SetPropertyValue(change.Root, change.Property, change.OldValue);
+                SetPropertyValue(GetChangeRoot(change), change.Property, change.OldValue);
             }
             else
             {
@@ -77,7 +87,7 @@ namespace PixiEditor.Models.Controllers
             Change change = RedoStack.Pop();
             if (change.Process == null)
             {
-                SetPropertyValue(change.Root, change.Property, change.NewValue);
+                SetPropertyValue(GetChangeRoot(change), change.Property, change.NewValue);
             }
             else
             {
@@ -87,17 +97,41 @@ namespace PixiEditor.Models.Controllers
             UndoStack.Push(change);
         }
 
+        private bool ChangeIsBlockedProperty(Change change)
+        {
+            return (change.Root != null || change.FindRootProcess != null)
+                && GetProperty(GetChangeRoot(change), change.Property).Item1 == newUndoChangeBlockedProperty;
+        }
+
+        private object GetChangeRoot(Change change)
+        {
+            return change.FindRootProcess != null ? change.FindRootProcess(change.FindRootProcessArgs) : change.Root;
+        }
+
         private void SetPropertyValue(object target, string propName, object value)
+        {
+            var properties = GetProperty(target, propName);
+            PropertyInfo propertyToSet = properties.Item1;
+            newUndoChangeBlockedProperty = propertyToSet;
+            propertyToSet.SetValue(properties.Item2, value, null);
+        }
+
+        /// <summary>
+        /// Gets property info for propName from target. Supports '.' format.
+        /// </summary>
+        /// <param name="target">A object where target can be found.</param>
+        /// <param name="propName">Name of property to get, supports nested property.</param>
+        /// <returns>PropertyInfo about property and target object where property can be found.</returns>
+        private Tuple<PropertyInfo, object> GetProperty(object target, string propName)
         {
             string[] bits = propName.Split('.');
             for (int i = 0; i < bits.Length - 1; i++)
             {
-                System.Reflection.PropertyInfo propertyToGet = target.GetType().GetProperty(bits[i]);
+                PropertyInfo propertyToGet = target.GetType().GetProperty(bits[i]);
                 target = propertyToGet.GetValue(target, null);
             }
 
-            System.Reflection.PropertyInfo propertyToSet = target.GetType().GetProperty(bits.Last());
-            propertyToSet.SetValue(target, value, null);
+            return new Tuple<PropertyInfo, object>(target.GetType().GetProperty(bits.Last()), target);
         }
     }
 }

+ 93 - 5
PixiEditor/Models/DataHolders/Document.cs

@@ -15,6 +15,7 @@ using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.DataHolders
@@ -308,11 +309,29 @@ namespace PixiEditor.Models.DataHolders
                 ActiveLayer.IsActive = false;
             }
 
+            if (Layers.Any(x => x.IsActive))
+            {
+                var guids = Layers.Where(x => x.IsActive).Select(y => y.LayerGuid);
+                guids.ToList().ForEach(x => Layers.First(layer => layer.LayerGuid == x).IsActive = false);
+            }
+
             ActiveLayerIndex = index;
             ActiveLayer.IsActive = true;
             LayersChanged?.Invoke(this, new LayersChangedEventArgs(index, LayerAction.SetActive));
         }
 
+        public void MoveLayerIndexBy(int layerIndex, int amount)
+        {
+            MoveLayerProcess(new object[] { layerIndex, amount });
+
+            UndoManager.AddUndoChange(new Change(
+                MoveLayerProcess,
+                new object[] { layerIndex + amount, -amount },
+                MoveLayerProcess,
+                new object[] { layerIndex, amount },
+                "Move layer"));
+        }
+
         public void AddNewLayer(string name, WriteableBitmap bitmap, bool setAsActive = true)
         {
             AddNewLayer(name, bitmap.PixelWidth, bitmap.PixelHeight, setAsActive);
@@ -336,9 +355,35 @@ namespace PixiEditor.Models.DataHolders
                 SetActiveLayer(Layers.Count - 1);
             }
 
+            if (Layers.Count > 1)
+            {
+                StorageBasedChange storageChange = new StorageBasedChange(this, new[] { Layers[^1] }, false);
+                UndoManager.AddUndoChange(
+                    storageChange.ToChange(
+                        RemoveLayerProcess,
+                        new object[] { Layers[^1].LayerGuid },
+                        RestoreLayersProcess,
+                        "Add layer"));
+            }
+
             LayersChanged?.Invoke(this, new LayersChangedEventArgs(0, LayerAction.Add));
         }
 
+        public void SetNextLayerAsActive(int lastLayerIndex)
+        {
+            if (Layers.Count > 0)
+            {
+                if (lastLayerIndex == 0)
+                {
+                    SetActiveLayer(0);
+                }
+                else
+                {
+                    SetActiveLayer(lastLayerIndex - 1);
+                }
+            }
+        }
+
         public void RemoveLayer(int layerIndex)
         {
             if (Layers.Count == 0)
@@ -347,14 +392,15 @@ namespace PixiEditor.Models.DataHolders
             }
 
             bool wasActive = Layers[layerIndex].IsActive;
+
+            StorageBasedChange change = new StorageBasedChange(this, new[] { Layers[layerIndex] });
+            UndoManager.AddUndoChange(
+                change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }, "Remove layer"));
+
             Layers.RemoveAt(layerIndex);
             if (wasActive)
             {
-                SetActiveLayer(0);
-            }
-            else if (ActiveLayerIndex > Layers.Count - 1)
-            {
-                SetActiveLayer(Layers.Count - 1);
+                SetNextLayerAsActive(layerIndex);
             }
         }
 
@@ -448,6 +494,48 @@ namespace PixiEditor.Models.DataHolders
                     "Center content"));
         }
 
+        private void MoveLayerProcess(object[] parameter)
+        {
+            int layerIndex = (int)parameter[0];
+            int amount = (int)parameter[1];
+
+            Layers.Move(layerIndex, layerIndex + amount);
+            if (ActiveLayerIndex == layerIndex)
+            {
+                SetActiveLayer(layerIndex + amount);
+            }
+        }
+
+        private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
+        {
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Layer layer = layers[i];
+
+                Layers.Insert(layersData[i].LayerIndex, layer);
+                if (layersData[i].IsActive)
+                {
+                    SetActiveLayer(Layers.IndexOf(layer));
+                }
+            }
+        }
+
+        private void RemoveLayerProcess(object[] parameters)
+        {
+            if (parameters != null && parameters.Length > 0 && parameters[0] is Guid layerGuid)
+            {
+                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+                int index = Layers.IndexOf(layer);
+                bool wasActive = layer.IsActive;
+                Layers.Remove(layer);
+
+                if (wasActive || ActiveLayerIndex >= index)
+                {
+                    SetNextLayerAsActive(index);
+                }
+            }
+        }
+
         private void SetAsActiveOnClick(object obj)
         {
             XamlAccesibleViewModel.BitmapManager.MouseController.StopRecordingMouseMovementChanges();

+ 7 - 7
PixiEditor/Models/DataHolders/LayerChange.cs

@@ -1,24 +1,24 @@
-using PixiEditor.Models.Layers;
-using PixiEditor.ViewModels;
-
+using System;
+using PixiEditor.Models.Layers;
+
 namespace PixiEditor.Models.DataHolders
 {
     public class LayerChange
     {
-        public LayerChange(BitmapPixelChanges pixelChanges, int layerIndex)
+        public LayerChange(BitmapPixelChanges pixelChanges, Guid layerGuid)
         {
             PixelChanges = pixelChanges;
-            LayerIndex = layerIndex;
+            LayerGuid = layerGuid;
         }
 
         public LayerChange(BitmapPixelChanges pixelChanges, Layer layer)
         {
             PixelChanges = pixelChanges;
-            LayerIndex = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.IndexOf(layer);
+            LayerGuid = layer.LayerGuid;
         }
 
         public BitmapPixelChanges PixelChanges { get; set; }
 
-        public int LayerIndex { get; set; }
+        public Guid LayerGuid { get; set; }
     }
 }

+ 3 - 2
PixiEditor/Models/IO/Importer.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.Models.IO
         /// <param name="path">Path of image.</param>
         /// <param name="width">New width of image.</param>
         /// <param name="height">New height of image.</param>
-        /// <returns>WriteableBitmap of improted image.</returns>
+        /// <returns>WriteableBitmap of imported image.</returns>
         public static WriteableBitmap ImportImage(string path, int width, int height)
         {
             WriteableBitmap wbmp = ImportImage(path);
@@ -38,10 +38,11 @@ namespace PixiEditor.Models.IO
         {
             try
             {
-                Uri uri = new Uri(path);
+                Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);
                 BitmapImage bitmap = new BitmapImage();
                 bitmap.BeginInit();
                 bitmap.UriSource = uri;
+                bitmap.CacheOption = BitmapCacheOption.OnLoad;
                 bitmap.EndInit();
 
                 return BitmapFactory.ConvertToPbgra32Format(bitmap);

+ 60 - 60
PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -1,12 +1,12 @@
-using System;
+using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Windows;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
 using System.Windows.Media.Imaging;
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
-using Color = System.Windows.Media.Color;
 
 namespace PixiEditor.Models.ImageManipulation
 {
@@ -22,9 +22,9 @@ namespace PixiEditor.Models.ImageManipulation
         public static WriteableBitmap BytesToWriteableBitmap(int currentBitmapWidth, int currentBitmapHeight, byte[] byteArray)
         {
             WriteableBitmap bitmap = BitmapFactory.New(currentBitmapWidth, currentBitmapHeight);
-            if (byteArray != null)
-            {
-                bitmap.FromByteArray(byteArray);
+            if (byteArray != null)
+            {
+                bitmap.FromByteArray(byteArray);
             }
 
             return bitmap;
@@ -43,13 +43,13 @@ namespace PixiEditor.Models.ImageManipulation
 
             using (finalBitmap.GetBitmapContext())
             {
-                for (int i = 0; i < layers.Length; i++)
-                {
-                    float layerOpacity = layers[i].Opacity;
-                    Layer layer = layers[i];
-
-                    for (int y = 0; y < layers[i].Height; y++)
-                    {
+                for (int i = 0; i < layers.Length; i++)
+                {
+                    float layerOpacity = layers[i].Opacity;
+                    Layer layer = layers[i];
+
+                    for (int y = 0; y < layers[i].Height; y++)
+                    {
                         for (int x = 0; x < layers[i].Width; x++)
                         {
                             Color color = layer.GetPixel(x, y);
@@ -67,69 +67,69 @@ namespace PixiEditor.Models.ImageManipulation
                             {
                                 color = Color.FromArgb(color.A, color.R, color.G, color.B);
                             }
-
-                            if (color.A > 0)
-                            {
-                                finalBitmap.SetPixel(x + layer.OffsetX, y + layer.OffsetY, color);
-                            }
-                        }
-                    }
-                }
+
+                            if (color.A > 0)
+                            {
+                                finalBitmap.SetPixel(x + layer.OffsetX, y + layer.OffsetY, color);
+                            }
+                        }
+                    }
+                }
             }
 
             return finalBitmap;
         }
 
-        /// <summary>
-        /// Generates simplified preview from Document, very fast, great for creating small previews. Creates uniform streched image.
-        /// </summary>
-        /// <param name="document">Document which be used to generate preview.</param>
-        /// <param name="maxPreviewWidth">Max width of preview.</param>
-        /// <param name="maxPreviewHeight">Max height of preview.</param>
+      /// <summary>
+        /// Generates simplified preview from Document, very fast, great for creating small previews. Creates uniform streched image.
+        /// </summary>
+        /// <param name="document">Document which be used to generate preview.</param>
+        /// <param name="maxPreviewWidth">Max width of preview.</param>
+        /// <param name="maxPreviewHeight">Max height of preview.</param>
         /// <returns>WriteableBitmap image.</returns>
-        public static WriteableBitmap GeneratePreviewBitmap(Document document, int maxPreviewWidth, int maxPreviewHeight)
-        {
-            WriteableBitmap previewBitmap = BitmapFactory.New(document.Width, document.Height);
-
-            // 0.8 because blit doesn't take into consideration layer opacity. Small images with opacity > 80% are simillar enough.
-            foreach (var layer in document.Layers.Where(x => x.IsVisible && x.Opacity > 0.8f))
-            {
-                previewBitmap.Blit(
-                    new Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height),
-                    layer.LayerBitmap,
-                    new Rect(0, 0, layer.Width, layer.Height));
-            }
-
-            int width = document.Width >= document.Height ? maxPreviewWidth : (int)Math.Ceiling(document.Width / ((float)document.Height / maxPreviewHeight));
-            int height = document.Height > document.Width ? maxPreviewHeight : (int)Math.Ceiling(document.Height / ((float)document.Width / maxPreviewWidth));
-
-            return previewBitmap.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+        public static WriteableBitmap GeneratePreviewBitmap(Document document, int maxPreviewWidth, int maxPreviewHeight)
+        {
+            WriteableBitmap previewBitmap = BitmapFactory.New(document.Width, document.Height);
+
+            // 0.8 because blit doesn't take into consideration layer opacity. Small images with opacity > 80% are simillar enough.
+            foreach (var layer in document.Layers.Where(x => x.IsVisible && x.Opacity > 0.8f))
+            {
+                previewBitmap.Blit(
+                    new Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height),
+                    layer.LayerBitmap,
+                    new Rect(0, 0, layer.Width, layer.Height));
+            }
+
+            int width = document.Width >= document.Height ? maxPreviewWidth : (int)Math.Ceiling(document.Width / ((float)document.Height / maxPreviewHeight));
+            int height = document.Height > document.Width ? maxPreviewHeight : (int)Math.Ceiling(document.Height / ((float)document.Width / maxPreviewWidth));
+
+            return previewBitmap.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
         }
 
-        public static Dictionary<Layer, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
+        public static Dictionary<Guid, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
         {
-            Dictionary<Layer, Color[]> result = new Dictionary<Layer, Color[]>();
+            Dictionary<Guid, Color[]> result = new Dictionary<Guid, Color[]>();
 
-            for (int i = 0; i < layers.Length; i++)
+            foreach (Layer layer in layers)
             {
                 Color[] pixels = new Color[selection.Length];
 
-                using (layers[i].LayerBitmap.GetBitmapContext())
+                using (layer.LayerBitmap.GetBitmapContext())
                 {
                     for (int j = 0; j < pixels.Length; j++)
                     {
-                        Coordinates position = layers[i].GetRelativePosition(selection[j]);
-                        if (position.X < 0 || position.X > layers[i].Width - 1 || position.Y < 0 ||
-                            position.Y > layers[i].Height - 1)
-                        {
-                            continue;
-                        }
-
-                        pixels[j] = layers[i].GetPixel(position.X, position.Y);
+                        Coordinates position = layer.GetRelativePosition(selection[j]);
+                        if (position.X < 0 || position.X > layer.Width - 1 || position.Y < 0 ||
+                            position.Y > layer.Height - 1)
+                        {
+                            continue;
+                        }
+
+                        pixels[j] = layer.GetPixel(position.X, position.Y);
                     }
                 }
-
-                result[layers[i]] = pixels;
+
+                result[layer.LayerGuid] = pixels;
             }
 
             return result;

+ 2 - 0
PixiEditor/Models/Layers/BasicLayer.cs

@@ -29,5 +29,7 @@ namespace PixiEditor.Models.Layers
                 RaisePropertyChanged("Height");
             }
         }
+
+        public Guid LayerGuid { get; init; }
     }
 }

+ 38 - 7
PixiEditor/Models/Layers/Layer.cs

@@ -4,8 +4,11 @@ using System.Linq;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Layers
 {
@@ -24,7 +27,7 @@ namespace PixiEditor.Models.Layers
 
         private Thickness offset;
 
-        private float opacity = 1;
+        private float opacity = 1f;
 
         public Layer(string name)
         {
@@ -32,6 +35,7 @@ namespace PixiEditor.Models.Layers
             LayerBitmap = BitmapFactory.New(0, 0);
             Width = 0;
             Height = 0;
+            LayerGuid = Guid.NewGuid();
         }
 
         public Layer(string name, int width, int height)
@@ -40,6 +44,7 @@ namespace PixiEditor.Models.Layers
             LayerBitmap = BitmapFactory.New(width, height);
             Width = width;
             Height = height;
+            LayerGuid = Guid.NewGuid();
         }
 
         public Layer(string name, WriteableBitmap layerBitmap)
@@ -48,6 +53,7 @@ namespace PixiEditor.Models.Layers
             LayerBitmap = layerBitmap;
             Width = layerBitmap.PixelWidth;
             Height = layerBitmap.PixelHeight;
+            LayerGuid = Guid.NewGuid();
         }
 
         public Dictionary<Coordinates, Color> LastRelativeCoordinates { get; set; }
@@ -77,8 +83,20 @@ namespace PixiEditor.Models.Layers
             get => isVisible;
             set
             {
-                isVisible = value;
-                RaisePropertyChanged("IsVisible");
+                if (isVisible != value)
+                {
+                    ViewModelMain.Current?.BitmapManager?.ActiveDocument?.UndoManager
+                        .AddUndoChange(
+                        new Change(
+                            nameof(IsVisible),
+                            isVisible,
+                            value,
+                            LayerHelper.FindLayerByGuidProcess,
+                            new object[] { LayerGuid },
+                            "Change layer visibility"), true);
+                    isVisible = value;
+                    RaisePropertyChanged("IsVisible");
+                }
             }
         }
 
@@ -107,8 +125,20 @@ namespace PixiEditor.Models.Layers
             get => opacity;
             set
             {
-                opacity = value;
-                RaisePropertyChanged("Opacity");
+                if (opacity != value)
+                {
+                    ViewModelMain.Current?.BitmapManager?.ActiveDocument?.UndoManager
+                        .AddUndoChange(
+                            new Change(
+                            nameof(Opacity),
+                            opacity,
+                            value,
+                            LayerHelper.FindLayerByGuidProcess,
+                            new object[] { LayerGuid },
+                            "Change layer opacity"), true);
+                    opacity = value;
+                    RaisePropertyChanged("Opacity");
+                }
             }
         }
 
@@ -133,7 +163,7 @@ namespace PixiEditor.Models.Layers
         /// <summary>
         ///     Returns clone of layer.
         /// </summary>
-        public Layer Clone()
+        public Layer Clone(bool generateNewGuid = false)
         {
             return new Layer(Name, LayerBitmap.Clone())
             {
@@ -143,7 +173,8 @@ namespace PixiEditor.Models.Layers
                 MaxWidth = MaxWidth,
                 Opacity = Opacity,
                 IsActive = IsActive,
-                IsRenaming = IsRenaming
+                IsRenaming = IsRenaming,
+                LayerGuid = generateNewGuid ? Guid.NewGuid() : LayerGuid
             };
         }
 

+ 25 - 0
PixiEditor/Models/Layers/LayerHelper.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Linq;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.Layers
+{
+    public static class LayerHelper
+    {
+         public static Layer FindLayerByGuid(Document document, Guid guid)
+         {
+            return document.Layers.FirstOrDefault(x => x.LayerGuid == guid);
+         }
+
+         public static object FindLayerByGuidProcess(object[] parameters)
+         {
+            if (parameters != null && parameters.Length > 0 && parameters[0] is Guid guid)
+            {
+                return FindLayerByGuid(ViewModelMain.Current.BitmapManager.ActiveDocument, guid);
+            }
+
+            return null;
+        }
+    }
+}

+ 5 - 4
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -1,4 +1,5 @@
-using System.Windows.Media;
+using System;
+using System.Windows.Media;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
@@ -13,7 +14,7 @@ namespace PixiEditor.Models.Tools
 
         public bool UseDefaultUndoMethod { get; set; } = true;
 
-        private readonly LayerChange[] onlyLayerArr = new LayerChange[] { new LayerChange(BitmapPixelChanges.Empty, 0) };
+        private readonly LayerChange[] onlyLayerArr = new LayerChange[] { new LayerChange(BitmapPixelChanges.Empty, Guid.Empty) };
 
         public abstract LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color);
 
@@ -23,9 +24,9 @@ namespace PixiEditor.Models.Tools
             return onlyLayerArr;
         }
 
-        protected LayerChange[] Only(BitmapPixelChanges changes, int layerIndex)
+        protected LayerChange[] Only(BitmapPixelChanges changes, Guid layerGuid)
         {
-            onlyLayerArr[0] = new LayerChange(changes, layerIndex);
+            onlyLayerArr[0] = new LayerChange(changes, layerGuid);
             return onlyLayerArr;
         }
     }

+ 6 - 5
PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs

@@ -4,6 +4,7 @@ using System.Windows.Interactivity;
 using System.Windows.Media;
 using ColorPicker;
 using PixiEditor.Helpers.Behaviours;
+using PixiEditor.Views;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
@@ -17,24 +18,24 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             Value = Color.FromArgb(255, 255, 255, 255);
         }
 
-        private PortableColorPicker GenerateColorPicker()
+        private ToolSettingColorPicker GenerateColorPicker()
         {
             var resourceDictionary = new ResourceDictionary();
             resourceDictionary.Source = new System.Uri(
                 "pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml",
                 System.UriKind.RelativeOrAbsolute);
-            PortableColorPicker picker = new PortableColorPicker
+            ToolSettingColorPicker picker = new ToolSettingColorPicker
             {
-                Style = (Style)resourceDictionary["DefaultColorPickerStyle"],
-                SecondaryColor = System.Windows.Media.Colors.Black
+                Style = (Style)resourceDictionary["DefaultColorPickerStyle"]
             };
+
             Binding binding = new Binding("Value")
             {
                 Mode = BindingMode.TwoWay
             };
             GlobalShortcutFocusBehavior behavor = new GlobalShortcutFocusBehavior();
             Interaction.GetBehaviors(picker).Add(behavor);
-            picker.SetBinding(PortableColorPicker.SelectedColorProperty, binding);
+            picker.SetBinding(ToolSettingColorPicker.SelectedColorProperty, binding);
             return picker;
         }
     }

+ 19 - 15
PixiEditor/Models/Tools/Tools/MoveTool.cs

@@ -11,6 +11,7 @@ using PixiEditor.Models.Enums;
 using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 using Transform = PixiEditor.Models.ImageManipulation.Transform;
 
@@ -19,12 +20,12 @@ namespace PixiEditor.Models.Tools.Tools
     public class MoveTool : BitmapOperationTool
     {
         private Layer[] affectedLayers;
-        private Dictionary<Layer, bool> clearedPixels = new Dictionary<Layer, bool>();
+        private Dictionary<Guid, bool> clearedPixels = new Dictionary<Guid, bool>();
         private Coordinates[] currentSelection;
         private Coordinates lastMouseMove;
         private Coordinates lastStartMousePos;
-        private Dictionary<Layer, Color[]> startPixelColors;
-        private Dictionary<Layer, Thickness> startingOffsets;
+        private Dictionary<Guid, Color[]> startPixelColors;
+        private Dictionary<Guid, Thickness> startingOffsets;
         private Coordinates[] startSelection;
         private bool updateViewModelSelection = true;
 
@@ -65,12 +66,12 @@ namespace PixiEditor.Models.Tools.Tools
                 {
                     BitmapPixelChanges beforeMovePixels = BitmapPixelChanges.FromArrays(startSelection, item.Value);
                     Change changes = undoManager.UndoStack.Peek();
-                    int layerIndex = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.IndexOf(item.Key);
+                    Guid layerGuid = item.Key;
 
-                    ((LayerChange[])changes.OldValue).First(x => x.LayerIndex == layerIndex).PixelChanges.ChangedPixels
+                    ((LayerChange[])changes.OldValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
                         .AddRangeOverride(beforeMovePixels.ChangedPixels);
 
-                    ((LayerChange[])changes.NewValue).First(x => x.LayerIndex == layerIndex).PixelChanges.ChangedPixels
+                    ((LayerChange[])changes.NewValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
                         .AddRangeNewOnly(BitmapPixelChanges
                             .FromSingleColoredArray(startSelection, System.Windows.Media.Colors.Transparent)
                             .ChangedPixels);
@@ -165,24 +166,26 @@ namespace PixiEditor.Models.Tools.Tools
             ClearSelectedPixels(layer, previousSelection);
 
             lastMouseMove = end;
-            return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer]);
+            return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
         }
 
         private void ApplyOffsets(object[] parameters)
         {
-            Dictionary<Layer, Thickness> offsets = (Dictionary<Layer, Thickness>)parameters[0];
+            Dictionary<Guid, Thickness> offsets = (Dictionary<Guid, Thickness>)parameters[0];
             foreach (var offset in offsets)
             {
-                offset.Key.Offset = offset.Value;
+                Layer layer = ViewModelMain.Current?.BitmapManager?.
+                    ActiveDocument?.Layers?.First(x => x.LayerGuid == offset.Key);
+                layer.Offset = offset.Value;
             }
         }
 
-        private Dictionary<Layer, Thickness> GetOffsets(Layer[] layers)
+        private Dictionary<Guid, Thickness> GetOffsets(Layer[] layers)
         {
-            Dictionary<Layer, Thickness> dict = new Dictionary<Layer, Thickness>();
+            Dictionary<Guid, Thickness> dict = new Dictionary<Guid, Thickness>();
             for (int i = 0; i < layers.Length; i++)
             {
-                dict.Add(layers[i], layers[i].Offset);
+                dict.Add(layers[i].LayerGuid, layers[i].Offset);
             }
 
             return dict;
@@ -202,7 +205,7 @@ namespace PixiEditor.Models.Tools.Tools
         {
             lastStartMousePos = start;
             lastMouseMove = start;
-            clearedPixels = new Dictionary<Layer, bool>();
+            clearedPixels = new Dictionary<Guid, bool>();
             updateViewModelSelection = true;
             startPixelColors = null;
             startSelection = null;
@@ -217,12 +220,13 @@ namespace PixiEditor.Models.Tools.Tools
 
         private void ClearSelectedPixels(Layer layer, Coordinates[] selection)
         {
-            if (!clearedPixels.ContainsKey(layer) || clearedPixels[layer] == false)
+            Guid layerGuid = layer.LayerGuid;
+            if (!clearedPixels.ContainsKey(layerGuid) || clearedPixels[layerGuid] == false)
             {
                 ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.First(x => x == layer)
                     .SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, System.Windows.Media.Colors.Transparent));
 
-                clearedPixels[layer] = true;
+                clearedPixels[layerGuid] = true;
             }
         }
     }

+ 8 - 4
PixiEditor/Models/Tools/Tools/SelectTool.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows.Controls;
 using System.Windows.Input;
@@ -9,6 +10,7 @@ using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Tools.Tools
@@ -48,10 +50,12 @@ namespace PixiEditor.Models.Tools.Tools
             }
 
             ViewModelMain.Current.BitmapManager.ActiveDocument.UndoManager.AddUndoChange(
-                new Change("ActiveSelection", 
-                oldSelection,
-                ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection,
-                "Select pixels", ViewModelMain.Current.SelectionSubViewModel));
+                new Change(
+                    "SelectedPoints",
+                    oldSelection.SelectedPoints,
+                    new ObservableCollection<Coordinates>(ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints),
+                    "Select pixels",
+                    ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection));
         }
 
         public override void Use(Coordinates[] pixels)

+ 126 - 97
PixiEditor/Models/DataHolders/Change.cs → PixiEditor/Models/Undo/Change.cs

@@ -1,98 +1,127 @@
-using System;
-
-namespace PixiEditor.Models.DataHolders
-{
-    [Serializable]
-    public class Change
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Change"/> class.
-        ///     Creates new change for property based undo system.
-        /// </summary>
-        /// <param name="property">Name of property.</param>
-        /// <param name="oldValue">Old value of property.</param>
-        /// <param name="newValue">New value of property.</param>
-        /// <param name="description">Description of change.</param>
-        /// <param name="root">Custom root for finding property.</param>
-        public Change(
-            string property,
-            object oldValue,
-            object newValue,
-            string description = "",
-            object root = null)
-        {
-            Property = property;
-            OldValue = oldValue;
-            Description = description;
-            NewValue = newValue;
-            Root = root;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Change"/> class.
-        ///     Creates new change for mixed reverse process based system with new value property based system.
-        /// </summary>
-        /// <param name="property">Name of property, which new value will be applied to.</param>
-        /// <param name="reverseProcess">Method with reversing value process.</param>
-        /// <param name="reverseArguments">Arguments for reverse method.</param>
-        /// <param name="newValue">New value of property.</param>
-        /// <param name="description">Description of change.</param>
-        /// <param name="root">Custom root for finding property.</param>
-        public Change(
-            string property,
-            Action<object[]> reverseProcess,
-            object[] reverseArguments,
-            object newValue,
-            string description = "",
-            object root = null)
-        {
-            Property = property;
-            ReverseProcess = reverseProcess;
-            ReverseProcessArguments = reverseArguments;
-            NewValue = newValue;
-            Description = description;
-            Root = root;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Change"/> class.
-        ///     Creates new change for reverse process based system.
-        /// </summary>
-        /// <param name="reverseProcess">Method with reversing value process.</param>
-        /// <param name="reverseArguments">Arguments for reverse method.</param>
-        /// <param name="process">Method with reversing the reversed value.</param>
-        /// <param name="processArguments">Arguments for process method.</param>
-        /// <param name="description">Description of change.</param>
-        public Change(
-            Action<object[]> reverseProcess,
-            object[] reverseArguments,
-            Action<object[]> process,
-            object[] processArguments,
-            string description = "")
-        {
-            ReverseProcess = reverseProcess;
-            ReverseProcessArguments = reverseArguments;
-            Process = process;
-            ProcessArguments = processArguments;
-            Description = description;
-        }
-
-        public object[] ProcessArguments { get; set; }
-
-        public object[] ReverseProcessArguments { get; set; }
-
-        public object OldValue { get; set; }
-
-        public object NewValue { get; set; }
-
-        public string Description { get; set; }
-
-        public string Property { get; set; }
-
-        public Action<object[]> ReverseProcess { get; set; }
-
-        public Action<object[]> Process { get; set; }
-
-        public object Root { get; set; }
-    }
+using System;
+
+namespace PixiEditor.Models.Undo
+{
+    [Serializable]
+    public class Change
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Change"/> class.
+        ///     Creates new change for property based undo system.
+        /// </summary>
+        /// <param name="property">Name of property.</param>
+        /// <param name="oldValue">Old value of property.</param>
+        /// <param name="newValue">New value of property.</param>
+        /// <param name="description">Description of change.</param>
+        /// <param name="root">Custom root for finding property.</param>
+        public Change(
+            string property,
+            object oldValue,
+            object newValue,
+            string description = "",
+            object root = null)
+        {
+            Property = property;
+            OldValue = oldValue;
+            Description = description;
+            NewValue = newValue;
+            Root = root;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Change"/> class.
+        ///     Creates new change for property based undo system.
+        /// </summary>
+        /// <param name="property">Name of property.</param>
+        /// <param name="oldValue">Old value of property.</param>
+        /// <param name="newValue">New value of property.</param>
+        /// <param name="description">Description of change.</param>
+        /// <param name="root">Custom root for finding property.</param>
+        public Change(
+            string property,
+            object oldValue,
+            object newValue,
+            Func<object[], object> findRootProcess,
+            object[] findRootProcessArgs = null,
+            string description = "")
+        {
+            Property = property;
+            OldValue = oldValue;
+            Description = description;
+            NewValue = newValue;
+            FindRootProcess = findRootProcess;
+            FindRootProcessArgs = findRootProcessArgs;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Change"/> class.
+        ///     Creates new change for mixed reverse process based system with new value property based system.
+        /// </summary>
+        /// <param name="property">Name of property, which new value will be applied to.</param>
+        /// <param name="reverseProcess">Method with reversing value process.</param>
+        /// <param name="reverseArguments">Arguments for reverse method.</param>
+        /// <param name="newValue">New value of property.</param>
+        /// <param name="description">Description of change.</param>
+        /// <param name="root">Custom root for finding property.</param>
+        public Change(
+            string property,
+            Action<object[]> reverseProcess,
+            object[] reverseArguments,
+            object newValue,
+            string description = "",
+            object root = null)
+        {
+            Property = property;
+            ReverseProcess = reverseProcess;
+            ReverseProcessArguments = reverseArguments;
+            NewValue = newValue;
+            Description = description;
+            Root = root;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Change"/> class.
+        ///     Creates new change for reverse process based system.
+        /// </summary>
+        /// <param name="reverseProcess">Method with reversing value process.</param>
+        /// <param name="reverseArguments">Arguments for reverse method.</param>
+        /// <param name="process">Method with reversing the reversed value.</param>
+        /// <param name="processArguments">Arguments for process method.</param>
+        /// <param name="description">Description of change.</param>
+        public Change(
+            Action<object[]> reverseProcess,
+            object[] reverseArguments,
+            Action<object[]> process,
+            object[] processArguments,
+            string description = "")
+        {
+            ReverseProcess = reverseProcess;
+            ReverseProcessArguments = reverseArguments;
+            Process = process;
+            ProcessArguments = processArguments;
+            Description = description;
+        }
+
+        public object[] ProcessArguments { get; set; }
+
+        public object[] ReverseProcessArguments { get; set; }
+
+        public object OldValue { get; set; }
+
+        public object NewValue { get; set; }
+
+        public string Description { get; set; }
+
+        public string Property { get; set; }
+
+        public Action<object[]> ReverseProcess { get; set; }
+
+        public Action<object[]> Process { get; set; }
+
+        public object Root { get; set; }
+
+        public Func<object[], object> FindRootProcess { get; set; }
+
+        public object[] FindRootProcessArgs { get; set; }
+    }
 }

+ 182 - 0
PixiEditor/Models/Undo/StorageBasedChange.cs

@@ -0,0 +1,182 @@
+using System;
+using System.Buffers.Text;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+
+namespace PixiEditor.Models.Undo
+{
+    /// <summary>
+    ///     A class that allows to save layers on disk and load them on Undo/Redo.
+    /// </summary>
+    public class StorageBasedChange
+    {
+        public static string DefaultUndoChangeLocation => Path.Join(Path.GetTempPath(), "PixiEditor", "UndoStack");
+
+        public string UndoChangeLocation { get; set; }
+
+        public UndoLayer[] StoredLayers { get; set; }
+
+        private IEnumerable<Guid> layersToStore;
+
+        private Document document;
+
+        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
+        {
+            document = doc;
+            layersToStore = layers.Select(x => x.LayerGuid);
+            UndoChangeLocation = DefaultUndoChangeLocation;
+            GenerateUndoLayers();
+            if (saveOnStartup)
+            {
+                SaveLayersOnDevice();
+            }
+        }
+
+        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup = true)
+        {
+            document = doc;
+            layersToStore = layers.Select(x => x.LayerGuid);
+            UndoChangeLocation = undoChangeLocation;
+            GenerateUndoLayers();
+
+            if (saveOnStartup)
+            {
+                SaveLayersOnDevice();
+            }
+        }
+
+        public void SaveLayersOnDevice()
+        {
+            int i = 0;
+            foreach (var layerGuid in layersToStore)
+            {
+                Layer layer = document.Layers.First(x => x.LayerGuid == layerGuid);
+                UndoLayer storedLayer = StoredLayers[i];
+                if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName)))
+                {
+                    Exporter.SaveAsPng(storedLayer.StoredPngLayerName, storedLayer.Width, storedLayer.Height, layer.LayerBitmap);
+                }
+
+                i++;
+            }
+
+            layersToStore = Array.Empty<Guid>();
+        }
+
+        /// <summary>
+        /// Loads saved layers from disk.
+        /// </summary>
+        /// <returns>Array of saved layers.</returns>
+        public Layer[] LoadLayersFromDevice()
+        {
+            Layer[] layers = new Layer[StoredLayers.Length];
+            for (int i = 0; i < StoredLayers.Length; i++)
+            {
+                UndoLayer storedLayer = StoredLayers[i];
+                var bitmap = Importer.ImportImage(storedLayer.StoredPngLayerName, storedLayer.Width, storedLayer.Height);
+                layers[i] = new Layer(storedLayer.Name, bitmap)
+                {
+                    Offset = new System.Windows.Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
+                    Opacity = storedLayer.Opacity,
+                    MaxWidth = storedLayer.MaxWidth,
+                    MaxHeight = storedLayer.MaxHeight,
+                    IsVisible = storedLayer.IsVisible,
+                    IsActive = storedLayer.IsActive,
+                    LayerGuid = storedLayer.LayerGuid,
+                    Width = storedLayer.Width,
+                    Height = storedLayer.Height,
+                };
+
+                File.Delete(StoredLayers[i].StoredPngLayerName);
+            }
+
+            layersToStore = layers.Select(x => x.LayerGuid);
+            return layers;
+        }
+
+        /// <summary>
+        ///     Creates UndoManager ready Change instance, where undo process loads layers from device, and redo saves them.
+        /// </summary>
+        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
+        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
+        /// <param name="redoProcessParameters">Parameters for redo process.</param>
+        /// <param name="description">Undo change description.</param>
+        /// <returns>UndoManager ready Change instance.</returns>
+        public Change ToChange(Action<Layer[], UndoLayer[]> undoProcess, Action<object[]> redoProcess, object[] redoProcessParameters, string description = "")
+        {
+            Action<object[]> finalUndoProcess = _ =>
+            {
+                Layer[] layers = LoadLayersFromDevice();
+                undoProcess(layers, StoredLayers);
+            };
+
+            Action<object[]> finalRedoProcess = parameters =>
+            {
+                SaveLayersOnDevice();
+                redoProcess(parameters);
+            };
+
+            return new Change(finalUndoProcess, null, finalRedoProcess, redoProcessParameters, description);
+        }
+
+        /// <summary>
+        ///     Creates UndoManager ready Change instance, where undo process saves layers on device, and redo loads them.
+        /// </summary>
+        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
+        /// <param name="undoProcessParameters">Parameters for undo process.</param>
+        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
+        /// <param name="description">Undo change description.</param>
+        /// <returns>UndoManager ready Change instance.</returns>
+        public Change ToChange(Action<object[]> undoProcess, object[] undoProcessParameters, Action<Layer[], UndoLayer[]> redoProcess, string description = "")
+        {
+            Action<object[]> finalUndoProcess = parameters =>
+            {
+                SaveLayersOnDevice();
+                undoProcess(parameters);
+            };
+
+            Action<object[]> finalRedoProcess = parameters =>
+            {
+                Layer[] layers = LoadLayersFromDevice();
+                redoProcess(layers, StoredLayers);
+            };
+
+            return new Change(finalUndoProcess, undoProcessParameters, finalRedoProcess, null, description);
+        }
+
+        /// <summary>
+        /// Generates UndoLayer[] StoredLayers data.
+        /// </summary>
+        private void GenerateUndoLayers()
+        {
+            StoredLayers = new UndoLayer[layersToStore.Count()];
+            int i = 0;
+            foreach (var layerGuid in layersToStore)
+            {
+                Layer layer = document.Layers.First(x => x.LayerGuid == layerGuid);
+                if (!document.Layers.Contains(layer))
+                {
+                    throw new ArgumentException("Provided document doesn't contain selected layer");
+                }
+
+                layer.ClipCanvas();
+
+                int index = document.Layers.IndexOf(layer);
+                string pngName = layer.Name + Guid.NewGuid().ToString();
+                StoredLayers[i] = new UndoLayer(
+                    Path.Join(
+                        UndoChangeLocation,
+                        Convert.ToBase64String(Encoding.UTF8.GetBytes(pngName)) + ".png"),
+                    layer,
+                    index);
+                i++;
+            }
+        }
+    }
+}

+ 52 - 0
PixiEditor/Models/Undo/UndoLayer.cs

@@ -0,0 +1,52 @@
+using System;
+using PixiEditor.Models.Layers;
+
+namespace PixiEditor.Models.Undo
+{
+    [Serializable]
+    public record UndoLayer
+    {
+        public string StoredPngLayerName { get; set; }
+
+        public Guid LayerGuid { get; init; }
+
+        public string Name { get; set; }
+
+        public int LayerIndex { get; set; }
+
+        public int Width { get; set; }
+
+        public int Height { get; set; }
+
+        public int MaxWidth { get; set; }
+
+        public int MaxHeight { get; set; }
+
+        public bool IsVisible { get; set; }
+
+        public bool IsActive { get; set; }
+
+        public int OffsetX { get; set; }
+
+        public int OffsetY { get; set; }
+
+        public float Opacity { get; set; }
+
+        public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex)
+        {
+            StoredPngLayerName = storedPngLayerName;
+            LayerIndex = layerIndex;
+            Name = layer.Name;
+            Width = layer.Width;
+            Height = layer.Height;
+            MaxWidth = layer.MaxWidth;
+            MaxHeight = layer.MaxHeight;
+            IsVisible = layer.IsVisible;
+            OffsetX = layer.OffsetX;
+            OffsetY = layer.OffsetY;
+            Opacity = layer.Opacity;
+            IsActive = layer.IsActive;
+            LayerGuid = layer.LayerGuid;
+        }
+    }
+}

+ 2 - 11
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -29,7 +29,6 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void NewLayer(object parameter)
         {
-            //TODO: Implement AddNewLayer to Document, not BitmapManager
             Owner.BitmapManager.ActiveDocument.AddNewLayer($"New Layer {Owner.BitmapManager.ActiveDocument.Layers.Count}");
         }
 
@@ -61,21 +60,13 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void MoveLayerToFront(object parameter)
         {
             int oldIndex = (int)parameter;
-            Owner.BitmapManager.ActiveDocument.Layers.Move(oldIndex, oldIndex + 1);
-            if (Owner.BitmapManager.ActiveDocument.ActiveLayerIndex == oldIndex)
-            {
-                Owner.BitmapManager.ActiveDocument.SetActiveLayer(oldIndex + 1);
-            }
+            Owner.BitmapManager.ActiveDocument.MoveLayerIndexBy(oldIndex, 1);
         }
 
         public void MoveLayerToBack(object parameter)
         {
             int oldIndex = (int)parameter;
-            Owner.BitmapManager.ActiveDocument.Layers.Move(oldIndex, oldIndex - 1);
-            if (Owner.BitmapManager.ActiveDocument.ActiveLayerIndex == oldIndex)
-            {
-                Owner.BitmapManager.ActiveDocument.SetActiveLayer(oldIndex - 1);
-            }
+            Owner.BitmapManager.ActiveDocument.MoveLayerIndexBy(oldIndex, -1);
         }
 
         public bool CanMoveToFront(object property)

+ 21 - 1
PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs

@@ -1,9 +1,11 @@
 using System;
+using System.IO;
 using System.Linq;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Tools;
+using PixiEditor.Models.Undo;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -23,7 +25,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 undoChanges = value;
                 for (int i = 0; i < value.Length; i++)
                 {
-                    Owner.BitmapManager.ActiveDocument.Layers[value[i].LayerIndex].SetPixels(value[i].PixelChanges);
+                    Owner.BitmapManager.ActiveDocument.Layers.First(x => x.LayerGuid == value[i].LayerGuid).SetPixels(value[i].PixelChanges);
                 }
             }
         }
@@ -33,6 +35,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             UndoCommand = new RelayCommand(Undo, CanUndo);
             RedoCommand = new RelayCommand(Redo, CanRedo);
+            if (!Directory.Exists(StorageBasedChange.DefaultUndoChangeLocation))
+            {
+                Directory.CreateDirectory(StorageBasedChange.DefaultUndoChangeLocation);
+            }
+
+            ClearUndoTempDirectory();
         }
 
         public void TriggerNewUndoChange(Tool toolUsed)
@@ -71,6 +79,18 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.ActiveDocument.UndoManager.Undo();
         }
 
+        /// <summary>
+        /// Removes all files from %tmp%/PixiEditor/UndoStack/.
+        /// </summary>
+        public void ClearUndoTempDirectory()
+        {
+            DirectoryInfo dirInfo = new DirectoryInfo(StorageBasedChange.DefaultUndoChangeLocation);
+            foreach (FileInfo file in dirInfo.GetFiles())
+            {
+                file.Delete();
+            }
+        }
+
         /// <summary>
         ///     Returns true if undo can be done.
         /// </summary>

+ 2 - 3
PixiEditor/ViewModels/ViewModelMain.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
-using System.Diagnostics;
 using System.Linq;
 using System.Windows;
 using System.Windows.Input;
@@ -254,8 +253,8 @@ namespace PixiEditor.ViewModels
         private void BitmapUtility_BitmapChanged(object sender, BitmapChangedEventArgs e)
         {
             ChangesController.AddChanges(
-                new LayerChange(e.PixelsChanged, e.ChangedLayerIndex),
-                new LayerChange(e.OldPixelsValues, e.ChangedLayerIndex));
+                new LayerChange(e.PixelsChanged, e.ChangedLayerGuid),
+                new LayerChange(e.OldPixelsValues, e.ChangedLayerGuid));
             BitmapManager.ActiveDocument.ChangesSaved = false;
             if (BitmapManager.IsOperationTool())
             {

+ 1 - 1
PixiEditor/Views/UserControls/DrawingViewPort.xaml

@@ -30,7 +30,7 @@
                 </i:EventTrigger>
             </i:Interaction.Triggers>
             <i:Interaction.Behaviors>
-                <behaviors:MouseBehaviour RelativeTo="{Binding ElementName=DrawingPanel, Path=Item}"
+                <behaviors:MouseBehavior RelativeTo="{Binding ElementName=DrawingPanel, Path=Item}"
                                                   MouseX="{Binding MouseXOnCanvas, Mode=TwoWay, ElementName=uc}"
                                                   MouseY="{Binding MouseYOnCanvas, Mode=TwoWay, ElementName=uc}" />
             </i:Interaction.Behaviors>

+ 1 - 1
PixiEditor/Views/UserControls/LayerItem.xaml

@@ -6,7 +6,7 @@
              xmlns:local="clr-namespace:PixiEditor.Views"
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
-             mc:Ignorable="d" 
+             mc:Ignorable="d" Focusable="True"
              d:DesignHeight="60" d:DesignWidth="250" Name="uc"
              MouseLeave="LayerItem_OnMouseLeave" MouseEnter="LayerItem_OnMouseEnter">
     <UserControl.Resources>

+ 2 - 2
PixiEditor/Views/UserControls/LayerItem.xaml.cs

@@ -47,7 +47,7 @@ namespace PixiEditor.Views
 
         public RelayCommand SetActiveLayerCommand
         {
-            get { return (RelayCommand) GetValue(SetActiveLayerCommandProperty); }
+            get { return (RelayCommand)GetValue(SetActiveLayerCommandProperty); }
             set { SetValue(SetActiveLayerCommandProperty, value); }
         }
 
@@ -103,7 +103,7 @@ namespace PixiEditor.Views
 
         public RelayCommand MoveToFrontCommand
         {
-            get { return (RelayCommand) GetValue(MoveToFrontCommandProperty); }
+            get { return (RelayCommand)GetValue(MoveToFrontCommandProperty); }
             set { SetValue(MoveToFrontCommandProperty, value); }
         }
 

+ 5 - 4
PixiEditor/Views/UserControls/NumberInput.xaml

@@ -7,12 +7,13 @@
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
              xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
              xmlns:ui="clr-namespace:PixiEditor.Helpers.UI"
-             mc:Ignorable="d"
+             mc:Ignorable="d" Focusable="True"
              d:DesignHeight="20" d:DesignWidth="40" x:Name="numberInput">
-    <TextBox TextAlignment="Center" Style="{StaticResource DarkTextBoxStyle}"
-                 PreviewTextInput="TextBox_PreviewTextInput" Text="{Binding ElementName=numberInput, Path=Value}">
+    <TextBox TextAlignment="Center" Style="{StaticResource DarkTextBoxStyle}" Focusable="True"
+               
+             PreviewTextInput="TextBox_PreviewTextInput" Text="{Binding ElementName=numberInput, Path=Value, UpdateSourceTrigger=PropertyChanged}">
         <i:Interaction.Behaviors>
-            <behaviours:TextBoxFocusBehavior />
+            <behaviours:TextBoxFocusBehavior/>
             <behaviours:GlobalShortcutFocusBehavior/>
         </i:Interaction.Behaviors>
     </TextBox>

+ 5 - 5
PixiEditor/Views/UserControls/NumberInput.xaml.cs

@@ -1,9 +1,9 @@
-using PixiEditor.Models.Controllers.Shortcuts;
-using System;
+using System;
 using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
+using PixiEditor.Models.Controllers.Shortcuts;
 
 namespace PixiEditor.Views
 {
@@ -32,10 +32,12 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty MaxProperty =
             DependencyProperty.Register(
                 "Max",
-                typeof(float), 
+                typeof(float),
                 typeof(NumberInput),
                 new PropertyMetadata(float.PositiveInfinity));
 
+        private Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");
+
         public NumberInput()
         {
             InitializeComponent();
@@ -53,7 +55,6 @@ namespace PixiEditor.Views
             set => SetValue(MinProperty, value);
         }
 
-
         public float Max
         {
             get => (float)GetValue(MaxProperty);
@@ -68,7 +69,6 @@ namespace PixiEditor.Views
 
         private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
         {
-            Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");
             e.Handled = !regex.IsMatch((sender as TextBox).Text.Insert((sender as TextBox).SelectionStart, e.Text));
         }
     }

+ 16 - 0
PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml

@@ -0,0 +1,16 @@
+<UserControl x:Class="PixiEditor.Views.ToolSettingColorPicker"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls" xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
+             mc:Ignorable="d" 
+             x:Name="uc"
+             d:Background="{StaticResource AccentColor}">
+    <Grid>
+        <StackPanel Orientation="Horizontal">
+            <colorpicker:PortableColorPicker x:Name="ColorPicker" SelectedColor="{Binding SelectedColor, ElementName=uc}"/>
+            <Button Command="{Binding CopyMainColorCommand, ElementName=uc}" Style="{StaticResource DarkRoundButton}" FontSize="12" Width="100" Margin="5,0,0,0">Copy Main Color</Button>
+        </StackPanel>
+    </Grid>
+</UserControl>

+ 50 - 0
PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml.cs

@@ -0,0 +1,50 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using ColorPicker;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Views
+{
+    /// <summary>
+    /// Interaction logic for ToolSettingColorPicker.xaml.
+    /// </summary>
+    public partial class ToolSettingColorPicker : UserControl
+    {
+        public static readonly DependencyProperty SelectedColorProperty =
+            DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ToolSettingColorPicker));
+
+        public Color SelectedColor
+        {
+            get => (Color)GetValue(SelectedColorProperty);
+            set
+            {
+                SetValue(SelectedColorProperty, value);
+            }
+        }
+
+        public static readonly DependencyProperty CopyMainColorCommandProperty = DependencyProperty.Register(
+            nameof(CopyMainColorCommand), typeof(RelayCommand), typeof(ToolSettingColorPicker));
+
+        public RelayCommand CopyMainColorCommand
+        {
+            get { return (RelayCommand)GetValue(CopyMainColorCommandProperty); }
+            set { SetValue(CopyMainColorCommandProperty, value); }
+        }
+
+        public ToolSettingColorPicker()
+        {
+            InitializeComponent();
+            ColorPicker.SecondaryColor = Colors.Black;
+
+            CopyMainColorCommand = new RelayCommand(CopyMainColor);
+        }
+
+        public void CopyMainColor(object parameter)
+        {
+            SelectedColor = ViewModelMain.Current.ColorsSubViewModel.PrimaryColor;
+        }
+    }
+}

+ 7 - 0
PixiEditorTests/ApplicationFixture.cs

@@ -1,6 +1,8 @@
 using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using System.Windows;
 using PixiEditor;
+using PixiEditor.Models.Undo;
 
 namespace PixiEditorTests
 {
@@ -14,6 +16,11 @@ namespace PixiEditorTests
                 App app = new App();
                 app.InitializeComponent();
             }
+
+            if (!Directory.Exists(Path.GetDirectoryName(StorageBasedChange.DefaultUndoChangeLocation)))
+            {
+                Directory.CreateDirectory(StorageBasedChange.DefaultUndoChangeLocation);
+            }
         }
     }
 }

+ 1 - 0
PixiEditorTests/ModelsTests/ControllersTests/BitmapManagerTests.cs

@@ -3,6 +3,7 @@ using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
+using PixiEditor.Models.Undo;
 using Xunit;
 
 namespace PixiEditorTests.ModelsTests.ControllersTests

+ 3 - 3
PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs

@@ -1,8 +1,8 @@
-using PixiEditor.Models.DataHolders;
+using System.Windows.Media;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
-using System.Windows.Media;
 
 namespace PixiEditorTests.ModelsTests.ControllersTests
 {
@@ -10,7 +10,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
     {
         public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
         {
-            return Only(BitmapPixelChanges.FromSingleColoredArray(new[] { mouseMove[0] }, color), 0);
+            return Only(BitmapPixelChanges.FromSingleColoredArray(new[] { mouseMove[0] }, color), layer.LayerGuid);
         }
     }
 }

+ 19 - 13
PixiEditorTests/ModelsTests/ControllersTests/PixelChangesControllerTests.cs

@@ -1,4 +1,5 @@
-using System.Windows.Media;
+using System;
+using System.Windows.Media;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
@@ -11,7 +12,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestThatPopChangesPopsChanges()
         {
-            PixelChangesController controller = CreateBasicController();
+            PixelChangesController controller = CreateBasicController().Item2;
 
             System.Tuple<LayerChange, LayerChange>[] changes = controller.PopChanges();
             Assert.NotEmpty(changes);
@@ -21,13 +22,15 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestThatAddChangesAddsAsNewChange()
         {
-            PixelChangesController controller = CreateBasicController();
+            var data = CreateBasicController();
+            PixelChangesController controller = data.Item2;
             Coordinates[] cords = { new Coordinates(5, 3), new Coordinates(7, 2) };
+            Guid guid = Guid.NewGuid();
 
             controller.AddChanges(
                 new LayerChange(
-                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), 1),
-                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), 1));
+                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), guid),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), guid));
 
             System.Tuple<LayerChange, LayerChange>[] changes = controller.PopChanges();
             Assert.Equal(2, changes.Length);
@@ -37,29 +40,32 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         public void TestThatAddChangesAddsToExistingChange()
         {
             Coordinates[] cords2 = { new Coordinates(2, 2), new Coordinates(5, 5) };
-            PixelChangesController controller = CreateBasicController();
+            var data = CreateBasicController();
+            PixelChangesController controller = data.Item2;
 
             controller.AddChanges(
                 new LayerChange(
-                    BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Black), 0),
-                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Transparent), 0));
+                    BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Black), data.Item1),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Transparent), data.Item1));
 
-            System.Tuple<LayerChange, LayerChange>[] changes = controller.PopChanges();
+            Tuple<LayerChange, LayerChange>[] changes = controller.PopChanges();
             Assert.Single(changes);
             Assert.Equal(4, changes[0].Item1.PixelChanges.ChangedPixels.Count);
             Assert.Equal(4, changes[0].Item2.PixelChanges.ChangedPixels.Count);
         }
 
-        private static PixelChangesController CreateBasicController()
+        private static Tuple<Guid, PixelChangesController> CreateBasicController()
         {
             Coordinates[] cords = { new Coordinates(0, 0), new Coordinates(1, 1) };
             PixelChangesController controller = new PixelChangesController();
 
+            Guid guid = Guid.NewGuid();
+
             controller.AddChanges(
                 new LayerChange(
-                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), 0),
-                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), 0));
-            return controller;
+                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), guid),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), guid));
+            return new Tuple<Guid, PixelChangesController>(guid, controller);
         }
     }
 }

+ 53 - 0
PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Undo;
 using Xunit;
 
 namespace PixiEditorTests.ModelsTests.ControllersTests
@@ -180,6 +181,58 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
             Assert.Equal(newVal, TestPropClass.IntProperty);
         }
 
+        [Fact]
+        public void TestThatFindRootProcessWorks()
+        {
+            PrepareUndoManagerForTest();
+            UndoManager undoManager = new UndoManager(this);
+
+            undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null));
+
+            Change change = undoManager.UndoStack.Peek();
+
+            Assert.Equal(TestPropClass, change.FindRootProcess(change.FindRootProcessArgs));
+        }
+
+        [Fact]
+        public void TestThatUndoForFindRootProcessWorks()
+        {
+            PrepareUndoManagerForTest();
+            UndoManager undoManager = new UndoManager(this);
+
+            undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null));
+
+            TestPropClass.IntProperty = 5;
+
+            undoManager.Undo();
+
+            Assert.Equal(0, TestPropClass.IntProperty);
+        }
+
+        [Fact]
+        public void TestThatUndoAndRedoForFindRootProcessWorks()
+        {
+            PrepareUndoManagerForTest();
+            UndoManager undoManager = new UndoManager(this);
+
+            undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null));
+
+            TestPropClass.IntProperty = 5;
+
+            undoManager.Undo();
+
+            Assert.Equal(0, TestPropClass.IntProperty);
+
+            undoManager.Redo();
+
+            Assert.Equal(5, TestPropClass.IntProperty);
+        }
+
+        private object FindRootProcess(object[] args)
+        {
+            return TestPropClass;
+        }
+
         private void ReverseProcess(object[] args)
         {
             ExampleProperty = (int)args[0];

+ 138 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -8,6 +8,7 @@ using Xunit;
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
 {
+    [Collection("Application collection")]
     public class DocumentTests
     {
         [Theory]
@@ -145,5 +146,142 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Equal(midWidth, manager.ActiveDocument.Layers[1].OffsetX);
             Assert.Equal(midHeight, manager.ActiveDocument.Layers[1].OffsetY);
         }
+
+        [Fact]
+        public void TestThatSetNextActiveLayerSetsLayerBelow()
+        {
+            Document doc = new Document(10, 10);
+            doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test"));
+            doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test 2"));
+
+            doc.SetActiveLayer(1);
+
+            doc.SetNextLayerAsActive(1);
+
+            Assert.False(doc.Layers[1].IsActive);
+            Assert.True(doc.Layers[0].IsActive);
+        }
+
+        [Fact]
+        public void TestThatAddNewLayerAddsUndoChange()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            Assert.Single(document.UndoManager.UndoStack);
+        }
+
+        [Fact]
+        public void TestThatAddNewLayerUndoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.UndoManager.Undo();
+
+            Assert.Single(document.Layers);
+        }
+
+        [Fact]
+        public void TestThatAddNewLayerRedoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.UndoManager.Undo();
+            document.UndoManager.Redo();
+
+            Assert.Equal(2, document.Layers.Count);
+        }
+
+        [Fact]
+        public void TestThatRemoveLayerUndoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.RemoveLayer(1);
+
+            document.UndoManager.Undo();
+
+            Assert.Equal(2, document.Layers.Count);
+        }
+
+        [Fact]
+        public void TestThatRemoveLayerRedoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.RemoveLayer(1);
+
+            document.UndoManager.Undo();
+            document.UndoManager.Redo();
+
+            Assert.Single(document.Layers);
+        }
+
+        [Theory]
+        [InlineData(2, 0, 1)]
+        [InlineData(2, 1, -1)]
+        [InlineData(3, 1, 1)]
+        [InlineData(3, 2, -2)]
+        [InlineData(10, 9, -5)]
+        public void TestThatMoveLayerIndexByWorks(int layersAmount, int index, int amount)
+        {
+            Document document = new Document(10, 10);
+            for (int i = 0; i < layersAmount; i++)
+            {
+                document.AddNewLayer("Layer " + i);
+            }
+
+            Guid oldGuid = document.Layers[index].LayerGuid;
+            document.MoveLayerIndexBy(index, amount);
+
+            Assert.Equal(oldGuid, document.Layers[index + amount].LayerGuid);
+        }
+
+        [Fact]
+        public void TestThatMoveLayerIndexByUndoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.MoveLayerIndexBy(0, 1);
+
+            document.UndoManager.Undo();
+
+            Assert.Equal("Test2", document.Layers[1].Name);
+            Assert.Equal("Test", document.Layers[0].Name);
+        }
+
+        [Fact]
+        public void TestThatMoveLayerIndexByRedoProcessWorks()
+        {
+            Document document = new Document(10, 10);
+
+            document.AddNewLayer("Test");
+            document.AddNewLayer("Test2");
+
+            document.MoveLayerIndexBy(0, 1);
+
+            document.UndoManager.Undo();
+            document.UndoManager.Redo();
+
+            Assert.Equal("Test", document.Layers[1].Name);
+            Assert.Equal("Test2", document.Layers[0].Name);
+        }
     }
 }

+ 4 - 3
PixiEditorTests/ModelsTests/ImageManipulationTests/BitmapUtilsTests.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
@@ -78,11 +79,11 @@ namespace PixiEditorTests.ModelsTests.ImageManipulationTests
             layers[0].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new[] { cords[0] }, Colors.Green));
             layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new[] { cords[1] }, Colors.Red));
 
-            Dictionary<Layer, Color[]> output = BitmapUtils.GetPixelsForSelection(layers, cords);
+            Dictionary<Guid, Color[]> output = BitmapUtils.GetPixelsForSelection(layers, cords);
 
             List<Color> colors = new List<Color>();
 
-            foreach (KeyValuePair<Layer, Color[]> layerColor in output.ToArray())
+            foreach (KeyValuePair<Guid, Color[]> layerColor in output.ToArray())
             {
                 foreach (Color color in layerColor.Value)
                 {

+ 1 - 10
PixiEditorTests/ModelsTests/LayersTests/LayerTests.cs

@@ -54,16 +54,7 @@ namespace PixiEditorTests.ModelsTests.LayersTests
 
             Layer clone = layer.Clone();
 
-            Assert.Equal(layer.Name, clone.Name);
-            Assert.Equal(layer.Offset, clone.Offset);
-            Assert.Equal(layer.Width, clone.Width);
-            Assert.Equal(layer.Height, clone.Height);
-            Assert.Equal(layer.MaxHeight, clone.MaxHeight);
-            Assert.Equal(layer.MaxWidth, clone.MaxWidth);
-            Assert.Equal(layer.Opacity, clone.Opacity);
-            Assert.Equal(layer.IsVisible, clone.IsVisible);
-            Assert.Equal(layer.IsRenaming, clone.IsRenaming);
-            Assert.Equal(layer.ConvertBitmapToBytes(), clone.ConvertBitmapToBytes());
+            LayersTestHelper.LayersAreEqual(layer, clone);
         }
 
         [Fact]

+ 22 - 0
PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs

@@ -0,0 +1,22 @@
+using PixiEditor.Models.Layers;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.LayersTests
+{
+    public static class LayersTestHelper
+    {
+        public static void LayersAreEqual(Layer expected, Layer actual)
+        {
+            Assert.Equal(expected.Name, actual.Name);
+            Assert.Equal(expected.Offset, actual.Offset);
+            Assert.Equal(expected.Width, actual.Width);
+            Assert.Equal(expected.Height, actual.Height);
+            Assert.Equal(expected.MaxHeight, actual.MaxHeight);
+            Assert.Equal(expected.MaxWidth, actual.MaxWidth);
+            Assert.Equal(expected.Opacity, actual.Opacity);
+            Assert.Equal(expected.IsVisible, actual.IsVisible);
+            Assert.Equal(expected.IsRenaming, actual.IsRenaming);
+            Assert.Equal(expected.ConvertBitmapToBytes(), actual.ConvertBitmapToBytes());
+        }
+    }
+}

+ 169 - 0
PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs

@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Undo;
+using PixiEditorTests.ModelsTests.LayersTests;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.UndoTests
+{
+    public class StorageBasedChangeTests
+    {
+        private const string UndoStoreLocation = "undoStack";
+
+        public StorageBasedChangeTests()
+        {
+            if (!Directory.Exists(UndoStoreLocation))
+            {
+                Directory.CreateDirectory(UndoStoreLocation);
+            }
+        }
+
+        public Document GenerateTestDocument()
+        {
+            Document testDocument = new Document(10, 10);
+            WriteableBitmap testBitmap = BitmapFactory.New(10, 10);
+            WriteableBitmap testBitmap2 = BitmapFactory.New(5, 8);
+            testBitmap.SetPixel(0, 0, Colors.Black);
+            testBitmap2.SetPixel(4, 4, Colors.Beige);
+            Random random = new Random();
+            testDocument.Layers = new ObservableCollection<Layer>()
+            {
+                new Layer("Test layer" + random.Next(int.MinValue, int.MaxValue), testBitmap),
+                new Layer("Test layer 2" + random.Next(int.MinValue, int.MaxValue), testBitmap2) { Offset = new System.Windows.Thickness(2, 3, 0, 0) }
+            };
+            return testDocument;
+        }
+
+        [Fact]
+        public void TestThatConstructorGeneratesUndoLayersProperly()
+        {
+            Document testDocument = GenerateTestDocument();
+
+            StorageBasedChange change = new StorageBasedChange(testDocument, testDocument.Layers, UndoStoreLocation);
+
+            Assert.Equal(testDocument.Layers.Count, change.StoredLayers.Length);
+
+            for (int i = 0; i < change.StoredLayers.Length; i++)
+            {
+                Layer testLayer = testDocument.Layers[i];
+                UndoLayer layer = change.StoredLayers[i];
+
+                Assert.Equal(testLayer.Name, layer.Name);
+                Assert.Equal(testLayer.Width, layer.Width);
+                Assert.Equal(testLayer.Height, layer.Height);
+                Assert.Equal(testLayer.IsActive, layer.IsActive);
+                Assert.Equal(testLayer.IsVisible, layer.IsVisible);
+                Assert.Equal(testLayer.OffsetX, layer.OffsetX);
+                Assert.Equal(testLayer.OffsetY, layer.OffsetY);
+                Assert.Equal(testLayer.MaxWidth, layer.MaxWidth);
+                Assert.Equal(testLayer.MaxHeight, layer.MaxHeight);
+                Assert.Equal(testLayer.Opacity, layer.Opacity);
+            }
+        }
+
+        [Fact]
+        public void TestThatSaveLayersOnDeviceSavesLayers()
+        {
+            Document document = GenerateTestDocument();
+
+            StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation);
+
+            foreach (var layer in change.StoredLayers)
+            {
+                Assert.True(File.Exists(layer.StoredPngLayerName));
+                File.Delete(layer.StoredPngLayerName);
+            }
+        }
+
+        [Fact]
+        public void TestThatLoadLayersFromDeviceLoadsLayers()
+        {
+            Document document = GenerateTestDocument();
+
+            StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation);
+
+            Layer[] layers = change.LoadLayersFromDevice();
+
+            Assert.Equal(document.Layers.Count, layers.Length);
+            for (int i = 0; i < document.Layers.Count; i++)
+            {
+                Layer expected = document.Layers[i];
+                Layer actual = layers[i];
+                LayersTestHelper.LayersAreEqual(expected, actual);
+            }
+        }
+
+        [Fact]
+        public void TestThatUndoInvokesLoadFromDeviceAndExecutesProcess()
+        {
+            Document document = GenerateTestDocument();
+
+            StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation);
+            bool undoInvoked = false;
+
+            Action<Layer[], UndoLayer[]> testUndoProcess = (layers, data) =>
+            {
+                undoInvoked = true;
+                Assert.Equal(document.Layers.Count, layers.Length);
+                Assert.Equal(document.Layers.Count, data.Length);
+                foreach (var undoLayer in data)
+                {
+                    Assert.False(File.Exists(undoLayer.StoredPngLayerName));
+                }
+            };
+
+            Action<object[]> testRedoProcess = parameters => { };
+
+            Change undoChange = change.ToChange(testUndoProcess, testRedoProcess, null);
+            UndoManager manager = new UndoManager(this);
+
+            manager.AddUndoChange(undoChange);
+            manager.Undo();
+
+            Assert.True(undoInvoked);
+        }
+
+        [Fact]
+        public void TestThatRedoInvokesSaveToDeviceAndExecutesProcess()
+        {
+            Document document = GenerateTestDocument();
+
+            StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation);
+            bool redoInvoked = false;
+
+            Action<Layer[], UndoLayer[]> testUndoProcess = (layers, data) => { };
+
+            Action<object[]> testRedoProcess = parameters =>
+            {
+                redoInvoked = true;
+                foreach (var undoLayer in change.StoredLayers)
+                {
+                    Assert.True(File.Exists(undoLayer.StoredPngLayerName));
+                    Assert.NotNull(parameters);
+                    Assert.Single(parameters);
+                    Assert.IsType<int>(parameters[0]);
+                    Assert.Equal(2, parameters[0]);
+                }
+            };
+
+            Change undoChange = change.ToChange(testUndoProcess, testRedoProcess, new object[] { 2 });
+            UndoManager manager = new UndoManager(this);
+
+            manager.AddUndoChange(undoChange);
+            manager.Undo();
+            manager.Redo();
+
+            Assert.True(redoInvoked);
+        }
+    }
+}