Procházet zdrojové kódy

Merge pull request #163 from PixiEditor/sync-middleman

Sync master with group-layers
Krzysztof Krysiński před 4 roky
rodič
revize
f754ab864d
57 změnil soubory, kde provedl 1375 přidání a 480 odebrání
  1. 0 27
      PixiEditor/Helpers/Converters/BoolToColorConverter.cs
  2. 15 0
      PixiEditor/Helpers/Extensions/ToolbarHelpers.cs
  3. 43 0
      PixiEditor/Helpers/SelectionHelpers.cs
  4. 17 3
      PixiEditor/Models/Controllers/BitmapManager.cs
  5. 39 12
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  6. 3 3
      PixiEditor/Models/Controllers/LayersChangedEventArgs.cs
  7. 2 0
      PixiEditor/Models/Controllers/UndoManager.cs
  8. 31 1
      PixiEditor/Models/DataHolders/Document/Document.IO.cs
  9. 34 19
      PixiEditor/Models/DataHolders/Document/Document.cs
  10. 2 0
      PixiEditor/Models/DataHolders/Selection.cs
  11. 2 2
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  12. 17 0
      PixiEditor/Models/Dialogs/NoticeDialog.cs
  13. 4 1
      PixiEditor/Models/Events/DocumentChangedEventArgs.cs
  14. 1 1
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  15. 14 1
      PixiEditor/Models/Layers/Layer.cs
  16. 5 0
      PixiEditor/Models/Tools/Tool.cs
  17. 96 0
      PixiEditor/Models/Tools/ToolSettings/Settings/EnumSetting.cs
  18. 17 4
      PixiEditor/Models/Tools/ToolSettings/Settings/Setting.cs
  19. 3 2
      PixiEditor/Models/Tools/ToolSettings/Toolbars/SelectToolToolbar.cs
  20. 69 61
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  21. 89 100
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  22. 1 0
      PixiEditor/Models/Undo/StorageBasedChange.cs
  23. 3 0
      PixiEditor/Models/Undo/UndoLayer.cs
  24. 32 0
      PixiEditor/Models/UserPreferences/IPreferences.cs
  25. 99 34
      PixiEditor/Models/UserPreferences/PreferencesSettings.cs
  26. 14 0
      PixiEditor/NotifyableObject.cs
  27. 1 0
      PixiEditor/PixiEditor.csproj
  28. 13 0
      PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  29. 15 10
      PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs
  30. 50 1
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  31. 32 3
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  32. 9 0
      PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs
  33. 0 1
      PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs
  34. 1 1
      PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  35. 89 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/DiscordSettings.cs
  36. 62 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  37. 18 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/UpdateSettings.cs
  38. 3 3
      PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs
  39. 3 143
      PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs
  40. 29 7
      PixiEditor/ViewModels/ViewModelMain.cs
  41. 40 0
      PixiEditor/Views/Dialogs/NoticePopup.xaml
  42. 41 0
      PixiEditor/Views/Dialogs/NoticePopup.xaml.cs
  43. 8 4
      PixiEditor/Views/Dialogs/SettingsWindow.xaml
  44. 1 1
      PixiEditor/Views/MainWindow.xaml
  45. 14 0
      PixiEditor/Views/MainWindow.xaml.cs
  46. 2 5
      PixiEditor/Views/UserControls/LayerItem.xaml
  47. 10 0
      PixiEditor/Views/UserControls/LayerItem.xaml.cs
  48. 24 0
      PixiEditorTests/Helpers.cs
  49. 56 0
      PixiEditorTests/Mocks/PreferenceSettingsMock.cs
  50. 82 11
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs
  51. 24 1
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  52. 20 0
      PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs
  53. 4 2
      PixiEditorTests/ModelsTests/ToolsTests/ZoomToolTests.cs
  54. 43 1
      PixiEditorTests/ModelsTests/UserPreferencesTests/PreferencesSettingsTests.cs
  55. 2 0
      PixiEditorTests/PixiEditorTests.csproj
  56. 24 12
      PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs
  57. 3 3
      README.md

+ 0 - 27
PixiEditor/Helpers/Converters/BoolToColorConverter.cs

@@ -1,27 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-
-namespace PixiEditor.Helpers.Converters
-{
-    public class BoolToColorConverter : IValueConverter
-    {
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            return value?.ToString() == "Transparent";
-        }
-
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            if (value is bool boolean)
-            {
-                if (boolean == false)
-                {
-                    return "Transparent";
-                }
-            }
-
-            return "#505056";
-        }
-    }
-}

+ 15 - 0
PixiEditor/Helpers/Extensions/ToolbarHelpers.cs

@@ -0,0 +1,15 @@
+using System;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class ToolbarHelpers
+    {
+        public static EnumSetting<TEnum> GetEnumSetting<TEnum>(this Toolbar toolbar, string name)
+            where TEnum : struct, Enum
+        {
+            return toolbar.GetSetting<EnumSetting<TEnum>>(name);
+        }
+    }
+}

+ 43 - 0
PixiEditor/Helpers/SelectionHelpers.cs

@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+
+namespace PixiEditor.Helpers
+{
+    public static class SelectionHelpers
+    {
+        public static void AddSelectionUndoStep(Document document, IEnumerable<Coordinates> oldPoints, SelectionType mode)
+        {
+#pragma warning disable SA1117 // Parameters should be on same line or separate lines. Justification: Making it readable
+            if (mode == SelectionType.New && document.ActiveSelection.SelectedPoints.Count != 0)
+            {
+                // Add empty selection as the old one get's fully deleted first
+                document.UndoManager.AddUndoChange(
+                    new Change(
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>(oldPoints) },
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>() }));
+                document.UndoManager.AddUndoChange(
+                    new Change(
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>() },
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>(document.ActiveSelection.SelectedPoints) }));
+            }
+            else
+            {
+                document.UndoManager.AddUndoChange(
+                    new Change(
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>(oldPoints) },
+                        SetSelectionProcess, new object[] { document, new List<Coordinates>(document.ActiveSelection.SelectedPoints) }));
+#pragma warning restore SA1117 // Parameters should be on same line or separate lines
+            }
+        }
+
+        private static void SetSelectionProcess(object[] arguments)
+        {
+            Document document = (Document)arguments[0];
+
+            document.ActiveSelection.SetSelection((IEnumerable<Coordinates>)arguments[1], SelectionType.New);
+        }
+    }
+}

+ 17 - 3
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
@@ -18,10 +19,12 @@ using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 
 namespace PixiEditor.Models.Controllers
 namespace PixiEditor.Models.Controllers
 {
 {
+    [DebuggerDisplay("{Documents.Count} Document(s)")]
     public class BitmapManager : NotifyableObject
     public class BitmapManager : NotifyableObject
     {
     {
         private Document activeDocument;
         private Document activeDocument;
         private Tool selectedTool;
         private Tool selectedTool;
+        private Coordinates? startPosition = null;
 
 
         public BitmapManager()
         public BitmapManager()
         {
         {
@@ -70,20 +73,23 @@ namespace PixiEditor.Models.Controllers
 
 
         public BitmapOperationsUtility BitmapOperations { get; set; }
         public BitmapOperationsUtility BitmapOperations { get; set; }
 
 
-        public ReadonlyToolUtility ReadonlyToolUtility { get; set; }
-
+        public ReadonlyToolUtility ReadonlyToolUtility { get; set; }
+
+#nullable enable
         public Document ActiveDocument
         public Document ActiveDocument
         {
         {
             get => activeDocument;
             get => activeDocument;
             set
             set
             {
             {
                 activeDocument?.UpdatePreviewImage();
                 activeDocument?.UpdatePreviewImage();
+                Document? oldDoc = activeDocument;
                 activeDocument = value;
                 activeDocument = value;
                 RaisePropertyChanged(nameof(ActiveDocument));
                 RaisePropertyChanged(nameof(ActiveDocument));
-                DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value));
+                DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
             }
             }
         }
         }
 
 
+#nullable disable
         public ObservableCollection<Document> Documents { get; set; } = new ObservableCollection<Document>();
         public ObservableCollection<Document> Documents { get; set; } = new ObservableCollection<Document>();
 
 
         /// <summary>
         /// <summary>
@@ -111,6 +117,12 @@ namespace PixiEditor.Models.Controllers
         {
         {
             if (SelectedTool.CanStartOutsideCanvas || clickedOnCanvas)
             if (SelectedTool.CanStartOutsideCanvas || clickedOnCanvas)
             {
             {
+                if (startPosition == null)
+                {
+                    SelectedTool.OnStart(newPosition);
+                    startPosition = newPosition;
+                }
+
                 if (IsOperationTool(SelectedTool))
                 if (IsOperationTool(SelectedTool))
                 {
                 {
                     BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool)SelectedTool);
                     BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool)SelectedTool);
@@ -191,6 +203,8 @@ namespace PixiEditor.Models.Controllers
             {
             {
                 BitmapOperations.ApplyPreviewLayer();
                 BitmapOperations.ApplyPreviewLayer();
             }
             }
+
+            startPosition = null;
         }
         }
 
 
         private void HighlightPixels(Coordinates newPosition)
         private void HighlightPixels(Coordinates newPosition)

+ 39 - 12
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -12,6 +12,7 @@ using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Undo;
 using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.Controllers
 namespace PixiEditor.Models.Controllers
 {
 {
@@ -87,20 +88,29 @@ namespace PixiEditor.Models.Controllers
                 return;
                 return;
             }
             }
 
 
-            foreach (var modifiedLayer in previewLayerChanges)
+            Layer[] layers = new Layer[previewLayerChanges.Count];
+
+            for (int i = 0; i < layers.Length; i++)
+            {
+                layers[i] = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == previewLayerChanges[i].LayerGuid);
+            }
+
+            if (layers.Length > 0)
             {
             {
-                Layer layer = Manager.ActiveDocument.Layers.FirstOrDefault(x => x.LayerGuid == modifiedLayer.LayerGuid);
+                IEnumerable<LayerChange> oldValues =
+                    ApplyToLayers(layers, previewLayerChanges.ToArray());
 
 
-                if (layer != null)
+                foreach (var oldValue in oldValues)
                 {
                 {
-                    BitmapPixelChanges oldValues = ApplyToLayer(layer, modifiedLayer).PixelChanges;
+                    var previewChanges = previewLayerChanges.First(x => x.LayerGuid == oldValue.LayerGuid);
 
 
                     BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
                     BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        modifiedLayer.PixelChanges,
-                        oldValues,
-                        modifiedLayer.LayerGuid));
-                    Manager.ActiveDocument.GeneratePreviewLayer();
+                        previewChanges.PixelChanges,
+                        oldValue.PixelChanges,
+                        previewChanges.LayerGuid));
                 }
                 }
+
+                Manager.ActiveDocument.GeneratePreviewLayer();
             }
             }
 
 
             previewLayerChanges = null;
             previewLayerChanges = null;
@@ -136,14 +146,31 @@ namespace PixiEditor.Models.Controllers
 
 
         private LayerChange ApplyToLayer(Layer layer, LayerChange change)
         private LayerChange ApplyToLayer(Layer layer, LayerChange change)
         {
         {
-            layer.DynamicResize(change.PixelChanges);
+            return ApplyToLayers(new Layer[] { layer }, new LayerChange[] { change })[0];
+        }
 
 
-            LayerChange oldPixelsValues = new LayerChange(
+        private LayerChange[] ApplyToLayers(Layer[] layers, LayerChange[] changes)
+        {
+            LayerChange[] oldPixelValues = new LayerChange[changes.Length];
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Layer layer = layers[i];
+                LayerChange change = changes.First(x => x.LayerGuid == layer.LayerGuid);
+                layer.DynamicResize(change.PixelChanges);
+
+                oldPixelValues[i] = new LayerChange(
                 GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
                 GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
                 change.LayerGuid);
                 change.LayerGuid);
+            }
+
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Layer layer = layers[i];
+                LayerChange change = changes.First(x => x.LayerGuid == layer.LayerGuid);
+                layer.SetPixels(change.PixelChanges, false);
+            }
 
 
-            layer.SetPixels(change.PixelChanges, false);
-            return oldPixelsValues;
+            return oldPixelValues;
         }
         }
 
 
         private bool MouseCordsNotInLine(List<Coordinates> cords)
         private bool MouseCordsNotInLine(List<Coordinates> cords)

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

