Browse Source

Palettes wip

flabbet 3 years ago
parent
commit
0e715a2e60
28 changed files with 536 additions and 179 deletions
  1. 1 0
      PixiEditor/App.xaml
  2. 2 2
      PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs
  3. 2 2
      PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs
  4. 5 5
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  5. 34 7
      PixiEditor/Helpers/RelayCommand.cs
  6. 85 85
      PixiEditor/Models/Controllers/BitmapManager.cs
  7. 5 5
      PixiEditor/Models/Controllers/LayerStackRenderer.cs
  8. 4 4
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  9. 1 0
      PixiEditor/Models/DataHolders/Document/Document.cs
  10. 7 7
      PixiEditor/Models/DataHolders/ObservableCollection.cs
  11. 7 7
      PixiEditor/Models/DataHolders/RangeObservableCollection.cs
  12. 1 1
      PixiEditor/Models/DataHolders/RecentlyOpenedCollection.cs
  13. 5 5
      PixiEditor/Models/DataHolders/Selection.cs
  14. 14 0
      PixiEditor/Models/IO/JascPalFile/JascFileData.cs
  15. 10 0
      PixiEditor/Models/IO/JascPalFile/JascFileException.cs
  16. 48 0
      PixiEditor/Models/IO/JascPalFile/JascFileParser.cs
  17. 6 6
      PixiEditor/Models/Layers/LayerStructure.cs
  18. 8 8
      PixiEditor/Models/Layers/StructuredLayerTree.cs
  19. 8 4
      PixiEditor/Views/MainWindow.xaml
  20. 9 9
      PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs
  21. 59 0
      PixiEditor/Views/UserControls/Palette.xaml
  22. 68 0
      PixiEditor/Views/UserControls/Palette.xaml.cs
  23. 28 0
      PixiEditor/Views/UserControls/PaletteColor.xaml
  24. 22 0
      PixiEditor/Views/UserControls/PaletteColor.xaml.cs
  25. 18 0
      PixiEditor/Views/UserControls/PaletteColorAdder.xaml
  26. 70 0
      PixiEditor/Views/UserControls/PaletteColorAdder.xaml.cs
  27. 8 21
      PixiEditor/Views/UserControls/SwatchesView.xaml
  28. 1 1
      PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs

+ 1 - 0
PixiEditor/App.xaml

@@ -25,6 +25,7 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
+                <ResourceDictionary Source="pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

+ 2 - 2
PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs

@@ -23,9 +23,9 @@ namespace PixiEditor.Helpers.Converters
             return GetSubGroups(structure.Groups);
         }
 
