2
0
Эх сурвалжийг харах

Merge pull request #163 from PixiEditor/sync-middleman

Sync master with group-layers
Krzysztof Krysiński 4 жил өмнө
parent
commit
f754ab864d
57 өөрчлөгдсөн 1375 нэмэгдсэн , 480 устгасан
  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.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Windows;
 using System.Windows.Input;
@@ -18,10 +19,12 @@ using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 namespace PixiEditor.Models.Controllers
 {
+    [DebuggerDisplay("{Documents.Count} Document(s)")]
     public class BitmapManager : NotifyableObject
     {
         private Document activeDocument;
         private Tool selectedTool;
+        private Coordinates? startPosition = null;
 
         public BitmapManager()
         {
@@ -70,20 +73,23 @@ namespace PixiEditor.Models.Controllers
 
         public BitmapOperationsUtility BitmapOperations { get; set; }
 
-        public ReadonlyToolUtility ReadonlyToolUtility { get; set; }
-
+        public ReadonlyToolUtility ReadonlyToolUtility { get; set; }
+
+#nullable enable
         public Document ActiveDocument
         {
             get => activeDocument;
             set
             {
                 activeDocument?.UpdatePreviewImage();
+                Document? oldDoc = activeDocument;
                 activeDocument = value;
                 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>();
 
         /// <summary>
@@ -111,6 +117,12 @@ namespace PixiEditor.Models.Controllers
         {
             if (SelectedTool.CanStartOutsideCanvas || clickedOnCanvas)
             {
+                if (startPosition == null)
+                {
+                    SelectedTool.OnStart(newPosition);
+                    startPosition = newPosition;
+                }
+
                 if (IsOperationTool(SelectedTool))
                 {
                     BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool)SelectedTool);
@@ -191,6 +203,8 @@ namespace PixiEditor.Models.Controllers
             {
                 BitmapOperations.ApplyPreviewLayer();
             }
+
+            startPosition = null;
         }
 
         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.Tools;
 using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers
 {
@@ -87,20 +88,29 @@ namespace PixiEditor.Models.Controllers
                 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(
-                        modifiedLayer.PixelChanges,
-                        oldValues,
-                        modifiedLayer.LayerGuid));
-                    Manager.ActiveDocument.GeneratePreviewLayer();
+                        previewChanges.PixelChanges,
+                        oldValue.PixelChanges,
+                        previewChanges.LayerGuid));
                 }
+
+                Manager.ActiveDocument.GeneratePreviewLayer();
             }
 
             previewLayerChanges = null;
@@ -136,14 +146,31 @@ namespace PixiEditor.Models.Controllers
 
         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()),
                 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)

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

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

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

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using PixiEditor.Models.Undo;
@@ -7,6 +8,7 @@ using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers
 {
+    [DebuggerDisplay("{UndoStack.Count} undo steps, {RedoStack.Count} redo step(s)")]
     public class UndoManager
     {
         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
 {
@@ -14,6 +16,7 @@ namespace PixiEditor.Models.DataHolders
                 documentFilePath = value;
                 RaisePropertyChanged(nameof(DocumentFilePath));
                 RaisePropertyChanged(nameof(Name));
+                UpdateRecentlyOpened(value);
             }
         }
 
@@ -47,5 +50,32 @@ namespace PixiEditor.Models.DataHolders
             DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
             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.Buffers;
+using System.Collections;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -20,6 +23,7 @@ using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.DataHolders
 {
+    [DebuggerDisplay("'{Name, nq}' {width}x{height} {Layers.Count} Layer(s)")]
     public partial class Document : NotifyableObject
     {
         private int height;
@@ -152,7 +156,7 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void ClipCanvas()
         {
-            DoubleCords points = GetEdgePoints();
+            DoubleCords points = GetEdgePoints(Layers);
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int biggestX = points.Coords2.X;
@@ -171,7 +175,7 @@ namespace PixiEditor.Models.DataHolders
             int oldWidth = Width;
             int oldHeight = Height;
 
-            MoveOffsets(moveVector);
+            MoveOffsets(Layers, moveVector);
             Width = width;
             Height = height;
 
@@ -187,11 +191,17 @@ namespace PixiEditor.Models.DataHolders
         }
 
         /// <summary>
-        /// Centers content inside document.
+        /// Centers selected, visible layers inside document.
         /// </summary>
         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 smallestY = points.Coords1.Y;
@@ -209,13 +219,13 @@ namespace PixiEditor.Models.DataHolders
                 new Coordinates(Width, Height));
             Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
 
-            MoveOffsets(moveVector);
+            MoveOffsets(layersToCenter, moveVector);
             UndoManager.AddUndoChange(
                 new Change(
                     MoveOffsetsProcess,
-                    new object[] { new Coordinates(-moveVector.X, -moveVector.Y) },
+                    new object[] { layersToCenter, new Coordinates(-moveVector.X, -moveVector.Y) },
                     MoveOffsetsProcess,
-                    new object[] { moveVector },
+                    new object[] { layersToCenter, moveVector },
                     "Center content"));
         }
 
@@ -264,35 +274,40 @@ namespace PixiEditor.Models.DataHolders
             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 smallestY = firstLayer.OffsetY;
             int biggestX = smallestX + firstLayer.Width;
             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.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Windows.Media;
 using PixiEditor.Helpers;
@@ -9,6 +10,7 @@ using PixiEditor.Models.Position;
 
 namespace PixiEditor.Models.DataHolders
 {
+    [DebuggerDisplay("{SelectedPoints.Count} selected Pixels")]
     public class Selection : NotifyableObject
     {
         private readonly Color selectionBlue;

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

@@ -7,9 +7,9 @@ namespace PixiEditor.Models.Dialogs
 {
     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
         {

+ 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 DocumentChangedEventArgs(Document newDocument)
+        public DocumentChangedEventArgs(Document newDocument, Document oldDocument)
         {
             NewDocument = newDocument;
+            OldDocument = oldDocument;
         }
 
+        public Document OldDocument { 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)
         {
-            Dictionary<Guid, Color[]> result = new Dictionary<Guid, Color[]>();
+            Dictionary<Guid, Color[]> result = new ();
 
             foreach (Layer layer in layers)
             {

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

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

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

@@ -2,6 +2,7 @@
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings;
 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)
         {
         }

+ 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 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
 {
-    [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
     {
         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
 {
@@ -6,7 +7,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
     {
         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 Coordinates[] currentSelection;
         private Coordinates lastMouseMove;
-        private Coordinates lastStartMousePos;
         private Dictionary<Guid, Color[]> startPixelColors;
+        private Dictionary<Guid, Color[]> endPixelColors;
         private Dictionary<Guid, Thickness> startingOffsets;
         private Coordinates[] startSelection;
         private bool updateViewModelSelection = true;
@@ -58,23 +58,31 @@ namespace PixiEditor.Models.Tools.Tools
         }
 
         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];
-            var end = mouseMove[0];
+            var end = mouseMove[0];
+            var lastSelection = currentSelection.ToArray();
             for (int i = 0; i < affectedLayers.Length; i++)
             {
                 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);
 
                     result[i] = new LayerChange(changes, affectedLayers[i]);
@@ -158,13 +167,11 @@ namespace PixiEditor.Models.Tools.Tools
         {
             Coordinates end = mouseMove[0];
 
-            currentSelection = TranslateSelection(end, out Coordinates[] previousSelection);
+            currentSelection = TranslateSelection(end);
             if (updateViewModelSelection)
             {
                 ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(currentSelection, SelectionType.New);
-            }
-
-            ClearSelectedPixels(layer, previousSelection);
+            }
 
             lastMouseMove = end;
             return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
@@ -204,19 +211,20 @@ namespace PixiEditor.Models.Tools.Tools
 
         private void ResetSelectionValues(Coordinates start)
         {
-            lastStartMousePos = start;
             lastMouseMove = start;
             clearedPixels = new Dictionary<Guid, bool>();
+            endPixelColors = new Dictionary<Guid, Color[]>();
+            currentSelection = null;
+            affectedLayers = null;
             updateViewModelSelection = true;
             startPixelColors = null;
             startSelection = null;
         }
 
-        private Coordinates[] TranslateSelection(Coordinates end, out Coordinates[] previousSelection)
+        private Coordinates[] TranslateSelection(Coordinates 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)

+ 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,
                     Width = storedLayer.Width,
                     Height = storedLayer.Height,
+                    LayerHighlightColor = storedLayer.LayerHighlightColor                    
                 };
                 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 string LayerHighlightColor { get; set; }
+
         public string Name { get; set; }
 
         public int LayerIndex { get; set; }
@@ -47,6 +49,7 @@ namespace PixiEditor.Models.Undo
             Opacity = layer.Opacity;
             IsActive = layer.IsActive;
             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.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using Newtonsoft.Json;
+using PixiEditor.ViewModels;
 
 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)
             {
-                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;
             }
         }
 
-        public static void UpdatePreference<T>(string name, T value)
+        public void UpdatePreference<T>(string name, T value)
         {
             if (IsLoaded == false)
             {
@@ -66,21 +61,40 @@ namespace PixiEditor.Models.UserPreferences
             Save();
         }
 
-        public static void Save()
+        public void UpdateLocalPreference<T>(string name, T value)
         {
             if (IsLoaded == false)
             {
                 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))
             {
@@ -91,12 +105,14 @@ namespace PixiEditor.Models.UserPreferences
             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));
         }
 
-        public static T? GetPreference<T>(string name, T? fallbackValue)
+        public T? GetPreference<T>(string name, T? fallbackValue)
         {
             if (IsLoaded == false)
             {
@@ -107,5 +123,54 @@ namespace PixiEditor.Models.UserPreferences
                 ? (T)Preferences[name]
                 : 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.Collections.Generic;
 using System.ComponentModel;
+using System.Runtime.CompilerServices;
 
 namespace PixiEditor.Helpers
 {
@@ -16,5 +18,17 @@ namespace PixiEditor.Helpers
                 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>
     </PackageReference>
     <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="Newtonsoft.Json" Version="12.0.3" />
     <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
 {
-    public class DiscordViewModel : SubViewModel<ViewModelMain>
+    public class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
     {
         private DiscordRpcClient client;
         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
         {
@@ -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
         {
@@ -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
         {
@@ -81,12 +81,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.DocumentChanged += DocumentChanged;
             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;
         }
 
@@ -142,6 +141,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             client.SetPresence(richPresence);
         }
 
+        public void Dispose()
+        {
+            Enabled = false;
+            GC.SuppressFinalize(this);
+        }
+
         private static RichPresence NewDefaultRP()
         {
             return new RichPresence

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

@@ -1,9 +1,12 @@
 using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.IO;
 using System.Linq;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using Microsoft.Win32;
+using Newtonsoft.Json.Linq;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
@@ -18,6 +21,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
     public class FileViewModel : SubViewModel<ViewModelMain>
     {
+        private bool hasRecent;
+
         public RelayCommand OpenNewFilePopupCommand { 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 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)
             : base(owner)
         {
@@ -33,7 +52,37 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             SaveDocumentCommand = new RelayCommand(SaveDocument, Owner.DocumentIsNotNull);
             OpenFileCommand = new RelayCommand(Open);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
+            OpenRecentCommand = new RelayCommand(OpenRecent);
             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>
@@ -98,7 +147,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else
             {
-                if (PreferencesSettings.GetPreference("ShowNewFilePopupOnStartup", true))
+                if (IPreferences.Current.GetPreference("ShowNewFilePopupOnStartup", true))
                 {
                     OpenNewFilePopup(null);
                 }

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

@@ -2,6 +2,7 @@
 using System.Linq;
 using System.Windows.Input;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
@@ -38,6 +39,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             MergeSelectedCommand = new RelayCommand(MergeSelected, CanMergeSelected);
             MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
             MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
+            Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
         }
 
         public bool CanMergeSelected(object obj)
@@ -58,6 +60,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void SetActiveLayer(object parameter)
         {
             int index = (int)parameter;
+
+            if (Owner.BitmapManager.ActiveDocument.Layers[index].IsActive && Mouse.RightButton == MouseButtonState.Pressed)
+            {
+                return;
+            }
+
             if (Keyboard.IsKeyDown(Key.LeftCtrl))
             {
                 Owner.BitmapManager.ActiveDocument.ToggleLayer(index);
@@ -68,7 +76,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else
             {
-                Owner.BitmapManager.ActiveDocument.SetActiveLayer(index);
+                Owner.BitmapManager.ActiveDocument.SetMainActiveLayer(index);
             }
         }
 
@@ -96,10 +104,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
             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)
@@ -162,5 +170,26 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             int index = (int)property;
             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.Collections.Generic;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
@@ -23,12 +24,20 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void SelectAll(object parameter)
         {
             SelectTool select = new SelectTool();
+
+            var oldSelection = new List<Coordinates>(Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints);
+
             Owner.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(select.GetAllSelection(), SelectionType.New);
+            SelectionHelpers.AddSelectionUndoStep(Owner.BitmapManager.ActiveDocument, oldSelection, SelectionType.New);
         }
 
         public void Deselect(object parameter)
         {
+            var oldSelection = new List<Coordinates>(Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints);
+
             Owner.BitmapManager.ActiveDocument.ActiveSelection?.Clear();
+
+            SelectionHelpers.AddSelectionUndoStep(Owner.BitmapManager.ActiveDocument, oldSelection, SelectionType.New);
         }
 
         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>
         public void Undo(object parameter)
         {
-            Owner.SelectionSubViewModel.Deselect(null);
             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)
         {
-            if (PreferencesSettings.GetPreference("CheckUpdatesOnStartup", true))
+            if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
             {
                 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)
         {
-            return PreferencesSettings.GetPreference<T>(name);
+            return IPreferences.Current.GetPreference<T>(name);
         }
 
 #nullable enable
 
         protected static T? GetPreference<T>(string name, T? fallbackValue)
         {
-            return PreferencesSettings.GetPreference(name, fallbackValue);
+            return IPreferences.Current.GetPreference(name, fallbackValue);
         }
 
 #nullable disable
@@ -23,7 +23,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
         protected void RaiseAndUpdatePreference<T>(string name, T value)
         {
             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.Configuration;
 using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings;
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
 {
     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 void RaiseAndUpdatePreference<T>(string name, T value)
-        {
-            RaisePropertyChanged(name);
-            PreferencesSettings.UpdatePreference(name, value);
-        }
-
         public SettingsViewModel(SettingsWindowViewModel owner)
             : base(owner)
         {

+ 29 - 7
PixiEditor/ViewModels/ViewModelMain.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Windows;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers.Shortcuts;
@@ -11,7 +12,6 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
-using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
@@ -58,15 +58,35 @@ namespace PixiEditor.ViewModels
 
         public DiscordViewModel DiscordViewModel { get; set; }
 
+#if DEBUG
+        public DebugViewModel DebugSubViewModel { get; set; }
+#endif
+
         public BitmapManager BitmapManager { get; set; }
 
         public PixelChangesController ChangesController { 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.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
@@ -91,6 +111,9 @@ namespace PixiEditor.ViewModels
             DocumentSubViewModel = new DocumentViewModel(this);
             MiscSubViewModel = new MiscViewModel(this);
             DiscordViewModel = new DiscordViewModel(this, "764168193685979138");
+#if DEBUG
+            DebugSubViewModel = new DebugViewModel(this);
+#endif
 
             ShortcutController = new ShortcutController
             {
@@ -137,14 +160,13 @@ namespace PixiEditor.ViewModels
                     new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
 
                     // Layers
-                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerIndex),
+                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerGuid),
 
                     // View
-                    new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, modifier: ModifierKeys.Control)
+                    new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, modifier: ModifierKeys.Control),
                 }
             };
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
-            Current = this;
         }
 
         /// <summary>
@@ -171,7 +193,7 @@ namespace PixiEditor.ViewModels
             return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, typeof(T), modifier);
         }
 
-        private void CloseWindow(object property)
+        public void CloseWindow(object property)
         {
             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}"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
                         <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"/>
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
                             <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}"/>
-                            <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>
@@ -74,7 +78,7 @@
                 <StackPanel Orientation="Vertical">
                     <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
                     <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>
             </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
         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.Windows;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Processes;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.UpdateModule;
 using PixiEditor.ViewModels;
 
@@ -23,12 +25,24 @@ namespace PixiEditor
         public MainWindow()
         {
             InitializeComponent();
+
+            IServiceCollection services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new PreferencesSettings());
+
+            DataContext = new ViewModelMain(services.BuildServiceProvider());
+
             StateChanged += MainWindowStateChangeRaised;
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             viewModel = (ViewModelMain)DataContext;
             viewModel.CloseAction = Close;
         }
 
+        protected override void OnClosing(CancelEventArgs e)
+        {
+            ((ViewModelMain)DataContext).CloseWindow(e);
+            viewModel.DiscordViewModel.Dispose();
+        }
+
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;

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

@@ -9,16 +9,13 @@
              mc:Ignorable="d" Focusable="True"
              d:DesignHeight="60" d:DesignWidth="250" Name="uc"
              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"
-            Background="{Binding IsActive, Mode=TwoWay, Converter={StaticResource BoolToColorConverter}}">
+            Background="{Binding LayerColor, ElementName=uc}" >
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
         <i:Interaction.Triggers>
-            <i:EventTrigger EventName="MouseLeftButtonDown">
+            <i:EventTrigger EventName="MouseDown">
                 <i:InvokeCommandAction Command="{Binding ElementName=uc, 
                             Path=SetActiveLayerCommand}"
                                        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 =
             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
         {
             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;
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
@@ -12,13 +9,27 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
     public class DocumentLayersTests
     {
         [Fact]
-        public void TestThatToggleLayerTogglesLayer()
+        public void TestThatToggleLayerDoesNotToggleLastLayer()
         {
-            Document doc = new Document(5, 5);
+            Document doc = new (5, 5);
             doc.AddNewLayer("layer");
             bool isActive = doc.Layers[^1].IsActive;
             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]
@@ -43,7 +54,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             document.AddNewLayer("2");
             document.AddNewLayer("3");
 
-            document.SetActiveLayer(startIndex);
+            document.SetMainActiveLayer(startIndex);
 
             document.SelectLayersRange(endIndex);
 
@@ -68,7 +79,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             document.AddNewLayer("2");
             document.AddNewLayer("3");
 
-            document.SetActiveLayer(0);
+            document.SetMainActiveLayer(0);
             document.Layers[1].IsActive = true;
             document.Layers[2].IsActive = true;
 
@@ -79,5 +90,65 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
                 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.Enums;
 using PixiEditor.Models.Position;
+using PixiEditor.ViewModels;
 using Xunit;
 
 namespace PixiEditorTests.ModelsTests.DataHoldersTests
@@ -135,6 +136,11 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             manager.ActiveDocument.AddNewLayer("test2");
             manager.ActiveLayer.SetPixel(new Coordinates(1, 1), Colors.Green);
 
+            foreach (var layer in manager.ActiveDocument.Layers)
+            {
+                layer.IsActive = true;
+            }
+
             doc.CenterContent();
 
             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 2"));
 
-            doc.SetActiveLayer(1);
+            doc.SetMainActiveLayer(1);
 
             doc.SetNextLayerAsActive(1);
 
@@ -283,5 +289,22 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Equal("Test", document.Layers[1].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.Collections.Generic;
+using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
@@ -55,5 +57,23 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             Assert.Empty(selection.SelectedPoints);
             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 Xunit;
 
@@ -10,7 +12,7 @@ namespace PixiEditorTests.ModelsTests.ToolsTests
         [StaFact]
         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);
             ZoomTool zoomTool = new ZoomTool();
             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 PathToLocalPreferencesFile { get; } = Path.Join("PixiEditor", "local_test_preferences.json");
+
+        public static readonly PreferencesSettings PreferencesSettings = new PreferencesSettings();
+
         public PreferencesSettingsTests()
         {
-            PreferencesSettings.Init(PathToPreferencesFile);
+            PreferencesSettings.Init(PathToPreferencesFile, PathToLocalPreferencesFile);
         }
 
         [Fact]
@@ -25,6 +29,7 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
         public void TestThatInitCreatesUserPreferencesJson()
         {
             Assert.True(File.Exists(PathToPreferencesFile));
+            Assert.True(File.Exists(PathToLocalPreferencesFile));
         }
 
         [Theory]
@@ -63,5 +68,42 @@ namespace PixiEditorTests.ModelsTests.UserPreferencesTests
                 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>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </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="Moq" Version="4.16.0" />
     <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.Media;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using Xunit;
 
@@ -15,10 +18,19 @@ namespace PixiEditorTests.ViewModelsTests
     [Collection("Application collection")]
     public class ViewModelMainTests
     {
+        public static IServiceProvider Services;
+
+        public ViewModelMainTests()
+        {
+            Services = new ServiceCollection()
+                .AddSingleton<IPreferences>(new Mocks.PreferenceSettingsMock())
+                .BuildServiceProvider();
+        }
+
         [StaFact]
         public void TestThatConstructorSetsUpControllersCorrectly()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ShortcutController);
@@ -30,7 +42,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatSwapColorsCommandSwapsColors()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             viewModel.ColorsSubViewModel.PrimaryColor = Colors.Black;
             viewModel.ColorsSubViewModel.SecondaryColor = Colors.White;
@@ -44,7 +56,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             viewModel.FileSubViewModel.NewDocument(5, 5);
 
@@ -55,7 +67,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(10, 10);
 
             Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
@@ -71,7 +83,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatSelectToolCommandSelectsNewTool()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             Assert.Equal(typeof(MoveViewportTool), viewModel.BitmapManager.SelectedTool.GetType());
 
@@ -83,7 +95,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatMouseUpCommandStopsRecordingMouseMovements()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
 
@@ -97,7 +109,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatNewLayerCommandCreatesNewLayer()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
@@ -111,7 +123,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatSaveDocumentCommandSavesFile()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             string fileName = "testFile.pixi";
 
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1)
@@ -129,7 +141,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatAddSwatchAddsNonDuplicateSwatch()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
             viewModel.BitmapManager.ActiveDocument = new Document(1, 1);
 
             viewModel.ColorsSubViewModel.AddSwatch(Colors.Green);
@@ -149,7 +161,7 @@ namespace PixiEditorTests.ViewModelsTests
         [InlineData(120, 150)]
         public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
         {
-            ViewModelMain viewModel = new ViewModelMain
+            ViewModelMain viewModel = new ViewModelMain(Services)
             {
                 BitmapManager = { ActiveDocument = new Document(docWidth, docHeight) }
             };
@@ -165,7 +177,7 @@ namespace PixiEditorTests.ViewModelsTests
         [StaFact]
         public void TestThatDocumentIsNotNullReturnsTrue()
         {
-            ViewModelMain viewModel = new ViewModelMain();
+            ViewModelMain viewModel = new ViewModelMain(Services);
 
             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
 
@@ -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.
 
-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
 3. Follow installer instructions