@@ -5,13 +5,13 @@ namespace PixiEditor.Models.Controllers
 {
 {
     public class LayersChangedEventArgs : EventArgs
     public class LayersChangedEventArgs : EventArgs
     {
     {
-        public LayersChangedEventArgs(int layerAffected, LayerAction layerChangeType)
+        public LayersChangedEventArgs(Guid layerAffectedGuid, LayerAction layerChangeType)
         {
         {
-            LayerAffected = layerAffected;
+            LayerAffectedGuid = layerAffectedGuid;
             LayerChangeType = layerChangeType;
             LayerChangeType = layerChangeType;
         }
         }
 
 
-        public int LayerAffected { get; set; }
+        public Guid LayerAffectedGuid { get; set; }
 
 
         public LayerAction LayerChangeType { get; set; }
         public LayerAction LayerChangeType { get; set; }
     }
     }

+ 2 - 0
PixiEditor/Models/Controllers/UndoManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using PixiEditor.Models.Undo;
 using PixiEditor.Models.Undo;
@@ -7,6 +8,7 @@ using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.Controllers
 namespace PixiEditor.Models.Controllers
 {
 {
+    [DebuggerDisplay("{UndoStack.Count} undo steps, {RedoStack.Count} redo step(s)")]
     public class UndoManager
     public class UndoManager
     {
     {
         private bool lastChangeWasUndo;
         private bool lastChangeWasUndo;

+ 31 - 1
PixiEditor/Models/DataHolders/Document/Document.IO.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Models.IO;
+using System.Collections.ObjectModel;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.UserPreferences;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
@@ -14,6 +16,7 @@ namespace PixiEditor.Models.DataHolders
                 documentFilePath = value;
                 documentFilePath = value;
                 RaisePropertyChanged(nameof(DocumentFilePath));
                 RaisePropertyChanged(nameof(DocumentFilePath));
                 RaisePropertyChanged(nameof(Name));
                 RaisePropertyChanged(nameof(Name));
+                UpdateRecentlyOpened(value);
             }
             }
         }
         }
 
 
@@ -47,5 +50,32 @@ namespace PixiEditor.Models.DataHolders
             DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
             DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
             ChangesSaved = true;
             ChangesSaved = true;
         }
         }
+
+        private void UpdateRecentlyOpened(string newPath)
+        {
+            ObservableCollection<string> recentlyOpened = XamlAccesibleViewModel.FileSubViewModel.RecentlyOpened;
+
+            if (!recentlyOpened.Contains(newPath))
+            {
+                recentlyOpened.Insert(0, newPath);
+            }
+            else
+            {
+                int index = recentlyOpened.IndexOf(newPath);
+                recentlyOpened.Move(index, 0);
+            }
+
+            if (recentlyOpened.Count > IPreferences.Current.GetPreference("maxOpenedRecently", 10))
+            {
+                for (int i = 4; i < recentlyOpened.Count; i++)
+                {
+                    recentlyOpened.RemoveAt(i);
+                }
+            }
+
+            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", recentlyOpened);
+
+            XamlAccesibleViewModel.FileSubViewModel.HasRecent = true;
+        }
     }
     }
 }
 }

+ 34 - 19
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -1,6 +1,9 @@
 using System;
 using System;
 using System.Buffers;
 using System.Buffers;
+using System.Collections;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
@@ -20,6 +23,7 @@ using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
+    [DebuggerDisplay("'{Name, nq}' {width}x{height} {Layers.Count} Layer(s)")]
     public partial class Document : NotifyableObject
     public partial class Document : NotifyableObject
     {
     {
         private int height;
         private int height;
@@ -152,7 +156,7 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         /// </summary>
         public void ClipCanvas()
         public void ClipCanvas()
         {
         {
-            DoubleCords points = GetEdgePoints();
+            DoubleCords points = GetEdgePoints(Layers);
             int smallestX = points.Coords1.X;
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int smallestY = points.Coords1.Y;
             int biggestX = points.Coords2.X;
             int biggestX = points.Coords2.X;
@@ -171,7 +175,7 @@ namespace PixiEditor.Models.DataHolders
             int oldWidth = Width;
             int oldWidth = Width;
             int oldHeight = Height;
             int oldHeight = Height;
 
 
-            MoveOffsets(moveVector);
+            MoveOffsets(Layers, moveVector);
             Width = width;
             Width = width;
             Height = height;
             Height = height;
 
 
@@ -187,11 +191,17 @@ namespace PixiEditor.Models.DataHolders
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Centers content inside document.
+        /// Centers selected, visible layers inside document.
         /// </summary>
         /// </summary>
         public void CenterContent()
         public void CenterContent()
         {
         {
-            DoubleCords points = GetEdgePoints();
+            var layersToCenter = Layers.Where(x => x.IsActive && x.IsVisible);
+            if (layersToCenter.Count() == 0)
+            {
+                return;
+            }
+
+            DoubleCords points = GetEdgePoints(layersToCenter);
 
 
             int smallestX = points.Coords1.X;
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int smallestY = points.Coords1.Y;
@@ -209,13 +219,13 @@ namespace PixiEditor.Models.DataHolders
                 new Coordinates(Width, Height));
                 new Coordinates(Width, Height));
             Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
             Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
 
 
-            MoveOffsets(moveVector);
+            MoveOffsets(layersToCenter, moveVector);
             UndoManager.AddUndoChange(
             UndoManager.AddUndoChange(
                 new Change(
                 new Change(
                     MoveOffsetsProcess,
                     MoveOffsetsProcess,
-                    new object[] { new Coordinates(-moveVector.X, -moveVector.Y) },
+                    new object[] { layersToCenter, new Coordinates(-moveVector.X, -moveVector.Y) },
                     MoveOffsetsProcess,
                     MoveOffsetsProcess,
-                    new object[] { moveVector },
+                    new object[] { layersToCenter, moveVector },
                     "Center content"));
                     "Center content"));
         }
         }
 
 
@@ -264,35 +274,40 @@ namespace PixiEditor.Models.DataHolders
             return 0;
             return 0;
         }
         }
 
 