-        private ObservableCollection<GuidStructureItem> GetSubGroups(IEnumerable<GuidStructureItem> groups)
+        private System.Collections.ObjectModel.ObservableCollection<GuidStructureItem> GetSubGroups(IEnumerable<GuidStructureItem> groups)
         {
-            WpfObservableRangeCollection<GuidStructureItem> finalGroups = new WpfObservableRangeCollection<GuidStructureItem>();
+            Models.DataHolders.ObservableCollection<GuidStructureItem> finalGroups = new Models.DataHolders.ObservableCollection<GuidStructureItem>();
             foreach (var group in groups)
             {
                 finalGroups.AddRange(GetSubGroups(group));

+ 2 - 2
PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs

@@ -15,11 +15,11 @@ namespace PixiEditor.Helpers.Converters
         private static StructuredLayerTree cachedTree;
         private List<Guid> lastLayerGuids = new List<Guid>();
         private IList<Layer> lastLayers = new List<Layer>();
-        private WpfObservableRangeCollection<GuidStructureItem> lastStructure = new WpfObservableRangeCollection<GuidStructureItem>();
+        private ObservableCollection<GuidStructureItem> lastStructure = new ObservableCollection<GuidStructureItem>();
 
         public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
         {
-            if (values[0] is WpfObservableRangeCollection<Layer> layers && values[1] is LayerStructure structure)
+            if (values[0] is ObservableCollection<Layer> layers && values[1] is LayerStructure structure)
             {
                 if (cachedTree == null)
                 {

+ 5 - 5
PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -16,7 +16,7 @@ namespace PixiEditor.Helpers.Extensions
             Document document = new Document(serializableDocument.Width, serializableDocument.Height)
             {
                 Layers = serializableDocument.ToLayers(),
-                Swatches = new ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
+                Swatches = new Models.DataHolders.ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
             };
 
             document.LayerStructure.Groups = serializableDocument.ToGroups(document);
@@ -30,9 +30,9 @@ namespace PixiEditor.Helpers.Extensions
             return document;
         }
 
-        public static WpfObservableRangeCollection<Layer> ToLayers(this SerializableDocument document)
+        public static Models.DataHolders.ObservableCollection<Layer> ToLayers(this SerializableDocument document)
         {
-            WpfObservableRangeCollection<Layer> layers = new();
+            Models.DataHolders.ObservableCollection<Layer> layers = new();
             foreach (SerializableLayer slayer in document)
             {
                 layers.Add(slayer.ToLayer());
@@ -51,9 +51,9 @@ namespace PixiEditor.Helpers.Extensions
             };
         }
 
-        public static WpfObservableRangeCollection<GuidStructureItem> ToGroups(this SerializableDocument sdocument, Document document)
+        public static Models.DataHolders.ObservableCollection<GuidStructureItem> ToGroups(this SerializableDocument sdocument, Document document)
         {
-            WpfObservableRangeCollection<GuidStructureItem> groups = new();
+            Models.DataHolders.ObservableCollection<GuidStructureItem> groups = new();
 
             if (sdocument.Groups == null)
             {

+ 34 - 7
PixiEditor/Helpers/RelayCommand.cs

@@ -3,17 +3,17 @@ using System.Windows.Input;
 
 namespace PixiEditor.Helpers
 {
-    public class RelayCommand : ICommand
+    public class RelayCommand<T> : ICommand
     {
-        private readonly Action<object> execute;
-        private readonly Predicate<object> canExecute;
+        private readonly Action<T> execute;
+        private readonly Predicate<T> canExecute;
 
-        public RelayCommand(Action<object> execute)
+        public RelayCommand(Action<T> execute)
             : this(execute, null)
         {
         }
 
-        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
+        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
         {
             if (execute == null)
             {
@@ -30,14 +30,41 @@ namespace PixiEditor.Helpers
             remove => CommandManager.RequerySuggested -= value;
         }
 
-        public bool CanExecute(object parameter)
+        public bool CanExecute(T parameter)
         {
             return canExecute == null ? true : canExecute(parameter);
         }
 
-        public void Execute(object parameter)
+        public bool CanExecute(object parameter)
+        {
+            if(parameter != null && parameter is not T)
+            {
+                throw new ArgumentException("Provided parameter type does not match RelayCommand parameter type");
+            }
+
+            return CanExecute((T)parameter);
+        }
+
+        public void Execute(T parameter)
         {
             execute(parameter);
         }
+
+        public void Execute(object parameter)
+        {
+            if (parameter != null && parameter is not T)
+            {
+                throw new ArgumentException("Provided parameter type does not match RelayCommand parameter type");
+            }
+
+            Execute((T)parameter);
+        }
+    }
+
+    public class RelayCommand : RelayCommand<object>
+    {
+        public RelayCommand(Action<object> execute, Predicate<object> canExecute) : base(execute, canExecute) { }
+
+        public RelayCommand(Action<object> execute) : base(execute) { }
     }
 }

+ 85 - 85
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -1,46 +1,46 @@
-using PixiEditor.Helpers;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Events;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
-using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Main;
 using SkiaSharp;
-using System;
+using System;
 using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Windows;
 
-namespace PixiEditor.Models.Controllers
-{
-    [DebuggerDisplay("{Documents.Count} Document(s)")]
-    public class BitmapManager : NotifyableObject
+namespace PixiEditor.Models.Controllers
+{
+    [DebuggerDisplay("{Documents.Count} Document(s)")]
+    public class BitmapManager : NotifyableObject
     {
         private ToolSessionController ToolSessionController { get; set; }
         public ICanvasInputTarget InputTarget => ToolSessionController;
         public BitmapOperationsUtility BitmapOperations { get; set; }
 
-        public ObservableCollection<Document> Documents { get; set; } = new ObservableCollection<Document>();
-
-        private Document activeDocument;
-        public Document ActiveDocument
-        {
-            get => activeDocument;
-            set
-            {
-                if (activeDocument == value)
-                    return;
-                activeDocument?.UpdatePreviewImage();
-                Document oldDoc = activeDocument;
-                activeDocument = value;
-                RaisePropertyChanged(nameof(ActiveDocument));
-                ActiveWindow = value;
-                DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
-            }
-        }
-
-        private object activeWindow;
+        public System.Collections.ObjectModel.ObservableCollection<Document> Documents { get; set; } = new System.Collections.ObjectModel.ObservableCollection<Document>();
+
+        private Document activeDocument;
+        public Document ActiveDocument
+        {
+            get => activeDocument;
+            set
+            {
+                if (activeDocument == value)
+                    return;
+                activeDocument?.UpdatePreviewImage();
+                Document oldDoc = activeDocument;
+                activeDocument = value;
+                RaisePropertyChanged(nameof(ActiveDocument));
+                ActiveWindow = value;
+                DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
+            }
+        }
+
+        private object activeWindow;
         public object ActiveWindow
         {
             get => activeWindow;
@@ -61,8 +61,8 @@ namespace PixiEditor.Models.Controllers
         public Layer ActiveLayer => ActiveDocument.ActiveLayer;
 
         public SKColor PrimaryColor { get; set; }
-
-        private bool hideReferenceLayer;
+
+        private bool hideReferenceLayer;
         public bool HideReferenceLayer
         {
             get => hideReferenceLayer;
@@ -74,8 +74,8 @@ namespace PixiEditor.Models.Controllers
         {
             get => onlyReferenceLayer;
             set => SetProperty(ref onlyReferenceLayer, value);
-        }
-
+        }
+
         private readonly ToolsViewModel _tools;
 
         private int previewLayerSize;
@@ -86,16 +86,16 @@ namespace PixiEditor.Models.Controllers
         private ToolSession activeSession = null;
 
 
-        public BitmapManager(ToolsViewModel tools, UndoViewModel undo)
-        {
-            _tools = tools;
-
-            ToolSessionController = new ToolSessionController();
-            ToolSessionController.SessionStarted += OnSessionStart;
-            ToolSessionController.SessionEnded += OnSessionEnd;
-            ToolSessionController.PixelMousePositionChanged += OnPixelMousePositionChange;
-            ToolSessionController.PreciseMousePositionChanged += OnPreciseMousePositionChange;
-            ToolSessionController.KeyStateChanged += (_, _) => UpdateActionDisplay(_tools.ActiveTool);
+        public BitmapManager(ToolsViewModel tools, UndoViewModel undo)
+        {
+            _tools = tools;
+
+            ToolSessionController = new ToolSessionController();
+            ToolSessionController.SessionStarted += OnSessionStart;
+            ToolSessionController.SessionEnded += OnSessionEnd;
+            ToolSessionController.PixelMousePositionChanged += OnPixelMousePositionChange;
+            ToolSessionController.PreciseMousePositionChanged += OnPreciseMousePositionChange;
+            ToolSessionController.KeyStateChanged += (_, _) => UpdateActionDisplay(_tools.ActiveTool);
             BitmapOperations = new BitmapOperationsUtility(this, tools);
 
             undo.UndoRedoCalled += (_, _) => ToolSessionController.ForceStopActiveSessionIfAny();
@@ -103,10 +103,10 @@ namespace PixiEditor.Models.Controllers
             DocumentChanged += BitmapManager_DocumentChanged;
 
             _highlightPen = new PenTool(this)
-            {
-                AutomaticallyResizeCanvas = false
-            };
-            _highlightColor = new SKColor(0, 0, 0, 77);
+            {
+                AutomaticallyResizeCanvas = false
+            };
+            _highlightColor = new SKColor(0, 0, 0, 77);
         }
 
         public void CloseDocument(Document document)
@@ -134,8 +134,8 @@ namespace PixiEditor.Models.Controllers
 
             ActiveDocument.PreviewLayer.Reset();
             ExecuteTool();
-        }
-
+        }
+
         private void OnSessionEnd(object sender, ToolSession e)
         {
             activeSession = null;
@@ -148,8 +148,8 @@ namespace PixiEditor.Models.Controllers
             ActiveDocument.PreviewLayer.Reset();
             HighlightPixels(ToolSessionController.LastPixelPosition);
             StopUsingTool?.Invoke(this, EventArgs.Empty);
-        }
-
+        }
+
         private void OnPreciseMousePositionChange(object sender, (double, double) e)
         {
             if (activeSession == null || !activeSession.Tool.RequiresPreciseMouseData)
@@ -165,16 +165,16 @@ namespace PixiEditor.Models.Controllers
                     return;
                 ExecuteTool();
                 return;
-            }
-            else
-            {
-                HighlightPixels(e.NewPosition);
             }
-        }
-
-        private void ExecuteTool()
-        {
-            if (activeSession == null)
+            else
+            {
+                HighlightPixels(e.NewPosition);
+            }
+        }
+
+        private void ExecuteTool()
+        {
+            if (activeSession == null)
                 throw new Exception("Can't execute tool's Use outside a session");
 
             if (activeSession.Tool is BitmapOperationTool operationTool)
@@ -189,25 +189,25 @@ namespace PixiEditor.Models.Controllers
             {
                 throw new InvalidOperationException($"'{activeSession.Tool.GetType().Name}' is either not a Tool or can't inherit '{nameof(Tool)}' directly.\nChanges the base type to either '{nameof(BitmapOperationTool)}' or '{nameof(ReadonlyTool)}'");
             }
-        }
-
+        }
+
         private void BitmapManager_DocumentChanged(object sender, DocumentChangedEventArgs e)
         {
             e.NewDocument?.GeneratePreviewLayer();
             if (e.OldDocument != e.NewDocument)
                 ToolSessionController.ForceStopActiveSessionIfAny();
-        }
-
+        }
+
         public void UpdateHighlightIfNecessary(bool forceHide = false)
         {
             if (activeSession != null)
                 return;
 
             HighlightPixels(forceHide ? new(-1, -1) : ToolSessionController.LastPixelPosition);
-        }
-
-        private void HighlightPixels(Coordinates newPosition)
-        {
+        }
+
+        private void HighlightPixels(Coordinates newPosition)
+        {
             if (ActiveDocument == null || ActiveDocument.Layers.Count == 0)
             {
                 return;
@@ -215,14 +215,14 @@ namespace PixiEditor.Models.Controllers
 
             var previewLayer = ActiveDocument.PreviewLayer;
 
-            if (newPosition.X > ActiveDocument.Width
-                || newPosition.Y > ActiveDocument.Height
-                || newPosition.X < 0 || newPosition.Y < 0
-                || _tools.ActiveTool.HideHighlight)
-            {
-                previewLayer.Reset();
-                previewLayerSize = -1;
-                return;
+            if (newPosition.X > ActiveDocument.Width
+                || newPosition.Y > ActiveDocument.Height
+                || newPosition.X < 0 || newPosition.Y < 0
+                || _tools.ActiveTool.HideHighlight)
+            {
+                previewLayer.Reset();
+                previewLayerSize = -1;
+                return;
             }
 
             if (_tools.ToolSize != previewLayerSize || previewLayer.IsReset)
@@ -236,15 +236,15 @@ namespace PixiEditor.Models.Controllers
                 previewLayer.Offset = new Thickness(0, 0, 0, 0);
                 _highlightPen.Draw(previewLayer, cords, cords, _highlightColor, _tools.ToolSize);
             }
-            AdjustOffset(newPosition, previewLayer);
+            AdjustOffset(newPosition, previewLayer);
+
+            previewLayer.InvokeLayerBitmapChange();
+        }
 
-            previewLayer.InvokeLayerBitmapChange();
-        }
-
         private void AdjustOffset(Coordinates newPosition, Layer previewLayer)
         {
-            Coordinates start = newPosition - halfSize;
+            Coordinates start = newPosition - halfSize;
             previewLayer.Offset = new Thickness(start.X, start.Y, 0, 0);
         }
-    }
-}
+    }
+}

+ 5 - 5
PixiEditor/Models/Controllers/LayerStackRenderer.cs

@@ -17,7 +17,7 @@ namespace PixiEditor.Models.Controllers
         private SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
         private SKPaint ClearPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
 
-        private ObservableCollection<Layer> layers;
+        private System.Collections.ObjectModel.ObservableCollection<Layer> layers;
         private LayerStructure structure;
 
         private Surface finalSurface;
@@ -36,7 +36,7 @@ namespace PixiEditor.Models.Controllers
         public Surface FinalSurface { get => finalSurface; }
 
         public event PropertyChangedEventHandler PropertyChanged;
-        public LayerStackRenderer(ObservableCollection<Layer> layers, LayerStructure structure, int width, int height)
+        public LayerStackRenderer(System.Collections.ObjectModel.ObservableCollection<Layer> layers, LayerStructure structure, int width, int height)
         {
             this.layers = layers;
             this.structure = structure;
@@ -56,7 +56,7 @@ namespace PixiEditor.Models.Controllers
             Update(new Int32Rect(0, 0, newWidth, newHeight));
         }
 
-        public void SetNewLayersCollection(ObservableCollection<Layer> layers)
+        public void SetNewLayersCollection(System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             layers.CollectionChanged -= OnLayersChanged;
             UnsubscribeFromAllLayers(this.layers);
@@ -80,7 +80,7 @@ namespace PixiEditor.Models.Controllers
             layers.CollectionChanged -= OnLayersChanged;
         }
 
-        private void SubscribeToAllLayers(ObservableCollection<Layer> layers)
+        private void SubscribeToAllLayers(System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             foreach (var layer in layers)
             {
@@ -88,7 +88,7 @@ namespace PixiEditor.Models.Controllers
             }
         }
 
-        private void UnsubscribeFromAllLayers(ObservableCollection<Layer> layers)
+        private void UnsubscribeFromAllLayers(System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             foreach (var layer in layers)
             {

+ 4 - 4
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -23,9 +23,9 @@ namespace PixiEditor.Models.DataHolders
         private Guid activeLayerGuid;
         private LayerStructure layerStructure;
 
-        private WpfObservableRangeCollection<Layer> layers = new();
+        private ObservableCollection<Layer> layers = new();
 
-        public WpfObservableRangeCollection<Layer> Layers
+        public ObservableCollection<Layer> Layers
         {
             get => layers;
             set
@@ -408,7 +408,7 @@ namespace PixiEditor.Models.DataHolders
 
         }
 
-        public void AddLayerStructureToUndo(WpfObservableRangeCollection<GuidStructureItem> oldLayerStructureGroups)
+        public void AddLayerStructureToUndo(ObservableCollection<GuidStructureItem> oldLayerStructureGroups)
         {
             UndoManager.AddUndoChange(
                 new Change(
@@ -516,7 +516,7 @@ namespace PixiEditor.Models.DataHolders
 
         public void BuildLayerStructureProcess(object[] parameters)
         {
-            if (parameters.Length > 0 && parameters[0] is WpfObservableRangeCollection<GuidStructureItem> groups)
+            if (parameters.Length > 0 && parameters[0] is ObservableCollection<GuidStructureItem> groups)
             {
                 LayerStructure.Groups.CollectionChanged -= Groups_CollectionChanged;
                 LayerStructure.Groups = LayerStructure.CloneGroups(groups);

+ 1 - 0
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -103,6 +103,7 @@ namespace PixiEditor.Models.DataHolders
         public UndoManager UndoManager { get; set; }
 
         public ObservableCollection<SKColor> Swatches { get; set; } = new ObservableCollection<SKColor>();
+        public ObservableCollection<SKColor> Palette { get; set; } = new ObservableCollection<SKColor>();
 
         public void RaisePropertyChange(string name)
         {

+ 7 - 7
PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs → PixiEditor/Models/DataHolders/ObservableCollection.cs

@@ -9,20 +9,20 @@ using System.Windows.Data;
 
 namespace PixiEditor.Models.DataHolders
 {
-public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
+public class ObservableCollection<T> : RangeObservableCollection<T>
 {
         public bool SuppressNotify { get; set; } = false;
   DeferredEventsCollection _deferredEvents;
 
-  public WpfObservableRangeCollection()
+  public ObservableCollection()
   {
   }
 
-  public WpfObservableRangeCollection(IEnumerable<T> collection) : base(collection)
+  public ObservableCollection(IEnumerable<T> collection) : base(collection)
   {
   }
 
-  public WpfObservableRangeCollection(List<T> list) : base(list)
+  public ObservableCollection(List<T> list) : base(list)
   {
   }
 
@@ -60,7 +60,7 @@ public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
 
   IEnumerable<NotifyCollectionChangedEventHandler> GetHandlers()
   {
-    var info = typeof(ObservableCollection<T>).GetField(nameof(CollectionChanged),
+    var info = typeof(System.Collections.ObjectModel.ObservableCollection<T>).GetField(nameof(CollectionChanged),
       BindingFlags.Instance | BindingFlags.NonPublic);
     var @event = (MulticastDelegate) info.GetValue(this);
     return @event?.GetInvocationList()
@@ -71,9 +71,9 @@ public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
 
   class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
   {
-    private readonly WpfObservableRangeCollection<T> _collection;
+    private readonly ObservableCollection<T> _collection;
 
-    public DeferredEventsCollection(WpfObservableRangeCollection<T> collection)
+    public DeferredEventsCollection(ObservableCollection<T> collection)
     {
       Debug.Assert(collection != null);
       Debug.Assert(collection._deferredEvents == null);

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

@@ -17,7 +17,7 @@ namespace PixiEditor.Models.DataHolders
   /// implementing INotifyCollectionChanged to notify listeners
   /// when items get added, removed or the whole list is refreshed.
   /// </summary>
-  public class RangeObservableCollection<T> : ObservableCollection<T>
+  public class RangeObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
   {
     //------------------------------------------------------
     //
@@ -103,10 +103,10 @@ namespace PixiEditor.Models.DataHolders
     #region Public Methods
 
     /// <summary>
-    /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
+    /// Adds the elements of the specified collection to the end of the <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>.
     /// </summary>
     /// <param name="collection">
-    /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
+    /// The collection whose elements should be added to the end of the <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>.
     /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
     /// </param>
     /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
@@ -116,7 +116,7 @@ namespace PixiEditor.Models.DataHolders
     }
 
     /// <summary>
-    /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
+    /// Inserts the elements of a collection into the <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/> at the specified index.
     /// </summary>
     /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
     /// <param name="collection">The collection whose elements should be inserted into the List<T>.
@@ -163,7 +163,7 @@ namespace PixiEditor.Models.DataHolders
 
 
     /// <summary> 
-    /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
+    /// Removes the first occurence of each item in the specified collection from the <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>.
     /// </summary>
     /// <param name="collection">The items to remove.</param>        
     /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
@@ -302,7 +302,7 @@ namespace PixiEditor.Models.DataHolders
     }
 
     /// <summary>
-    /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
+    /// Removes a range of elements from the <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>>.
     /// </summary>
     /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
     /// <param name="count">The number of elements to remove.</param>
@@ -655,7 +655,7 @@ namespace PixiEditor.Models.DataHolders
   }
 
   /// <remarks>
-  /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
+  /// To be kept outside <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
   /// </remarks>
   internal static class EventArgsCache
   {

+ 1 - 1
PixiEditor/Models/DataHolders/RecentlyOpenedCollection.cs

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
 
 namespace PixiEditor.Models.DataHolders
 {
-    public class RecentlyOpenedCollection : ObservableCollection<RecentlyOpenedDocument>
+    public class RecentlyOpenedCollection : System.Collections.ObjectModel.ObservableCollection<RecentlyOpenedDocument>
     {
         public RecentlyOpenedDocument this[string path]
         {

+ 5 - 5
PixiEditor/Models/DataHolders/Selection.cs

@@ -19,12 +19,12 @@ namespace PixiEditor.Models.DataHolders
 
         public Selection(Coordinates[] selectedPoints)
         {
-            SelectedPoints = new ObservableCollection<Coordinates>(selectedPoints);
+            SelectedPoints = new System.Collections.ObjectModel.ObservableCollection<Coordinates>(selectedPoints);
             SelectionLayer = new Layer("_selectionLayer");
             selectionBlue = new SKColor(142, 202, 255, 255);
         }
 
-        public ObservableCollection<Coordinates> SelectedPoints { get; private set; }
+        public System.Collections.ObjectModel.ObservableCollection<Coordinates> SelectedPoints { get; private set; }
 
         public Layer SelectionLayer
         {
@@ -42,14 +42,14 @@ namespace PixiEditor.Models.DataHolders
             switch (mode)
             {
                 case SelectionType.New:
-                    SelectedPoints = new ObservableCollection<Coordinates>(selection);
+                    SelectedPoints = new System.Collections.ObjectModel.ObservableCollection<Coordinates>(selection);
                     SelectionLayer.Reset();
                     break;
                 case SelectionType.Add:
-                    SelectedPoints = new ObservableCollection<Coordinates>(SelectedPoints.Concat(selection).Distinct());
+                    SelectedPoints = new System.Collections.ObjectModel.ObservableCollection<Coordinates>(SelectedPoints.Concat(selection).Distinct());
                     break;
                 case SelectionType.Subtract:
-                    SelectedPoints = new ObservableCollection<Coordinates>(SelectedPoints.Except(selection));
+                    SelectedPoints = new System.Collections.ObjectModel.ObservableCollection<Coordinates>(SelectedPoints.Except(selection));
                     selectionColor = SKColors.Transparent;
                     break;
             }

+ 14 - 0
PixiEditor/Models/IO/JascPalFile/JascFileData.cs

@@ -0,0 +1,14 @@
+using SkiaSharp;
+
+namespace PixiEditor.Models.IO.JascPalFile;
+
+public class JascFileData
+{
+    public SKColor[] Colors { get; set; }
+
+    public JascFileData(SKColor[] colors)
+    {
+        Colors = colors;
+    }
+
+}

+ 10 - 0
PixiEditor/Models/IO/JascPalFile/JascFileException.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace PixiEditor.Models.IO.JascPalFile;
+
+public class JascFileException : Exception
+{
+    public JascFileException(string message) : base(message)
+    {
+    }
+}

+ 48 - 0
PixiEditor/Models/IO/JascPalFile/JascFileParser.cs

@@ -0,0 +1,48 @@
+using System.IO;
+using SkiaSharp;
+
+namespace PixiEditor.Models.IO.JascPalFile;
+
+/// <summary>
+///     This class is responsible for parsing JASC-PAL files. Which holds the color palette data.
+/// </summary>
+public static class JascFileParser
+{
+    public static JascFileData Parse(string path)
+    {
+        string fileContent = File.ReadAllText(path);
+        string[] lines = fileContent.Split('\n');
+        string fileType = lines[0];
+        string magicBytes = lines[1];
+        if (ValidateFile(fileType, magicBytes))
+        {
+            int colorCount = int.Parse(lines[2]);
+            SKColor[] colors = new SKColor[colorCount];
+            for (int i = 0; i < colorCount; i++)
+            {
+                string[] colorData = lines[i + 3].Split(' ');
+                colors[i] = new SKColor(byte.Parse(colorData[0]), byte.Parse(colorData[1]), byte.Parse(colorData[2]));
+            }
+
+            return new JascFileData(colors);
+        }
+
+        throw new JascFileException("Invalid JASC-PAL file.");
+    }
+
+    public static void Save(string path, JascFileData data)
+    {
+        string fileContent = "JASC-PAL\n0100\n" + data.Colors.Length;
+        for (int i = 0; i < data.Colors.Length; i++)
+        {
+            fileContent += "\n" + data.Colors[i].Red + " " + data.Colors[i].Green + " " + data.Colors[i].Blue;
+        }
+
+        File.WriteAllText(path, fileContent);
+    }
+
+    private static bool ValidateFile(string fileType, string magicBytes)
+    {
+        return fileType.Length > 7 && fileType[..8].ToUpper() == "JASC-PAL" && magicBytes.Length > 3 && magicBytes[..4] == "0100";
+    }
+}

+ 6 - 6
PixiEditor/Models/Layers/LayerStructure.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.Models.Layers
     {
         public event EventHandler<LayerStructureChangedEventArgs> LayerStructureChanged;
 
-        public WpfObservableRangeCollection<GuidStructureItem> Groups { get; set; }
+        public DataHolders.ObservableCollection<GuidStructureItem> Groups { get; set; }
 
         private Document Owner { get; }
 
@@ -38,9 +38,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="groups">Groups to clone.</param>
         /// <returns>ObservableCollection with cloned groups.</returns>
-        public static WpfObservableRangeCollection<GuidStructureItem> CloneGroups(WpfObservableRangeCollection<GuidStructureItem> groups)
+        public static DataHolders.ObservableCollection<GuidStructureItem> CloneGroups(DataHolders.ObservableCollection<GuidStructureItem> groups)
         {
-            WpfObservableRangeCollection<GuidStructureItem> outputGroups = new();
+            DataHolders.ObservableCollection<GuidStructureItem> outputGroups = new();
             foreach (var group in groups.ToArray())
             {
                 outputGroups.Add(group.CloneGroup());
@@ -69,7 +69,7 @@ namespace PixiEditor.Models.Layers
             return GetGroupByGuid(groupGuid, Groups);
         }
 
-        public WpfObservableRangeCollection<GuidStructureItem> CloneGroups()
+        public DataHolders.ObservableCollection<GuidStructureItem> CloneGroups()
         {
             return CloneGroups(Groups);
         }
@@ -709,7 +709,7 @@ namespace PixiEditor.Models.Layers
             return null;
         }
 
-        public LayerStructure(WpfObservableRangeCollection<GuidStructureItem> items, Document owner)
+        public LayerStructure(DataHolders.ObservableCollection<GuidStructureItem> items, Document owner)
         {
             Groups = items;
             Owner = owner;
@@ -717,7 +717,7 @@ namespace PixiEditor.Models.Layers
 
         public LayerStructure(Document owner)
         {
-            Groups = new WpfObservableRangeCollection<GuidStructureItem>();
+            Groups = new DataHolders.ObservableCollection<GuidStructureItem>();
             Owner = owner;
         }
     }

+ 8 - 8
PixiEditor/Models/Layers/StructuredLayerTree.cs

@@ -11,14 +11,14 @@ namespace PixiEditor.Models.Layers
     {
         private List<Guid> layersInStructure = new();
 
-        public WpfObservableRangeCollection<IHasGuid> RootDirectoryItems { get; } = new WpfObservableRangeCollection<IHasGuid>();
+        public DataHolders.ObservableCollection<IHasGuid> RootDirectoryItems { get; } = new DataHolders.ObservableCollection<IHasGuid>();
 
         private static void Swap(ref int startIndex, ref int endIndex)
         {
             (startIndex, endIndex) = (endIndex, startIndex);
         }
 
-        public StructuredLayerTree(WpfObservableRangeCollection<Layer> layers, LayerStructure structure)
+        public StructuredLayerTree(DataHolders.ObservableCollection<Layer> layers, LayerStructure structure)
         {
             if (layers == null || structure == null)
             {
@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Layers
             layersInStructure.Clear();
         }
 
-        private void PlaceItems(List<LayerGroup> parsedFolders, ObservableCollection<Layer> layers)
+        private void PlaceItems(List<LayerGroup> parsedFolders, System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             LayerGroup currentFolder = null;
             List<LayerGroup> groupsAtIndex = new();
@@ -75,7 +75,7 @@ namespace PixiEditor.Models.Layers
             }
         }
 
-        private void AssignGroup(List<LayerGroup> parsedFolders, ObservableCollection<Layer> layers, ref LayerGroup currentFolder, ref List<LayerGroup> groupsAtIndex, Stack<LayerGroup> unfinishedFolders, int i)
+        private void AssignGroup(List<LayerGroup> parsedFolders, System.Collections.ObjectModel.ObservableCollection<Layer> layers, ref LayerGroup currentFolder, ref List<LayerGroup> groupsAtIndex, Stack<LayerGroup> unfinishedFolders, int i)
         {
             if (parsedFolders.Any(x => x.StructureData.StartLayerGuid == layers[i].GuidValue))
             {
@@ -100,7 +100,7 @@ namespace PixiEditor.Models.Layers
             }
         }
 
-        private int CalculateTopIndex(int displayIndex, GuidStructureItem structureData, ObservableCollection<Layer> layers)
+        private int CalculateTopIndex(int displayIndex, GuidStructureItem structureData, System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             var endLayer = layers.FirstOrDefault(x => x.GuidValue == structureData.EndLayerGuid);
             var bottomLayer = layers.FirstOrDefault(x => x.GuidValue == structureData.StartLayerGuid);
@@ -118,7 +118,7 @@ namespace PixiEditor.Models.Layers
             return displayIndex + (originalTopIndex - originalBottomIndex);
         }
 
-        private List<LayerGroup> ParseFolders(IEnumerable<GuidStructureItem> folders, ObservableCollection<Layer> layers)
+        private List<LayerGroup> ParseFolders(IEnumerable<GuidStructureItem> folders, System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             List<LayerGroup> parsedFolders = new();
             foreach (var structureItem in folders)
@@ -129,7 +129,7 @@ namespace PixiEditor.Models.Layers
             return parsedFolders;
         }
 
-        private LayerGroup ParseFolder(GuidStructureItem structureItem, ObservableCollection<Layer> layers)
+        private LayerGroup ParseFolder(GuidStructureItem structureItem, System.Collections.ObjectModel.ObservableCollection<Layer> layers)
         {
             List<Layer> structureItemLayers = new();
 
@@ -168,7 +168,7 @@ namespace PixiEditor.Models.Layers
             return folder;
         }
 
-        private Guid[] GetLayersInGroup(ObservableCollection<Layer> layers, GuidStructureItem structureItem)
+        private Guid[] GetLayersInGroup(System.Collections.ObjectModel.ObservableCollection<Layer> layers, GuidStructureItem structureItem)
         {
             var startLayer = layers.FirstOrDefault(x => x.GuidValue == structureItem.StartLayerGuid);
             var endLayer = layers.FirstOrDefault(x => x.GuidValue == structureItem.EndLayerGuid);

+ 8 - 4
PixiEditor/Views/MainWindow.xaml

@@ -31,9 +31,6 @@
             <converters:IsSpecifiedTypeConverter SpecifiedType="{x:Type tools:ZoomTool}" x:Key="IsZoomToolConverter"/>
             <converters:IsSpecifiedTypeConverter SpecifiedType="{x:Type tools:MoveViewportTool}" x:Key="IsMoveViewportToolConverter"/>
             <converters:SKColorToMediaColorConverter x:Key="SKColorToMediaColorConverter"/>
-            <ResourceDictionary.MergedDictionaries>
-                <ResourceDictionary Source="pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml" />
-            </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Window.Resources>
 
@@ -343,13 +340,20 @@
                                             </i:Interaction.Behaviors>
                                         </colorpicker:ColorSliders>
                                     </LayoutAnchorable>
+                                    <avalondock:LayoutAnchorable ContentId="palette" Title="Palette" CanHide="False"
+                                                                 CanClose="False" CanAutoHide="False"
+                                                                 CanDockAsTabbedDocument="False" CanFloat="True">
+                                        <usercontrols:Palette Colors="{Binding BitmapManager.ActiveDocument.Palette}"/>
+                                    </avalondock:LayoutAnchorable>
                                     <avalondock:LayoutAnchorable ContentId="swatches" Title="Swatches" CanHide="False"
                                                                  CanClose="False" CanAutoHide="False"
                                                                  CanDockAsTabbedDocument="False" CanFloat="True">
                                         <usercontrols:SwatchesView
-                                            SelectSwatchCommand="{Binding ColorsSubViewModel.SelectColorCommand}" RemoveSwatchCommand="{Binding ColorsSubViewModel.RemoveSwatchCommand}"
+                                            SelectSwatchCommand="{Binding ColorsSubViewModel.SelectColorCommand}"
+                                            RemoveSwatchCommand="{Binding ColorsSubViewModel.RemoveSwatchCommand}"
                                             Swatches="{Binding BitmapManager.ActiveDocument.Swatches}"/>
                                     </avalondock:LayoutAnchorable>
+
                                 </LayoutAnchorablePane>
                                 <LayoutAnchorablePane>
                                     <LayoutAnchorable ContentId="layers" Title="Layers" CanHide="False"

+ 9 - 9
PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs

@@ -26,31 +26,31 @@ namespace PixiEditor.Views.UserControls.Layers
         public static readonly DependencyProperty SelectedItemProperty =
             DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(LayersManager), new PropertyMetadata(0));
 
-        public ObservableCollection<IHasGuid> LayerTreeRoot
+        public System.Collections.ObjectModel.ObservableCollection<IHasGuid> LayerTreeRoot
         {
-            get { return (ObservableCollection<IHasGuid>)GetValue(LayerTreeRootProperty); }
+            get { return (System.Collections.ObjectModel.ObservableCollection<IHasGuid>)GetValue(LayerTreeRootProperty); }
             set { SetValue(LayerTreeRootProperty, value); }
         }
 
         public static readonly DependencyProperty LayerTreeRootProperty =
             DependencyProperty.Register(
                 nameof(LayerTreeRoot),
-                typeof(ObservableCollection<IHasGuid>),
+                typeof(System.Collections.ObjectModel.ObservableCollection<IHasGuid>),
                 typeof(LayersManager),
-                new PropertyMetadata(default(ObservableCollection<IHasGuid>), LayerTreeRootChanged));
+                new PropertyMetadata(default(System.Collections.ObjectModel.ObservableCollection<IHasGuid>), LayerTreeRootChanged));
 
-        public ObservableCollection<IHasGuid> CachedLayerTreeRoot
+        public System.Collections.ObjectModel.ObservableCollection<IHasGuid> CachedLayerTreeRoot
         {
-            get { return (ObservableCollection<IHasGuid>)GetValue(CachedLayerTreeRootProperty); }
+            get { return (System.Collections.ObjectModel.ObservableCollection<IHasGuid>)GetValue(CachedLayerTreeRootProperty); }
             set { SetValue(CachedLayerTreeRootProperty, value); }
         }
 
         public static readonly DependencyProperty CachedLayerTreeRootProperty =
             DependencyProperty.Register(
                 nameof(CachedLayerTreeRoot),
-                typeof(ObservableCollection<IHasGuid>),
+                typeof(System.Collections.ObjectModel.ObservableCollection<IHasGuid>),
                 typeof(LayersManager),
-                new PropertyMetadata(default(ObservableCollection<IHasGuid>)));
+                new PropertyMetadata(default(System.Collections.ObjectModel.ObservableCollection<IHasGuid>)));
 
         public LayersViewModel LayerCommandsViewModel
         {
@@ -78,7 +78,7 @@ namespace PixiEditor.Views.UserControls.Layers
         private static void LayerTreeRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
         {
             var manager = (LayersManager)d;
-            var newRoot = (ObservableCollection<IHasGuid>)e.NewValue;
+            var newRoot = (System.Collections.ObjectModel.ObservableCollection<IHasGuid>)e.NewValue;
 
             manager.CachedLayerTreeRoot = newRoot;
             return;

+ 59 - 0
PixiEditor/Views/UserControls/Palette.xaml

@@ -0,0 +1,59 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.Palette"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
+             xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             mc:Ignorable="d" 
+             d:DesignHeight="450" d:DesignWidth="300" Name="paletteControl">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35"/>
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+        <StackPanel Orientation="Vertical" Grid.Row="0">
+            <StackPanel Orientation="Horizontal">
+                <local:PaletteColorAdder Colors="{Binding ElementName=paletteControl, Path=Colors}"/>
+                <Button Style="{StaticResource DarkRoundButton}" Content="Import Palette" Click="ImportPalette_OnClick"/>
+                <Button Style="{StaticResource DarkRoundButton}" Content="Save Palette" Click="SavePalette_OnClick"/>
+            </StackPanel>
+            <Separator/>
+    </StackPanel>
+        <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+           <ItemsControl ItemsSource="{Binding Colors, ElementName=paletteControl}">
+               <d:ItemsControl.ItemsSource>
+                   <x:Array Type="{x:Type Color}">
+                       <Color R="0" G="0" B="0" A="255"/>
+                       <Color R="255" G="255" B="255" A="255"/>
+                       <Color R="255" G="255" B="255" A="150"/>
+                       <Color R="255" G="255" B="255" A="0"/>
+                       <Color R="255" G="0" B="0" A="255"/>
+                       <Color R="0" G="255" B="0" A="255"/>
+                       <Color R="0" G="0" B="255" A="255"/>
+                   </x:Array>
+               </d:ItemsControl.ItemsSource>
+               <ItemsControl.ItemsPanel>
+                   <ItemsPanelTemplate>
+                       <WrapPanel Margin="10,10,0,10" Orientation="Horizontal"
+                                  HorizontalAlignment="Left" VerticalAlignment="Top"/>
+                   </ItemsPanelTemplate>
+               </ItemsControl.ItemsPanel>
+               <ItemsControl.ItemTemplate>
+                    <DataTemplate>
+                        <local:PaletteColor Color="{Binding}" Margin="0 5 5 5">
+                                    <local:PaletteColor.ContextMenu>
+                                        <ContextMenu>
+                                            <MenuItem Header="Remove" Foreground="White"
+                                              Click="RemoveColorMenuItem_OnClick"
+                                              CommandParameter="{Binding}" />
+                                        </ContextMenu>
+                                    </local:PaletteColor.ContextMenu>
+                                </local:PaletteColor>
+                    </DataTemplate>
+               </ItemsControl.ItemTemplate>
+            </ItemsControl>
+        </ScrollViewer>
+    </Grid>
+</UserControl>

+ 68 - 0
PixiEditor/Views/UserControls/Palette.xaml.cs

@@ -0,0 +1,68 @@
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using Microsoft.Win32;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO.JascPalFile;
+using SkiaSharp;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for Palette.xaml
+    /// </summary>
+    public partial class Palette : UserControl
+    {
+        public static readonly DependencyProperty ColorsProperty = DependencyProperty.Register(
+            "Colors", typeof(ObservableCollection<SKColor>), typeof(Palette));
+
+        public ObservableCollection<SKColor> Colors
+        {
+            get { return (ObservableCollection<SKColor>)GetValue(ColorsProperty); }
+            set { SetValue(ColorsProperty, value); }
+        }
+
+        public Palette()
+        {
+            InitializeComponent();
+        }
+
+        private void RemoveColorMenuItem_OnClick(object sender, RoutedEventArgs e)
+        {
+            MenuItem menuItem = (MenuItem)sender;
+            SKColor color = (SKColor)menuItem.CommandParameter;
+            if (Colors.Contains(color))
+            {
+                Colors.Remove(color);
+            }
+        }
+
+        private void ImportPalette_OnClick(object sender, RoutedEventArgs e)
+        {
+            OpenFileDialog openFileDialog = new OpenFileDialog
+            {
+                Filter = "Palette (*.pal)|*.pal"
+            };
+            if (openFileDialog.ShowDialog() == true)
+            {
+                string fileName = openFileDialog.FileName;
+                var jascData = JascFileParser.Parse(fileName);
+                Colors.Clear();
+                Colors.AddRange(jascData.Colors);
+            }
+        }
+
+        private void SavePalette_OnClick(object sender, RoutedEventArgs e)
+        {
+            SaveFileDialog saveFileDialog = new SaveFileDialog
+            {
+                Filter = "Palette (*.pal)|*.pal"
+            };
+            if (saveFileDialog.ShowDialog() == true)
+            {
+                string fileName = saveFileDialog.FileName;
+                JascFileParser.Save(fileName, new JascFileData(Colors.ToArray()));
+            }
+        }
+    }
+}

+ 28 - 0
PixiEditor/Views/UserControls/PaletteColor.xaml

@@ -0,0 +1,28 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.PaletteColor"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             mc:Ignorable="d"
+             d:DesignHeight="45" d:DesignWidth="45" Name="uc">
+    <UserControl.Resources>
+        <converters:SKColorToMediaColorConverter x:Key="SKColorToMediaColorConverter" />
+    </UserControl.Resources>
+    <Grid Width="45" Height="45">
+        <Border CornerRadius="5.5" Width="44" Height="44" RenderOptions.BitmapScalingMode="NearestNeighbor">
+            <Border.Background>
+                <VisualBrush>
+                    <VisualBrush.Visual>
+                        <Image Source="/Images/CheckerTile.png" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
+                    </VisualBrush.Visual>
+                </VisualBrush>
+            </Border.Background>
+        </Border>
+        <Border CornerRadius="5.5" BorderThickness="0 0 0 0.1" BorderBrush="White" Cursor="Hand">
+            <Border.Background>
+                <SolidColorBrush Color="{Binding Color, Converter={StaticResource SKColorToMediaColorConverter}, ElementName=uc}" />
+            </Border.Background>
+        </Border>
+    </Grid>
+</UserControl>

+ 22 - 0
PixiEditor/Views/UserControls/PaletteColor.xaml.cs

@@ -0,0 +1,22 @@
+using System.Windows;
+using System.Windows.Controls;
+using SkiaSharp;
+
+namespace PixiEditor.Views.UserControls;
+
+public partial class PaletteColor : UserControl
+{
+    public static readonly DependencyProperty ColorProperty = DependencyProperty.Register(
+        "Color", typeof(SKColor), typeof(PaletteColor), new PropertyMetadata(default(SKColor)));
+
+    public SKColor Color
+    {
+        get { return (SKColor)GetValue(ColorProperty); }
+        set { SetValue(ColorProperty, value); }
+    }
+
+    public PaletteColor()
+    {
+        InitializeComponent();
+    }
+}

+ 18 - 0
PixiEditor/Views/UserControls/PaletteColorAdder.xaml

@@ -0,0 +1,18 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.PaletteColorAdder"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls" 
+             xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
+             mc:Ignorable="d" Name="paletteColorAdder"
+             d:DesignHeight="30" d:DesignWidth="100">
+    <StackPanel Orientation="Horizontal">
+        <colorpicker:PortableColorPicker
+            ColorChanged="PortableColorPicker_ColorChanged"
+            SelectedColor="{Binding SelectedColor, ElementName=paletteColorAdder, Mode=TwoWay}"
+            Style="{StaticResource DefaultColorPickerStyle}" Margin="0 0 10 0"
+            ShowAlpha="False"/>
+        <Button Name="AddButton" Content="Add" Style="{StaticResource DarkRoundButton}" Click="Button_Click"/>
+    </StackPanel>
+</UserControl>

+ 70 - 0
PixiEditor/Views/UserControls/PaletteColorAdder.xaml.cs

@@ -0,0 +1,70 @@
+using PixiEditor.Models.DataHolders;
+using SkiaSharp;
+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.Navigation;
+using System.Windows.Shapes;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for PaletteColorAdder.xaml
+    /// </summary>
+    public partial class PaletteColorAdder : UserControl
+    {
+        public ObservableCollection<SKColor> Colors
+        {
+            get { return (ObservableCollection<SKColor>)GetValue(ColorsProperty); }
+            set { SetValue(ColorsProperty, value); }
+        }
+
+
+
+        public Color SelectedColor
+        {
+            get { return (Color)GetValue(SelectedColorProperty); }
+            set { SetValue(SelectedColorProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for SelectedColor.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty SelectedColorProperty =
+            DependencyProperty.Register("SelectedColor", typeof(Color), typeof(PaletteColorAdder), 
+                new PropertyMetadata(System.Windows.Media.Colors.Black));
+
+
+        // Using a DependencyProperty as the backing store for Colors.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty ColorsProperty =
+            DependencyProperty.Register("Colors", typeof(ObservableCollection<SKColor>), typeof(PaletteColorAdder), new PropertyMetadata(default(ObservableCollection<SKColor>)));
+
+
+        public PaletteColorAdder()
+        {
+            InitializeComponent();
+        }
+
+        private void Button_Click(object sender, RoutedEventArgs e)
+        {
+            SKColor color = ToSKColor(SelectedColor);
+            if (!Colors.Contains(color))
+            {
+                Colors.Add(color);
+                AddButton.IsEnabled = false;
+            }
+        }
+
+        private void PortableColorPicker_ColorChanged(object sender, RoutedEventArgs e) => 
+            AddButton.IsEnabled = !Colors.Contains(ToSKColor(SelectedColor));
+
+        private SKColor ToSKColor(Color color) => new SKColor(color.R, color.G, color.B, color.A);
+    }
+}

+ 8 - 21
PixiEditor/Views/UserControls/SwatchesView.xaml

@@ -3,7 +3,8 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
-             xmlns:local="clr-namespace:PixiEditor.Views.UserControls" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls" 
+             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
              xmlns:conv="clr-namespace:PixiEditor.Helpers.Converters"
              mc:Ignorable="d" Name="uc"
              d:DesignHeight="450" d:DesignWidth="300">
@@ -33,36 +34,22 @@
             </ItemsControl.ItemsPanel>
             <ItemsControl.ItemTemplate>
                 <DataTemplate>
-                    <Grid Width="45" Height="45" Margin="0 5 5 5">
-                        <Border CornerRadius="5.5" Width="44" Height="44" RenderOptions.BitmapScalingMode="NearestNeighbor">
-                            <Border.Background>
-                                <VisualBrush>
-                                    <VisualBrush.Visual>
-                                        <Image Source="../../Images/CheckerTile.png" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
-                                    </VisualBrush.Visual>
-                                </VisualBrush>
-                            </Border.Background>
-                        </Border>
-                        <Border CornerRadius="5.5" BorderThickness="0 0 0 0.1" BorderBrush="White" Cursor="Hand">
-                            <Border.Background>
-                                <SolidColorBrush Color="{Binding Converter={StaticResource SKColorToMediaColorConverter}}" />
-                            </Border.Background>
-                        </Border>
+                    <local:PaletteColor Color="{Binding}" Margin="0 5 5 5">
                         <i:Interaction.Triggers>
                             <i:EventTrigger EventName="MouseDown">
-                                <i:InvokeCommandAction 
+                                <i:InvokeCommandAction
                                     Command="{Binding SelectSwatchCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SwatchesView}}}"
                                     CommandParameter="{Binding}" />
                             </i:EventTrigger>
                         </i:Interaction.Triggers>
-                        <Grid.ContextMenu>
+                        <local:PaletteColor.ContextMenu>
                             <ContextMenu>
                                 <MenuItem Header="Remove" Foreground="White"
-                                          Command="{Binding RemoveSwatchCommand, Source={x:Reference uc}}"
+                                          Command="{Binding RemoveSwatchCommand, ElementName=uc}"
                                           CommandParameter="{Binding}" />
                             </ContextMenu>
-                        </Grid.ContextMenu>
-                    </Grid>
+                        </local:PaletteColor.ContextMenu>
+                    </local:PaletteColor>
                 </DataTemplate>
             </ItemsControl.ItemTemplate>
         </ItemsControl>

+ 1 - 1
PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs

@@ -31,7 +31,7 @@ namespace PixiEditorTests.ModelsTests.UndoTests
             testBitmap.SetSRGBPixel(0, 0, SKColors.Black);
             testBitmap2.SetSRGBPixel(4, 4, SKColors.Blue);
             Random random = new Random();
-            testDocument.Layers = new WpfObservableRangeCollection<Layer>()
+            testDocument.Layers = new PixiEditor.Models.DataHolders.ObservableCollection<Layer>()
             {
                 new Layer("Test layer" + random.Next(int.MinValue, int.MaxValue), testBitmap),
                 new Layer("Test layer 2" + random.Next(int.MinValue, int.MaxValue), testBitmap2) { Offset = new System.Windows.Thickness(2, 3, 0, 0) }