-        private DoubleCords GetEdgePoints()
+        private DoubleCords GetEdgePoints(IEnumerable<Layer> layers)
         {
         {
-            Layer firstLayer = Layers[0];
+            if (Layers.Count == 0)
+            {
+                throw new ArgumentException("Not enough layers");
+            }
+
+            Layer firstLayer = layers.First();
             int smallestX = firstLayer.OffsetX;
             int smallestX = firstLayer.OffsetX;
             int smallestY = firstLayer.OffsetY;
             int smallestY = firstLayer.OffsetY;
             int biggestX = smallestX + firstLayer.Width;
             int biggestX = smallestX + firstLayer.Width;
             int biggestY = smallestY + firstLayer.Height;
             int biggestY = smallestY + firstLayer.Height;
 
 
-            for (int i = 0; i < Layers.Count; i++)
+            foreach (Layer layer in layers)
             {
             {
-                Layers[i].ClipCanvas();
-                if (Layers[i].OffsetX < smallestX)
+                layer.ClipCanvas();
+                if (layer.OffsetX < smallestX)
                 {
                 {
-                    smallestX = Layers[i].OffsetX;
+                    smallestX = layer.OffsetX;
                 }
                 }
 
 
-                if (Layers[i].OffsetX + Layers[i].Width > biggestX)
+                if (layer.OffsetX + layer.Width > biggestX)
                 {
                 {
-                    biggestX = Layers[i].OffsetX + Layers[i].Width;
+                    biggestX = layer.OffsetX + layer.Width;
                 }
                 }
 
 
-                if (Layers[i].OffsetY < smallestY)
+                if (layer.OffsetY < smallestY)
                 {
                 {
-                    smallestY = Layers[i].OffsetY;
+                    smallestY = layer.OffsetY;
                 }
                 }
 
 
-                if (Layers[i].OffsetY + Layers[i].Height > biggestY)
+                if (layer.OffsetY + layer.Height > biggestY)
                 {
                 {
-                    biggestY = Layers[i].OffsetY + Layers[i].Height;
+                    biggestY = layer.OffsetY + layer.Height;
                 }
                 }
             }
             }
 
 

+ 2 - 0
PixiEditor/Models/DataHolders/Selection.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
 using System.Windows.Media;
 using System.Windows.Media;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
@@ -9,6 +10,7 @@ using PixiEditor.Models.Position;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
+    [DebuggerDisplay("{SelectedPoints.Count} selected Pixels")]
     public class Selection : NotifyableObject
     public class Selection : NotifyableObject
     {
     {
         private readonly Color selectionBlue;
         private readonly Color selectionBlue;

+ 2 - 2
PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -7,9 +7,9 @@ namespace PixiEditor.Models.Dialogs
 {
 {
     public class NewFileDialog : CustomDialog
     public class NewFileDialog : CustomDialog
     {
     {
-        private int height = (int)PreferencesSettings.GetPreference("DefaultNewFileHeight", 16L);
+        private int height = (int)IPreferences.Current.GetPreference("DefaultNewFileHeight", 16L);
 
 
-        private int width = (int)PreferencesSettings.GetPreference("DefaultNewFileWidth", 16L);
+        private int width = (int)IPreferences.Current.GetPreference("DefaultNewFileWidth", 16L);
 
 
         public int Width
         public int Width
         {
         {

+ 17 - 0
PixiEditor/Models/Dialogs/NoticeDialog.cs

@@ -0,0 +1,17 @@
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Models.Dialogs
+{
+    public static class NoticeDialog
+    {
+        public static void Show(string message)
+        {
+            NoticePopup popup = new NoticePopup
+            {
+                Body = message
+            };
+
+            popup.ShowDialog();
+        }
+    }
+}

+ 4 - 1
PixiEditor/Models/Events/DocumentChangedEventArgs.cs

@@ -4,11 +4,14 @@ namespace PixiEditor.Models.Events
 {
 {
     public class DocumentChangedEventArgs
     public class DocumentChangedEventArgs
     {
     {
-        public DocumentChangedEventArgs(Document newDocument)
+        public DocumentChangedEventArgs(Document newDocument, Document oldDocument)
         {
         {
             NewDocument = newDocument;
             NewDocument = newDocument;
+            OldDocument = oldDocument;
         }
         }
 
 
+        public Document OldDocument { get; set; }
+
         public Document NewDocument { get; set; }
         public Document NewDocument { get; set; }
     }
     }
 }
 }

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

@@ -108,7 +108,7 @@ namespace PixiEditor.Models.ImageManipulation
 
 
         public static Dictionary<Guid, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
         public static Dictionary<Guid, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
         {
         {
-            Dictionary<Guid, Color[]> result = new Dictionary<Guid, Color[]>();
+            Dictionary<Guid, Color[]> result = new ();
 
 
             foreach (Layer layer in layers)
             foreach (Layer layer in layers)
             {
             {

+ 14 - 1
PixiEditor/Models/Layers/Layer.cs

@@ -1,10 +1,10 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
-using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
 using PixiEditor.Models.Undo;
@@ -12,6 +12,7 @@ using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.Layers
 namespace PixiEditor.Models.Layers
 {
 {
+    [DebuggerDisplay("'{name,nq}' {width}x{height}")]
     public class Layer : BasicLayer
     public class Layer : BasicLayer
     {
     {
         private const int SizeOfArgb = 4;
         private const int SizeOfArgb = 4;
@@ -29,6 +30,8 @@ namespace PixiEditor.Models.Layers
 
 
         private float opacity = 1f;
         private float opacity = 1f;
 
 
+        private string layerHighlightColor = "#666666";
+
         public Layer(string name)
         public Layer(string name)
         {
         {
             Name = name;
             Name = name;
@@ -58,6 +61,15 @@ namespace PixiEditor.Models.Layers
 
 
         public Dictionary<Coordinates, Color> LastRelativeCoordinates { get; set; }
         public Dictionary<Coordinates, Color> LastRelativeCoordinates { get; set; }
 
 
+        public string LayerHighlightColor
+        {
+            get => IsActive ? layerHighlightColor : "#00000000";
+            set
+            {
+                SetProperty(ref layerHighlightColor, value);
+            }
+        }
+
         public string Name
         public string Name
         {
         {
             get => name;
             get => name;
@@ -75,6 +87,7 @@ namespace PixiEditor.Models.Layers
             {
             {
                 isActive = value;
                 isActive = value;
                 RaisePropertyChanged(nameof(IsActive));
                 RaisePropertyChanged(nameof(IsActive));
+                RaisePropertyChanged(nameof(LayerHighlightColor));
             }
             }
         }
         }
 
 

+ 5 - 0
PixiEditor/Models/Tools/Tool.cs

@@ -2,6 +2,7 @@
 using System.Windows.Input;
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings;
 using PixiEditor.Models.Tools.ToolSettings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 
@@ -74,6 +75,10 @@ namespace PixiEditor.Models.Tools
         {
         {
         }
         }
 
 
+        public virtual void OnStart(Coordinates clickPosition)
+        {
+        }
+
         public virtual void OnRecordingLeftMouseDown(MouseEventArgs e)
         public virtual void OnRecordingLeftMouseDown(MouseEventArgs e)
         {
         {
         }
         }

+ 96 - 0
PixiEditor/Models/Tools/ToolSettings/Settings/EnumSetting.cs

@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+
+namespace PixiEditor.Models.Tools.ToolSettings.Settings
+{
+    public class EnumSetting<TEnum> : Setting<TEnum, ComboBox>
+        where TEnum : struct, Enum
+    {
+        private int selectedIndex = 0;
+
+        /// <summary>
+        /// Gets or sets the selected Index of the <see cref="ComboBox"/>.
+        /// </summary>
+        public int SelectedIndex
+        {
+            get => selectedIndex;
+            set
+            {
+                if (SetProperty(ref selectedIndex, value))
+                {
+                    RaisePropertyChanged(nameof(Value));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the selected value of the <see cref="ComboBox"/>.
+        /// </summary>
+        public new TEnum Value
+        {
+            get => (TEnum)(SettingControl.SelectedItem as ComboBoxItem).Tag;
+            set
+            {
+                SettingControl.SelectedItem = SettingControl.Items.Cast<ComboBoxItem>().First(x => x.Tag == (object)value);
+                RaisePropertyChanged(nameof(Value));
+            }
+        }
+
+        public EnumSetting(string name, string label)
+            : base(name)
+        {
+            SettingControl = GenerateDropdown();
+
+            Label = label;
+        }
+
+        public EnumSetting(string name, string label, TEnum defaultValue)
+            : this(name, label)
+        {
+            Value = defaultValue;
+        }
+
+        private static ComboBox GenerateDropdown()
+        {
+            ComboBox combobox = new ComboBox
+            {
+                VerticalAlignment = VerticalAlignment.Center
+            };
+
+            GenerateItems(combobox);
+
+            Binding binding = new Binding(nameof(SelectedIndex))
+            {
+                Mode = BindingMode.TwoWay
+            };
+
+            combobox.SetBinding(Selector.SelectedIndexProperty, binding);
+
+            return combobox;
+        }
+
+        private static void GenerateItems(ComboBox comboBox)
+        {
+            string[] names = Enum.GetNames<TEnum>();
+            TEnum[] values = Enum.GetValues<TEnum>();
+
+            for (int i = 0; i < names.Length; i++)
+            {
+                ComboBoxItem item = new ComboBoxItem
+                {
+                    Content = names[i],
+                    Tag = values[i]
+                };
+
+                comboBox.Items.Add(item);
+            }
+        }
+    }
+}

+ 17 - 4
PixiEditor/Models/Tools/ToolSettings/Settings/Setting.cs

@@ -2,12 +2,25 @@
 using System.Windows.Controls;
 using System.Windows.Controls;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 
 
+#pragma warning disable SA1402 // File may only contain a single type, Justification: "Same class with generic value"
+
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
 {
-    [System.Diagnostics.CodeAnalysis.SuppressMessage(
-        "StyleCop.CSharp.MaintainabilityRules",
-        "SA1402:File may only contain a single type",
-        Justification = "Same class with generic value")]
+    public abstract class Setting<T, TControl> : Setting<T>
+        where TControl : Control
+    {
+        protected Setting(string name)
+            : base(name)
+        {
+        }
+
+        public new TControl SettingControl
+        {
+            get => (TControl)base.SettingControl;
+            set => base.SettingControl = value;
+        }
+    }
+
     public abstract class Setting<T> : Setting
     public abstract class Setting<T> : Setting
     {
     {
         protected Setting(string name)
         protected Setting(string name)

+ 3 - 2
PixiEditor/Models/Tools/ToolSettings/Toolbars/SelectToolToolbar.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Tools.ToolSettings.Settings;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 
 namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
 namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
 {
 {
@@ -6,7 +7,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
     {
     {
         public SelectToolToolbar()
         public SelectToolToolbar()
         {
         {
-            Settings.Add(new DropdownSetting("SelectMode", new[] { "New", "Add", "Subtract" }, "Selection type"));
+            Settings.Add(new EnumSetting<SelectionType>("SelectMode", "Selection type"));
         }
         }
     }
     }
 }
 }

+ 69 - 61
PixiEditor/Models/Tools/Tools/MoveTool.cs

@@ -23,8 +23,8 @@ namespace PixiEditor.Models.Tools.Tools
         private Dictionary<Guid, bool> clearedPixels = new Dictionary<Guid, bool>();
         private Dictionary<Guid, bool> clearedPixels = new Dictionary<Guid, bool>();
         private Coordinates[] currentSelection;
         private Coordinates[] currentSelection;
         private Coordinates lastMouseMove;
         private Coordinates lastMouseMove;
-        private Coordinates lastStartMousePos;
         private Dictionary<Guid, Color[]> startPixelColors;
         private Dictionary<Guid, Color[]> startPixelColors;
+        private Dictionary<Guid, Color[]> endPixelColors;
         private Dictionary<Guid, Thickness> startingOffsets;
         private Dictionary<Guid, Thickness> startingOffsets;
         private Coordinates[] startSelection;
         private Coordinates[] startSelection;
         private bool updateViewModelSelection = true;
         private bool updateViewModelSelection = true;
@@ -58,23 +58,31 @@ namespace PixiEditor.Models.Tools.Tools
         }
         }
 
 
         public override void AfterAddedUndo(UndoManager undoManager)
         public override void AfterAddedUndo(UndoManager undoManager)
-        {
-            if (currentSelection != null && currentSelection.Length != 0)
-            {
-                // Inject to default undo system change custom changes made by this tool
-                foreach (var item in startPixelColors)
-                {
-                    BitmapPixelChanges beforeMovePixels = BitmapPixelChanges.FromArrays(startSelection, item.Value);
-                    Change changes = undoManager.UndoStack.Peek();
-                    Guid layerGuid = item.Key;
-
-                    ((LayerChange[])changes.OldValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
-                        .AddRangeOverride(beforeMovePixels.ChangedPixels);
-
-                    ((LayerChange[])changes.NewValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
-                        .AddRangeNewOnly(BitmapPixelChanges
-                            .FromSingleColoredArray(startSelection, System.Windows.Media.Colors.Transparent)
-                            .ChangedPixels);
+        {
+            if (currentSelection != null && currentSelection.Length > 0)
+            {
+                Change changes = undoManager.UndoStack.Peek();
+
+                // Inject to default undo system change custom changes made by this tool
+                foreach (var item in startPixelColors)
+                {
+                    BitmapPixelChanges beforeMovePixels = BitmapPixelChanges.FromArrays(startSelection, item.Value);
+                    BitmapPixelChanges afterMovePixels = BitmapPixelChanges.FromArrays(currentSelection, endPixelColors[item.Key]);
+                    Guid layerGuid = item.Key;
+                    var oldValue = (LayerChange[])changes.OldValue;
+
+                    if (oldValue.Any(x => x.LayerGuid == layerGuid))
+                    {
+                        var layer = oldValue.First(x => x.LayerGuid == layerGuid);
+                        layer.PixelChanges.ChangedPixels.AddRangeOverride(afterMovePixels.ChangedPixels);
+                        layer.PixelChanges.ChangedPixels
+                            .AddRangeOverride(beforeMovePixels.ChangedPixels);
+
+                        ((LayerChange[])changes.NewValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
+                            .AddRangeNewOnly(BitmapPixelChanges
+                                .FromSingleColoredArray(startSelection, System.Windows.Media.Colors.Transparent)
+                                .ChangedPixels);
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -94,49 +102,50 @@ namespace PixiEditor.Models.Tools.Tools
             }
             }
         }
         }
 
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
-        {
-            Coordinates start = mouseMove[^1];
+        public override void OnStart(Coordinates startPos)
+        {
+            ResetSelectionValues(startPos);
 
 
-            // I am aware that this could be moved to OnMouseDown, but it is executed before Use, so I didn't want to complicate for now
-            if (lastStartMousePos != start)
-            {
-                ResetSelectionValues(start);
-
-                // Move offset if no selection
-                Selection selection = ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection;
-                if (selection != null && selection.SelectedPoints.Count > 0)
-                {
-                    currentSelection = selection.SelectedPoints.ToArray();
-                }
-                else
-                {
-                    currentSelection = Array.Empty<Coordinates>();
-                }
-
-                if (Keyboard.IsKeyDown(Key.LeftCtrl) || MoveAll)
-                {
-                    affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Where(x => x.IsVisible)
-                        .ToArray();
-                }
-                else
-                {
-                    affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument
-                        .Layers.Where(x => x.IsActive && x.IsVisible).ToArray();
-                }
+            // Move offset if no selection
+            Selection selection = ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection;
+            if (selection != null && selection.SelectedPoints.Count > 0)
+            {
+                currentSelection = selection.SelectedPoints.ToArray();
+            }
+            else
+            {
+                currentSelection = Array.Empty<Coordinates>();
+            }
 
 
-                startSelection = currentSelection;
-                startPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, startSelection);
-                startingOffsets = GetOffsets(affectedLayers);
-            }
+            if (Keyboard.IsKeyDown(Key.LeftCtrl) || MoveAll)
+            {
+                affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Where(x => x.IsVisible)
+                    .ToArray();
+            }
+            else
+            {
+                affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument
+                    .Layers.Where(x => x.IsActive && x.IsVisible).ToArray();
+            }
+
+            startSelection = currentSelection;
+            startPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, startSelection);
+            startingOffsets = GetOffsets(affectedLayers);
+        }
 
 
+        public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
+        {
             LayerChange[] result = new LayerChange[affectedLayers.Length];
             LayerChange[] result = new LayerChange[affectedLayers.Length];
-            var end = mouseMove[0];
+            var end = mouseMove[0];
+            var lastSelection = currentSelection.ToArray();
             for (int i = 0; i < affectedLayers.Length; i++)
             for (int i = 0; i < affectedLayers.Length; i++)
             {
             {
                 if (currentSelection.Length > 0)
                 if (currentSelection.Length > 0)
                 {
                 {
-                    var changes = MoveSelection(affectedLayers[i], mouseMove);
+                    endPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, currentSelection);
+                    var changes = MoveSelection(affectedLayers[i], mouseMove);
+                    ClearSelectedPixels(affectedLayers[i], lastSelection);
+
                     changes = RemoveTransparentPixels(changes);
                     changes = RemoveTransparentPixels(changes);
 
 
                     result[i] = new LayerChange(changes, affectedLayers[i]);
                     result[i] = new LayerChange(changes, affectedLayers[i]);
@@ -158,13 +167,11 @@ namespace PixiEditor.Models.Tools.Tools
         {
         {
             Coordinates end = mouseMove[0];
             Coordinates end = mouseMove[0];
 
 
-            currentSelection = TranslateSelection(end, out Coordinates[] previousSelection);
+            currentSelection = TranslateSelection(end);
             if (updateViewModelSelection)
             if (updateViewModelSelection)
             {
             {
                 ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(currentSelection, SelectionType.New);
                 ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(currentSelection, SelectionType.New);
-            }
-
-            ClearSelectedPixels(layer, previousSelection);
+            }
 
 
             lastMouseMove = end;
             lastMouseMove = end;
             return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
             return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
@@ -204,19 +211,20 @@ namespace PixiEditor.Models.Tools.Tools
 
 
         private void ResetSelectionValues(Coordinates start)
         private void ResetSelectionValues(Coordinates start)
         {
         {
-            lastStartMousePos = start;
             lastMouseMove = start;
             lastMouseMove = start;
             clearedPixels = new Dictionary<Guid, bool>();
             clearedPixels = new Dictionary<Guid, bool>();
+            endPixelColors = new Dictionary<Guid, Color[]>();
+            currentSelection = null;
+            affectedLayers = null;
             updateViewModelSelection = true;
             updateViewModelSelection = true;
             startPixelColors = null;
             startPixelColors = null;
             startSelection = null;
             startSelection = null;
         }
         }
 
 
-        private Coordinates[] TranslateSelection(Coordinates end, out Coordinates[] previousSelection)
+        private Coordinates[] TranslateSelection(Coordinates end)
         {
         {
             Coordinates translation = Transform.GetTranslation(lastMouseMove, end);
             Coordinates translation = Transform.GetTranslation(lastMouseMove, end);
-            previousSelection = currentSelection.ToArray();
-            return Transform.Translate(previousSelection, translation);
+            return Transform.Translate(currentSelection, translation);
         }
         }
 
 
         private void ClearSelectedPixels(Layer layer, Coordinates[] selection)
         private void ClearSelectedPixels(Layer layer, Coordinates[] selection)

+ 89 - 100
PixiEditor/Models/Tools/Tools/SelectTool.cs

@@ -1,101 +1,90 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Windows.Controls;
-using System.Windows.Input;
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.DataHolders;
-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
-{
-    public class SelectTool : ReadonlyTool
-    {
-        private Selection oldSelection;
-
-        public SelectTool()
-        {
-            ActionDisplay = "Click and move to select an area.";
-            Tooltip = "Selects area. (M)";
-            Toolbar = new SelectToolToolbar();
-        }
-
-        public SelectionType SelectionType { get; set; } = SelectionType.Add;
-
-        public override void OnRecordingLeftMouseDown(MouseEventArgs e)
-        {
-            Enum.TryParse((Toolbar.GetSetting<DropdownSetting>("SelectMode")?.Value as ComboBoxItem)?.Content as string, out SelectionType selectionType);
-            SelectionType = selectionType;
-
-            oldSelection = null;
-            Selection selection = ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection;
-            if (selection != null && selection.SelectedPoints != null)
-            {
-                oldSelection = selection;
-            }
-        }
-
-        public override void OnStoppedRecordingMouseUp(MouseEventArgs e)
-        {
-            if (ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints.Count() <= 1)
-            {
-                // If we have not selected multiple points, clear the selection
-                ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.Clear();
-            }
-
-            if (oldSelection != null)
-            {
-                ViewModelMain.Current.BitmapManager.ActiveDocument.UndoManager.AddUndoChange(
-                    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)
-        {
-            Select(pixels);
-        }
-
-        public IEnumerable<Coordinates> GetRectangleSelectionForPoints(Coordinates start, Coordinates end)
-        {
-            RectangleTool rectangleTool = new RectangleTool();
-            List<Coordinates> selection = rectangleTool.CreateRectangle(start, end, 1).ToList();
-            selection.AddRange(rectangleTool.CalculateFillForRectangle(start, end, 1));
-            return selection;
-        }
-
-        /// <summary>
-        ///     Gets coordinates of every pixel in root layer.
-        /// </summary>
-        /// <returns>Coordinates array of pixels.</returns>
-        public IEnumerable<Coordinates> GetAllSelection()
-        {
-            return GetAllSelection(ViewModelMain.Current.BitmapManager.ActiveDocument);
-        }
-
-        /// <summary>
-        ///     Gets coordinates of every pixel in chosen document.
-        /// </summary>
-        /// <returns>Coordinates array of pixels.</returns>
-        public IEnumerable<Coordinates> GetAllSelection(Document document)
-        {
-            return GetRectangleSelectionForPoints(new Coordinates(0, 0), new Coordinates(document.Width - 1, document.Height - 1));
-        }
-
-        private void Select(Coordinates[] pixels)
-        {
-            IEnumerable<Coordinates> selection = GetRectangleSelectionForPoints(pixels[^1], pixels[0]);
-            ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(selection, SelectionType);
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+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
+{
+    public class SelectTool : ReadonlyTool
+    {
+        private IEnumerable<Coordinates> oldSelectedPoints;
+
+        private static Selection ActiveSelection { get => ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection; }
+
+        public SelectTool()
+        {
+            ActionDisplay = "Click and move to select an area.";
+            Tooltip = "Selects area. (M)";
+            Toolbar = new SelectToolToolbar();
+        }
+
+        public SelectionType SelectionType { get; set; } = SelectionType.Add;
+
+        public override void OnRecordingLeftMouseDown(MouseEventArgs e)
+        {
+            SelectionType = Toolbar.GetEnumSetting<SelectionType>("SelectMode").Value;
+
+            oldSelectedPoints = new ReadOnlyCollection<Coordinates>(ActiveSelection.SelectedPoints);
+        }
+
+        public override void OnStoppedRecordingMouseUp(MouseEventArgs e)
+        {
+            if (ActiveSelection.SelectedPoints.Count <= 1)
+            {
+                // If we have not selected multiple points, clear the selection
+                ActiveSelection.Clear();
+            }
+
+            SelectionHelpers.AddSelectionUndoStep(ViewModelMain.Current.BitmapManager.ActiveDocument, oldSelectedPoints, SelectionType);
+        }
+
+        public override void Use(Coordinates[] pixels)
+        {
+            Select(pixels);
+        }
+
+        public IEnumerable<Coordinates> GetRectangleSelectionForPoints(Coordinates start, Coordinates end)
+        {
+            RectangleTool rectangleTool = new RectangleTool();
+            List<Coordinates> selection = rectangleTool.CreateRectangle(start, end, 1).ToList();
+            selection.AddRange(rectangleTool.CalculateFillForRectangle(start, end, 1));
+            return selection;
+        }
+
+        /// <summary>
+        ///     Gets coordinates of every pixel in root layer.
+        /// </summary>
+        /// <returns>Coordinates array of pixels.</returns>
+        public IEnumerable<Coordinates> GetAllSelection()
+        {
+            return GetAllSelection(ViewModelMain.Current.BitmapManager.ActiveDocument);
+        }
+
+        /// <summary>
+        ///     Gets coordinates of every pixel in chosen document.
+        /// </summary>
+        /// <returns>Coordinates array of pixels.</returns>
+        public IEnumerable<Coordinates> GetAllSelection(Document document)
+        {
+            return GetRectangleSelectionForPoints(new Coordinates(0, 0), new Coordinates(document.Width - 1, document.Height - 1));
+        }
+
+        private void Select(Coordinates[] pixels)
+        {
+            IEnumerable<Coordinates> selection = GetRectangleSelectionForPoints(pixels[^1], pixels[0]);
+            ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(selection, SelectionType);
+        }
+    }
 }
 }

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

@@ -90,6 +90,7 @@ namespace PixiEditor.Models.Undo
                     IsActive = storedLayer.IsActive,
                     IsActive = storedLayer.IsActive,
                     Width = storedLayer.Width,
                     Width = storedLayer.Width,
                     Height = storedLayer.Height,
                     Height = storedLayer.Height,
+                    LayerHighlightColor = storedLayer.LayerHighlightColor                    
                 };
                 };
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
 
 

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

@@ -10,6 +10,8 @@ namespace PixiEditor.Models.Undo
 
 
         public Guid LayerGuid { get; init; }
         public Guid LayerGuid { get; init; }
 
 
+        public string LayerHighlightColor { get; set; }
+
         public string Name { get; set; }
         public string Name { get; set; }
 
 
         public int LayerIndex { get; set; }
         public int LayerIndex { get; set; }
@@ -47,6 +49,7 @@ namespace PixiEditor.Models.Undo
             Opacity = layer.Opacity;
             Opacity = layer.Opacity;
             IsActive = layer.IsActive;
             IsActive = layer.IsActive;
             LayerGuid = layer.LayerGuid;
             LayerGuid = layer.LayerGuid;
+            LayerHighlightColor = layer.LayerHighlightColor;
         }
         }
     }
     }
 }
 }

+ 32 - 0
PixiEditor/Models/UserPreferences/IPreferences.cs

@@ -0,0 +1,32 @@
+using System;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.UserPreferences
+{
+    public interface IPreferences
+    {
+        public static IPreferences Current => ViewModelMain.Current.Preferences;
+
+        public void Save();
+
+        public void AddCallback(string setting, Action<object> action);
+
+        public void Init();
+
+        public void Init(string path, string localPath);
+
+        public void UpdatePreference<T>(string name, T value);
+
+        public void UpdateLocalPreference<T>(string name, T value);
+
+#nullable enable
+
+        public T? GetPreference<T>(string name);
+
+        public T? GetPreference<T>(string name, T? fallbackValue);
+
+        public T? GetLocalPreference<T>(string name);
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue);
+    }
+}

+ 99 - 34
PixiEditor/Models/UserPreferences/PreferencesSettings.cs

@@ -1,52 +1,47 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.IO;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
+using PixiEditor.ViewModels;
 
 
 namespace PixiEditor.Models.UserPreferences
 namespace PixiEditor.Models.UserPreferences
 {
 {
-    public static class PreferencesSettings
+    [DebuggerDisplay("{Preferences.Count + LocalPreferences.Count} Preference(s)")]
+    public class PreferencesSettings : IPreferences
     {
     {
-        public static bool IsLoaded { get; private set; } = false;
+        public static IPreferences Current => ViewModelMain.Current.Preferences;
 
 
-        public static string PathToUserPreferences { get; private set; } = Path.Join(
-            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-            "PixiEditor",
-            "user_preferences.json");
+        public bool IsLoaded { get; private set; } = false;
+
+        public string PathToRoamingUserPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.ApplicationData, "user_preferences.json");
+
+        public string PathToLocalPreferences { get; private set; } = GetPathToSettings(Environment.SpecialFolder.LocalApplicationData, "editor_data.json");
+
+        public Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
 
 
-        public static Dictionary<string, object> Preferences { get; set; } = new Dictionary<string, object>();
+        public Dictionary<string, object> LocalPreferences { get; set; } = new Dictionary<string, object>();
 
 
-        public static void Init()
+        public void Init()
         {
         {
-            Init(PathToUserPreferences);
+            Init(PathToRoamingUserPreferences, PathToLocalPreferences);
         }
         }
 
 
-        public static void Init(string path)
+        public void Init(string path, string localPath)
         {
         {
-            PathToUserPreferences = path;
+            PathToRoamingUserPreferences = path;
+            PathToLocalPreferences = localPath;
+
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
-                string dir = Path.GetDirectoryName(path);
-                if (!Directory.Exists(dir))
-                {
-                    Directory.CreateDirectory(dir);
-                }
-
-                if (!File.Exists(path))
-                {
-                    File.WriteAllText(path, "{\n}");
-                }
-                else
-                {
-                    string json = File.ReadAllText(path);
-                    Preferences = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
-                }
+                Preferences = InitPath(path);
+                LocalPreferences = InitPath(localPath);
 
 
                 IsLoaded = true;
                 IsLoaded = true;
             }
             }
         }
         }
 
 
-        public static void UpdatePreference<T>(string name, T value)
+        public void UpdatePreference<T>(string name, T value)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
@@ -66,21 +61,40 @@ namespace PixiEditor.Models.UserPreferences
             Save();
             Save();
         }
         }
 
 
-        public static void Save()
+        public void UpdateLocalPreference<T>(string name, T value)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
                 Init();
                 Init();
             }
             }
 
 
-            File.WriteAllText(PathToUserPreferences, JsonConvert.SerializeObject(Preferences));
+            LocalPreferences[name] = value;
+
+            if (Callbacks.ContainsKey(name))
+            {
+                foreach (var action in Callbacks[name])
+                {
+                    action.Invoke(value);
+                }
+            }
+
+            Save();
         }
         }
 
 
-#nullable enable
+        public void Save()
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
 
 
-        public static Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
+            File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
+            File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        }
+
+        public Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
 
 
-        public static void AddCallback(string setting, Action<object> action)
+        public void AddCallback(string setting, Action<object> action)
         {
         {
             if (Callbacks.ContainsKey(setting))
             if (Callbacks.ContainsKey(setting))
             {
             {
@@ -91,12 +105,14 @@ namespace PixiEditor.Models.UserPreferences
             Callbacks.Add(setting, new List<Action<object>>() { action });
             Callbacks.Add(setting, new List<Action<object>>() { action });
         }
         }
 
 
-        public static T? GetPreference<T>(string name)
+#nullable enable
+
+        public T? GetPreference<T>(string name)
         {
         {
             return GetPreference(name, default(T));
             return GetPreference(name, default(T));
         }
         }
 
 
-        public static T? GetPreference<T>(string name, T? fallbackValue)
+        public T? GetPreference<T>(string name, T? fallbackValue)
         {
         {
             if (IsLoaded == false)
             if (IsLoaded == false)
             {
             {
@@ -107,5 +123,54 @@ namespace PixiEditor.Models.UserPreferences
                 ? (T)Preferences[name]
                 ? (T)Preferences[name]
                 : fallbackValue;
                 : fallbackValue;
         }
         }
+
+        public T? GetLocalPreference<T>(string name)
+        {
+            return GetPreference(name, default(T));
+        }
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue)
+        {
+            if (IsLoaded == false)
+            {
+                Init();
+            }
+
+            return LocalPreferences.ContainsKey(name)
+                ? (T)LocalPreferences[name]
+                : fallbackValue;
+        }
+
+#nullable disable
+
+        private static string GetPathToSettings(Environment.SpecialFolder folder, string fileName)
+        {
+            return Path.Join(
+            Environment.GetFolderPath(folder),
+            "PixiEditor",
+            fileName);
+        }
+
+        private static Dictionary<string, object> InitPath(string path)
+        {
+            string dir = Path.GetDirectoryName(path);
+
+            if (!Directory.Exists(dir))
+            {
+                Directory.CreateDirectory(dir);
+            }
+
+            if (!File.Exists(path))
+            {
+                File.WriteAllText(path, "{\n}");
+            }
+            else
+            {
+                string json = File.ReadAllText(path);
+                return JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
+            }
+
+            return new Dictionary<string, object>();
+        }
     }
     }
 }
 }

+ 14 - 0
PixiEditor/NotifyableObject.cs

@@ -1,5 +1,7 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.ComponentModel;
+using System.Runtime.CompilerServices;
 
 
 namespace PixiEditor.Helpers
 namespace PixiEditor.Helpers
 {
 {
@@ -16,5 +18,17 @@ namespace PixiEditor.Helpers
                 PropertyChanged(this, new PropertyChangedEventArgs(property));
                 PropertyChanged(this, new PropertyChangedEventArgs(property));
             }
             }
         }
         }
+
+        protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "")
+        {
+            if (EqualityComparer<T>.Default.Equals(backingStore, value))
+            {
+                return false;
+            }
+
+            backingStore = value;
+            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
+            return true;
+        }
     }
     }
 }
 }

+ 1 - 0
PixiEditor/PixiEditor.csproj

@@ -59,6 +59,7 @@
       <Version>1.0.2</Version>
       <Version>1.0.2</Version>
     </PackageReference>
     </PackageReference>
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="2.0.0" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="2.0.0" />

+ 13 - 0
PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -0,0 +1,13 @@
+using System.Diagnostics;
+using PixiEditor.Helpers;
+
+namespace PixiEditor.ViewModels.SubViewModels.Main
+{
+    public class DebugViewModel : SubViewModel<ViewModelMain>
+    {
+        public DebugViewModel(ViewModelMain owner)
+            : base(owner)
+        {
+        }
+    }
+}

+ 15 - 10
PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs

@@ -5,7 +5,7 @@ using PixiEditor.Models.UserPreferences;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
 {
-    public class DiscordViewModel : SubViewModel<ViewModelMain>
+    public class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
     {
     {
         private DiscordRpcClient client;
         private DiscordRpcClient client;
         private string clientId;
         private string clientId;
@@ -30,7 +30,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showDocumentName = PreferencesSettings.GetPreference(nameof(ShowDocumentName), true);
+        private bool showDocumentName = IPreferences.Current.GetPreference(nameof(ShowDocumentName), true);
 
 
         public bool ShowDocumentName
         public bool ShowDocumentName
         {
         {
@@ -45,7 +45,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showDocumentSize = PreferencesSettings.GetPreference(nameof(ShowDocumentSize), true);
+        private bool showDocumentSize = IPreferences.Current.GetPreference(nameof(ShowDocumentSize), true);
 
 
         public bool ShowDocumentSize
         public bool ShowDocumentSize
         {
         {
@@ -60,7 +60,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
         }
         }
 
 
-        private bool showLayerCount = PreferencesSettings.GetPreference(nameof(ShowLayerCount), true);
+        private bool showLayerCount = IPreferences.Current.GetPreference(nameof(ShowLayerCount), true);
 
 
         public bool ShowLayerCount
         public bool ShowLayerCount
         {
         {
@@ -81,12 +81,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.DocumentChanged += DocumentChanged;
             Owner.BitmapManager.DocumentChanged += DocumentChanged;
             this.clientId = clientId;
             this.clientId = clientId;
 
 
-            Enabled = PreferencesSettings.GetPreference<bool>("EnableRichPresence");
-            PreferencesSettings.AddCallback("EnableRichPresence", x => Enabled = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowDocumentName), x => ShowDocumentName = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowDocumentSize), x => ShowDocumentSize = (bool)x);
-            PreferencesSettings.AddCallback(nameof(ShowLayerCount), x => ShowLayerCount = (bool)x);
-
+            Enabled = IPreferences.Current.GetPreference("EnableRichPresence", true);
+            IPreferences.Current.AddCallback("EnableRichPresence", x => Enabled = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowDocumentName), x => ShowDocumentName = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowDocumentSize), x => ShowDocumentSize = (bool)x);
+            IPreferences.Current.AddCallback(nameof(ShowLayerCount), x => ShowLayerCount = (bool)x);
             AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
             AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
         }
         }
 
 
@@ -142,6 +141,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             client.SetPresence(richPresence);
             client.SetPresence(richPresence);
         }
         }
 
 
+        public void Dispose()
+        {
+            Enabled = false;
+            GC.SuppressFinalize(this);
+        }
+
         private static RichPresence NewDefaultRP()
         private static RichPresence NewDefaultRP()
         {
         {
             return new RichPresence
             return new RichPresence

+ 50 - 1
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -1,9 +1,12 @@
 using System;
 using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
 using Microsoft.Win32;
 using Microsoft.Win32;
+using Newtonsoft.Json.Linq;
 using PixiEditor.Exceptions;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
@@ -18,6 +21,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
 {
     public class FileViewModel : SubViewModel<ViewModelMain>
     public class FileViewModel : SubViewModel<ViewModelMain>
     {
     {
+        private bool hasRecent;
+
         public RelayCommand OpenNewFilePopupCommand { get; set; }
         public RelayCommand OpenNewFilePopupCommand { get; set; }
 
 
         public RelayCommand SaveDocumentCommand { get; set; }
         public RelayCommand SaveDocumentCommand { get; set; }
@@ -26,6 +31,20 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         public RelayCommand ExportFileCommand { get; set; } // Command that is used to save file
         public RelayCommand ExportFileCommand { get; set; } // Command that is used to save file
 
 
+        public RelayCommand OpenRecentCommand { get; set; }
+
+        public bool HasRecent
+        {
+            get => hasRecent;
+            set
+            {
+                hasRecent = value;
+                RaisePropertyChanged(nameof(HasRecent));
+            }
+        }
+
+        public ObservableCollection<string> RecentlyOpened { get; set; } = new ObservableCollection<string>();
+
         public FileViewModel(ViewModelMain owner)
         public FileViewModel(ViewModelMain owner)
             : base(owner)
             : base(owner)
         {
         {
@@ -33,7 +52,37 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             OpenFileCommand = new RelayCommand(Open);
             OpenFileCommand = new RelayCommand(Open);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
+            OpenRecentCommand = new RelayCommand(OpenRecent);
             Owner.OnStartupEvent += Owner_OnStartupEvent;
             Owner.OnStartupEvent += Owner_OnStartupEvent;
+            RecentlyOpened = new ObservableCollection<string>(IPreferences.Current.GetLocalPreference<JArray>(nameof(RecentlyOpened), new JArray()).ToObject<string[]>());
+
+            if (RecentlyOpened.Count > 0)
+            {
+                HasRecent = true;
+            }
+        }
+
+        public void OpenRecent(object parameter)
+        {
+            string path = (string)parameter;
+
+            foreach (Document document in Owner.BitmapManager.Documents)
+            {
+                if (document.DocumentFilePath == path)
+                {
+                    Owner.BitmapManager.ActiveDocument = document;
+                    return;
+                }
+            }
+
+            if (!File.Exists(path))
+            {
+                NoticeDialog.Show("The file does no longer exist at that path");
+                RecentlyOpened.Remove(path);
+                return;
+            }
+
+            Open((string)parameter);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -98,7 +147,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
             else
             else
             {
             {
-                if (PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true))
+                if (IPreferences.Current.GetPreference("ShowNewFilePopupOnStartup", true))
                 {
                 {
                     OpenNewFilePopup(null);
                     OpenNewFilePopup(null);
                 }
                 }

+ 32 - 3
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using System.Linq;
 using System.Windows.Input;
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 namespace PixiEditor.ViewModels.SubViewModels.Main
@@ -38,6 +39,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             MergeSelectedCommand = new RelayCommand(MergeSelected, CanMergeSelected);
             MergeSelectedCommand = new RelayCommand(MergeSelected, CanMergeSelected);
             MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
             MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
             MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
             MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
+            Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
         }
         }
 
 
         public bool CanMergeSelected(object obj)
         public bool CanMergeSelected(object obj)
@@ -58,6 +60,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void SetActiveLayer(object parameter)
         public void SetActiveLayer(object parameter)
         {
         {
             int index = (int)parameter;
             int index = (int)parameter;
+
+            if (Owner.BitmapManager.ActiveDocument.Layers[index].IsActive && Mouse.RightButton == MouseButtonState.Pressed)
+            {
+                return;
+            }
+
             if (Keyboard.IsKeyDown(Key.LeftCtrl))
             if (Keyboard.IsKeyDown(Key.LeftCtrl))
             {
             {
                 Owner.BitmapManager.ActiveDocument.ToggleLayer(index);
                 Owner.BitmapManager.ActiveDocument.ToggleLayer(index);
@@ -68,7 +76,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             }
             else
             else
             {
             {
-                Owner.BitmapManager.ActiveDocument.SetActiveLayer(index);
+                Owner.BitmapManager.ActiveDocument.SetMainActiveLayer(index);
             }
             }
         }
         }
 
 
@@ -96,10 +104,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
             if (index == null)
             if (index == null)
             {
             {
-                index = Owner.BitmapManager.ActiveDocument.ActiveLayerIndex;
+                index = Owner.BitmapManager.ActiveDocument.Layers.IndexOf(Owner.BitmapManager.ActiveDocument.ActiveLayer);
             }
             }
 
 
-            Owner.BitmapManager.ActiveDocument.Layers[index.Value].IsRenaming = true;
+            Owner.BitmapManager.ActiveDocument.Layers[(int)index].IsRenaming = true;
         }
         }
 
 
         public bool CanRenameLayer(object parameter)
         public bool CanRenameLayer(object parameter)
@@ -162,5 +170,26 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             int index = (int)property;
             int index = (int)property;
             return Owner.DocumentIsNotNull(null) && index != 0 && Owner.BitmapManager.ActiveDocument.Layers.Count(x => x.IsActive) == 1;
             return Owner.DocumentIsNotNull(null) && index != 0 && Owner.BitmapManager.ActiveDocument.Layers.Count(x => x.IsActive) == 1;
         }
         }
+
+        private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
+        {
+            if (e.OldDocument != null)
+            {
+                e.OldDocument.LayersChanged -= Document_LayersChanged;
+            }
+
+            if (e.NewDocument != null)
+            {
+                e.NewDocument.LayersChanged += Document_LayersChanged;
+            }
+        }
+
+        private void Document_LayersChanged(object sender, LayersChangedEventArgs e)
+        {
+            if (e.LayerChangeType == Models.Enums.LayerAction.SetActive)
+            {
+                Owner.BitmapManager.ActiveDocument.UpdateLayersColor();
+            }
+        }
     }
     }
 }
 }

+ 9 - 0
PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Generic;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
@@ -23,12 +24,20 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void SelectAll(object parameter)
         public void SelectAll(object parameter)
         {
         {
             SelectTool select = new SelectTool();
             SelectTool select = new SelectTool();
+
+            var oldSelection = new List<Coordinates>(Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints);
+
             Owner.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(select.GetAllSelection(), SelectionType.New);
             Owner.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(select.GetAllSelection(), SelectionType.New);
+            SelectionHelpers.AddSelectionUndoStep(Owner.BitmapManager.ActiveDocument, oldSelection, SelectionType.New);
         }
         }
 
 
         public void Deselect(object parameter)
         public void Deselect(object parameter)
         {
         {
+            var oldSelection = new List<Coordinates>(Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints);
+
             Owner.BitmapManager.ActiveDocument.ActiveSelection?.Clear();
             Owner.BitmapManager.ActiveDocument.ActiveSelection?.Clear();
+
+            SelectionHelpers.AddSelectionUndoStep(Owner.BitmapManager.ActiveDocument, oldSelection, SelectionType.New);
         }
         }
 
 
         public bool SelectionIsNotEmpty(object property)
         public bool SelectionIsNotEmpty(object property)

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

@@ -75,7 +75,6 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         /// <param name="parameter">CommandParameter.</param>
         /// <param name="parameter">CommandParameter.</param>
         public void Undo(object parameter)
         public void Undo(object parameter)
         {
         {
-            Owner.SelectionSubViewModel.Deselect(null);
             Owner.BitmapManager.ActiveDocument.UndoManager.Undo();
             Owner.BitmapManager.ActiveDocument.UndoManager.Undo();
         }
         }
 
 

+ 1 - 1
PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -86,7 +86,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         private async void Owner_OnStartupEvent(object sender, EventArgs e)
         private async void Owner_OnStartupEvent(object sender, EventArgs e)
         {
         {
-            if (PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true))
+            if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
             {
             {
                 await CheckForUpdate();
                 await CheckForUpdate();
             }
             }

+ 89 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/DiscordSettings.cs

@@ -0,0 +1,89 @@
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class DiscordSettings : SettingsGroup
+    {
+        private bool enableRichPresence = GetPreference(nameof(EnableRichPresence), true);
+
+        public bool EnableRichPresence
+        {
+            get => enableRichPresence;
+            set
+            {
+                enableRichPresence = value;
+                RaiseAndUpdatePreference(nameof(EnableRichPresence), value);
+            }
+        }
+
+        private bool showDocumentName = GetPreference(nameof(ShowDocumentName), true);
+
+        public bool ShowDocumentName
+        {
+            get => showDocumentName;
+            set
+            {
+                showDocumentName = value;
+                RaiseAndUpdatePreference(nameof(ShowDocumentName), value);
+                RaisePropertyChanged(nameof(DetailPreview));
+            }
+        }
+
+        private bool showDocumentSize = GetPreference(nameof(ShowDocumentSize), true);
+
+        public bool ShowDocumentSize
+        {
+            get => showDocumentSize;
+            set
+            {
+                showDocumentSize = value;
+                RaiseAndUpdatePreference(nameof(ShowDocumentSize), value);
+                RaisePropertyChanged(nameof(StatePreview));
+            }
+        }
+
+        private bool showLayerCount = GetPreference(nameof(ShowLayerCount), true);
+
+        public bool ShowLayerCount
+        {
+            get => showLayerCount;
+            set
+            {
+                showLayerCount = value;
+                RaiseAndUpdatePreference(nameof(ShowLayerCount), value);
+                RaisePropertyChanged(nameof(StatePreview));
+            }
+        }
+
+        public string DetailPreview
+        {
+            get
+            {
+                return ShowDocumentName ? $"Editing coolPixelArt.pixi" : "Editing something (incognito)";
+            }
+        }
+
+        public string StatePreview
+        {
+            get
+            {
+                string state = string.Empty;
+
+                if (ShowDocumentSize)
+                {
+                    state = "16x16";
+                }
+
+                if (ShowDocumentSize && ShowLayerCount)
+                {
+                    state += ", ";
+                }
+
+                if (ShowLayerCount)
+                {
+                    state += "2 Layers";
+                }
+
+                return state;
+            }
+        }
+    }
+}

+ 62 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class FileSettings : SettingsGroup
+    {
+        private bool showNewFilePopupOnStartup = GetPreference("ShowNewFilePopupOnStartup", true);
+
+        public bool ShowNewFilePopupOnStartup
+        {
+            get => showNewFilePopupOnStartup;
+            set
+            {
+                showNewFilePopupOnStartup = value;
+                string name = nameof(ShowNewFilePopupOnStartup);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private long defaultNewFileWidth = GetPreference("DefaultNewFileWidth", 16L);
+
+        public long DefaultNewFileWidth
+        {
+            get => defaultNewFileWidth;
+            set
+            {
+                defaultNewFileWidth = value;
+                string name = nameof(DefaultNewFileWidth);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private long defaultNewFileHeight = GetPreference("DefaultNewFileHeight", 16L);
+
+        public long DefaultNewFileHeight
+        {
+            get => defaultNewFileHeight;
+            set
+            {
+                defaultNewFileHeight = value;
+                string name = nameof(DefaultNewFileHeight);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+
+        private int maxOpenedRecently = GetPreference(nameof(MaxOpenedRecently), 10);
+
+        public int MaxOpenedRecently
+        {
+            get => maxOpenedRecently;
+            set
+            {
+                maxOpenedRecently = value;
+                RaiseAndUpdatePreference(nameof(MaxOpenedRecently), value);
+            }
+        }
+    }
+}

+ 18 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/UpdateSettings.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
+{
+    public class UpdateSettings : SettingsGroup
+    {
+        private bool checkUpdatesOnStartup = GetPreference("CheckUpdatesOnStartup", true);
+
+        public bool CheckUpdatesOnStartup
+        {
+            get => checkUpdatesOnStartup;
+            set
+            {
+                checkUpdatesOnStartup = value;
+                string name = nameof(CheckUpdatesOnStartup);
+                RaiseAndUpdatePreference(name, value);
+            }
+        }
+    }
+}

+ 3 - 3
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs

@@ -8,14 +8,14 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
     {
     {
         protected static T GetPreference<T>(string name)
         protected static T GetPreference<T>(string name)
         {
         {
-            return PreferencesSettings.GetPreference<T>(name);
+            return IPreferences.Current.GetPreference<T>(name);
         }
         }
 
 
 #nullable enable
 #nullable enable
 
 
         protected static T? GetPreference<T>(string name, T? fallbackValue)
         protected static T? GetPreference<T>(string name, T? fallbackValue)
         {
         {
-            return PreferencesSettings.GetPreference(name, fallbackValue);
+            return IPreferences.Current.GetPreference(name, fallbackValue);
         }
         }
 
 
 #nullable disable
 #nullable disable
@@ -23,7 +23,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
         protected void RaiseAndUpdatePreference<T>(string name, T value)
         protected void RaiseAndUpdatePreference<T>(string name, T value)
         {
         {
             RaisePropertyChanged(name);
             RaisePropertyChanged(name);
-            PreferencesSettings.UpdatePreference(name, value);
+            IPreferences.Current.UpdatePreference(name, value);
         }
         }
     }
     }
 }
 }

+ 3 - 143
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs

@@ -1,158 +1,18 @@
 using System;
 using System;
 using System.Configuration;
 using System.Configuration;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
 {
 {
     public class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
     public class SettingsViewModel : SubViewModel<SettingsWindowViewModel>
     {
     {
-        private bool showNewFilePopupOnStartup = PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true);
+        public FileSettings File { get; set; } = new FileSettings();
 
 
-        public bool ShowNewFilePopupOnStartup
-        {
-            get => showNewFilePopupOnStartup;
-            set
-            {
-                showNewFilePopupOnStartup = value;
-                string name = nameof(ShowNewFilePopupOnStartup);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private bool checkUpdatesOnStartup = PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true);
-
-        public bool CheckUpdatesOnStartup
-        {
-            get => checkUpdatesOnStartup;
-            set
-            {
-                checkUpdatesOnStartup = value;
-                string name = nameof(CheckUpdatesOnStartup);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private long defaultNewFileWidth = (int)PreferencesSettings.GetPreference("DefaultNewFileWidth", 16L);
-
-        public long DefaultNewFileWidth
-        {
-            get => defaultNewFileWidth;
-            set
-            {
-                defaultNewFileWidth = value;
-                string name = nameof(DefaultNewFileWidth);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        private long defaultNewFileHeight = (int)PreferencesSettings.GetPreference("DefaultNewFileHeight", 16L);
-
-        public long DefaultNewFileHeight
-        {
-            get => defaultNewFileHeight;
-            set
-            {
-                defaultNewFileHeight = value;
-                string name = nameof(DefaultNewFileHeight);
-                RaiseAndUpdatePreference(name, value);
-            }
-        }
-
-        public class DiscordSettings : SettingsGroup
-        {
-            private bool enableRichPresence = GetPreference(nameof(EnableRichPresence), true);
-
-            public bool EnableRichPresence
-            {
-                get => enableRichPresence;
-                set
-                {
-                    enableRichPresence = value;
-                    RaiseAndUpdatePreference(nameof(EnableRichPresence), value);
-                }
-            }
-
-            private bool showDocumentName = GetPreference(nameof(ShowDocumentName), true);
-
-            public bool ShowDocumentName
-            {
-                get => showDocumentName;
-                set
-                {
-                    showDocumentName = value;
-                    RaiseAndUpdatePreference(nameof(ShowDocumentName), value);
-                    RaisePropertyChanged(nameof(DetailPreview));
-                }
-            }
-
-            private bool showDocumentSize = GetPreference(nameof(ShowDocumentSize), true);
-
-            public bool ShowDocumentSize
-            {
-                get => showDocumentSize;
-                set
-                {
-                    showDocumentSize = value;
-                    RaiseAndUpdatePreference(nameof(ShowDocumentSize), value);
-                    RaisePropertyChanged(nameof(StatePreview));
-                }
-            }
-
-            private bool showLayerCount = GetPreference(nameof(ShowLayerCount), true);
-
-            public bool ShowLayerCount
-            {
-                get => showLayerCount;
-                set
-                {
-                    showLayerCount = value;
-                    RaiseAndUpdatePreference(nameof(ShowLayerCount), value);
-                    RaisePropertyChanged(nameof(StatePreview));
-                }
-            }
-
-            public string DetailPreview
-            {
-                get
-                {
-                    return ShowDocumentName ? $"Editing coolPixelArt.pixi" : "Editing something (incognito)";
-                }
-            }
-
-            public string StatePreview
-            {
-                get
-                {
-                    string state = string.Empty;
-
-                    if (ShowDocumentSize)
-                    {
-                        state = "16x16";
-                    }
-
-                    if (ShowDocumentSize && ShowLayerCount)
-                    {
-                        state += ", ";
-                    }
-
-                    if (ShowLayerCount)
-                    {
-                        state += "2 Layers";
-                    }
-
-                    return state;
-                }
-            }
-        }
+        public UpdateSettings Update { get; set; } = new UpdateSettings();
 
 
         public DiscordSettings Discord { get; set; } = new DiscordSettings();
         public DiscordSettings Discord { get; set; } = new DiscordSettings();
 
 
-        public void RaiseAndUpdatePreference<T>(string name, T value)
-        {
-            RaisePropertyChanged(name);
-            PreferencesSettings.UpdatePreference(name, value);
-        }
-
         public SettingsViewModel(SettingsWindowViewModel owner)
         public SettingsViewModel(SettingsWindowViewModel owner)
             : base(owner)
             : base(owner)
         {
         {

+ 29 - 7
PixiEditor/ViewModels/ViewModelMain.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Linq;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers.Shortcuts;
 using PixiEditor.Models.Controllers.Shortcuts;
@@ -11,7 +12,6 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Events;
-using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.Tools.Tools;
@@ -58,15 +58,35 @@ namespace PixiEditor.ViewModels
 
 
         public DiscordViewModel DiscordViewModel { get; set; }
         public DiscordViewModel DiscordViewModel { get; set; }
 
 
+#if DEBUG
+        public DebugViewModel DebugSubViewModel { get; set; }
+#endif
+
         public BitmapManager BitmapManager { get; set; }
         public BitmapManager BitmapManager { get; set; }
 
 
         public PixelChangesController ChangesController { get; set; }
         public PixelChangesController ChangesController { get; set; }
 
 
         public ShortcutController ShortcutController { get; set; }
         public ShortcutController ShortcutController { get; set; }
 
 
-        public ViewModelMain()
+        public IPreferences Preferences { get; set; }
+
+        public bool IsDebug
         {
         {
-            PreferencesSettings.Init();
+            get =>
+#if DEBUG
+                true;
+#else
+                false;
+#endif
+        }
+
+        public ViewModelMain(IServiceProvider services)
+        {
+            Current = this;
+
+            Preferences = services.GetRequiredService<IPreferences>();
+
+            Preferences.Init();
 
 
             BitmapManager = new BitmapManager();
             BitmapManager = new BitmapManager();
             BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
             BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
@@ -91,6 +111,9 @@ namespace PixiEditor.ViewModels
             DocumentSubViewModel = new DocumentViewModel(this);
             DocumentSubViewModel = new DocumentViewModel(this);
             MiscSubViewModel = new MiscViewModel(this);
             MiscSubViewModel = new MiscViewModel(this);
             DiscordViewModel = new DiscordViewModel(this, "764168193685979138");
             DiscordViewModel = new DiscordViewModel(this, "764168193685979138");
+#if DEBUG
+            DebugSubViewModel = new DebugViewModel(this);
+#endif
 
 
             ShortcutController = new ShortcutController
             ShortcutController = new ShortcutController
             {
             {
@@ -137,14 +160,13 @@ namespace PixiEditor.ViewModels
                     new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
                     new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
 
 
                     // Layers
                     // Layers
-                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerIndex),
+                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerGuid),
 
 
                     // View
                     // View
-                    new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, modifier: ModifierKeys.Control)
+                    new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, modifier: ModifierKeys.Control),
                 }
                 }
             };
             };
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
-            Current = this;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -171,7 +193,7 @@ namespace PixiEditor.ViewModels
             return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, typeof(T), modifier);
             return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, typeof(T), modifier);
         }
         }
 
 
-        private void CloseWindow(object property)
+        public void CloseWindow(object property)
         {
         {
             if (!(property is CancelEventArgs))
             if (!(property is CancelEventArgs))
             {
             {

+ 40 - 0
PixiEditor/Views/Dialogs/NoticePopup.xaml

@@ -0,0 +1,40 @@
+<Window x:Class="PixiEditor.Views.Dialogs.NoticePopup"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
+        mc:Ignorable="d"
+        Title="NoticePopup" Height="200" Width="500"
+        x:Name="popup">
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Grid Background="{StaticResource AccentColor}" Focusable="True">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="34*" />
+            <RowDefinition Height="21*" />
+        </Grid.RowDefinitions>
+        <i:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
+        <TextBlock Grid.Row="1" Text="{Binding Body, ElementName=popup}" HorizontalAlignment="Center"
+                   VerticalAlignment="Center" FontSize="18" Foreground="White" />
+        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{Binding DataContext.CancelCommand, ElementName=popup}" />
+        </DockPanel>
+        <StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Center"
+                    Margin="0,0,10,10">
+            <Button Height="30" Width="60"
+                    Click="OkButton_Close"
+                    Style="{StaticResource DarkRoundButton}" Content="Ok">
+            </Button>
+        </StackPanel>
+    </Grid>
+</Window>

+ 41 - 0
PixiEditor/Views/Dialogs/NoticePopup.xaml.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for NoticePopup.xaml.
+    /// </summary>
+    public partial class NoticePopup : Window
+    {
+        public static readonly DependencyProperty BodyProperty =
+            DependencyProperty.Register(nameof(Body), typeof(string), typeof(NoticePopup));
+
+        public string Body
+        {
+            get => (string)GetValue(BodyProperty);
+            set => SetValue(BodyProperty, value);
+        }
+
+        public NoticePopup()
+        {
+            InitializeComponent();
+        }
+
+        private void OkButton_Close(object sender, RoutedEventArgs e)
+        {
+            Close();
+        }
+    }
+}

+ 8 - 4
PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -58,13 +58,17 @@
                     <Label Content="File" Style="{StaticResource Header1}"/>
                     <Label Content="File" Style="{StaticResource Header1}"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                         <CheckBox Content="Show New File dialog on startup" 
                         <CheckBox Content="Show New File dialog on startup" 
-                                  IsChecked="{Binding SettingsSubViewModel.ShowNewFilePopupOnStartup}"/>
+                                  IsChecked="{Binding SettingsSubViewModel.File.ShowNewFilePopupOnStartup}"/>
+                        <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
+                            <Label Content="Max Saved Opened Recently:" ToolTip="How many documents are shown under File > Recent. Default: 10" Style="{StaticResource BaseLabel}"/>
+                            <views:NumberInput FontSize="16" Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently}" Width="40"/>
+                        </StackPanel>
                         <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
                         <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
                             <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
                             <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.DefaultNewFileWidth, Mode=TwoWay}" Width="60" Height="25"/>
+                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileWidth, Mode=TwoWay}" Width="60" Height="25"/>
                             <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
                             <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.DefaultNewFileHeight, Mode=TwoWay}" Width="60" Height="25"/>
+                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileHeight, Mode=TwoWay}" Width="60" Height="25"/>
                         </StackPanel>
                         </StackPanel>
                     </StackPanel>
                     </StackPanel>
                 </StackPanel>
                 </StackPanel>
@@ -74,7 +78,7 @@
                 <StackPanel Orientation="Vertical">
                 <StackPanel Orientation="Vertical">
                     <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
                     <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox IsChecked="{Binding SettingsSubViewModel.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
+                        <CheckBox IsChecked="{Binding SettingsSubViewModel.Update.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
                     </StackPanel>
                     </StackPanel>
                 </StackPanel>
                 </StackPanel>
             </Grid>
             </Grid>

+ 1 - 1
PixiEditor/Views/MainWindow.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.MainWindow" MinHeight="500" MinWidth="1100"
+<Window x:Class="PixiEditor.MainWindow" MinHeight="500" MinWidth="1100"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

+ 14 - 0
PixiEditor/Views/MainWindow.xaml.cs

@@ -5,9 +5,11 @@ using System.IO;
 using System.Reflection;
 using System.Reflection;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Processes;
 using PixiEditor.Models.Processes;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.UpdateModule;
 using PixiEditor.UpdateModule;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 
 
@@ -23,12 +25,24 @@ namespace PixiEditor
         public MainWindow()
         public MainWindow()
         {
         {
             InitializeComponent();
             InitializeComponent();
+
+            IServiceCollection services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new PreferencesSettings());
+
+            DataContext = new ViewModelMain(services.BuildServiceProvider());
+
             StateChanged += MainWindowStateChangeRaised;
             StateChanged += MainWindowStateChangeRaised;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             viewModel = (ViewModelMain)DataContext;
             viewModel = (ViewModelMain)DataContext;
             viewModel.CloseAction = Close;
             viewModel.CloseAction = Close;
         }
         }
 
 
+        protected override void OnClosing(CancelEventArgs e)
+        {
+            ((ViewModelMain)DataContext).CloseWindow(e);
+            viewModel.DiscordViewModel.Dispose();
+        }
+
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
         {
             e.CanExecute = true;
             e.CanExecute = true;

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

@@ -9,16 +9,13 @@
              mc:Ignorable="d" Focusable="True"
              mc:Ignorable="d" Focusable="True"
              d:DesignHeight="60" d:DesignWidth="250" Name="uc"
              d:DesignHeight="60" d:DesignWidth="250" Name="uc"
              MouseLeave="LayerItem_OnMouseLeave" MouseEnter="LayerItem_OnMouseEnter">
              MouseLeave="LayerItem_OnMouseLeave" MouseEnter="LayerItem_OnMouseEnter">
-    <UserControl.Resources>
-        <converters:BoolToColorConverter x:Key="BoolToColorConverter" />
-    </UserControl.Resources>
     <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True"
     <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True"
-            Background="{Binding IsActive, Mode=TwoWay, Converter={StaticResource BoolToColorConverter}}">
+            Background="{Binding LayerColor, ElementName=uc}" >
         <i:Interaction.Behaviors>
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
         </i:Interaction.Behaviors>
         <i:Interaction.Triggers>
         <i:Interaction.Triggers>
-            <i:EventTrigger EventName="MouseLeftButtonDown">
+            <i:EventTrigger EventName="MouseDown">
                 <i:InvokeCommandAction Command="{Binding ElementName=uc, 
                 <i:InvokeCommandAction Command="{Binding ElementName=uc, 
                             Path=SetActiveLayerCommand}"
                             Path=SetActiveLayerCommand}"
                                        CommandParameter="{Binding Path=LayerIndex, ElementName=uc}"/>
                                        CommandParameter="{Binding Path=LayerIndex, ElementName=uc}"/>

+ 10 - 0
PixiEditor/Views/UserControls/LayerItem.xaml.cs

@@ -78,6 +78,16 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty PreviewImageProperty =
         public static readonly DependencyProperty PreviewImageProperty =
             DependencyProperty.Register("PreviewImage", typeof(WriteableBitmap), typeof(LayerItem), new PropertyMetadata(null));
             DependencyProperty.Register("PreviewImage", typeof(WriteableBitmap), typeof(LayerItem), new PropertyMetadata(null));
 
 
+        public string LayerColor
+        {
+            get { return (string)GetValue(LayerColorProperty); }
+            set { SetValue(LayerColorProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for LayerColor.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty LayerColorProperty =
+            DependencyProperty.Register("LayerColor", typeof(string), typeof(LayerItem), new PropertyMetadata("#00000000"));
+
         public Visibility ControlButtonsVisible
         public Visibility ControlButtonsVisible
         {
         {
             get { return (Visibility)GetValue(ControlButtonsVisibleProperty); }
             get { return (Visibility)GetValue(ControlButtonsVisibleProperty); }

+ 24 - 0
PixiEditorTests/Helpers.cs

@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels;
+
+namespace PixiEditorTests
+{
+    public static class Helpers
+    {
+        public static ViewModelMain MockedViewModelMain()
+        {
+            IServiceProvider provider = MockedServiceProvider();
+
+            return new ViewModelMain(provider);
+        }
+
+        public static IServiceProvider MockedServiceProvider()
+        {
+            return new ServiceCollection()
+                .AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock())
+                .BuildServiceProvider();
+        }
+    }
+}

+ 56 - 0
PixiEditorTests/Mocks/PreferenceSettingsMock.cs

@@ -0,0 +1,56 @@
+using System;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditorTests.Mocks
+{
+    public class PreferenceSettingsMock : IPreferences
+    {
+        public void AddCallback(string setting, Action<object> action)
+        {
+        }
+
+#nullable enable
+
+        public T? GetLocalPreference<T>(string name)
+        {
+            return default;
+        }
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue)
+        {
+            return fallbackValue;
+        }
+
+        public T? GetPreference<T>(string name)
+        {
+            return default;
+        }
+
+        public T? GetPreference<T>(string name, T? fallbackValue)
+        {
+            return fallbackValue;
+        }
+
+#nullable disable
+
+        public void Init()
+        {
+        }
+
+        public void Init(string path, string localPath)
+        {
+        }
+
+        public void Save()
+        {
+        }
+
+        public void UpdateLocalPreference<T>(string name, T value)
+        {
+        }
+
+        public void UpdatePreference<T>(string name, T value)
+        {
+        }
+    }
+}

+ 82 - 11
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs

@@ -1,9 +1,6 @@
-using PixiEditor.Models.DataHolders;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.ViewModels.SubViewModels.Main;
 using Xunit;
 using Xunit;
 
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
@@ -12,13 +9,27 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
     public class DocumentLayersTests
     public class DocumentLayersTests
     {
     {
         [Fact]
         [Fact]
-        public void TestThatToggleLayerTogglesLayer()
+        public void TestThatToggleLayerDoesNotToggleLastLayer()
         {
         {
-            Document doc = new Document(5, 5);
+            Document doc = new (5, 5);
             doc.AddNewLayer("layer");
             doc.AddNewLayer("layer");
             bool isActive = doc.Layers[^1].IsActive;
             bool isActive = doc.Layers[^1].IsActive;
             doc.ToggleLayer(0);
             doc.ToggleLayer(0);
-            Assert.True(doc.Layers[^1].IsActive != isActive);
+            Assert.False(doc.Layers[^1].IsActive != isActive);
+        }
+
+        [Fact]
+        public void TestThatToggleLayerTogglesLayer()
+        {
+            Document doc = new (5, 5);
+            doc.AddNewLayer("layer");
+            doc.AddNewLayer("layer 1");
+            doc.Layers[0].IsActive = true;
+            doc.Layers[^1].IsActive = true;
+
+            doc.ToggleLayer(0);
+            Assert.False(doc.Layers[0].IsActive);
+            Assert.True(doc.Layers[1].IsActive);
         }
         }
 
 
         [Fact]
         [Fact]
@@ -43,7 +54,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             document.AddNewLayer("2");
             document.AddNewLayer("2");
             document.AddNewLayer("3");
             document.AddNewLayer("3");
 
 
-            document.SetActiveLayer(startIndex);
+            document.SetMainActiveLayer(startIndex);
 
 
             document.SelectLayersRange(endIndex);
             document.SelectLayersRange(endIndex);
 
 
@@ -68,7 +79,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             document.AddNewLayer("2");
             document.AddNewLayer("2");
             document.AddNewLayer("3");
             document.AddNewLayer("3");
 
 
-            document.SetActiveLayer(0);
+            document.SetMainActiveLayer(0);
             document.Layers[1].IsActive = true;
             document.Layers[1].IsActive = true;
             document.Layers[2].IsActive = true;
             document.Layers[2].IsActive = true;
 
 
@@ -79,5 +90,65 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
                 Assert.Equal(layer == document.Layers[index], layer.IsActive);
                 Assert.Equal(layer == document.Layers[index], layer.IsActive);
             }
             }
         }
         }
+
+        [Fact]
+        public void TestThatUpdateLayersColorMakesOnlyOneLayerMainColorAndOtherSecondary()
+        {
+            Document document = new Document(1, 1);
+
+            document.AddNewLayer("1");
+            document.AddNewLayer("2");
+            document.AddNewLayer("3");
+
+            document.SetMainActiveLayer(0);
+            document.Layers[1].IsActive = true; // This makes layer selected, but not main
+            document.Layers[2].IsActive = true;
+
+            document.UpdateLayersColor();
+
+            Assert.Equal(Document.MainSelectedLayerColor, document.Layers[0].LayerHighlightColor);
+            Assert.Equal(Document.SecondarySelectedLayerColor, document.Layers[1].LayerHighlightColor);
+            Assert.Equal(Document.SecondarySelectedLayerColor, document.Layers[2].LayerHighlightColor);
+        }
+
+        [Fact]
+        public void TestThatUpdateLayersColorMakesLayerMainColorAndRestNonActiveReturnsTransparent()
+        {
+            Document document = new Document(1, 1);
+
+            document.AddNewLayer("1");
+            document.AddNewLayer("2");
+            document.AddNewLayer("3");
+
+            document.SetMainActiveLayer(1);
+
+            document.UpdateLayersColor();
+
+            string transparentHex = "#00000000";
+
+            Assert.Equal(transparentHex, document.Layers[0].LayerHighlightColor);
+            Assert.Equal(Document.MainSelectedLayerColor, document.Layers[1].LayerHighlightColor);
+            Assert.Equal(transparentHex, document.Layers[2].LayerHighlightColor);
+        }
+
+        [Fact]
+        public void TestThatSetNextSelectedLayerAsActiveSelectsFirstAvailableLayer()
+        {
+            Document document = new Document(1, 1);
+
+            document.AddNewLayer("1");
+            document.AddNewLayer("2");
+            document.AddNewLayer("3");
+            document.AddNewLayer("4");
+
+            foreach (var layer in document.Layers)
+            {
+                layer.IsActive = true;
+            }
+
+            document.SetNextSelectedLayerAsActive(document.Layers[1].LayerGuid);
+
+            Assert.Equal(document.Layers[0].LayerGuid, document.ActiveLayerGuid);
+        }
     }
     }
 }
 }

+ 24 - 1
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -4,6 +4,7 @@ using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
+using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
@@ -135,6 +136,11 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             manager.ActiveDocument.AddNewLayer("test2");
             manager.ActiveDocument.AddNewLayer("test2");
             manager.ActiveLayer.SetPixel(new Coordinates(1, 1), Colors.Green);
             manager.ActiveLayer.SetPixel(new Coordinates(1, 1), Colors.Green);
 
 
+            foreach (var layer in manager.ActiveDocument.Layers)
+            {
+                layer.IsActive = true;
+            }
+
             doc.CenterContent();
             doc.CenterContent();
 
 
             int midWidth = (int)Math.Floor(docWidth / 2f);
             int midWidth = (int)Math.Floor(docWidth / 2f);
@@ -154,7 +160,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test"));
             doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test"));
             doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test 2"));
             doc.Layers.Add(new PixiEditor.Models.Layers.Layer("Test 2"));
 
 
-            doc.SetActiveLayer(1);
+            doc.SetMainActiveLayer(1);
 
 
             doc.SetNextLayerAsActive(1);
             doc.SetNextLayerAsActive(1);
 
 
@@ -283,5 +289,22 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Equal("Test", document.Layers[1].Name);
             Assert.Equal("Test", document.Layers[1].Name);
             Assert.Equal("Test2", document.Layers[0].Name);
             Assert.Equal("Test2", document.Layers[0].Name);
         }
         }
+
+        [StaFact]
+        public void TestThatDocumentGetsAddedToRecentlyOpenedList()
+        {
+            ViewModelMain viewModel = Helpers.MockedViewModelMain();
+
+            Document document = new Document(1, 1)
+            {
+                XamlAccesibleViewModel = viewModel
+            };
+
+            string testFilePath = @"C:\idk\somewhere\homework";
+
+            document.DocumentFilePath = testFilePath;
+
+            Assert.Contains(viewModel.FileSubViewModel.RecentlyOpened, x => x == testFilePath);
+        }
     }
     }
 }
 }

+ 20 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs

@@ -1,4 +1,6 @@
 using System;
 using System;
+using System.Collections.Generic;
+using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
@@ -55,5 +57,23 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Empty(selection.SelectedPoints);
             Assert.Empty(selection.SelectedPoints);
             Assert.Equal(0, selection.SelectionLayer.Width + selection.SelectionLayer.Height);
             Assert.Equal(0, selection.SelectionLayer.Width + selection.SelectionLayer.Height);
         }
         }
+
+        [Fact]
+        public void TestThatUndoWorks()
+        {
+            Document document = new Document(10, 10);
+
+            IEnumerable<Coordinates> oldSelection = new List<Coordinates>(document.ActiveSelection.SelectedPoints);
+
+            document.ActiveSelection.SetSelection(new[] { new Coordinates(0, 0), new Coordinates(5, 7) }, SelectionType.Add);
+
+            Assert.NotEqual(oldSelection, document.ActiveSelection.SelectedPoints);
+
+            SelectionHelpers.AddSelectionUndoStep(document, oldSelection, SelectionType.Add);
+
+            document.UndoManager.Undo();
+
+            Assert.Equal(oldSelection, document.ActiveSelection.SelectedPoints);
+        }
     }
     }
 }
 }

+ 4 - 2
PixiEditorTests/ModelsTests/ToolsTests/ZoomToolTests.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Models.Tools.Tools;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
@@ -10,7 +12,7 @@ namespace PixiEditorTests.ModelsTests.ToolsTests
         [StaFact]
         [StaFact]
         public void TestThatZoomSetsActiveDocumentZoomPercentage()
         public void TestThatZoomSetsActiveDocumentZoomPercentage()
         {
         {
-            ViewModelMain vm = new ViewModelMain();
+            ViewModelMain vm = new ViewModelMain(new ServiceCollection().AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock()).BuildServiceProvider());
             vm.BitmapManager.ActiveDocument = new PixiEditor.Models.DataHolders.Document(10, 10);
             vm.BitmapManager.ActiveDocument = new PixiEditor.Models.DataHolders.Document(10, 10);
             ZoomTool zoomTool = new ZoomTool();
             ZoomTool zoomTool = new ZoomTool();
             double zoom = 110;
             double zoom = 110;

+ 43 - 1
PixiEditorTests/ModelsTests/UserPreferencesTests/PreferencesSettingsTests.cs

@@ -10,9 +10,13 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
     {
     {
         public static string PathToPreferencesFile { get; } = Path.Join("PixiEditor", "test_preferences.json");
         public static string PathToPreferencesFile { get; } = Path.Join("PixiEditor", "test_preferences.json");
 
 
+        public static string PathToLocalPreferencesFile { get; } = Path.Join("PixiEditor", "local_test_preferences.json");
+
+        public static readonly PreferencesSettings PreferencesSettings = new PreferencesSettings();
+
         public PreferencesSettingsTests()
         public PreferencesSettingsTests()
         {
         {
-            PreferencesSettings.Init(PathToPreferencesFile);
+            PreferencesSettings.Init(PathToPreferencesFile, PathToLocalPreferencesFile);
         }
         }
 
 
         [Fact]
         [Fact]
@@ -25,6 +29,7 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
         public void TestThatInitCreatesUserPreferencesJson()
         public void TestThatInitCreatesUserPreferencesJson()
         {
         {
             Assert.True(File.Exists(PathToPreferencesFile));
             Assert.True(File.Exists(PathToPreferencesFile));
+            Assert.True(File.Exists(PathToLocalPreferencesFile));
         }
         }
 
 
         [Theory]
         [Theory]
@@ -63,5 +68,42 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
                 Assert.Equal(value, dict[name]);
                 Assert.Equal(value, dict[name]);
             }
             }
         }
         }
+
+        [Theory]
+        [InlineData(-2)]
+        [InlineData(false)]
+        [InlineData("string")]
+        [InlineData(null)]
+        public void TestThatGetPreferenceOnNonExistingKeyReturnsFallbackValueLocal<T>(T value)
+        {
+            T fallbackValue = value;
+            T preferenceValue = PreferencesSettings.GetLocalPreference<T>("NonExistingPreference", fallbackValue);
+            Assert.Equal(fallbackValue, preferenceValue);
+        }
+
+        [Theory]
+        [InlineData("IntPreference", 1)]
+        [InlineData("BoolPreference", true)]
+        public void TestThatUpdatePreferenceUpdatesDictionaryLocal<T>(string name, T value)
+        {
+            PreferencesSettings.UpdateLocalPreference(name, value);
+            Assert.Equal(value, PreferencesSettings.GetLocalPreference<T>(name));
+        }
+
+        [Theory]
+        [InlineData("LongPreference", 1L)]
+        public void TestThatSaveUpdatesFileLocal<T>(string name, T value)
+        {
+            PreferencesSettings.LocalPreferences[name] = value;
+            PreferencesSettings.Save();
+            using (var fs = new FileStream(PathToPreferencesFile, FileMode.Open, FileAccess.Read, FileShare.Read))
+            {
+                using StreamReader sr = new StreamReader(fs);
+                string json = sr.ReadToEnd();
+                var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
+                Assert.True(dict.ContainsKey(name));
+                Assert.Equal(value, dict[name]);
+            }
+        }
     }
     }
 }
 }

+ 2 - 0
PixiEditorTests/PixiEditorTests.csproj

@@ -23,6 +23,8 @@
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
     <PackageReference Include="Moq" Version="4.16.0" />
     <PackageReference Include="Moq" Version="4.16.0" />
     <PackageReference Include="OpenCover" Version="4.7.922" />
     <PackageReference Include="OpenCover" Version="4.7.922" />

+ 24 - 12
PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

@@ -1,12 +1,15 @@
-using System.IO;
+using System;
+using System.IO;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 using Xunit;
 using Xunit;
 
 
@@ -15,10 +18,19 @@ namespace PixiEditorTests.ViewModelsTests
     [Collection("Application collection")]
     [Collection("Application collection")]
     public class ViewModelMainTests
     public class ViewModelMainTests
     {
     {
+        public static IServiceProvider Services;
+
+        public ViewModelMainTests()
+        {
+            Services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock())
+                .BuildServiceProvider();
+        }
+
         [StaFact]
         [StaFact]
         public void TestThatConstructorSetsUpControllersCorrectly()
         public void TestThatConstructorSetsUpControllersCorrectly()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ShortcutController);
             Assert.NotNull(viewModel.ShortcutController);
@@ -30,7 +42,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSwapColorsCommandSwapsColors()
         public void TestThatSwapColorsCommandSwapsColors()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.ColorsSubViewModel.PrimaryColor = Colors.Black;
             viewModel.ColorsSubViewModel.PrimaryColor = Colors.Black;
             viewModel.ColorsSubViewModel.SecondaryColor = Colors.White;
             viewModel.ColorsSubViewModel.SecondaryColor = Colors.White;
@@ -44,7 +56,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
         public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.FileSubViewModel.NewDocument(5, 5);
             viewModel.FileSubViewModel.NewDocument(5, 5);
 
 
@@ -55,7 +67,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
         public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(10, 10);
             viewModel.BitmapManager.ActiveDocument = new Document(10, 10);
 
 
             Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
             Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
@@ -71,7 +83,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSelectToolCommandSelectsNewTool()
         public void TestThatSelectToolCommandSelectsNewTool()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             Assert.Equal(typeof(MoveViewportTool), viewModel.BitmapManager.SelectedTool.GetType());
             Assert.Equal(typeof(MoveViewportTool), viewModel.BitmapManager.SelectedTool.GetType());
 
 
@@ -83,7 +95,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatMouseUpCommandStopsRecordingMouseMovements()
         public void TestThatMouseUpCommandStopsRecordingMouseMovements()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
             viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
 
 
@@ -97,7 +109,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatNewLayerCommandCreatesNewLayer()
         public void TestThatNewLayerCommandCreatesNewLayer()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
 
@@ -111,7 +123,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatSaveDocumentCommandSavesFile()
         public void TestThatSaveDocumentCommandSavesFile()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             string fileName = "testFile.pixi";
             string fileName = "testFile.pixi";
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1)
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1)
@@ -129,7 +141,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatAddSwatchAddsNonDuplicateSwatch()
         public void TestThatAddSwatchAddsNonDuplicateSwatch()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
 
             viewModel.ColorsSubViewModel.AddSwatch(Colors.Green);
             viewModel.ColorsSubViewModel.AddSwatch(Colors.Green);
@@ -149,7 +161,7 @@ namespace PixiEditorTests.ViewModelsTests
         [InlineData(120, 150)]
         [InlineData(120, 150)]
         public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
         public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
         {
         {
-            ViewModelMain viewModel = new ViewModelMain
+            ViewModelMain viewModel = new ViewModelMain(Services)
             {
             {
                 BitmapManager = { ActiveDocument = new Document(docWidth, docHeight) }
                 BitmapManager = { ActiveDocument = new Document(docWidth, docHeight) }
             };
             };
@@ -165,7 +177,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         [StaFact]
         public void TestThatDocumentIsNotNullReturnsTrue()
         public void TestThatDocumentIsNotNullReturnsTrue()
         {
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
 

+ 3 - 3
README.md

@@ -24,9 +24,9 @@ Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we w
 
 
 
 
 
 
-### Light weighted
+### Lightweight
 
 
-Program weights only 3.3 MB! Already have .NET 5 installed? Download installer and enjoy saved space.
+Program weighs only 3.3 MB! Already have .NET 5 installed? Download installer and enjoy saved space.
 
 
 ### Active development
 ### Active development
 
 
@@ -38,7 +38,7 @@ PixiEditor started in 2018 and it's been actively developed since. We continuous
 
 
 Follow these instructions to get PixiEditor working on your machine.
 Follow these instructions to get PixiEditor working on your machine.
 
 
-1. Download .exe file from [here](https://github.com/flabbet/PixiEditor/releases)
+1. Download the .exe file from [here](https://github.com/flabbet/PixiEditor/releases)
 2. Open installer
 2. Open installer
 3. Follow installer instructions
 3. Follow installer instructions