Browse Source

Merge branch 'master' into command-manager

Egor Mozgovoy 3 years ago
parent
commit
f37f0b9c1d
100 changed files with 2877 additions and 498 deletions
  1. 1 0
      Custom.ruleset
  2. 5 0
      Installer/installer-setup-x64-light.iss
  3. 5 0
      Installer/installer-setup-x86-light.iss
  4. 1 0
      PixiEditor/App.xaml
  5. 79 8
      PixiEditor/App.xaml.cs
  6. 20 2
      PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  7. 26 0
      PixiEditor/Helpers/BindingProxy.cs
  8. 24 0
      PixiEditor/Helpers/Converters/CountToVisibilityConverter.cs
  9. 23 0
      PixiEditor/Helpers/Converters/IndexToAssociatedKeyConverter.cs
  10. 1 1
      PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs
  11. 1 1
      PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs
  12. 23 0
      PixiEditor/Helpers/Converters/PaletteItemsToWidthConverter.cs
  13. 19 0
      PixiEditor/Helpers/Converters/PaletteViewerWidthToVisibilityConverter.cs
  14. 3 3
      PixiEditor/Helpers/Converters/SKColorToMediaColorConverter.cs
  15. 19 0
      PixiEditor/Helpers/Extensions/DirectoryExtensions.cs
  16. 12 3
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  17. 11 1
      PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  18. 29 0
      PixiEditor/Helpers/PaletteHelpers.cs
  19. 35 7
      PixiEditor/Helpers/RelayCommand.cs
  20. 12 3
      PixiEditor/Helpers/SupportedFilesHelper.cs
  21. BIN
      PixiEditor/Images/Arrow-right.png
  22. BIN
      PixiEditor/Images/Check-square.png
  23. BIN
      PixiEditor/Images/ChevronsDown.png
  24. BIN
      PixiEditor/Images/CopyAdd.png
  25. BIN
      PixiEditor/Images/Database.png
  26. BIN
      PixiEditor/Images/Download.png
  27. BIN
      PixiEditor/Images/Edit.png
  28. BIN
      PixiEditor/Images/Globe.png
  29. BIN
      PixiEditor/Images/Load.png
  30. BIN
      PixiEditor/Images/Plus-square.png
  31. BIN
      PixiEditor/Images/Processing.gif
  32. BIN
      PixiEditor/Images/Replace.png
  33. BIN
      PixiEditor/Images/Save.png
  34. BIN
      PixiEditor/Images/Search.png
  35. BIN
      PixiEditor/Images/Shuffle.png
  36. BIN
      PixiEditor/Images/Star-filled.png
  37. BIN
      PixiEditor/Images/Star.png
  38. BIN
      PixiEditor/Images/hard-drive.png
  39. 86 86
      PixiEditor/Models/Controllers/BitmapManager.cs
  40. 20 22
      PixiEditor/Models/Controllers/LayerStackRenderer.cs
  41. 11 0
      PixiEditor/Models/Controllers/StartupArgs.cs
  42. 1 1
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  43. 29 0
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  44. 2 1
      PixiEditor/Models/DataHolders/Document/Document.cs
  45. 60 0
      PixiEditor/Models/DataHolders/Palettes/FilteringSettings.cs
  46. 44 0
      PixiEditor/Models/DataHolders/Palettes/Palette.cs
  47. 13 0
      PixiEditor/Models/DataHolders/Palettes/PaletteFileType.cs
  48. 11 0
      PixiEditor/Models/DataHolders/Palettes/PaletteList.cs
  49. 10 0
      PixiEditor/Models/DataHolders/Palettes/RefreshType.cs
  50. 9 0
      PixiEditor/Models/DataHolders/Palettes/SortingType.cs
  51. 1 1
      PixiEditor/Models/DataHolders/RecentlyOpenedCollection.cs
  52. 4 4
      PixiEditor/Models/DataHolders/Selection.cs
  53. 12 0
      PixiEditor/Models/DataHolders/Surface.cs
  54. 351 0
      PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs
  55. 15 0
      PixiEditor/Models/DataProviders/PaletteListDataSource.cs
  56. 1 6
      PixiEditor/Models/Dialogs/ExportFileDialog.cs
  57. 16 0
      PixiEditor/Models/Enums/ColorsNumberMode.cs
  58. 18 0
      PixiEditor/Models/Events/InputBoxEventArgs.cs
  59. 55 0
      PixiEditor/Models/ExternalServices/LospecPaletteFetcher.cs
  60. 64 0
      PixiEditor/Models/IO/ClsFile/ClsFileParser.cs
  61. 41 17
      PixiEditor/Models/IO/Exporter.cs
  62. 10 0
      PixiEditor/Models/IO/JascPalFile/JascFileException.cs
  63. 75 0
      PixiEditor/Models/IO/JascPalFile/JascFileParser.cs
  64. 46 0
      PixiEditor/Models/IO/PaletteFileData.cs
  65. 12 0
      PixiEditor/Models/IO/PaletteFileParser.cs
  66. 44 0
      PixiEditor/Models/Layers/Layer.cs
  67. 1 1
      PixiEditor/Models/Layers/StructuredLayerTree.cs
  68. 6 0
      PixiEditor/Models/Processes/ProcessHelper.cs
  69. 1 1
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  70. 9 5
      PixiEditor/Models/Tools/ToolSettings/Settings/BoolSetting.cs
  71. 22 16
      PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs
  72. 5 1
      PixiEditor/Models/Tools/ToolSettings/Settings/DropdownSetting.cs
  73. 5 2
      PixiEditor/Models/Tools/ToolSettings/Settings/EnumSetting.cs
  74. 7 2
      PixiEditor/Models/Tools/ToolSettings/Settings/FloatSetting.cs
  75. 2 0
      PixiEditor/Models/Tools/ToolSettings/Settings/Setting.cs
  76. 14 9
      PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs
  77. 1 1
      PixiEditor/Models/Tools/ToolSettings/Toolbars/BasicShapeToolbar.cs
  78. 11 0
      PixiEditor/Models/Tools/ToolSettings/Toolbars/Toolbar.cs
  79. 1 1
      PixiEditor/Models/Undo/StorageBasedChange.cs
  80. 6 0
      PixiEditor/Models/UserPreferences/PreferencesConstants.cs
  81. 16 6
      PixiEditor/Models/UserPreferences/PreferencesSettings.cs
  82. 44 7
      PixiEditor/PixiEditor.csproj
  83. 2 2
      PixiEditor/Properties/AssemblyInfo.cs
  84. 2 2
      PixiEditor/Styles/ComboBoxDarkStyle.xaml
  85. 1 1
      PixiEditor/Styles/DarkCheckboxStyle.xaml
  86. 1 0
      PixiEditor/Styles/ThemeColors.xaml
  87. 14 3
      PixiEditor/Styles/ThemeStyle.xaml
  88. 1 0
      PixiEditor/Styles/Titlebar.xaml
  89. 185 3
      PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs
  90. 2 0
      PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  91. 3 0
      PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs
  92. 6 4
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  93. 74 0
      PixiEditor/ViewModels/SubViewModels/Main/RegistryViewModel.cs
  94. 6 0
      PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  95. 27 1
      PixiEditor/ViewModels/ViewModelMain.cs
  96. 11 5
      PixiEditor/Views/Dialogs/ExportFilePopup.xaml
  97. 27 4
      PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs
  98. 140 0
      PixiEditor/Views/Dialogs/PalettesBrowser.xaml
  99. 567 0
      PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs
  100. 247 254
      PixiEditor/Views/MainWindow.xaml

+ 1 - 0
Custom.ruleset

@@ -55,6 +55,7 @@
     <Rule Id="SA1313" Action="None" />
     <Rule Id="SA1400" Action="None" />
     <Rule Id="SA1401" Action="None" />
+    <Rule Id="SA1402" Action="None" />
     <Rule Id="SA1405" Action="None" />
     <Rule Id="SA1406" Action="None" />
     <Rule Id="SA1407" Action="None" />

+ 5 - 0
Installer/installer-setup-x64-light.iss

@@ -390,6 +390,11 @@ Root: HKCR; Subkey: "{#MyAppName}";                     ValueData: "Program {#My
 Root: HKCR; Subkey: "{#MyAppName}\DefaultIcon";             ValueData: "{app}\{#MyAppExeName},0";               ValueType: string;  ValueName: ""
 Root: HKCR; Subkey: "{#MyAppName}\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
 
+// lospec-palette URL protocol association
+Root: HKCR; Subkey: "lospec-palette";                   ValueData: "{#MyAppName}";  Flags: uninsdeletevalue; ValueType: string;  ValueName: ""
+Root: HKCR; Subkey: "lospec-palette";                   ValueData: "";  Flags: uninsdeletekey;   ValueType: string;  ValueName: "URL Protocol"
+Root: HKCR; Subkey: "lospec-palette\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
+
 [Code]
 function InitializeSetup: Boolean;
 var

+ 5 - 0
Installer/installer-setup-x86-light.iss

@@ -389,6 +389,11 @@ Root: HKCR; Subkey: "{#MyAppName}";                     ValueData: "Program {#My
 Root: HKCR; Subkey: "{#MyAppName}\DefaultIcon";             ValueData: "{app}\{#MyAppExeName},0";               ValueType: string;  ValueName: ""
 Root: HKCR; Subkey: "{#MyAppName}\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
 
+// lospec-palette URL protocol association
+Root: HKCR; Subkey: "lospec-palette";                   ValueData: "{#MyAppName}";  Flags: uninsdeletevalue; ValueType: string;  ValueName: ""
+Root: HKCR; Subkey: "lospec-palette";                   ValueData: "";  Flags: uninsdeletekey;   ValueType: string;  ValueName: "URL Protocol"
+Root: HKCR; Subkey: "lospec-palette\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
+
 [Code]
 function InitializeSetup: Boolean;
 var

+ 1 - 0
PixiEditor/App.xaml

@@ -29,6 +29,7 @@
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
                 <ResourceDictionary Source="Styles/RadioButtonStyle.xaml" />
                 <ResourceDictionary Source="Styles/Fonts.xaml" />
+                <ResourceDictionary Source="pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

+ 79 - 8
PixiEditor/App.xaml.cs

@@ -4,10 +4,15 @@ using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
 using PixiEditor.Views.Dialogs;
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
+using System.Threading;
 using System.Windows;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.UserPreferences;
 
 namespace PixiEditor
 {
@@ -16,21 +21,87 @@ namespace PixiEditor
     /// </summary>
     public partial class App : Application
     {
+        /// <summary>The event mutex name.</summary>
+        private const string UniqueEventName = "33f1410b-2ad7-412a-a468-34fe0a85747c";
+
+        /// <summary>The unique mutex name.</summary>
+        private const string UniqueMutexName = "ab2afe27-b9ee-4f03-a1e4-c18da16a349c";
+
+        /// <summary>The event wait handle.</summary>
+        private EventWaitHandle _eventWaitHandle;
+
+        private string passedArgsFile = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PixiEditor", ".passedArgs");
+
+        /// <summary>The mutex.</summary>
+        private Mutex _mutex;
+
         protected override void OnStartup(StartupEventArgs e)
         {
-            string arguments = string.Join(' ', e.Args);
-
-            if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+            if (HandleNewInstance())
             {
-                CrashReport report = CrashReport.Parse(groups[2].Value);
-                MainWindow = new CrashReportDialog(report);
+                StartupArgs.Args = e.Args.ToList();
+                string arguments = string.Join(' ', e.Args);
+
+                if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+                {
+                    CrashReport report = CrashReport.Parse(groups[2].Value);
+                    MainWindow = new CrashReportDialog(report);
+                }
+                else
+                {
+                    MainWindow = new MainWindow();
+                }
+
+                MainWindow.Show();
             }
-            else
+        }
+
+        private bool HandleNewInstance()
+        {
+            bool isOwned;
+            _mutex = new Mutex(true, UniqueMutexName, out isOwned);
+            _eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
+
+            GC.KeepAlive(_mutex);
+
+            if (isOwned)
             {
-                MainWindow = new MainWindow();
+                var thread = new Thread(
+                    () =>
+                    {
+                        while (_eventWaitHandle.WaitOne())
+                        {
+                            Current.Dispatcher.BeginInvoke(
+                                (Action)(() =>
+                                {
+                                    MainWindow mainWindow = ((MainWindow)Current.MainWindow);
+                                    if (mainWindow != null)
+                                    {
+                                        mainWindow.BringToForeground();
+                                        StartupArgs.Args = File.ReadAllText(passedArgsFile).Split(' ').ToList();
+                                        File.Delete(passedArgsFile);
+                                        StartupArgs.Args.Add("--openedInExisting");
+                                        mainWindow.DataContext.OnStartupCommand.Execute(null);
+                                    }
+                                }));
+                        }
+                    })
+                {
+                    // It is important mark it as background otherwise it will prevent app from exiting.
+                    IsBackground = true
+                };
+
+                thread.Start();
+                return true;
             }
 
-            MainWindow.Show();
+            // Notify other instance so it could bring itself to foreground.
+            File.WriteAllText(passedArgsFile, string.Join(' ', Environment.GetCommandLineArgs()));
+            _eventWaitHandle.Set();
+
+            // Terminate this instance.
+            Shutdown();
+            return false;
         }
 
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)

+ 20 - 2
PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -1,4 +1,5 @@
-using System.Windows;
+using System.Linq;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Interactivity;
@@ -45,6 +46,15 @@ namespace PixiEditor.Helpers.Behaviours
             set => SetValue(DeselectOnFocusLossProperty, value);
         }
 
+        public static readonly DependencyProperty FocusNextProperty = DependencyProperty.Register(
+            "FocusNext", typeof(bool), typeof(TextBoxFocusBehavior), new PropertyMetadata(false));
+
+        public bool FocusNext
+        {
+            get { return (bool)GetValue(FocusNextProperty); }
+            set { SetValue(FocusNextProperty, value); }
+        }
+
         protected override void OnAttached()
         {
             base.OnAttached();
@@ -76,7 +86,15 @@ namespace PixiEditor.Helpers.Behaviours
 
         private void RemoveFocus()
         {
-            MainWindow.Current.mainGrid.Focus();
+            if (!FocusNext)
+            {
+                MainWindow.Current.mainGrid.Focus();
+            }
+            else
+            {
+                AssociatedObject.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
+            }
+
         }
 
         private void AssociatedObjectGotKeyboardFocus(

+ 26 - 0
PixiEditor/Helpers/BindingProxy.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.Helpers
+{
+    public class BindingProxy : Freezable
+    {
+        protected override Freezable CreateInstanceCore()
+        {
+            return new BindingProxy();
+        }
+
+        public object Data
+        {
+            get { return (object)GetValue(DataProperty); }
+            set { SetValue(DataProperty, value); }
+        }
+
+        public static readonly DependencyProperty DataProperty =
+            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
+    }
+}

+ 24 - 0
PixiEditor/Helpers/Converters/CountToVisibilityConverter.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class CountToVisibilityConverter : SingleInstanceConverter<CountToVisibilityConverter>
+    {
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is int intVal)
+            {
+                return intVal == 0 ? Visibility.Visible : Visibility.Collapsed;
+            }
+
+            return Visibility.Visible;
+        }
+    }
+}

+ 23 - 0
PixiEditor/Helpers/Converters/IndexToAssociatedKeyConverter.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class IndexToAssociatedKeyConverter : SingleInstanceConverter<IndexToAssociatedKeyConverter>
+    {
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if(value is int index && index < 10)
+            {
+                if (index == 9) return 0;
+                return (int?)index + 1;
+            }
+
+            return (int?)null;
+        }
+    }
+}

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

@@ -23,7 +23,7 @@ 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>();
             foreach (var group in groups)

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

@@ -109,4 +109,4 @@ namespace PixiEditor.Helpers.Converters
             return rootMismatch;
         }
     }
-}
+}

+ 23 - 0
PixiEditor/Helpers/Converters/PaletteItemsToWidthConverter.cs

@@ -0,0 +1,23 @@
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class PaletteItemsToWidthConverter : SingleInstanceConverter<PaletteItemsToWidthConverter>
+    {
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if(value is IList<SKColor> colors && colors.Count == 0)
+            {
+                return 0;
+            }
+
+            return 120;
+        }
+    }
+}

+ 19 - 0
PixiEditor/Helpers/Converters/PaletteViewerWidthToVisibilityConverter.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Globalization;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Converters;
+
+public class PaletteViewerWidthToVisibilityConverter : SingleInstanceConverter<PaletteViewerWidthToVisibilityConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        bool isCompact = value is double and < 100;
+        if(parameter is string and "Hidden")
+        {
+            return isCompact ? Visibility.Hidden : Visibility.Visible;
+        }
+
+        return isCompact ? Visibility.Visible : Visibility.Hidden;
+    }
+}

+ 3 - 3
PixiEditor/Helpers/Converters/SKColorToMediaColorConverter.cs

@@ -6,15 +6,15 @@ using System.Windows.Media;
 
 namespace PixiEditor.Helpers.Converters
 {
-    class SKColorToMediaColorConverter : IValueConverter
+    public class SKColorToMediaColorConverter : SingleInstanceConverter<SKColorToMediaColorConverter>
     {
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
             var skcolor = (SKColor)value;
             return Color.FromArgb(skcolor.Alpha, skcolor.Red, skcolor.Green, skcolor.Blue);
         }
 
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
         {
             var color = (Color)value;
             return new SKColor(color.R, color.G, color.B, color.A);

+ 19 - 0
PixiEditor/Helpers/Extensions/DirectoryExtensions.cs

@@ -0,0 +1,19 @@
+using System.Linq;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class DirectoryExtensions
+    {
+        /// <summary>
+        ///     Gets files in directory with multiple filters.
+        /// </summary>
+        /// <param name="sourceFolder">Folder to get files from.</param>
+        /// <param name="filters">Filters separated by '|' character.</param>
+        /// <param name="searchOption">Search option for directory.</param>
+        /// <returns>List of file paths found.</returns>
+        public static string[] GetFiles(string sourceFolder, string filters, System.IO.SearchOption searchOption)
+        {
+            return filters.Split('|').SelectMany(filter => System.IO.Directory.GetFiles(sourceFolder, $"*{filter}", searchOption)).ToArray();
+        }
+    }
+}

+ 12 - 3
PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -16,7 +16,8 @@ namespace PixiEditor.Helpers.Extensions
             Document document = new Document(serializableDocument.Width, serializableDocument.Height)
             {
                 Layers = serializableDocument.ToLayers(),
-                Swatches = new ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
+                Swatches = new WpfObservableRangeCollection<SKColor>(serializableDocument.Swatches.ToSKColors()),
+                Palette = new WpfObservableRangeCollection<SKColor>(serializableDocument.Palette.ToSKColors())
             };
 
             document.LayerStructure.Groups = serializableDocument.ToGroups(document);
@@ -88,7 +89,9 @@ namespace PixiEditor.Helpers.Extensions
         {
             return new SerializableDocument(document.Width, document.Height,
                                             document.LayerStructure.Groups.ToSerializable(document),
-                                            document.Layers.ToSerializable()).AddSwatches(document.Swatches);
+                                            document.Layers.ToSerializable())
+                .AddSwatches(document.Swatches)
+                .AddPalette(document.Palette);
         }
 
         public static IEnumerable<SerializableLayer> ToSerializable(this IEnumerable<Layer> layers)
@@ -154,5 +157,11 @@ namespace PixiEditor.Helpers.Extensions
             document.Swatches.AddRange(colors);
             return document;
         }
+
+        private static SerializableDocument AddPalette(this SerializableDocument document, IEnumerable<SKColor> palette)
+        {
+            document.Palette.AddRange(palette);
+            return document;
+        }
     }
-}
+}

+ 11 - 1
PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -1,6 +1,11 @@
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Controllers.Shortcuts;
+using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.IO.ClsFile;
+using PixiEditor.Models.IO.JascPalFile;
 using PixiEditor.Models.Services;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
@@ -32,7 +37,7 @@ namespace PixiEditor.Helpers.Extensions
                 .AddSingleton<ViewportViewModel>()
                 .AddSingleton<ColorsViewModel>()
                 .AddSingleton<DocumentViewModel>()
-                .AddSingleton<MiscViewModel>()
+                .AddSingleton<RegistryViewModel>()
                 .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
                 .AddSingleton<DebugViewModel>()
                 .AddSingleton<SearchViewModel>()
@@ -54,6 +59,11 @@ namespace PixiEditor.Helpers.Extensions
                 .AddSingleton<Tool, ColorPickerTool>()
                 .AddSingleton<Tool, BrightnessTool>()
                 .AddSingleton<Tool, ZoomTool>()
+                // Palette Parsers
+                .AddSingleton<PaletteFileParser, JascFileParser>()
+                .AddSingleton<PaletteFileParser, ClsFileParser>()
+                // Palette data sources
+                .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>()
                 // Other
                 .AddSingleton<DocumentProvider>();
     }

+ 29 - 0
PixiEditor/Helpers/PaletteHelpers.cs

@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Helpers
+{
+    public static class PaletteHelpers
+    {
+        public static string GetFilter(IList<PaletteFileParser> parsers)
+        {
+            string filter = "";
+
+            List<string> allSupportedFormats = new();
+            foreach (var parser in parsers)
+            {
+                allSupportedFormats.AddRange(parser.SupportedFileExtensions);
+            }
+            string allSupportedFormatsString = string.Join(';', allSupportedFormats).Replace(".", "*.");
+            filter += $"Palette Files ({allSupportedFormatsString})|{allSupportedFormatsString}|";
+            
+            foreach (var parser in parsers)
+            {
+                string supportedFormats = string.Join(';', parser.SupportedFileExtensions).Replace(".", "*.");
+                filter += $"{parser.FileName} ({supportedFormats})|{supportedFormats}|";
+            }
+
+            return filter.Remove(filter.Length - 1);
+        }
+    }
+}

+ 35 - 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,42 @@ 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 (canExecute == null) return true;
+            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) { }
     }
 }

+ 12 - 3
PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -1,5 +1,4 @@
-using PixiEditor.Models;
-using PixiEditor.Models.Enums;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using System;
 using System.Collections.Generic;
@@ -21,7 +20,7 @@ namespace PixiEditor.Helpers
             allFileTypeDialogsData = new List<FileTypeDialogData>();
 
             var allFormats = Enum.GetValues(typeof(FileType)).Cast<FileType>().ToList();
-            
+
             foreach (var format in allFormats)
             {
                 var fileTypeDialogData = new FileTypeDialogData(format);
@@ -75,6 +74,16 @@ namespace PixiEditor.Helpers
             return filter;
         }
 
+        public static FileType GetSaveFileTypeFromFilterIndex(bool includePixi, int filterIndex)
+        {
+            var allSupportedExtensions = GetAllSupportedFileTypes(includePixi);
+            //filter index starts at 1 for some reason
+            int index = filterIndex - 1;
+            if (allSupportedExtensions.Count <= index)
+                return FileType.Unset;
+            return allSupportedExtensions[index].FileType;
+        }
+
         public static string BuildOpenFilter()
         {
             var any = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Any).GetFormattedTypes();

BIN
PixiEditor/Images/Arrow-right.png


BIN
PixiEditor/Images/Check-square.png


BIN
PixiEditor/Images/ChevronsDown.png


BIN
PixiEditor/Images/CopyAdd.png


BIN
PixiEditor/Images/Database.png


BIN
PixiEditor/Images/Download.png


BIN
PixiEditor/Images/Edit.png


BIN
PixiEditor/Images/Globe.png


BIN
PixiEditor/Images/Load.png


BIN
PixiEditor/Images/Plus-square.png


BIN
PixiEditor/Images/Processing.gif


BIN
PixiEditor/Images/Replace.png


BIN
PixiEditor/Images/Save.png


BIN
PixiEditor/Images/Search.png


BIN
PixiEditor/Images/Shuffle.png


BIN
PixiEditor/Images/Star-filled.png


BIN
PixiEditor/Images/Star.png


BIN
PixiEditor/Images/hard-drive.png


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

@@ -1,48 +1,48 @@
-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;
 using PixiEditor.Models.Commands.Attributes;
 
-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;
-                activeDocument?.UpdatePreviewImage();
-                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;
+                activeDocument?.UpdatePreviewImage();
+                RaisePropertyChanged(nameof(ActiveDocument));
+                ActiveWindow = value;
+                DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
+            }
+        }
+
+        private object activeWindow;
         public object ActiveWindow
         {
             get => activeWindow;
@@ -63,8 +63,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;
@@ -76,8 +76,8 @@ namespace PixiEditor.Models.Controllers
         {
             get => onlyReferenceLayer;
             set => SetProperty(ref onlyReferenceLayer, value);
-        }
-
+        }
+
         private readonly ToolsViewModel _tools;
 
         private int previewLayerSize;
@@ -88,16 +88,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();
@@ -105,10 +105,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);
         }
 
         [Evaluator.CanExecute("PixiEditor.HasDocument")]
@@ -139,8 +139,8 @@ namespace PixiEditor.Models.Controllers
 
             ActiveDocument.PreviewLayer.Reset();
             ExecuteTool();
-        }
-
+        }
+
         private void OnSessionEnd(object sender, ToolSession e)
         {
             activeSession = null;
@@ -153,8 +153,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)
@@ -170,16 +170,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)
@@ -194,25 +194,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;
@@ -220,14 +220,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)
@@ -241,15 +241,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);
         }
-    }
-}
+    }
+}

+ 20 - 22
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)
             {
@@ -116,23 +116,21 @@ namespace PixiEditor.Models.Controllers
                 Int32Rect layerRect = new Int32Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height);
                 Int32Rect layerPortion = layerRect.Intersect(dirtyRectangle);
 
-                using (var snapshot = layer.LayerBitmap.SkiaSurface.Snapshot())
-                {
-                    finalSurface.SkiaSurface.Canvas.DrawImage(
-                        snapshot,
-                        new SKRect(
-                            layerPortion.X - layer.OffsetX,
-                            layerPortion.Y - layer.OffsetY,
-                            layerPortion.X - layer.OffsetX + layerPortion.Width,
-                            layerPortion.Y - layer.OffsetY + layerPortion.Height),
-                        new SKRect(
-                            layerPortion.X,
-                            layerPortion.Y,
-                            layerPortion.X + layerPortion.Width,
-                            layerPortion.Y + layerPortion.Height
-                        ),
-                        BlendingPaint);
-                }
+                using var snapshot = layer.LayerBitmap.SkiaSurface.Snapshot();
+                finalSurface.SkiaSurface.Canvas.DrawImage(
+                    snapshot,
+                    new SKRect(
+                        layerPortion.X - layer.OffsetX,
+                        layerPortion.Y - layer.OffsetY,
+                        layerPortion.X - layer.OffsetX + layerPortion.Width,
+                        layerPortion.Y - layer.OffsetY + layerPortion.Height),
+                    new SKRect(
+                        layerPortion.X,
+                        layerPortion.Y,
+                        layerPortion.X + layerPortion.Width,
+                        layerPortion.Y + layerPortion.Height
+                    ),
+                    BlendingPaint);
             }
             finalBitmap.Lock();
             using (var snapshot = finalSurface.SkiaSurface.Snapshot())

+ 11 - 0
PixiEditor/Models/Controllers/StartupArgs.cs

@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Controllers;
+
+/// <summary>
+///     A class that holds startup command line arguments + custom passed ones.
+/// </summary>
+public static class StartupArgs
+{
+    public static List<string> Args { get; set; }
+}

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

@@ -871,4 +871,4 @@ namespace PixiEditor.Models.DataHolders
             }
         }
     }
-}
+}

+ 29 - 0
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -14,6 +14,25 @@ namespace PixiEditor.Models.DataHolders
     {
         public event EventHandler<DocumentSizeChangedEventArgs> DocumentSizeChanged;
 
+        public void ReplaceColor(SKColor oldColor, SKColor newColor)
+        {
+            StorageBasedChange change = new(this, Layers);
+
+            var args = new object[] { oldColor, newColor };
+
+            ReplaceColorProcess(args);
+
+            ChangesSaved = false;
+
+            UndoManager.AddUndoChange(change.ToChange(
+                StorageBasedChange.BasicUndoProcess,
+                new object[] { this },
+                ReplaceColorProcess,
+                args,
+                "Resize canvas"));
+        }
+
+
         /// <summary>
         ///     Resizes canvas to specified width and height to selected anchor.
         /// </summary>
@@ -110,6 +129,16 @@ namespace PixiEditor.Models.DataHolders
                     "Resize document"));
         }
 
+        private void ReplaceColorProcess(object[] args)
+        {
+            SKColor oldColor = (SKColor)args[0];
+            SKColor newColor = (SKColor)args[1];
+
+            foreach (var layer in Layers)
+            {
+                layer.ReplaceColor(oldColor, newColor);
+            }
+        }
 
         private void FlipDocumentProcess(object[] processArgs)
         {

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

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

+ 60 - 0
PixiEditor/Models/DataHolders/Palettes/FilteringSettings.cs

@@ -0,0 +1,60 @@
+using PixiEditor.Models.Enums;
+using System;
+using System.Linq;
+
+namespace PixiEditor.Models.DataHolders.Palettes
+{
+    public class FilteringSettings
+    {
+        public ColorsNumberMode ColorsNumberMode { get; set; }
+        public int ColorsCount { get; set; }
+        public string Name { get; set; }
+
+        public bool ShowOnlyFavourites { get; set; }
+
+        public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites)
+        {
+            ColorsNumberMode = colorsNumberMode;
+            ColorsCount = colorsCount;
+            Name = name;
+            ShowOnlyFavourites = showOnlyFavourites;
+        }
+
+        public bool Filter(Palette palette)
+        {
+            // Lexical comparison
+            bool result = string.IsNullOrWhiteSpace(Name) || palette.Name.Contains(Name, StringComparison.OrdinalIgnoreCase);
+
+            if(!result)
+            {
+                return false;
+            }
+
+            result = (ShowOnlyFavourites && palette.IsFavourite) || !ShowOnlyFavourites;
+
+            if(!result)
+            {
+                return false;
+            }
+
+            switch (ColorsNumberMode)
+            {
+                case ColorsNumberMode.Any:
+                    break;
+                case ColorsNumberMode.Max:
+                    result = palette.Colors.Count <= ColorsCount;
+                    break;
+                case ColorsNumberMode.Min:
+                    result = palette.Colors.Count >= ColorsCount;
+                    break;
+                case ColorsNumberMode.Exact:
+                    result = palette.Colors.Count == ColorsCount;
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            return result;
+        }
+    }
+}

+ 44 - 0
PixiEditor/Models/DataHolders/Palettes/Palette.cs

@@ -0,0 +1,44 @@
+#nullable enable
+using PixiEditor.Helpers;
+using System.Collections.Generic;
+using System.IO;
+namespace PixiEditor.Models.DataHolders.Palettes
+{
+    public class Palette : NotifyableObject
+    {
+        private string _name = "";
+
+        public string Name
+        {
+            get => _name;
+            set => SetProperty(ref _name, value);
+        }
+        public List<string> Colors { get; set; }
+
+        private string? fileName;
+
+        public string? FileName
+        {
+            get => fileName;
+            set
+            {
+                fileName = ReplaceInvalidChars(value);
+                RaisePropertyChanged(nameof(FileName));
+            }
+        }
+
+        public bool IsFavourite { get; set; }
+
+        public Palette(string name, List<string> colors, string fileName)
+        {
+            Name = name;
+            Colors = colors;
+            FileName = fileName;
+        }
+
+        public static string? ReplaceInvalidChars(string? filename)
+        {
+            return filename == null ? null : string.Join("_", filename.Split(Path.GetInvalidFileNameChars()));
+        }
+    }
+}

+ 13 - 0
PixiEditor/Models/DataHolders/Palettes/PaletteFileType.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataHolders.Palettes
+{
+    public enum PaletteFileType
+    {
+        JascPal
+    }
+}

+ 11 - 0
PixiEditor/Models/DataHolders/Palettes/PaletteList.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Helpers;
+using System.Collections.ObjectModel;
+
+namespace PixiEditor.Models.DataHolders.Palettes
+{
+    public class PaletteList : NotifyableObject
+    {
+        public bool FetchedCorrectly { get; set; } = false;
+        public WpfObservableRangeCollection<Palette> Palettes { get; set; } = new WpfObservableRangeCollection<Palette>();
+    }
+}

+ 10 - 0
PixiEditor/Models/DataHolders/Palettes/RefreshType.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.Models.DataHolders.Palettes;
+
+public enum RefreshType
+{
+    All,
+    Created,
+    Updated,
+    Deleted,
+    Renamed
+}

+ 9 - 0
PixiEditor/Models/DataHolders/Palettes/SortingType.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.Models.DataHolders.Palettes
+{
+    public enum SortingType
+    {
+        Default,
+        Alphabetical,
+        ColorCount
+    }
+}

+ 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]
         {

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

@@ -24,7 +24,7 @@ namespace PixiEditor.Models.DataHolders
             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;
             }

+ 12 - 0
PixiEditor/Models/DataHolders/Surface.cs

@@ -129,6 +129,18 @@ namespace PixiEditor.Models.DataHolders
             SkiaSurface.Canvas.DrawPoint(x, y, drawingPaint);
         }
 
+        public unsafe void SetSRGBPixelUnmanaged(int x, int y, SKColor color)
+        {
+            Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
+
+            float normalizedAlpha = color.Alpha / 255.0f;
+
+            ptr[0] = (Half)(color.Red / 255f * normalizedAlpha);
+            ptr[1] = (Half)(color.Green / 255f * normalizedAlpha);
+            ptr[2] = (Half)(color.Blue / 255f * normalizedAlpha);
+            ptr[3] = (Half)(normalizedAlpha);
+        }
+
         public unsafe byte[] ToByteArray(SKColorType colorType = SKColorType.Bgra8888, SKAlphaType alphaType = SKAlphaType.Premul)
         {
             var imageInfo = new SKImageInfo(Width, Height, colorType, alphaType, SKColorSpace.CreateSrgb());

+ 351 - 0
PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs

@@ -0,0 +1,351 @@
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.UserPreferences;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataProviders
+{
+    public delegate void CacheUpdate(RefreshType refreshType, Palette itemAffected, string oldName);
+
+    public class LocalPalettesFetcher : PaletteListDataSource
+    {
+        public static string PathToPalettesFolder { get; } = Path.Join(
+            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+            "PixiEditor", "Palettes");
+
+        private List<Palette> cachedPalettes;
+
+        public event CacheUpdate CacheUpdated;
+
+        private List<string> cachedFavoritePalettes;
+
+        private FileSystemWatcher watcher;
+
+        public override void Initialize()
+        {
+            InitDir();
+            watcher = new FileSystemWatcher(PathToPalettesFolder);
+            watcher.Filter = "*.pal";
+            watcher.Changed += FileSystemChanged;
+            watcher.Deleted += FileSystemChanged;
+            watcher.Renamed += RenamedFile;
+            watcher.Created += FileSystemChanged;
+
+            watcher.EnableRaisingEvents = true;
+            cachedFavoritePalettes = IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+
+            IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, updated =>
+            {
+                cachedFavoritePalettes = (List<string>)updated;
+            });
+        }
+
+        public override async Task<PaletteList> FetchPaletteList(int startIndex, int count, FilteringSettings filtering)
+        {
+            if (cachedPalettes == null)
+            {
+                await RefreshCacheAll();
+            }
+
+            PaletteList result = new PaletteList
+            {
+                Palettes = new WpfObservableRangeCollection<Palette>()
+            };
+
+            var filteredPalettes = cachedPalettes.Where(filtering.Filter).ToArray();
+
+            if (startIndex >= filteredPalettes.Length) return result;
+
+            for (int i = 0; i < count; i++)
+            {
+                if (startIndex + i >= filteredPalettes.Length) break;
+                Palette palette = filteredPalettes[startIndex + i];
+                result.Palettes.Add(palette);
+            }
+
+            result.FetchedCorrectly = true;
+            return result;
+        }
+
+        public static bool PaletteExists(string paletteName)
+        {
+            string finalFileName = paletteName;
+            if (!paletteName.EndsWith(".pal"))
+            {
+                finalFileName += ".pal";
+            }
+
+            return File.Exists(Path.Join(PathToPalettesFolder, finalFileName));
+        }
+
+        public static string GetNonExistingName(string currentName, bool appendExtension = false)
+        {
+            string newName = Path.GetFileNameWithoutExtension(currentName);
+
+            if (File.Exists(Path.Join(PathToPalettesFolder, newName + ".pal")))
+            {
+                int number = 1;
+                while (true)
+                {
+                    string potentialName = $"{newName} ({number})";
+                    number++;
+                    if (File.Exists(Path.Join(PathToPalettesFolder, potentialName + ".pal")))
+                        continue;
+                    newName = potentialName;
+                    break;
+                }
+            }
+
+            if (appendExtension)
+                newName += ".pal";
+
+            return newName;
+        }
+
+        public async Task SavePalette(string fileName, SKColor[] colors)
+        {
+            watcher.EnableRaisingEvents = false;
+            string path = Path.Join(PathToPalettesFolder, fileName);
+            InitDir();
+            await JascFileParser.SaveFile(path, new PaletteFileData(colors));
+            watcher.EnableRaisingEvents = true;
+            
+            await RefreshCache(RefreshType.Created, path);
+        }
+
+        public async Task DeletePalette(string name)
+        {
+            if (!Directory.Exists(PathToPalettesFolder)) return;
+            string path = Path.Join(PathToPalettesFolder, name);
+            if (!File.Exists(path)) return;
+
+            watcher.EnableRaisingEvents = false;
+            File.Delete(path);
+            watcher.EnableRaisingEvents = true;
+            
+            await RefreshCache(RefreshType.Deleted, path);
+        }
+
+        public void RenamePalette(string oldFileName, string newFileName)
+        {
+            if (!Directory.Exists(PathToPalettesFolder)) 
+                return;
+            
+            string oldPath = Path.Join(PathToPalettesFolder, oldFileName);
+            string newPath = Path.Join(PathToPalettesFolder, newFileName);
+            if (!File.Exists(oldPath) || File.Exists(newPath)) 
+                return;
+
+            watcher.EnableRaisingEvents = false;
+            File.Move(oldPath, newPath);
+            watcher.EnableRaisingEvents = true;
+
+            RefreshCacheRenamed(newPath, oldPath);
+        }
+
+        public async Task RefreshCacheAll()
+        {
+            string[] files = DirectoryExtensions.GetFiles(
+                PathToPalettesFolder,
+                string.Join("|", AvailableParsers.SelectMany(x => x.SupportedFileExtensions)),
+                SearchOption.TopDirectoryOnly);
+            cachedPalettes = await ParseAll(files);
+            CacheUpdated?.Invoke(RefreshType.All, null, null);
+        }
+
+        private async void FileSystemChanged(object sender, FileSystemEventArgs e)
+        {
+            bool waitableExceptionOccured = false;
+            do
+            {
+                try
+                {
+                    switch (e.ChangeType)
+                    {
+                        case WatcherChangeTypes.Created:
+                            await RefreshCache(RefreshType.Created, e.FullPath);
+                            break;
+                        case WatcherChangeTypes.Deleted:
+                            await RefreshCache(RefreshType.Deleted, e.FullPath);
+                            break;
+                        case WatcherChangeTypes.Changed:
+                            await RefreshCache(RefreshType.Updated, e.FullPath);
+                            break;
+                        case WatcherChangeTypes.Renamed:
+                            // Handled by method below
+                            break;
+                        case WatcherChangeTypes.All:
+                            await RefreshCache(RefreshType.Created, e.FullPath);
+                            break;
+                        default:
+                            throw new ArgumentOutOfRangeException();
+                    }
+
+                    waitableExceptionOccured = false;
+                }
+                catch (IOException)
+                {
+                    waitableExceptionOccured = true;
+                    await Task.Delay(100);
+                }
+
+            }
+            while (waitableExceptionOccured);
+        }
+
+        private async Task RefreshCache(RefreshType refreshType, string file)
+        {
+            Palette updated = null;
+            string affectedFileName = null;
+
+            switch (refreshType)
+            {
+                case RefreshType.All:
+                    throw new ArgumentException("To handle refreshing all items, use RefreshCacheAll");
+                case RefreshType.Created:
+                    updated = await RefreshCacheAdded(file);
+                    break;
+                case RefreshType.Updated:
+                    updated = await RefreshCacheUpdated(file);
+                    break;
+                case RefreshType.Deleted:
+                    affectedFileName = RefreshCacheDeleted(file);
+                    break;
+                case RefreshType.Renamed:
+                    throw new ArgumentException("To handle renaming, use RefreshCacheRenamed");
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(refreshType), refreshType, null);
+            }
+            CacheUpdated?.Invoke(refreshType, updated, affectedFileName);
+        }
+
+        private void RefreshCacheRenamed(string newFilePath, string oldFilePath)
+        {
+            string oldFileName = Path.GetFileName(oldFilePath);
+            int index = cachedPalettes.FindIndex(p => p.FileName == oldFileName);
+            if (index == -1) return;
+
+            Palette palette = cachedPalettes[index];
+            palette.FileName = Path.GetFileName(newFilePath);
+            palette.Name = Path.GetFileNameWithoutExtension(newFilePath);
+
+            CacheUpdated?.Invoke(RefreshType.Renamed, palette, oldFileName);
+        }
+
+        private string RefreshCacheDeleted(string filePath)
+        {
+            string fileName = Path.GetFileName(filePath);
+            int index = cachedPalettes.FindIndex(p => p.FileName == fileName);
+            if (index == -1) return null;
+
+            cachedPalettes.RemoveAt(index);
+            return fileName;
+        }
+
+        private async Task<Palette> RefreshCacheItem(string file, Action<Palette> action)
+        {
+            if (File.Exists(file))
+            {
+                string extension = Path.GetExtension(file);
+                var foundParser = AvailableParsers.FirstOrDefault(x => x.SupportedFileExtensions.Contains(extension));
+                if (foundParser != null)
+                {
+                    var newPalette = await foundParser.Parse(file);
+                    if (newPalette is { IsCorrupted: false })
+                    {
+                        Palette pal = CreatePalette(newPalette, file,
+                            cachedFavoritePalettes?.Contains(newPalette.Title) ?? false);
+                        action(pal);
+
+                        return pal;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private async Task<Palette> RefreshCacheUpdated(string file)
+        {
+            return await RefreshCacheItem(file, palette =>
+            {
+                Palette existingPalette = cachedPalettes.FirstOrDefault(x => x.FileName == palette.FileName);
+                if (existingPalette != null)
+                {
+                    existingPalette.Colors = palette.Colors.ToList();
+                    existingPalette.Name = palette.Name;
+                    existingPalette.FileName = palette.FileName;
+                }
+            });
+        }
+
+        private async Task<Palette> RefreshCacheAdded(string file)
+        {
+            return await RefreshCacheItem(file, palette =>
+            {
+                string fileName = Path.GetFileName(file);
+                int index = cachedPalettes.FindIndex(p => p.FileName == fileName);
+                if (index != -1)
+                {
+                    cachedPalettes.RemoveAt(index);
+                }
+                cachedPalettes.Add(palette);
+            });
+        }
+
+        private async Task<List<Palette>> ParseAll(string[] files)
+        {
+            List<Palette> result = new List<Palette>();
+
+            foreach (var file in files)
+            {
+                string extension = Path.GetExtension(file);
+                if (!File.Exists(file)) continue;
+                var foundParser = AvailableParsers.First(x => x.SupportedFileExtensions.Contains(extension));
+                {
+                    PaletteFileData fileData = await foundParser.Parse(file);
+                    if (fileData.IsCorrupted) continue;
+                    var palette = CreatePalette(fileData, file, cachedFavoritePalettes?.Contains(fileData.Title) ?? false);
+
+                    result.Add(palette);
+                }
+            }
+
+            return result;
+        }
+
+        private static Palette CreatePalette(PaletteFileData fileData, string file, bool isFavourite)
+        {
+            var palette = new Palette(
+                fileData.Title,
+                new List<string>(fileData.GetHexColors()),
+                Path.GetFileName(file))
+            {
+                IsFavourite = isFavourite
+            };
+
+            return palette;
+        }
+
+        private void RenamedFile(object sender, RenamedEventArgs e)
+        {
+            RefreshCacheRenamed(e.FullPath, e.OldFullPath);
+        }
+
+        private static void InitDir()
+        {
+            if (!Directory.Exists(PathToPalettesFolder))
+            {
+                Directory.CreateDirectory(PathToPalettesFolder);
+            }
+        }
+    }
+}

+ 15 - 0
PixiEditor/Models/DataProviders/PaletteListDataSource.cs

@@ -0,0 +1,15 @@
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.IO;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataProviders
+{
+    public abstract class PaletteListDataSource
+    {
+        public virtual void Initialize() { }
+        public abstract Task<PaletteList> FetchPaletteList(int startIndex, int items, FilteringSettings filtering);
+        public List<PaletteFileParser> AvailableParsers { get; set; }
+
+    }
+}

+ 1 - 6
PixiEditor/Models/Dialogs/ExportFileDialog.cs

@@ -1,6 +1,5 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Views;
-using System.Drawing.Imaging;
 using System.Windows;
 
 namespace PixiEditor.Models.Dialogs
@@ -75,11 +74,7 @@ namespace PixiEditor.Models.Dialogs
 
         public override bool ShowDialog()
         {
-            ExportFilePopup popup = new ExportFilePopup
-            {
-                SaveWidth = FileWidth,
-                SaveHeight = FileHeight
-            };
+            ExportFilePopup popup = new ExportFilePopup(FileWidth, FileHeight);
             popup.ShowDialog();
             if (popup.DialogResult == true)
             {

+ 16 - 0
PixiEditor/Models/Enums/ColorsNumberMode.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.Enums
+{
+    public enum ColorsNumberMode
+    {
+        Any,
+        Max,
+        Min,
+        Exact
+    }
+}

+ 18 - 0
PixiEditor/Models/Events/InputBoxEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.Events
+{
+    public class InputBoxEventArgs : EventArgs
+    {
+        public string Input { get; set; }
+
+        public InputBoxEventArgs(string input)
+        {
+            Input = input;
+        }
+    }
+}

+ 55 - 0
PixiEditor/Models/ExternalServices/LospecPaletteFetcher.cs

@@ -0,0 +1,55 @@
+using Newtonsoft.Json;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.Enums;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using PixiEditor.Models.Dialogs;
+
+namespace PixiEditor.Models.ExternalServices
+{
+    public static class LospecPaletteFetcher
+    {
+        public const string LospecApiUrl = "https://lospec.com/palette-list";
+
+        public static async Task<Palette> FetchPalette(string slug)
+        {
+            try
+            {
+                using HttpClient client = new HttpClient();
+                string url = @$"{LospecApiUrl}/{slug}.json";
+
+                HttpResponseMessage response = await client.GetAsync(url);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    string content = await response.Content.ReadAsStringAsync();
+                    var obj = JsonConvert.DeserializeObject<Palette>(content);
+
+                    if (obj is { Colors: { } })
+                    {
+                        ReadjustColors(obj.Colors);
+                    }
+
+                    return obj;
+                }
+            }
+            catch(HttpRequestException)
+            {
+                NoticeDialog.Show("Failed to download palette.", "Error");
+                return null;
+            }
+
+            return null;
+        }
+
+        private static void ReadjustColors(List<string> colors)
+        {
+            for (int i = 0; i < colors.Count; i++)
+            {
+                colors[i] = colors[i].Insert(0, "#");
+            }
+        }
+    }
+}

+ 64 - 0
PixiEditor/Models/IO/ClsFile/ClsFileParser.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using CLSEncoderDecoder;
+using PixiEditor.Models.DataHolders.Palettes;
+using SkiaSharp;
+
+namespace PixiEditor.Models.IO.ClsFile;
+
+public class ClsFileParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Clip Studio Paint Color Set";
+
+    public override string[] SupportedFileExtensions { get; } = { ".cls" };
+    
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        return await Task.Run(() =>
+        {
+            ClsColorSet set;
+            try
+            {
+                set = ClsColorSet.Load(path);
+            }
+            catch
+            {
+                return new PaletteFileData("Corrupted", Array.Empty<SKColor>()) { IsCorrupted = true };
+            }
+            
+            PaletteFileData data = new(
+                set.Utf8Name,
+                set.Colors
+                    .Where(static color => color.Alpha > 0)
+                    .Select(static color => new SKColor(color.Red, color.Green, color.Blue, 255))
+                    .ToArray()
+            );
+            return data;
+        });
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        if (data?.Colors == null || data.Colors.Length <= 0) return false;
+        
+        string name = data.Title;
+        List<ClsColor> colors = data.Colors
+            .Select(color => new ClsColor(color.Red, color.Green, color.Blue, color.Alpha)).ToList();
+        await Task.Run(() =>
+        {
+            if (name.Length == 0)
+                name = Path.GetFileNameWithoutExtension(path);
+            if (name.Length > 64)
+                name = name.Substring(0, 64);
+            ClsColorSet set = new(colors, name);
+            set.Save(path);
+        });
+            
+        return true;
+
+    }
+
+}

+ 41 - 17
PixiEditor/Models/IO/Exporter.cs

@@ -7,11 +7,9 @@ using PixiEditor.Models.Enums;
 using SkiaSharp;
 using System;
 using System.Collections.Generic;
-using System.Drawing.Imaging;
 using System.IO;
 using System.IO.Compression;
 using System.Linq;
-using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Media.Imaging;
@@ -30,11 +28,14 @@ namespace PixiEditor.Models.IO
             SaveFileDialog dialog = new SaveFileDialog
             {
                 Filter = SupportedFilesHelper.BuildSaveFilter(true),
-                FilterIndex = 0
+                FilterIndex = 0,
+                DefaultExt = "pixi"
+
             };
             if ((bool)dialog.ShowDialog())
             {
-                path = SaveAsEditableFile(document, dialog.FileName);
+                FileType filetype = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(true, dialog.FilterIndex);
+                path = SaveAsEditableFile(document, dialog.FileName, filetype);
                 return true;
             }
 
@@ -48,15 +49,28 @@ namespace PixiEditor.Models.IO
         /// <param name="document">Document to be saved.</param>
         /// <param name="path">Path where to save file.</param>
         /// <returns>Path.</returns>
-        public static string SaveAsEditableFile(Document document, string path)
+        public static string SaveAsEditableFile(Document document, string path, FileType requestedType = FileType.Unset)
         {
-            if (Path.GetExtension(path) != Constants.NativeExtension)
+            var typeFromPath = ParseImageFormat(Path.GetExtension(path));
+            FileType finalType = (typeFromPath, requestedType) switch
+            {
+                (FileType.Unset, FileType.Unset) => FileType.Pixi,
+                (var first, FileType.Unset) => first,
+                (FileType.Unset, var second) => second,
+                _ => typeFromPath,
+            };
+
+            if (typeFromPath == FileType.Unset)
+            {
+                path = AppendExtension(path, SupportedFilesHelper.GetFileTypeDialogData(finalType));
+            }
+
+            if (finalType != FileType.Pixi)
             {
-                var chosenFormat = ParseImageFormat(Path.GetExtension(path));
                 var bitmap = document.Renderer.FinalBitmap;
-                SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+                SaveAs(encodersFactory[finalType](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
             }
-            else if(Directory.Exists(Path.GetDirectoryName(path)))
+            else if (Directory.Exists(Path.GetDirectoryName(path)))
             {
                 Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
             }
@@ -68,6 +82,16 @@ namespace PixiEditor.Models.IO
             return path;
         }
 
+        private static string AppendExtension(string path, FileTypeDialogData data)
+        {
+            string ext = data.Extensions.First();
+            string filename = Path.GetFileName(path);
+            if (filename.Length + ext.Length > 255)
+                filename = filename.Substring(0, 255 - ext.Length);
+            filename += ext;
+            return Path.Combine(Path.GetDirectoryName(path), filename);
+        }
+
         public static FileType ParseImageFormat(string extension)
         {
             return SupportedFilesHelper.ParseImageFormat(extension);
@@ -79,7 +103,7 @@ namespace PixiEditor.Models.IO
         {
             encodersFactory[FileType.Png] = () => new PngBitmapEncoder();
             encodersFactory[FileType.Jpeg] = () => new JpegBitmapEncoder();
-            encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder(); 
+            encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder();
             encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
         }
 
@@ -90,14 +114,14 @@ namespace PixiEditor.Models.IO
         /// <param name="fileDimensions">Size of file.</param>
         public static void Export(WriteableBitmap bitmap, Size fileDimensions)
         {
-          ExportFileDialog info = new ExportFileDialog(fileDimensions);
+            ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
-          // If OK on dialog has been clicked
-          if (info.ShowDialog())
-          {
-            if(encodersFactory.ContainsKey(info.ChosenFormat))
-              SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
-          }
+            // If OK on dialog has been clicked
+            if (info.ShowDialog())
+            {
+                if (encodersFactory.ContainsKey(info.ChosenFormat))
+                    SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
+            }
         }
         public static void SaveAsGZippedBytes(string path, Surface surface)
         {

+ 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)
+    {
+    }
+}

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

@@ -0,0 +1,75 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+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 class JascFileParser : PaletteFileParser
+{
+    private static readonly string[] _supportedFileExtensions = new string[] { ".pal" };
+    public override string[] SupportedFileExtensions => _supportedFileExtensions;
+    public override string FileName => "Jasc Palette";
+    
+    private static async Task<PaletteFileData> ParseFile(string path)
+    { 
+        using var stream = File.OpenText(path);
+        
+        string fileContent = await stream.ReadToEndAsync();
+        string[] lines = fileContent.Split('\n');
+        string name = Path.GetFileNameWithoutExtension(path);
+        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 PaletteFileData(name, colors);
+        }
+
+        throw new JascFileException("Invalid JASC-PAL file.");
+    }
+
+    public static async Task<bool> SaveFile(string path, PaletteFileData data)
+    {
+        if (data is not { Colors.Length: > 0 }) return false;
+        
+        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;
+        }
+
+        await File.WriteAllTextAsync(path, fileContent);
+        return true;
+
+    }
+
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return new PaletteFileData("Corrupted", Array.Empty<SKColor>()) { IsCorrupted = true };
+        }
+    }
+
+    public override async Task Save(string path, PaletteFileData data) => await SaveFile(path, data);
+
+    private static bool ValidateFile(string fileType, string magicBytes)
+    {
+        return fileType.Length > 7 && fileType[..8].ToUpper() == "JASC-PAL" && magicBytes.Length > 3 && magicBytes[..4] == "0100";
+    }
+}

+ 46 - 0
PixiEditor/Models/IO/PaletteFileData.cs

@@ -0,0 +1,46 @@
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.IO
+{
+    public class PaletteFileData
+    {
+        public string Title { get; set; }
+        public SKColor[] Colors { get; set; }
+        public bool IsCorrupted { get; set; } = false;
+
+        public PaletteFileData(SKColor[] colors)
+        {
+            Colors = colors;
+            Title = "";
+        }
+
+        public PaletteFileData(List<string> colors)
+        {
+            Colors = new SKColor[colors.Count];
+            for (int i = 0; i < colors.Count; i++)
+            {
+                Colors[i] = SKColor.Parse(colors[i]);
+            }
+
+            Title = "";
+        }
+
+        public PaletteFileData(string title, SKColor[] colors)
+        {
+            Title = title;
+            Colors = colors;
+        }
+
+        public string[] GetHexColors()
+        {
+            string[] colors = new string[Colors.Length];
+            for (int i = 0; i < Colors.Length; i++)
+            {
+                colors[i] = Colors[i].ToString();
+            }
+            return colors;
+        }
+    }
+}

+ 12 - 0
PixiEditor/Models/IO/PaletteFileParser.cs

@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.IO
+{
+    public abstract class PaletteFileParser
+    {
+        public abstract Task<PaletteFileData> Parse(string path);
+        public abstract Task Save(string path, PaletteFileData data);
+        public abstract string FileName { get; }
+        public abstract string[] SupportedFileExtensions { get; }
+    }
+}

+ 44 - 0
PixiEditor/Models/Layers/Layer.cs

@@ -8,7 +8,13 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
 using System.Windows;
+using System.Windows.Media;
+using PixiEditor.Models.Dialogs;
 
 namespace PixiEditor.Models.Layers
 {
@@ -31,6 +37,7 @@ namespace PixiEditor.Models.Layers
 
         private string layerHighlightColor = "#666666";
 
+
         public Layer(string name, int maxWidth, int maxHeight)
         {
             Name = name;
@@ -693,5 +700,42 @@ namespace PixiEditor.Models.Layers
             Width = newWidth;
             Height = newHeight;
         }
+
+
+        public void ReplaceColor(SKColor oldColor, SKColor newColor)
+        {
+            if (LayerBitmap == null)
+            {
+                return;
+            }
+
+            int maxThreads = Environment.ProcessorCount;
+            int rowsPerThread = Height / maxThreads;
+
+            Parallel.For(0, maxThreads, i =>
+            {
+                int startRow = i * rowsPerThread;
+                int endRow = (i + 1) * rowsPerThread;
+                if (i == maxThreads - 1)
+                {
+                    endRow = Height;
+                }
+
+                for (int y = startRow; y < endRow; y++)
+                {
+                    for (int x = 0; x < Width; x++)
+                    {
+                        if (LayerBitmap.GetSRGBPixel(x, y) == oldColor)
+                        {
+                            LayerBitmap.SetSRGBPixelUnmanaged(x, y, newColor);
+                        }
+                    }
+                }
+            });
+
+            layerBitmap.SkiaSurface.Canvas.DrawPaint(new SKPaint { BlendMode = SKBlendMode.Dst });
+
+            InvokeLayerBitmapChange();
+        }
     }
 }

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

@@ -198,4 +198,4 @@ namespace PixiEditor.Models.Layers
             return guids;
         }
     }
-}
+}

+ 6 - 0
PixiEditor/Models/Processes/ProcessHelper.cs

@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using System.Diagnostics;
+using System.Security.Principal;
 
 namespace PixiEditor.Models.Processes
 {
@@ -22,5 +23,10 @@ namespace PixiEditor.Models.Processes
 
             return proc;
         }
+
+        public static bool IsRunningAsAdministrator()
+        {
+            return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
+        }
     }
 }

+ 1 - 1
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -91,4 +91,4 @@ namespace PixiEditor.Models.Tools
             _change = new StorageBasedChange(doc, new[] { new LayerChunk(doc.ActiveLayer, finalRect) });
         }
     }
-}
+}

+ 9 - 5
PixiEditor/Models/Tools/ToolSettings/Settings/BoolSetting.cs

@@ -2,23 +2,22 @@
 using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Data;
-using System.Windows.Media;
-
+using System.Windows.Media;
+
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
     public class BoolSetting : Setting<bool>
     {
-        public BoolSetting(string name, string label = "")
+        public BoolSetting(string name, string label = "")
             : this(name, false, label)
         {
         }
 
-        public BoolSetting(string name, bool isChecked, string label = "")
+        public BoolSetting(string name, bool isChecked, string label = "")
             : base(name)
         {
             Label = label;
             Value = isChecked;
-            SettingControl = GenerateCheckBox();
         }
 
         private Control GenerateCheckBox()
@@ -38,5 +37,10 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
 
             return checkBox;
         }
+
+        public override Control GenerateControl()
+        {
+            return GenerateCheckBox();
+        }
     }
 }

+ 22 - 16
PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs

@@ -1,42 +1,48 @@
-using System.Windows;
+using System.Windows;
+using System.Windows.Controls;
 using System.Windows.Data;
-using System.Windows.Interactivity;
+using System.Windows.Interactivity;
 using System.Windows.Media;
 using ColorPicker;
-using PixiEditor.Helpers.Behaviours;
-using PixiEditor.Views;
-
+using PixiEditor.Helpers.Behaviours;
+using PixiEditor.Views;
+
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
     public class ColorSetting : Setting<Color>
     {
-        public ColorSetting(string name, string label = "")
+        public ColorSetting(string name, string label = "")
             : base(name)
         {
             Label = label;
-            SettingControl = GenerateColorPicker();
             Value = Color.FromArgb(255, 255, 255, 255);
         }
 
         private ToolSettingColorPicker GenerateColorPicker()
         {
             var resourceDictionary = new ResourceDictionary();
-            resourceDictionary.Source = new System.Uri(
-                "pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml",
+            resourceDictionary.Source = new System.Uri(
+                "pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml",
                 System.UriKind.RelativeOrAbsolute);
-            ToolSettingColorPicker picker = new ToolSettingColorPicker
-            {
-                Style = (Style)resourceDictionary["DefaultColorPickerStyle"]
+            ToolSettingColorPicker picker = new ToolSettingColorPicker
+            {
+                Style = (Style)resourceDictionary["DefaultColorPickerStyle"]
             };
 
-            Binding binding = new Binding("Value")
+            Binding selectedColorBinding = new Binding("Value")
             {
                 Mode = BindingMode.TwoWay
             };
-            GlobalShortcutFocusBehavior behavor = new GlobalShortcutFocusBehavior();
-            Interaction.GetBehaviors(picker).Add(behavor);
-            picker.SetBinding(ToolSettingColorPicker.SelectedColorProperty, binding);
+
+            GlobalShortcutFocusBehavior behavior = new GlobalShortcutFocusBehavior();
+            Interaction.GetBehaviors(picker).Add(behavior);
+            picker.SetBinding(ToolSettingColorPicker.SelectedColorProperty, selectedColorBinding);
             return picker;
         }
+
+        public override Control GenerateControl()
+        {
+            return GenerateColorPicker();
+        }
     }
 }

+ 5 - 1
PixiEditor/Models/Tools/ToolSettings/Settings/DropdownSetting.cs

@@ -11,7 +11,6 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             : base(name)
         {
             Values = values;
-            SettingControl = GenerateDropdown();
             Value = ((ComboBox)SettingControl).Items[0];
             Label = label;
         }
@@ -45,5 +44,10 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
                 comboBox.Items.Add(item);
             }
         }
+
+        public override Control GenerateControl()
+        {
+            return GenerateDropdown();
+        }
     }
 }

+ 5 - 2
PixiEditor/Models/Tools/ToolSettings/Settings/EnumSetting.cs

@@ -51,11 +51,14 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             }
         }
 
+        public override Control GenerateControl()
+        {
+            return GenerateDropdown();
+        }
+
         public EnumSetting(string name, string label)
             : base(name)
         {
-            SettingControl = GenerateDropdown();
-
             Label = label;
         }
 

+ 7 - 2
PixiEditor/Models/Tools/ToolSettings/Settings/FloatSetting.cs

@@ -1,4 +1,5 @@
-using System.Windows.Data;
+using System.Windows.Controls;
+using System.Windows.Data;
 using PixiEditor.Views;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
@@ -17,7 +18,6 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             Value = initialValue;
             Min = min;
             Max = max;
-            SettingControl = GenerateNumberInput();
         }
 
         public float Min { get; set; }
@@ -40,5 +40,10 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             numbrInput.SetBinding(NumberInput.ValueProperty, binding);
             return numbrInput;
         }
+
+        public override Control GenerateControl()
+        {
+            return GenerateNumberInput();
+        }
     }
 }

+ 2 - 0
PixiEditor/Models/Tools/ToolSettings/Settings/Setting.cs

@@ -64,5 +64,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
         public bool HasLabel => !string.IsNullOrEmpty(Label);
 
         public Control SettingControl { get; set; }
+
+        public abstract Control GenerateControl();
     }
 }

+ 14 - 9
PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs

@@ -1,16 +1,16 @@
-using PixiEditor.Views;
+using PixiEditor.Views;
 using System.Windows;
+using System.Windows.Controls;
 using System.Windows.Data;
-
+
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
     public class SizeSetting : Setting<int>
     {
-        public SizeSetting(string name, string label = null)
+        public SizeSetting(string name, string label = null)
             : base(name)
         {
             Value = 1;
-            SettingControl = GenerateTextBox();
             Label = label;
         }
 
@@ -23,14 +23,19 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
                 VerticalAlignment = VerticalAlignment.Center,
                 MaxSize = 9999,
                 IsEnabled = true
-            };
-
-            Binding binding = new Binding("Value")
-            {
-                Mode = BindingMode.TwoWay,
+            };
+
+            Binding binding = new Binding("Value")
+            {
+                Mode = BindingMode.TwoWay,
             };
             tb.SetBinding(SizeInput.SizeProperty, binding);
             return tb;
         }
+
+        public override Control GenerateControl()
+        {
+            return GenerateTextBox();
+        }
     }
 }

+ 1 - 1
PixiEditor/Models/Tools/ToolSettings/Toolbars/BasicShapeToolbar.cs

@@ -7,7 +7,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
         public BasicShapeToolbar()
         {
             Settings.Add(new BoolSetting("Fill", "Fill shape: "));
-            Settings.Add(new ColorSetting("FillColor", "Fill color"));
+            Settings.Add(new ColorSetting("FillColor",  "Fill color"));
         }
     }
 }

+ 11 - 0
PixiEditor/Models/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -11,6 +11,17 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
         private static readonly List<Setting> SharedSettings = new List<Setting>();
 
         public ObservableCollection<Setting> Settings { get; set; } = new ObservableCollection<Setting>();
+        public bool SettingsGenerated { get; private set; }
+
+        public void GenerateSettings()
+        {
+            foreach (var setting in Settings)
+            {
+                setting.SettingControl = setting.GenerateControl();
+            }
+
+            SettingsGenerated = true;
+        }
 
         /// <summary>
         ///     Gets setting in toolbar by name.

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

@@ -386,4 +386,4 @@ namespace PixiEditor.Models.Undo
             }
         }
     }
-}
+}

+ 6 - 0
PixiEditor/Models/UserPreferences/PreferencesConstants.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.UserPreferences;
+
+public static class PreferencesConstants
+{
+    public const string FavouritePalettes = "FavouritePalettes";
+}

+ 16 - 6
PixiEditor/Models/UserPreferences/PreferencesSettings.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.UserPreferences
@@ -136,9 +137,7 @@ namespace PixiEditor.Models.UserPreferences
 
             try
             {
-                return Preferences.ContainsKey(name)
-                        ? (T)Convert.ChangeType(Preferences[name], typeof(T))
-                        : fallbackValue;
+                return GetValue(Preferences, name, fallbackValue);
             }
             catch (InvalidCastException)
             {
@@ -163,9 +162,7 @@ namespace PixiEditor.Models.UserPreferences
 
             try
             {
-                return LocalPreferences.ContainsKey(name)
-                    ? (T)Convert.ChangeType(LocalPreferences[name], typeof(T))
-                    : fallbackValue;
+                return GetValue(LocalPreferences, name, fallbackValue);
             }
             catch (InvalidCastException)
             {
@@ -176,6 +173,19 @@ namespace PixiEditor.Models.UserPreferences
             }
         }
 
+        private T? GetValue<T>(Dictionary<string, object> dict, string name, T? fallbackValue)
+        {
+            if (!dict.ContainsKey(name)) return fallbackValue;
+            var preference = dict[name];
+            if(typeof(T) == preference.GetType()) return (T)preference;
+            if (preference.GetType() == typeof(JArray))
+            {
+                return ((JArray)preference).ToObject<T>();
+            }
+
+            return (T)Convert.ChangeType(dict[name], typeof(T));
+        }
+
 #nullable disable
 
         private static string GetPathToSettings(Environment.SpecialFolder folder, string fileName)

+ 44 - 7
PixiEditor/PixiEditor.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
 	<PropertyGroup>
 		<OutputType>WinExe</OutputType>
@@ -135,6 +135,8 @@
 
 	<ItemGroup>
 		<None Remove="Images\AnchorDot.png" />
+		<None Remove="Images\Arrow-right.png" />
+		<None Remove="Images\Check-square.png" />
 		<None Remove="Images\CheckerTile.png" />
 		<None Remove="Images\ChevronDown.png" />
 		<None Remove="Images\Commands\PixiEditor\Clipboard\Copy.png" />
@@ -144,11 +146,18 @@
 		<None Remove="Images\Commands\PixiEditor\Document\CenterContent.png" />
 		<None Remove="Images\Commands\PixiEditor\Document\ResizeCanvas.png" />
 		<None Remove="Images\Commands\PixiEditor\Document\ResizeDocument.png" />
+		<None Remove="Images\ChevronsDown.png" />
+		<None Remove="Images\CopyAdd.png" />
+		<None Remove="Images\Database.png" />
 		<None Remove="Images\DiagonalRed.png" />
+		<None Remove="Images\Download.png" />
+		<None Remove="Images\Edit.png" />
 		<None Remove="Images\Eye-off.png" />
 		<None Remove="Images\Eye.png" />
 		<None Remove="Images\Folder-add.png" />
 		<None Remove="Images\Folder.png" />
+		<None Remove="Images\Globe.png" />
+		<None Remove="Images\hard-drive.png" />
 		<None Remove="Images\Layer-add.png" />
 		<None Remove="Images\MoveImage.png" />
 		<None Remove="Images\MoveViewportImage.png" />
@@ -157,6 +166,9 @@
 		<None Remove="Images\PixiEditorLogo.png" />
 		<None Remove="Images\PixiParserLogo.png" />
 		<None Remove="Images\Placeholder.png" />
+		<None Remove="Images\Processing.gif" />
+		<None Remove="Images\Replace.png" />
+		<None Remove="Images\Search.png" />
 		<None Remove="Images\SelectImage.png" />
 		<None Remove="Images\SocialMedia\DiscordIcon.png" />
 		<None Remove="Images\SocialMedia\DonateIcon.png" />
@@ -190,14 +202,15 @@
 		</None>
 	</ItemGroup>
 	<ItemGroup>
-		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.1" />
+		<PackageReference Include="CLSEncoderDecoder" Version="1.0.0" />
+		<PackageReference Include="Dirkster.AvalonDock" Version="4.70.1" />
 		<PackageReference Include="ByteSize" Version="2.1.1" />
 		<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
 		<PackageReference Include="Expression.Blend.Sdk">
 			<Version>1.0.2</Version>
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
-		<PackageReference Include="Hardware.Info" Version="1.1.1.1" />
+		<PackageReference Include="Hardware.Info" Version="10.0.0" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
@@ -205,17 +218,20 @@
 		</PackageReference>
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
 		<PackageReference Include="OneOf" Version="3.0.216" />
-		<PackageReference Include="PixiEditor.ColorPicker" Version="3.2.0" />
-		<PackageReference Include="PixiEditor.Parser" Version="2.1.0.2" />
+		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
+		<PackageReference Include="PixiEditor.Parser" Version="2.1.0.3" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.1.0" />
-		<PackageReference Include="SkiaSharp" Version="2.80.3" />
+		<PackageReference Include="SkiaSharp" Version="2.88.0" />
 		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
+		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
 		<PackageReference Include="WriteableBitmapEx">
 			<Version>1.6.8</Version>
 		</PackageReference>
 	</ItemGroup>
 	<ItemGroup>
 		<Resource Include="Images\AnchorDot.png" />
+		<Resource Include="Images\Arrow-right.png" />
+		<Resource Include="Images\Check-square.png" />
 		<Resource Include="Images\CheckerTile.png" />
 		<Resource Include="Images\ChevronDown.png" />
 		<Resource Include="Images\Commands\PixiEditor\Clipboard\Copy.png" />
@@ -226,17 +242,27 @@
 		<Resource Include="Images\Commands\PixiEditor\Document\ResizeCanvas.png" />
 		<Resource Include="Images\Commands\PixiEditor\Document\ResizeDocument.png" />
 		<Resource Include="Images\Commands\PixiEditor\File\New.png" />
+		<Resource Include="Images\ChevronsDown.png" />
+		<Resource Include="Images\CopyAdd.png" />
+		<Resource Include="Images\Database.png" />
 		<Resource Include="Images\DiagonalRed.png" />
+		<Resource Include="Images\Download.png" />
+		<Resource Include="Images\Edit.png" />
 		<Resource Include="Images\Eye-off.png" />
 		<Resource Include="Images\Eye.png" />
 		<Resource Include="Images\Folder-add.png" />
 		<Resource Include="Images\Folder.png" />
+		<Resource Include="Images\Globe.png" />
+		<Resource Include="Images\hard-drive.png" />
 		<Resource Include="Images\Layer-add.png" />
 		<Resource Include="Images\penMode.png" />
 		<Resource Include="Images\PixiBotLogo.png" />
 		<Resource Include="Images\PixiEditorLogo.png" />
 		<Resource Include="Images\PixiParserLogo.png" />
 		<Resource Include="Images\Placeholder.png" />
+		<Resource Include="Images\Processing.gif" />
+		<Resource Include="Images\Replace.png" />
+		<Resource Include="Images\Search.png" />
 		<Resource Include="Images\SocialMedia\DiscordIcon.png" />
 		<Resource Include="Images\SocialMedia\DonateIcon.png" />
 		<Resource Include="Images\SocialMedia\GitHubIcon.png" />
@@ -288,6 +314,18 @@
 		<Resource Include="Images\Commands\PixiEditor\Document\ClipCanvas.png" />
 		<None Remove="Fonts\feather.ttf" />
 		<Resource Include="Fonts\feather.ttf" />
+		<None Remove="Images\Load.png" />
+		<Resource Include="Images\Load.png" />
+		<None Remove="Images\Plus-square.png" />
+		<Resource Include="Images\Plus-square.png" />
+		<None Remove="Images\Save.png" />
+		<Resource Include="Images\Save.png" />
+		<None Remove="Images\Star.png" />
+		<Resource Include="Images\Star.png" />
+		<None Remove="Images\Star-filled.png" />
+		<Resource Include="Images\Star-filled.png" />
+		<None Remove="Images\Shuffle.png" />
+		<Resource Include="Images\Shuffle.png" />
 	</ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">
@@ -319,5 +357,4 @@
 			<LastGenOutput>Settings.Designer.cs</LastGenOutput>
 		</None>
 	</ItemGroup>
-
 </Project>

+ 2 - 2
PixiEditor/Properties/AssemblyInfo.cs

@@ -50,5 +50,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.1.8.0")]
-[assembly: AssemblyFileVersion("0.1.8.0")]
+[assembly: AssemblyVersion("0.1.9.0")]
+[assembly: AssemblyFileVersion("0.1.9.0")]

+ 2 - 2
PixiEditor/Styles/ComboBoxDarkStyle.xaml

@@ -267,10 +267,10 @@
                     </Border>
                     <ControlTemplate.Triggers>
                         <Trigger Property="IsHighlighted" Value="True">
-                            <Setter Property="Background" TargetName="Bd" Value="Gray" />
+                            <Setter Property="Background" TargetName="Bd" Value="{StaticResource MainColor}" />
                         </Trigger>
                         <Trigger Property="IsEnabled" Value="False">
-                            <Setter Property="Foreground" Value="Gray" />
+                            <Setter Property="Foreground" Value="{StaticResource MainColor}" />
                         </Trigger>
                     </ControlTemplate.Triggers>
                 </ControlTemplate>

+ 1 - 1
PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -11,7 +11,7 @@
                 <ControlTemplate TargetType="CheckBox">
                     <BulletDecorator Background="Transparent">
                         <BulletDecorator.Bullet>
-                            <Border x:Name="Border" Width="18" Height="18" CornerRadius="2" Background="#FF1B1B1B"
+                            <Border x:Name="Border" Width="20" Height="20" CornerRadius="2.5" Background="#FF1B1B1B"
                                     BorderThickness="1">
                                 <Path Width="9" Height="9" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="{StaticResource UIElementBlue}" StrokeThickness="1.5" Data="M 0 4 L 3 8 8 0" />
                             </Border>

+ 1 - 0
PixiEditor/Styles/ThemeColors.xaml

@@ -2,6 +2,7 @@
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
     <SolidColorBrush x:Key="PixiRed" Color="#e3002d" />
+    <SolidColorBrush x:Key="AccentRed" Color="#B00022"/>
     <SolidColorBrush x:Key="MainColor" Color="#2D2D30" />
     <SolidColorBrush x:Key="AccentColor" Color="#252525" />
     <SolidColorBrush x:Key="DarkerAccentColor" Color="#202020" />

+ 14 - 3
PixiEditor/Styles/ThemeStyle.xaml

@@ -51,6 +51,17 @@
         </Setter>
     </Style>
 
+    <Style TargetType="Hyperlink">
+        <Setter Property="TextBlock.TextDecorations" Value="{x:Null}" />
+        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
+        <Setter Property="Foreground" Value="White"/>
+        <Style.Triggers>
+            <Trigger Property="IsMouseOver" Value="True">
+                <Setter Property="TextBlock.TextDecorations" Value="Underline" />
+            </Trigger>
+        </Style.Triggers>
+    </Style>
+
     <Style TargetType="Button" x:Key="DarkRoundButton" BasedOn="{StaticResource BaseDarkButton}">
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="Background" Value="#303030" />
@@ -120,6 +131,7 @@
     <Style TargetType="Button" x:Key="ImageButtonStyle">
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="Focusable" Value="False" />
+        <Setter Property="Cursor" Value="Hand" />
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="Button">
@@ -141,8 +153,7 @@
            BasedOn="{StaticResource BaseDarkButton}">
         <Setter Property="TextBlock.FontFamily" Value="Segoe MDL2 Assets"/>
         <Setter Property="TextBlock.FontSize" Value="15"/>
-        <Setter Property="Focusable" Value="False" />
-        <Setter Property="TextBlock.Width" Value="30"/>
+        <Setter Property="Width" Value="30"/>
 
         <Style.Triggers>
             <Trigger Property="IsEnabled" Value="True">
@@ -256,7 +267,7 @@
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="{x:Type ContextMenu}">
-                    <Border Background="#202020" BorderBrush="Black" BorderThickness="1" Opacity="0.96">
+                    <Border Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
                         <StackPanel ClipToBounds="True" Orientation="Vertical" IsItemsHost="True" />
                     </Border>
                 </ControlTemplate>

+ 1 - 0
PixiEditor/Styles/Titlebar.xaml

@@ -29,6 +29,7 @@
 
     <Style x:Key="MinimizeButtonStyle" TargetType="Button" BasedOn="{StaticResource CaptionButtonStyle}">
         <Setter Property="Content" Value="&#xE949;" />
+        <Setter Property="Margin" Value="0,1,0,0" />
     </Style>
 
     <Style x:Key="MaximizeButtonStyle" TargetType="Button" BasedOn="{StaticResource CaptionButtonStyle}">

+ 185 - 3
PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -1,12 +1,48 @@
 using PixiEditor.Models.Commands.Attributes;
 using SkiaSharp;
 using System.Windows.Input;
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.IO;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.ExternalServices;
+using PixiEditor.Models.Undo;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
     public class ColorsViewModel : SubViewModel<ViewModelMain>
     {
+        public RelayCommand<(SKColor, SKColor)> ReplaceColorsCommand { get; set; }
+
+        public RelayCommand SwapColorsCommand { get; set; }
+
+        public RelayCommand SelectColorCommand { get; set; }
+
+        public RelayCommand RemoveSwatchCommand { get; set; }
+
+        public RelayCommand<List<string>> ImportPaletteCommand { get; set; }
+
+        public RelayCommand<int> SelectPaletteColorCommand { get; set; }
+
+        public WpfObservableRangeCollection<PaletteFileParser> PaletteParsers { get; private set; }
+        public WpfObservableRangeCollection<PaletteListDataSource> PaletteDataSources { get; private set; }
+
+        public LocalPalettesFetcher LocalPaletteFetcher => _localPaletteFetcher ??=
+            (LocalPalettesFetcher)PaletteDataSources.FirstOrDefault(x => x is LocalPalettesFetcher)!;
+
         private SKColor primaryColor = SKColors.Black;
+        private LocalPalettesFetcher _localPaletteFetcher;
 
         public SKColor PrimaryColor // Primary color, hooked with left mouse button
         {
@@ -40,14 +76,147 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public ColorsViewModel(ViewModelMain owner)
             : base(owner)
         {
+            SelectColorCommand = new RelayCommand(SelectColor);
+            RemoveSwatchCommand = new RelayCommand(RemoveSwatch);
+            SwapColorsCommand = new RelayCommand(SwapColors);
+            SelectPaletteColorCommand = new RelayCommand<int>(SelectPaletteColor);
+            ImportPaletteCommand = new RelayCommand<List<string>>(ImportPalette, CanImportPalette);
+            ReplaceColorsCommand = new RelayCommand<(SKColor oldColor, SKColor newColor)>(ReplaceColors, Owner.DocumentIsNotNull);
+            Owner.OnStartupEvent += OwnerOnStartupEvent;
+        }
+
+        private bool CanImportPalette(List<string> paletteColors)
+        {
+            return Owner.DocumentIsNotNull(paletteColors) && paletteColors.Count > 0;
+        }
+
+        private void ReplaceColors((SKColor oldColor, SKColor newColor) colors)
+        {
+            Document activeDocument = Owner.BitmapManager?.ActiveDocument;
+            if (activeDocument != null)
+            {
+                activeDocument.ReplaceColor(colors.oldColor, colors.newColor);
+                ReplacePaletteColor(colors, activeDocument);
+                activeDocument.UndoManager.AddUndoChange(new Change(
+                    ReplacePaletteColorProcess,
+                    new object[] { (colors.newColor, colors.oldColor), activeDocument },
+                    ReplacePaletteColorProcess,
+                    new object[] { colors, activeDocument }));
+                activeDocument.UndoManager.SquashUndoChanges(2, $"Replace color {colors.oldColor} with {colors.newColor}");
+            }
+
+        }
+
+        private static void ReplacePaletteColorProcess(object[] args)
+        {
+            (SKColor oldColor, SKColor newColor) colors = ((SKColor, SKColor))args[0];
+            Document activeDocument = (Document)args[1];
+
+            ReplacePaletteColor(colors, activeDocument);
+        }
+
+        private static void ReplacePaletteColor((SKColor oldColor, SKColor newColor) colors, Document activeDocument)
+        {
+            int oldIndex = activeDocument.Palette.IndexOf(colors.oldColor);
+            if (oldIndex != -1)
+            {
+                activeDocument.Palette[oldIndex] = colors.newColor;
+            }
+        }
+
+        private async void OwnerOnStartupEvent(object sender, EventArgs e)
+        {
+            await ImportLospecPalette();
+        }
+
+        private async Task ImportLospecPalette()
+        {
+            var args = StartupArgs.Args;
+            var lospecPaletteArg = args.FirstOrDefault(x => x.StartsWith("lospec-palette://"));
+
+            if (lospecPaletteArg != null)
+            {
+                var browser = PalettesBrowser.Open(PaletteDataSources, ImportPaletteCommand,
+                    new WpfObservableRangeCollection<SKColor>());
+
+                browser.IsFetching = true;
+                var palette = await LospecPaletteFetcher.FetchPalette(lospecPaletteArg.Split(@"://")[1].Replace("/", ""));
+                if (palette != null)
+                {
+                    if (LocalPalettesFetcher.PaletteExists(palette.Name))
+                    {
+                        var consent = ConfirmationDialog.Show(
+                            $"Palette '{palette.Name}' already exists, do you want to overwrite it?", "Palette exists");
+                        if (consent == ConfirmationType.No)
+                        {
+                            palette.Name = LocalPalettesFetcher.GetNonExistingName(palette.Name);
+                        }
+                        else if (consent == ConfirmationType.Canceled)
+                        {
+                            browser.IsFetching = false;
+                            return;
+                        }
+                    }
+
+                    await SavePalette(palette, browser);
+                }
+                else
+                {
+                    await browser.UpdatePaletteList();
+                }
+            }
+        }
+
+        private async Task SavePalette(Palette palette, PalettesBrowser browser)
+        {
+            palette.FileName = $"{palette.Name}.pal";
+            
+            await LocalPaletteFetcher.SavePalette(
+                palette.FileName,
+                palette.Colors.Select(SKColor.Parse).ToArray());
+
+            await browser.UpdatePaletteList();
+            if (browser.SortedResults.Any(x => x.FileName == palette.FileName))
+            {
+                int indexOfImported =
+                    browser.SortedResults.IndexOf(browser.SortedResults.First(x => x.FileName == palette.FileName));
+                browser.SortedResults.Move(indexOfImported, 0);
+            }
+            else
+            {
+                browser.SortedResults.Insert(0, palette);
+            }
+        }
+
+        public void ImportPalette(List<string> palette)
+        {
+            var doc = Owner.BitmapManager.ActiveDocument;
+            if (doc == null) return;
+
+            if (ConfirmationDialog.Show("Replace current palette with selected one?", "Replace current palette") == ConfirmationType.Yes)
+            {
+                if (doc.Palette == null)
+                {
+                    doc.Palette = new WpfObservableRangeCollection<SKColor>();
+                }
+
+                doc.Palette.ReplaceRange(palette.Select(x => SKColor.Parse(x)));
+            }
+        }
+
+        private void SelectPaletteColor(int index)
+        {
+            var document = Owner.BitmapManager.ActiveDocument;
+            if(document.Palette != null && document.Palette.Count > index)
+            {
+                PrimaryColor = document.Palette[index];
+            }
         }
 
         [Command.Basic("PixiEditor.Colors.Swap", "Swap colors", "Swap primary and secondary colors", Key = Key.X)]
         public void SwapColors(object parameter)
         {
-            var tmp = PrimaryColor;
-            PrimaryColor = SecondaryColor;
-            SecondaryColor = tmp;
+            (PrimaryColor, SecondaryColor) = (SecondaryColor, PrimaryColor);
         }
 
         public void AddSwatch(SKColor color)
@@ -72,5 +241,18 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             PrimaryColor = color;
         }
+
+        public void SetupPaletteParsers(IServiceProvider services)
+        {
+            PaletteParsers = new WpfObservableRangeCollection<PaletteFileParser>(services.GetServices<PaletteFileParser>());
+            PaletteDataSources = new WpfObservableRangeCollection<PaletteListDataSource>(services.GetServices<PaletteListDataSource>());
+            var parsers = PaletteParsers.ToList();
+
+            foreach (var dataSource in PaletteDataSources)
+            {
+                dataSource.AvailableParsers = parsers;
+                dataSource.Initialize();
+            }
+        }
     }
 }

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

@@ -5,6 +5,8 @@ using PixiEditor.Models.UserPreferences;
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
+using System.Windows.Media;
+using SkiaSharp;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {

+ 3 - 0
PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs

@@ -3,6 +3,9 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using System.Windows.Input;
+using SkiaSharp;
+using System;
+using System.Linq;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {

+ 6 - 4
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -13,6 +13,7 @@ using System.IO;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -144,13 +145,14 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         {
-            var args = Environment.GetCommandLineArgs();
-            var file = args.Last();
-            if (Importer.IsSupportedFile(file) && File.Exists(file))
+            var args = StartupArgs.Args;
+            var file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
+            if (file != null)
             {
                 Open(file);
             }
-            else if (Owner.BitmapManager.Documents.Count == 0 || !args.Contains("--crash"))
+            else if ((Owner.BitmapManager.Documents.Count == 0
+                     || !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
             {
                 if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 {

+ 74 - 0
PixiEditor/ViewModels/SubViewModels/Main/RegistryViewModel.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Diagnostics;
+using System.Security.AccessControl;
+using System.Windows;
+using Microsoft.Win32;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Processes;
+
+namespace PixiEditor.ViewModels.SubViewModels.Main
+{
+    public class RegistryViewModel : SubViewModel<ViewModelMain>
+    {
+        public RegistryViewModel(ViewModelMain owner) : base(owner)
+        {
+            Owner.OnStartupEvent += OwnerOnStartupEvent;
+        }
+
+        private void OwnerOnStartupEvent(object sender, EventArgs e)
+        {
+            // Check if lospec-palette is associated in registry
+
+            if (!LospecPaletteIsAssociated())
+            {
+                // Associate lospec-palette URL protocol
+                AssociateLospecPalette();
+            }
+        }
+
+        private void AssociateLospecPalette()
+        {
+            if (!ProcessHelper.IsRunningAsAdministrator())
+            {
+                ProcessHelper.RunAsAdmin(Process.GetCurrentProcess().MainModule?.FileName);
+                Application.Current.Shutdown();
+            }
+            else
+            {
+                AssociateLospecPaletteInRegistry();
+            }
+        }
+
+        private void AssociateLospecPaletteInRegistry()
+        {
+            try
+            {
+                using RegistryKey key = Registry.ClassesRoot.CreateSubKey("lospec-palette");
+
+                key.SetValue("", "PixiEditor");
+                key.SetValue("URL Protocol", "");
+
+                // Create a new key
+                using RegistryKey shellKey = key.CreateSubKey("shell");
+                // Create a new key
+                using RegistryKey openKey = shellKey.CreateSubKey("open");
+                // Create a new key
+                using RegistryKey commandKey = openKey.CreateSubKey("command");
+                // Set the default value of the key
+                commandKey.SetValue("", $"\"{Process.GetCurrentProcess().MainModule?.FileName}\" \"%1\"");
+            }
+            catch
+            {
+                NoticeDialog.Show("Failed to associate lospec-palette protocol", "Error");
+            }
+        }
+
+        private bool LospecPaletteIsAssociated()
+        {
+            // Check if HKEY_CLASSES_ROOT\lospec-palette is present
+
+            RegistryKey lospecPaletteKey = Registry.ClassesRoot.OpenSubKey("lospec-palette", RegistryRights.ReadKey);
+            return lospecPaletteKey != null;
+        }
+    }
+}

+ 6 - 0
PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -86,6 +86,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void SetActiveTool(Tool tool)
         {
             if (ActiveTool == tool) return;
+
+            if (!tool.Toolbar.SettingsGenerated)
+            {
+                tool.Toolbar.GenerateSettings();
+            }
+
             ActiveToolIsTransient = false;
             bool shareToolbar = IPreferences.Current.GetPreference<bool>("EnableSharedToolbar");
             if (ActiveTool != null)

+ 27 - 1
PixiEditor/ViewModels/ViewModelMain.cs

@@ -20,6 +20,7 @@ using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Main;
 using PixiEditor.Views.Dialogs;
+using SkiaSharp;
 
 namespace PixiEditor.ViewModels
 {
@@ -79,6 +80,8 @@ namespace PixiEditor.ViewModels
         public WindowViewModel WindowSubViewModel { get; set; }
 
         public SearchViewModel SearchSubViewModel { get; set; }
+        
+        public RegistryViewModel RegistrySubViewModel { get; set; }
 
         public IPreferences Preferences { get; set; }
 
@@ -136,7 +139,6 @@ namespace PixiEditor.ViewModels
             FileSubViewModel = services.GetService<FileViewModel>();
             ToolsSubViewModel = services.GetService<ToolsViewModel>();
             ToolsSubViewModel.SelectedToolChanged += BitmapManager_SelectedToolChanged;
-            ToolsSubViewModel?.SetupTools(services);
 
             IoSubViewModel = services.GetService<IoViewModel>();
             LayersSubViewModel = services.GetService<LayersViewModel>();
@@ -144,6 +146,10 @@ namespace PixiEditor.ViewModels
             UndoSubViewModel = services.GetService<UndoViewModel>();
             ViewportSubViewModel = services.GetService<ViewportViewModel>();
             ColorsSubViewModel = services.GetService<ColorsViewModel>();
+            ColorsSubViewModel?.SetupPaletteParsers(services);
+
+            ToolsSubViewModel?.SetupTools(services);
+
             DocumentSubViewModel = services.GetService<DocumentViewModel>();
             DiscordViewModel = services.GetService<DiscordViewModel>();
             UpdateSubViewModel = services.GetService<UpdateViewModel>();
@@ -151,7 +157,21 @@ namespace PixiEditor.ViewModels
 
             WindowSubViewModel = services.GetService<WindowViewModel>();
             StylusSubViewModel = services.GetService<StylusViewModel>();
+            RegistrySubViewModel = services.GetService<RegistryViewModel>();
+            AddDebugOnlyViewModels();
+            AddReleaseOnlyViewModels();
+            
+            Shortcut[] colorShortcuts = new Shortcut[10];
+            colorShortcuts[9] = new Shortcut(
+                Key.D0, ColorsSubViewModel.SelectPaletteColorCommand, 9);
+            for (int i = 0; i < colorShortcuts.Length - 1; i++)
+            {
+                //35 is a D1 key integer value
+                colorShortcuts[i] = new Shortcut((Key)35 + i, ColorsSubViewModel.SelectPaletteColorCommand, i);
+            }
 
+            ShortcutController.ShortcutGroups.Add(new ShortcutGroup("Palette Colors", colorShortcuts));
+            
             MiscSubViewModel = services.GetService<MiscViewModel>();
 
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
@@ -180,6 +200,12 @@ namespace PixiEditor.ViewModels
         {
             return BitmapManager.ActiveDocument != null;
         }
+
+        public bool DocumentIsNotNull((SKColor oldColor, SKColor newColor) obj)
+        {
+            return DocumentIsNotNull(null);
+        }
+
         public void CloseWindow(object property)
         {
             if (!(property is CancelEventArgs))

+ 11 - 5
PixiEditor/Views/Dialogs/ExportFilePopup.xaml

@@ -8,7 +8,7 @@
         xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
         mc:Ignorable="d" BorderBrush="Black" BorderThickness="1"
-        Title="SaveFilePopup" Height="250" Width="300" WindowStyle="None" MinHeight="250" MinWidth="300"
+        Title="SaveFilePopup" WindowStyle="None" Height="330" Width="310" MinHeight="330" MinWidth="310"
         WindowStartupLocation="CenterScreen" Name="saveFilePopup">
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
@@ -33,10 +33,16 @@
                     Margin="15" Style="{StaticResource DarkRoundButton}" Content="Export" Command="{Binding OkCommand}"
                     CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
 
-        <local:SizePicker Width="230" Height="125" Margin="0,30,0,0"
-            x:Name="sizePicker"
-            ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
-            ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
+        <Grid HorizontalAlignment="Center" Margin="0,30,0,0" Background="{StaticResource MainColor}"
+                    VerticalAlignment="Top" Grid.Row="1" Width="240" Height="205">
+            <local:SizePicker Width="240" Height="180" Margin="0,0,0,25"
+                x:Name="sizePicker"
+                SizeUnitSelectionVisibility="Visible"
+                ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
+                ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
+            <TextBlock Foreground="Snow" Margin="10,5" TextWrapping="Wrap" VerticalAlignment="Bottom" TextAlignment="Center" 
+                       d:Text="If you want to share the image, try 400% for the best clarity" Text="{Binding SizeHint, ElementName=saveFilePopup}"/>
+        </Grid>
 
     </DockPanel>
 </Window>

+ 27 - 4
PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs

@@ -1,12 +1,13 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
-using System.Drawing.Imaging;
+using System;
+using System.ComponentModel;
 using System.Windows;
 using System.Windows.Input;
 
 namespace PixiEditor.Views
 {
-    public partial class ExportFilePopup : Window
+    public partial class ExportFilePopup : Window, INotifyPropertyChanged
     {
         public static readonly DependencyProperty SaveHeightProperty =
             DependencyProperty.Register("SaveHeight", typeof(int), typeof(ExportFilePopup), new PropertyMetadata(32));
@@ -17,6 +18,12 @@ namespace PixiEditor.Views
 
         private readonly SaveFilePopupViewModel dataContext = new SaveFilePopupViewModel();
 
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        private int imageWidth;
+        private int imageHeight;
+        public string SizeHint => $"If you want to share the image, try {GetBestPercentage()}% for the best clarity";
+
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;
@@ -27,14 +34,30 @@ namespace PixiEditor.Views
             SystemCommands.CloseWindow(this);
         }
 
-        public ExportFilePopup()
+        public ExportFilePopup(int imageWidth, int imageHeight)
         {
+            this.imageWidth = imageWidth;
+            this.imageHeight = imageHeight;
+
             InitializeComponent();
             Owner = Application.Current.MainWindow;
             DataContext = dataContext;
             Loaded += (_, _) => sizePicker.FocusWidthPicker();
+
+            SaveWidth = imageWidth;
+            SaveHeight = imageHeight;
         }
 
+        private int GetBestPercentage()
+        {
+            int maxDim = Math.Max(imageWidth, imageWidth);
+            for (int i = 16; i >= 1; i--)
+            {
+                if (maxDim * i <= 1280)
+                    return i * 100;
+            }
+            return 100;
+        }
 
         public int SaveWidth
         {
@@ -55,7 +78,7 @@ namespace PixiEditor.Views
             set => dataContext.FilePath = value;
         }
 
-        public FileType SaveFormat 
+        public FileType SaveFormat
         {
             get => dataContext.ChosenFormat;
             set => dataContext.ChosenFormat = value;

+ 140 - 0
PixiEditor/Views/Dialogs/PalettesBrowser.xaml

@@ -0,0 +1,140 @@
+<Window
+             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.Palettes" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" 
+             xmlns:gif="http://wpfanimatedgif.codeplex.com" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" xmlns:views="clr-namespace:PixiEditor.Views"
+             xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+             xmlns:PixiEditor="clr-namespace:PixiEditor"
+             xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
+             x:Class="PixiEditor.Views.Dialogs.PalettesBrowser"
+             mc:Ignorable="d"
+             Title="Palettes Browser" WindowStartupLocation="CenterScreen" MinWidth="200" Height="600" Width="850" WindowStyle="None"
+             x:Name="palettesBrowser">
+    <Window.Resources>
+        <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
+    </Window.Resources>
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource AccentColor}" FocusVisualStyle="{x:Null}" Focusable="True" MouseDown="Grid_MouseDown">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="45"/>
+            <RowDefinition Height="1*"/>
+        </Grid.RowDefinitions>
+
+        <dialogs:DialogTitleBar TitleText="Palette Browser" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
+
+        <DockPanel Background="{StaticResource MainColor}" Grid.Row="1">
+            <StackPanel HorizontalAlignment="Left" Margin="10" Orientation="Horizontal" VerticalAlignment="Center">
+                <Label Content="Sort by:" Style="{StaticResource BaseLabel}" VerticalAlignment="Center"/>
+                <ComboBox x:Name="sortingComboBox" VerticalAlignment="Center" SelectionChanged="SortingComboBox_SelectionChanged">
+                    <ComboBoxItem IsSelected="True" Content="Default"/>
+                    <ComboBoxItem Content="Alphabetical"/>
+                    <ComboBoxItem Content="Color Count"/>
+                </ComboBox>
+                <ToggleButton Margin="10 0 0 0" x:Name="toggleBtn"
+                              IsChecked="{Binding SortAscending, ElementName=palettesBrowser}"
+                              Focusable="False"
+                              Style="{StaticResource PixiEditorDockThemeToolButtonStyle}">
+                    <Image Width="24" Height="24" Source="/Images/ChevronsDown.png">
+                        <Image.Style>
+                            <Style TargetType="{x:Type Image}">
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding IsChecked, ElementName=toggleBtn}" Value="true">
+                                        <Setter Property="RenderTransform">
+                                            <Setter.Value>
+                                                <RotateTransform Angle="180" CenterX="11.5" CenterY="11.5"/>
+                                            </Setter.Value>
+                                        </Setter>
+                                        <Setter Property="ToolTip" Value="Ascending"/>
+                                    </DataTrigger>
+                                    <DataTrigger Binding="{Binding IsChecked, ElementName=toggleBtn}" Value="false">
+                                        <Setter Property="ToolTip" Value="Descending"/>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Image.Style>
+                    </Image>
+                </ToggleButton>
+                <Label Margin="10 0 0 0" Content="Name:" Style="{StaticResource BaseLabel}" VerticalAlignment="Center"/>
+                <usercontrols:InputBox
+                                       Text="{Binding NameFilter, Delay=100, ElementName=palettesBrowser, UpdateSourceTrigger=PropertyChanged}"
+                                       VerticalAlignment="Center"
+                                       Style="{StaticResource DarkTextBoxStyle}" Width="150" />
+
+                <Label Margin="10 0 0 0" Content="Colors:" Style="{StaticResource BaseLabel}" VerticalAlignment="Center"/>
+                <ComboBox x:Name="colorsComboBox" VerticalAlignment="Center" SelectionChanged="ColorsComboBox_SelectionChanged">
+                    <ComboBoxItem IsSelected="True" Content="Any"/>
+                    <ComboBoxItem Content="Max"/>
+                    <ComboBoxItem Content="Min"/>
+                    <ComboBoxItem Content="Exact"/>
+                </ComboBox>
+                <views:NumberInput Width="50" VerticalAlignment="Center" Margin="10 0 0 0"
+                                   FocusNext="True"
+                                   Value="{Binding ColorsNumber, ElementName=palettesBrowser, Mode=TwoWay}"/>
+                <CheckBox Margin="10 0 0 0" VerticalAlignment="Center"
+                          IsChecked="{Binding ShowOnlyFavourites, ElementName=palettesBrowser}" Content="Favorites"/>
+            </StackPanel>
+            <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 10 0">
+                <Button ToolTip="Add from current palette" Click="AddFromPalette_OnClick" Cursor="Hand" Margin="10 0" Style="{StaticResource ImageButtonStyle}" Width="24" Height="24">
+                    <Image Source="/Images/Plus-square.png"/>
+                </Button>
+                <Button Cursor="Hand" Click="OpenFolder_OnClick" Style="{StaticResource ImageButtonStyle}" Width="24" Height="24"
+                        ToolTip="Open palettes directory in explorer">
+                    <Image Source="/Images/Folder.png"/>
+                </Button>
+                <Button HorizontalAlignment="Right" Margin="10 0 0 0" ToolTip="Browse palettes on Lospec"
+                        Style="{StaticResource ImageButtonStyle}" Width="24" Height="24"
+                        Click="BrowseOnLospec_OnClick"
+                        CommandParameter="https://lospec.com/palette-list">
+                    <Image Source="/Images/Globe.png"/>
+                </Button>
+                <Button HorizontalAlignment="Right" Margin="10 0 0 0" ToolTip="Import from file"
+                        Style="{StaticResource ImageButtonStyle}" Width="24" Height="24"
+                        Click="ImportFromFile_OnClick">
+                    <Image Source="/Images/hard-drive.png"/>
+                </Button>
+            </StackPanel>
+        </DockPanel>
+        <Grid Grid.Row="2" Margin="10">
+            <TextBlock Text="Couldn't fetch palettes" Foreground="White" FontSize="20" HorizontalAlignment="Center" 
+                       VerticalAlignment="Center" Visibility="{Binding Visibility, Converter={converters:OppositeVisibilityConverter}, ElementName=itemsControl}"/>
+            <StackPanel Panel.ZIndex="10" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding ElementName=palettesBrowser, Path=SortedResults.Count, Converter={converters:CountToVisibilityConverter}}">
+                <TextBlock Text="No palettes found." Foreground="White" FontSize="20" TextAlignment="Center"/>
+                <TextBlock Margin="0 10 0 0">
+                    <Hyperlink Foreground="Gray" Cursor="Hand" FontSize="18" NavigateUri="https://lospec.com/palette-list" RequestNavigate="Hyperlink_OnRequestNavigate">
+                        I heard you can find some here: lospec.com/palette-list
+                    </Hyperlink>
+                </TextBlock>
+                <Image Width="128" Height="128" Source="/Images/Search.png"/>
+            </StackPanel>
+            <ScrollViewer x:Name="scrollViewer" Margin="5" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollChanged="ScrollViewer_ScrollChanged">
+                <ItemsControl x:Name="itemsControl" ItemsSource="{Binding SortedResults, ElementName=palettesBrowser}"
+                          Visibility="{Binding PaletteList.FetchedCorrectly, Converter={StaticResource BoolToVisibilityConverter}, ElementName=palettesBrowser}">
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate>
+                            <local:PaletteItem Palette="{Binding}"
+                                               OnRename="PaletteItem_OnRename"
+                                               ToggleFavouriteCommand="{Binding ToggleFavouriteCommand, ElementName=palettesBrowser}"
+                                               DeletePaletteCommand="{Binding DeletePaletteCommand, ElementName=palettesBrowser}"
+                                               ImportPaletteCommand="{Binding ImportPaletteCommand, ElementName=palettesBrowser}"/>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </ScrollViewer>
+            <Image gif:ImageBehavior.AnimatedSource="/Images/Processing.gif" HorizontalAlignment="Center" VerticalAlignment="Center"
+                   Visibility="{Binding IsFetching, Converter={StaticResource BoolToVisibilityConverter}, ElementName=palettesBrowser}"
+                   Height="50" gif:ImageBehavior.AnimationSpeedRatio="1.5"/>
+        </Grid>
+    </Grid>
+</Window>

+ 567 - 0
PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs

@@ -0,0 +1,567 @@
+using Microsoft.Win32;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels;
+using PixiEditor.Views.UserControls.Palettes;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Navigation;
+
+namespace PixiEditor.Views.Dialogs
+{
+    public partial class PalettesBrowser : Window
+    {
+        private const int ItemsPerLoad = 10;
+
+        private readonly string[] stopItTexts = new[]
+        {
+            "That's enough. Tidy up your file names.",
+            "Can you stop copying these names please?", "No, really, stop it.", "Don't you have anything better to do?"
+        };
+
+        public PaletteList PaletteList
+        {
+            get => (PaletteList)GetValue(PaletteListProperty);
+            set => SetValue(PaletteListProperty, value);
+        }
+
+        public static readonly DependencyProperty PaletteListProperty =
+            DependencyProperty.Register(nameof(PaletteList), typeof(PaletteList), typeof(PalettesBrowser));
+
+        public ICommand ImportPaletteCommand
+        {
+            get => (ICommand)GetValue(ImportPaletteCommandProperty);
+            set => SetValue(ImportPaletteCommandProperty, value);
+        }
+
+        public static readonly DependencyProperty ImportPaletteCommandProperty =
+            DependencyProperty.Register(nameof(ImportPaletteCommand), typeof(ICommand), typeof(PalettesBrowser));
+
+        public static readonly DependencyProperty DeletePaletteCommandProperty = DependencyProperty.Register(
+            nameof(DeletePaletteCommand), typeof(ICommand), typeof(PalettesBrowser), new PropertyMetadata(default(ICommand)));
+
+        public ICommand DeletePaletteCommand
+        {
+            get => (ICommand)GetValue(DeletePaletteCommandProperty);
+            set => SetValue(DeletePaletteCommandProperty, value);
+        }
+
+        public bool IsFetching
+        {
+            get => (bool)GetValue(IsFetchingProperty);
+            set => SetValue(IsFetchingProperty, value);
+        }
+
+        public static readonly DependencyProperty IsFetchingProperty =
+            DependencyProperty.Register(nameof(IsFetching), typeof(bool), typeof(PalettesBrowser), new PropertyMetadata(false));
+
+        public int ColorsNumber
+        {
+            get => (int)GetValue(ColorsNumberProperty);
+            set => SetValue(ColorsNumberProperty, value);
+        }
+
+        public static readonly DependencyProperty ColorsNumberProperty =
+            DependencyProperty.Register(nameof(ColorsNumber), typeof(int), typeof(PalettesBrowser),
+                new PropertyMetadata(8, ColorsNumberChanged));
+
+        public WpfObservableRangeCollection<PaletteListDataSource> PaletteListDataSources
+        {
+            get => (WpfObservableRangeCollection<PaletteListDataSource>)GetValue(PaletteListDataSourcesProperty);
+            set => SetValue(PaletteListDataSourcesProperty, value);
+        }
+
+        public static readonly DependencyProperty PaletteListDataSourcesProperty =
+            DependencyProperty.Register(nameof(PaletteListDataSources), typeof(WpfObservableRangeCollection<PaletteListDataSource>), typeof(PalettesBrowser), new PropertyMetadata(new WpfObservableRangeCollection<PaletteListDataSource>()));
+        public bool SortAscending
+        {
+            get => (bool)GetValue(SortAscendingProperty);
+            set => SetValue(SortAscendingProperty, value);
+        }
+
+        public static readonly DependencyProperty SortAscendingProperty =
+            DependencyProperty.Register(nameof(SortAscending), typeof(bool), typeof(PalettesBrowser), new PropertyMetadata(true, OnSortAscendingChanged));
+
+
+        public static readonly DependencyProperty SortedResultsProperty = DependencyProperty.Register(
+            nameof(SortedResults), typeof(WpfObservableRangeCollection<Palette>), typeof(PalettesBrowser), new PropertyMetadata(default(WpfObservableRangeCollection<Palette>)));
+
+        public WpfObservableRangeCollection<Palette> SortedResults
+        {
+            get => (WpfObservableRangeCollection<Palette>)GetValue(SortedResultsProperty);
+            set => SetValue(SortedResultsProperty, value);
+        }
+
+        public static readonly DependencyProperty NameFilterProperty = DependencyProperty.Register(
+            nameof(NameFilter), typeof(string), typeof(PalettesBrowser),
+            new PropertyMetadata(default(string), OnNameFilterChanged));
+
+        public string NameFilter
+        {
+            get => (string)GetValue(NameFilterProperty);
+            set => SetValue(NameFilterProperty, value);
+        }
+
+        public static readonly DependencyProperty ShowOnlyFavouritesProperty = DependencyProperty.Register(
+            nameof(ShowOnlyFavourites), typeof(bool), typeof(PalettesBrowser),
+            new PropertyMetadata(false, OnShowOnlyFavouritesChanged));
+
+        public bool ShowOnlyFavourites
+        {
+            get => (bool)GetValue(ShowOnlyFavouritesProperty);
+            set => SetValue(ShowOnlyFavouritesProperty, value);
+        }
+
+        public RelayCommand<Palette> ToggleFavouriteCommand { get; set; }
+
+        public string SortingType { get; set; } = "Default";
+        public ColorsNumberMode ColorsNumberMode { get; set; } = ColorsNumberMode.Any;
+
+        private FilteringSettings filteringSettings;
+
+        public FilteringSettings Filtering => filteringSettings ??=
+            new FilteringSettings(ColorsNumberMode, ColorsNumber, NameFilter, ShowOnlyFavourites);
+
+        private char[] separators = new char[] { ' ', ',' };
+
+        private SortingType InternalSortingType => (SortingType)Enum.Parse(typeof(SortingType), SortingType.Replace(" ", ""));
+        public WpfObservableRangeCollection<SKColor> CurrentEditingPalette { get; set; }
+        public static PalettesBrowser Instance { get; internal set; }
+
+        private LocalPalettesFetcher LocalPalettesFetcher
+        {
+            get
+            {
+                return localPalettesFetcher ??= (LocalPalettesFetcher)PaletteListDataSources.First(x => x is LocalPalettesFetcher);
+            }
+        }
+
+        private LocalPalettesFetcher localPalettesFetcher;
+
+        public PalettesBrowser()
+        {
+            InitializeComponent();
+            Instance = this;
+            DeletePaletteCommand = new RelayCommand<Palette>(DeletePalette);
+            ToggleFavouriteCommand = new RelayCommand<Palette>(ToggleFavourite, CanToggleFavourite);
+            Loaded += async (_, _) =>
+            {
+                LocalPalettesFetcher.CacheUpdated += LocalCacheRefreshed;
+                await LocalPalettesFetcher.RefreshCacheAll();
+            };
+            Closed += (_, _) =>
+            {
+                Instance = null;
+                LocalPalettesFetcher.CacheUpdated -= LocalCacheRefreshed;
+            };
+        }
+
+        public static PalettesBrowser Open(WpfObservableRangeCollection<PaletteListDataSource> dataSources, ICommand importPaletteCommand, WpfObservableRangeCollection<SKColor> currentEditingPalette)
+        {
+            if (Instance != null) return Instance;
+            PalettesBrowser browser = new PalettesBrowser
+            {
+                Owner = Application.Current.MainWindow,
+                ImportPaletteCommand = importPaletteCommand,
+                PaletteListDataSources = dataSources,
+                CurrentEditingPalette = currentEditingPalette
+            };
+
+            browser.Show();
+            return browser;
+        }
+
+        private async void LocalCacheRefreshed(RefreshType refreshType, Palette itemAffected, string fileNameAffected)
+        {
+            await Dispatcher.InvokeAsync(async () =>
+            {
+                switch (refreshType)
+                {
+                    case RefreshType.All:
+                        await UpdatePaletteList();
+                        break;
+                    case RefreshType.Created:
+                        HandleCachePaletteCreated(itemAffected);
+                        break;
+                    case RefreshType.Updated:
+                        HandleCacheItemUpdated(itemAffected);
+                        break;
+                    case RefreshType.Deleted:
+                        HandleCacheItemDeleted(fileNameAffected);
+                        break;
+                    case RefreshType.Renamed:
+                        HandleCacheItemRenamed(itemAffected, fileNameAffected);
+                        break;
+                    default:
+                        throw new ArgumentOutOfRangeException(nameof(refreshType), refreshType, null);
+                }
+
+            });
+        }
+
+        private void HandleCacheItemRenamed(Palette itemAffected, string oldFileName)
+        {
+            var old = SortedResults.FirstOrDefault(x => x.FileName == oldFileName);
+            if (old != null)
+            {
+                old.Name = itemAffected.Name;
+                old.FileName = itemAffected.FileName;
+            }
+
+            UpdateRenamedFavourite(Path.GetFileNameWithoutExtension(oldFileName), itemAffected.Name);
+        }
+
+        private void HandleCacheItemDeleted(string deletedItemFileName)
+        {
+            Palette item = SortedResults.FirstOrDefault(x => x.FileName == deletedItemFileName);
+            if (item != null)
+            {
+                SortedResults.Remove(item);
+            }
+        }
+
+        private void HandleCacheItemUpdated(Palette updatedItem)
+        {
+            var item = SortedResults.FirstOrDefault(x => x.FileName == updatedItem.FileName);
+            if (item is null)
+                return;
+
+            item.Name = updatedItem.Name;
+            item.IsFavourite = updatedItem.IsFavourite;
+            item.Colors = updatedItem.Colors;
+
+            Sort();
+        }
+
+        private void HandleCachePaletteCreated(Palette updatedItem)
+        {
+            SortedResults.Add(updatedItem);
+            Sort();
+        }
+
+        private async void ToggleFavourite(Palette palette)
+        {
+            palette.IsFavourite = !palette.IsFavourite;
+            var favouritePalettes = IPreferences.Current.GetLocalPreference(PreferencesConstants.FavouritePalettes, new List<string>());
+
+            if (palette.IsFavourite)
+            {
+                favouritePalettes.Add(palette.Name);
+            }
+            else
+            {
+                favouritePalettes.Remove(palette.Name);
+            }
+
+            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+            await UpdatePaletteList();
+        }
+
+        private void DeletePalette(Palette palette)
+        {
+            if (palette == null) return;
+
+            string filePath = Path.Join(LocalPalettesFetcher.PathToPalettesFolder, palette.FileName);
+            if (File.Exists(filePath))
+            {
+                if (ConfirmationDialog.Show("Are you sure you want to delete this palette? This cannot be undone.", "Warning!") == ConfirmationType.Yes)
+                {
+                    _ = LocalPalettesFetcher.DeletePalette(palette.FileName);
+                    RemoveFavouritePalette(palette);
+                }
+            }
+        }
+
+        private static void RemoveFavouritePalette(Palette palette)
+        {
+            var favouritePalettes =
+                IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+            if (favouritePalettes != null && favouritePalettes.Contains(palette.Name))
+            {
+                favouritePalettes.Remove(palette.Name);
+                IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+            }
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private static async void OnShowOnlyFavouritesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            PalettesBrowser browser = (PalettesBrowser)d;
+            browser.Filtering.ShowOnlyFavourites = (bool)e.NewValue;
+            await browser.UpdatePaletteList();
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        private static async void ColorsNumberChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            PalettesBrowser browser = (PalettesBrowser)d;
+            browser.Filtering.ColorsCount = (int)e.NewValue;
+            await browser.UpdatePaletteList();
+        }
+
+        private static void OnSortAscendingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            PalettesBrowser browser = (PalettesBrowser)d;
+            browser.Sort();
+        }
+
+        public async Task UpdatePaletteList()
+        {
+            IsFetching = true;
+            PaletteList?.Palettes?.Clear();
+
+            for (int i = 0; i < PaletteListDataSources.Count; i++)
+            {
+                PaletteList src = await FetchPaletteList(i, Filtering);
+                if (!src.FetchedCorrectly) continue;
+                if (PaletteList == null)
+                {
+                    PaletteList = src;
+                }
+                else
+                {
+                    PaletteList.Palettes?.AddRange(src.Palettes);
+                }
+            }
+
+            Sort();
+
+            IsFetching = false;
+        }
+
+        private async Task<PaletteList> FetchPaletteList(int index, FilteringSettings filtering)
+        {
+            int startIndex = PaletteList != null ? PaletteList.Palettes.Count : 0;
+            var src = await PaletteListDataSources[index].FetchPaletteList(startIndex, ItemsPerLoad, filtering);
+            return src;
+        }
+
+        private static async void OnNameFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            var browser = (PalettesBrowser)d;
+            browser.Filtering.Name = browser.NameFilter;
+            await browser.UpdatePaletteList();
+            browser.scrollViewer.ScrollToHome();
+        }
+
+        private async void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
+        {
+            if (PaletteList?.Palettes == null) return;
+            var viewer = (ScrollViewer)sender;
+            if (viewer.VerticalOffset == viewer.ScrollableHeight)
+            {
+                IsFetching = true;
+                var newPalettes = await FetchPaletteList(0, Filtering);
+                if (newPalettes is not { FetchedCorrectly: true } || newPalettes.Palettes == null)
+                {
+                    IsFetching = false;
+                    return;
+                }
+
+                PaletteList.Palettes.AddRange(newPalettes.Palettes);
+                Sort();
+                IsFetching = false;
+            }
+        }
+
+        private void SortingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (e.AddedItems is { Count: > 0 } && e.AddedItems[0] is ComboBoxItem { Content: string value })
+            {
+                SortingType = value;
+                Sort();
+                scrollViewer.ScrollToHome();
+            }
+        }
+
+        private async void ColorsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (e.AddedItems is { Count: > 0 } && e.AddedItems[0] is ComboBoxItem { Content: string value })
+            {
+                ColorsNumberMode = Enum.Parse<ColorsNumberMode>(value);
+                Filtering.ColorsNumberMode = ColorsNumberMode;
+                await UpdatePaletteList();
+                scrollViewer.ScrollToHome();
+            }
+        }
+
+        private bool CanToggleFavourite(Palette palette)
+        {
+            return palette != null && palette.Colors.Count > 0;
+        }
+
+        private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
+        {
+            ((Grid)sender).Focus();
+        }
+
+        private void Sort()
+        {
+            Sort(!SortAscending);
+        }
+
+        private void Sort(bool descending)
+        {
+            if (PaletteList?.Palettes == null) return;
+
+            IOrderedEnumerable<Palette> sorted = null;
+            if (!descending)
+            {
+                switch (InternalSortingType)
+                {
+                    case Models.DataHolders.Palettes.SortingType.Default:
+                        sorted = PaletteList.Palettes.OrderByDescending(x => x.IsFavourite).ThenBy(x => PaletteList.Palettes.IndexOf(x));
+                        break;
+                    case Models.DataHolders.Palettes.SortingType.Alphabetical:
+                        sorted = PaletteList.Palettes.OrderBy(x => x.Name);
+                        break;
+                    case Models.DataHolders.Palettes.SortingType.ColorCount:
+                        sorted = PaletteList.Palettes.OrderBy(x => x.Colors.Count);
+                        break;
+                }
+            }
+            else
+            {
+                switch (InternalSortingType)
+                {
+                    case Models.DataHolders.Palettes.SortingType.Default:
+                        sorted = PaletteList.Palettes.OrderByDescending(x => PaletteList.Palettes.IndexOf(x));
+                        break;
+                    case Models.DataHolders.Palettes.SortingType.Alphabetical:
+                        sorted = PaletteList.Palettes.OrderByDescending(x => x.Name);
+                        break;
+                    case Models.DataHolders.Palettes.SortingType.ColorCount:
+                        sorted = PaletteList.Palettes.OrderByDescending(x => x.Colors.Count);
+                        break;
+                }
+            }
+
+            if (sorted != null)
+            {
+                SortedResults = new WpfObservableRangeCollection<Palette>(sorted);
+            }
+        }
+
+        private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
+        {
+            if (Directory.Exists(LocalPalettesFetcher.PathToPalettesFolder))
+            {
+                Process.Start(new ProcessStartInfo
+                {
+                    FileName = LocalPalettesFetcher.PathToPalettesFolder,
+                    UseShellExecute = true,
+                    Verb = "open"
+                });
+            }
+        }
+
+        private async void AddFromPalette_OnClick(object sender, RoutedEventArgs e)
+        {
+            if (CurrentEditingPalette?.Count == 0)
+                return;
+
+            string finalFileName = LocalPalettesFetcher.GetNonExistingName("Unnamed Palette.pal", true);
+            await LocalPalettesFetcher.SavePalette(finalFileName, CurrentEditingPalette.ToArray());
+        }
+
+        private void PaletteItem_OnRename(object sender, EditableTextBlock.TextChangedEventArgs e)
+        {
+            PaletteItem item = (PaletteItem)sender;
+            item.Palette.Name = e.OldText;
+
+            if (string.IsNullOrWhiteSpace(e.NewText) || e.NewText == item.Palette.Name || e.NewText.Length > 50)
+                return;
+
+            string oldFileName = $"{e.OldText}.pal";
+
+            string finalNewName = LocalPalettesFetcher.GetNonExistingName($"{Palette.ReplaceInvalidChars(e.NewText)}.pal", true);
+            string newPath = Path.Join(LocalPalettesFetcher.PathToPalettesFolder, finalNewName);
+
+            if (newPath.Length > 250)
+            {
+                NoticeDialog.Show(stopItTexts[Random.Shared.Next(stopItTexts.Length - 1)], "The name is too long.");
+                return;
+            }
+
+            LocalPalettesFetcher.RenamePalette(oldFileName, finalNewName);
+        }
+
+        private static void UpdateRenamedFavourite(string old, string newName)
+        {
+            var favourites = IPreferences.Current.GetLocalPreference(
+                PreferencesConstants.FavouritePalettes,
+                new List<string>());
+
+            if (favourites.Contains(old))
+            {
+                favourites.Remove(old);
+                favourites.Add(newName);
+            }
+
+            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favourites);
+        }
+
+        private void BrowseOnLospec_OnClick(object sender, RoutedEventArgs e)
+        {
+            Button button = sender as Button;
+            string url = (string)button.CommandParameter;
+
+            ProcessHelpers.ShellExecute(url);
+        }
+
+
+        private async void ImportFromFile_OnClick(object sender, RoutedEventArgs e)
+        {
+            var parsers = ViewModelMain.Current.ColorsSubViewModel.PaletteParsers;
+            OpenFileDialog openFileDialog = new OpenFileDialog
+            {
+                Filter = PaletteHelpers.GetFilter(parsers)
+            };
+
+            if (openFileDialog.ShowDialog() == true)
+            {
+                await ImportPalette(openFileDialog.FileName, parsers);
+            }
+        }
+
+        private async Task ImportPalette(string fileName, IList<PaletteFileParser> parsers)
+        {
+            var parser = parsers.FirstOrDefault(x => x.SupportedFileExtensions.Contains(Path.GetExtension(fileName)));
+            if (parser != null)
+            {
+                var data = await parser.Parse(fileName);
+
+                if (data.IsCorrupted) return;
+                string name = LocalPalettesFetcher.GetNonExistingName(Path.GetFileName(fileName), true);
+                await LocalPalettesFetcher.SavePalette(name, data.Colors.ToArray());
+            }
+        }
+
+        private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
+        {
+            ProcessHelpers.ShellExecute(e.Uri.ToString());
+        }
+    }
+}

+ 247 - 254
PixiEditor/Views/MainWindow.xaml

@@ -135,10 +135,10 @@
                     Source="/Images/PixiEditorLogo.png"
                     Width="20"
                     Height="20"
-                    Margin="5,5,0,0" />
+                    Margin="5,2,0,0" />
                 <cmds:Menu
                     WindowChrome.IsHitTestVisibleInChrome="True"
-                    Margin="10, 4, 0, 0"
+                    Margin="10, 0, 0, 0"
                     DockPanel.Dock="Left"
                     HorizontalAlignment="Left"
                     VerticalAlignment="Top"
@@ -413,253 +413,246 @@
                 <Button
                     Margin="1,0,0,0"
                     Command="{cmds:Command PixiEditor.Undo.Undo}"
-                    ToolTip="Undo"
-                    Style="{StaticResource ToolSettingsGlyphButton}"
-                    Content="&#xE7A7;" />
-                <Button
-                    Command="{cmds:Command PixiEditor.Undo.Redo}"
-                    ToolTip="Redo"
-                    Style="{StaticResource ToolSettingsGlyphButton}"
-                    Content="&#xE7A6;" />
-                <ToggleButton
-                    Width="30"
-                    BorderThickness="0"
-                    ToolTip="Pen Mode"
-                    Focusable="False"
-                    IsChecked="{Binding StylusSubViewModel.IsPenModeEnabled}">
-                    <ToggleButton.Style>
-                        <Style
-                            TargetType="ToggleButton">
-                            <Setter
-                                Property="Template">
-                                <Setter.Value>
-                                    <ControlTemplate
-                                        TargetType="ToggleButton">
-                                        <Border
-                                            BorderBrush="{TemplateBinding BorderBrush}"
-                                            Background="{TemplateBinding Background}"
-                                            Focusable="False">
-                                            <ContentPresenter
-                                                HorizontalAlignment="Center"
-                                                VerticalAlignment="Center"
-                                                Focusable="False" />
-                                        </Border>
-                                    </ControlTemplate>
-                                </Setter.Value>
-                            </Setter>
-                            <Style.Triggers>
-                                <Trigger
-                                    Property="IsChecked"
-                                    Value="False">
-                                    <Setter
-                                        Property="Background"
-                                        Value="Transparent" />
-                                </Trigger>
-                                <Trigger
-                                    Property="IsMouseOver"
-                                    Value="True">
-                                    <Setter
-                                        Property="Background"
-                                        Value="#404040" />
-                                </Trigger>
-                                <Trigger
-                                    Property="IsChecked"
-                                    Value="True">
-                                    <Setter
-                                        Property="Background"
-                                        Value="#707070" />
-                                </Trigger>
-                            </Style.Triggers>
-                        </Style>
-                    </ToggleButton.Style>
-                    <Image
-                        Height="20"
-                        Source="../Images/penMode.png" />
-                </ToggleButton>
-                <Grid
-                    Margin="5,5,10,5"
-                    Background="{StaticResource BrighterAccentColor}"
-                    Width="5" />
-                <Label
-                    Style="{StaticResource BaseLabel}"
-                    FontSize="12"
-                    VerticalAlignment="Center"
-                    Content="{Binding ToolsSubViewModel.ActiveTool.DisplayName}"
-                    ToolTip="{Binding ToolsSubViewModel.ActiveTool.ActionDisplay}" />
-                <ItemsControl
-                    ItemsSource="{Binding ToolsSubViewModel.ActiveTool.Toolbar.Settings}">
-                    <ItemsControl.ItemsPanel>
-                        <ItemsPanelTemplate>
-                            <StackPanel
-                                Orientation="Horizontal"
-                                Margin="10, 0, 0, 0" />
-                        </ItemsPanelTemplate>
-                    </ItemsControl.ItemsPanel>
-                    <ItemsControl.ItemTemplate>
-                        <DataTemplate>
-                            <StackPanel
-                                Orientation="Horizontal"
-                                VerticalAlignment="Center"
-                                Margin="10,0,10,0">
-                                <Label
-                                    Visibility="{Binding HasLabel, Converter={StaticResource BoolToVisibilityConverter}}"
-                                    Foreground="White"
-                                    Content="{Binding Label}" />
-                                <ContentControl
-                                    Content="{Binding SettingControl}" />
-                            </StackPanel>
-                        </DataTemplate>
-                    </ItemsControl.ItemTemplate>
-                </ItemsControl>
-            </StackPanel>
-            <Grid
-                Grid.Column="1"
-                Grid.Row="2"
-                Background="#303030">
-                <Grid
-                    AllowDrop="True"
-                    Drop="MainWindow_Drop">
-                    <DockingManager
-                        ActiveContent="{Binding BitmapManager.ActiveWindow, Mode=TwoWay}"
-                        DocumentsSource="{Binding BitmapManager.Documents}">
-                        <DockingManager.Theme>
-                            <avalonDockTheme:PixiEditorDockTheme />
-                        </DockingManager.Theme>
+                    ToolTip="Undo"Style="{StaticResource ToolSettingsGlyphButton}" 
+					Content="&#xE7A7;"/>
+            <Button 
+				Command="{cmds:Command PixiEditor.Undo.Redo}" 
+				ToolTip="Redo"
+                Style="{StaticResource ToolSettingsGlyphButton}" 
+				Content="&#xE7A6;"/>
+            <ToggleButton 
+				Width="30" 
+				BorderThickness="0"
+				ToolTip="Pen Mode" 
+				Focusable="False"
+                IsChecked="{Binding StylusSubViewModel.IsPenModeEnabled}">
+                <ToggleButton.Style>
+                    <Style TargetType="ToggleButton">
+                        <Setter Property="Template">
+                            <Setter.Value>
+                                <ControlTemplate TargetType="ToggleButton">
+                                    <Border BorderBrush="{TemplateBinding BorderBrush}" 
+                                            Background="{TemplateBinding Background}" Focusable="False">
+                                        <ContentPresenter HorizontalAlignment="Center"
+                                              VerticalAlignment="Center" Focusable="False"/>
+                                    </Border>
+                                </ControlTemplate>
+                            </Setter.Value>
+                        </Setter>
+                        <Style.Triggers>
+                            <Trigger Property="IsChecked" Value="False">
+                                <Setter Property="Background" Value="Transparent"/>
+                            </Trigger>
+                            <Trigger Property="IsMouseOver" Value="True">
+                                <Setter Property="Background" Value="#404040"/>
+                            </Trigger>
+                            <Trigger Property="IsChecked" Value="True">
+                                <Setter Property="Background" Value="#707070"/>
+                            </Trigger>
+                        </Style.Triggers>
+                    </Style>
+                </ToggleButton.Style>
+                <Image Height="20" Source="../Images/penMode.png"/>
+            </ToggleButton>
+            <Grid Margin="5,5,10,5" Background="{StaticResource BrighterAccentColor}" Width="5"/>
+            <Label Style="{StaticResource BaseLabel}" FontSize="12"
+                   VerticalAlignment="Center" Content="{Binding ToolsSubViewModel.ActiveTool.DisplayName}"
+                   ToolTip="{Binding ToolsSubViewModel.ActiveTool.ActionDisplay}"/>
+            <ItemsControl ItemsSource="{Binding ToolsSubViewModel.ActiveTool.Toolbar.Settings}">
+                <ItemsControl.ItemsPanel>
+                    <ItemsPanelTemplate>
+                        <StackPanel Orientation="Horizontal" Margin="10, 0, 0, 0" />
+                    </ItemsPanelTemplate>
+                </ItemsControl.ItemsPanel>
+                <ItemsControl.ItemTemplate>
+                    <DataTemplate>
+                        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,10,0">
+                            <Label
+                                Visibility="{Binding HasLabel, Converter={StaticResource BoolToVisibilityConverter}}"
+                                Foreground="White" Content="{Binding Label}" />
+                            <ContentControl Content="{Binding SettingControl}" />
+                        </StackPanel>
+                    </DataTemplate>
+                </ItemsControl.ItemTemplate>
+            </ItemsControl>
+        </StackPanel>
+        <Grid Grid.Column="1" Grid.Row="2" Background="#303030" >
+            <Grid AllowDrop="True" Drop="MainWindow_Drop">
+                <DockingManager ActiveContent="{Binding BitmapManager.ActiveWindow, Mode=TwoWay}"
+                                           DocumentsSource="{Binding BitmapManager.Documents}">
+                    <DockingManager.Theme>
+                        <avalonDockTheme:PixiEditorDockTheme />
+                    </DockingManager.Theme>
 
-                        <avalondock:DockingManager.LayoutItemContainerStyleSelector>
-                            <ui:PanelsStyleSelector>
-                                <ui:PanelsStyleSelector.DocumentTabStyle>
-                                    <Style
-                                        TargetType="{x:Type avalondock:LayoutItem}">
-                                        <Setter
-                                            Property="Title"
-                                            Value="{Binding Model.Name}" />
-                                        <Setter
-                                            Property="CloseCommand"
-                                            Value="{Binding Model.RequestCloseDocumentCommand}" />
-                                    </Style>
-                                </ui:PanelsStyleSelector.DocumentTabStyle>
-                            </ui:PanelsStyleSelector>
-                        </avalondock:DockingManager.LayoutItemContainerStyleSelector>
-                        <DockingManager.LayoutItemTemplateSelector>
-                            <ui:DocumentsTemplateSelector>
-                                <ui:DocumentsTemplateSelector.DocumentsViewTemplate>
-                                    <DataTemplate
-                                        DataType="{x:Type dataHolders:Document}">
-                                        <usercontrols:DrawingViewPort
-                                            CenterViewportTrigger="{Binding CenterViewportTrigger}"
-                                            ZoomViewportTrigger="{Binding ZoomViewportTrigger}"
-                                            GridLinesVisible="{Binding XamlAccesibleViewModel.ViewportSubViewModel.GridLinesEnabled}"
-                                            Cursor="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ToolCursor}"
-                                            MiddleMouseClickedCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.PreviewMouseMiddleButtonCommand}"
-                                            MouseMoveCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseMoveCommand}"
-                                            MouseDownCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseDownCommand}"
-                                            MouseUpCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseUpCommand}"
-                                            MouseXOnCanvas="{Binding MouseXOnCanvas, Mode=TwoWay}"
-                                            MouseYOnCanvas="{Binding MouseYOnCanvas, Mode=TwoWay}"
-                                            StylusButtonDownCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusDownCommand}"
-                                            StylusButtonUpCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusUpCommand}"
-                                            StylusGestureCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusGestureCommand}"
-                                            StylusOutOfRangeCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusOutOfRangeCommand}"
-                                            UseTouchGestures="{Binding XamlAccesibleViewModel.StylusSubViewModel.UseTouchGestures}"
-                                            IsUsingZoomTool="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ActiveTool, Converter={converters:IsSpecifiedTypeConverter SpecifiedType={x:Type tools:ZoomTool}}}"
-                                            IsUsingMoveViewportTool="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ActiveTool, Converter={converters:IsSpecifiedTypeConverter SpecifiedType={x:Type tools:MoveViewportTool}}}"
-                                            Stylus.IsTapFeedbackEnabled="False"
-                                            Stylus.IsTouchFeedbackEnabled="False">
-                                            <i:Interaction.Triggers>
-                                                <i:EventTrigger
-                                                    EventName="PreviewMouseDown">
-                                                    <i:InvokeCommandAction
-                                                        Command="{Binding SetAsActiveOnClickCommand}" />
-                                                </i:EventTrigger>
-                                            </i:Interaction.Triggers>
-                                            <usercontrols:DrawingViewPort.ContextMenu>
-                                                <cmds:ContextMenu>
-                                                    <MenuItem
-                                                        Header="_Select All"
-                                                        cmds:ContextMenu.Command="PixiEditor.Selection.SelectAll" />
-                                                    <MenuItem
-                                                        Header="_Deselect"
-                                                        cmds:ContextMenu.Command="PixiEditor.Selection.Clear" />
-                                                    <Separator />
-                                                    <MenuItem
-                                                        Header="_Cut"
-                                                        cmds:ContextMenu.Command="PixiEditor.Clipboard.Cut" />
-                                                    <MenuItem
-                                                        Header="_Copy"
-                                                        cmds:ContextMenu.Command="PixiEditor.Clipboard.Copy" />
-                                                    <MenuItem
-                                                        Header="_Paste"
-                                                        cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
-                                                </cmds:ContextMenu>
-                                            </usercontrols:DrawingViewPort.ContextMenu>
-                                        </usercontrols:DrawingViewPort>
-                                    </DataTemplate>
-                                </ui:DocumentsTemplateSelector.DocumentsViewTemplate>
-                            </ui:DocumentsTemplateSelector>
-                        </DockingManager.LayoutItemTemplateSelector>
-                        <avalondock:LayoutRoot
-                            x:Name="LayoutRoot">
-                            <LayoutPanel
-                                Orientation="Horizontal">
-                                <LayoutDocumentPane />
-                                <LayoutAnchorablePaneGroup
-                                    Orientation="Vertical"
-                                    DockWidth="290">
+                    <avalondock:DockingManager.LayoutItemContainerStyleSelector>
+                        <ui:PanelsStyleSelector>
+                            <ui:PanelsStyleSelector.DocumentTabStyle>
+                                <Style TargetType="{x:Type avalondock:LayoutItem}">
+                                    <Setter Property="Title" Value="{Binding Model.Name}" />
+                                    <Setter Property="CloseCommand" Value="{Binding Model.RequestCloseDocumentCommand}" />
+                                </Style>
+                            </ui:PanelsStyleSelector.DocumentTabStyle>
+                        </ui:PanelsStyleSelector>
+                    </avalondock:DockingManager.LayoutItemContainerStyleSelector>
+                    <DockingManager.LayoutItemTemplateSelector>
+                        <ui:DocumentsTemplateSelector>
+                            <ui:DocumentsTemplateSelector.DocumentsViewTemplate>
+                                <DataTemplate DataType="{x:Type dataHolders:Document}">
+                                    <usercontrols:DrawingViewPort
+                                        CenterViewportTrigger="{Binding CenterViewportTrigger}"
+                                        ZoomViewportTrigger="{Binding ZoomViewportTrigger}"
+                                        GridLinesVisible="{Binding XamlAccesibleViewModel.ViewportSubViewModel.GridLinesEnabled}"
+                                        Cursor="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ToolCursor}"
+                                        MiddleMouseClickedCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.PreviewMouseMiddleButtonCommand}"
+                                        MouseMoveCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseMoveCommand}"
+                                        MouseDownCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseDownCommand}"
+                                        MouseUpCommand="{Binding XamlAccesibleViewModel.IoSubViewModel.MouseUpCommand}"
+                                        MouseXOnCanvas="{Binding MouseXOnCanvas, Mode=TwoWay}"
+                                        MouseYOnCanvas="{Binding MouseYOnCanvas, Mode=TwoWay}"
+                                        StylusButtonDownCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusDownCommand}"
+                                        StylusButtonUpCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusUpCommand}"
+                                        StylusGestureCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusGestureCommand}"
+                                        StylusOutOfRangeCommand="{Binding XamlAccesibleViewModel.StylusSubViewModel.StylusOutOfRangeCommand}"
+                                        UseTouchGestures="{Binding XamlAccesibleViewModel.StylusSubViewModel.UseTouchGestures}"
+                                        IsUsingZoomTool="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ActiveTool, Converter={converters:IsSpecifiedTypeConverter SpecifiedType={x:Type tools:ZoomTool}}}"
+                                        IsUsingMoveViewportTool="{Binding XamlAccesibleViewModel.ToolsSubViewModel.ActiveTool, Converter={converters:IsSpecifiedTypeConverter SpecifiedType={x:Type tools:MoveViewportTool}}}"
+                                        Stylus.IsTapFeedbackEnabled="False" Stylus.IsTouchFeedbackEnabled="False">
+                                        <i:Interaction.Triggers>
+                                            <i:EventTrigger EventName="PreviewMouseDown">
+                                                <i:InvokeCommandAction Command="{Binding SetAsActiveOnClickCommand}"/>
+                                            </i:EventTrigger>
+                                        </i:Interaction.Triggers>
+                                        <usercontrols:DrawingViewPort.ContextMenu>
+                                            <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
+                                                <ContextMenu.Template>
+                                                    <ControlTemplate>
+                                                        <Border Height="120" Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
+                                                            <Grid>
+                                                                <Grid.ColumnDefinitions>
+                                                                    <ColumnDefinition Width="100"/>
+                                                                    <ColumnDefinition Width="{Binding XamlAccesibleViewModel.BitmapManager.ActiveDocument.Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
+                                                                </Grid.ColumnDefinitions>
+                                                                <Border BorderThickness="0 0 1 0" BorderBrush="Black">
+                                                                    <StackPanel Orientation="Vertical" Grid.Column="0">
+                                                                    <MenuItem
+																		Header="_Select All"
+																		cmds:ContextMenu.Command="PixiEditor.Selection.SelectAll" />
+																	<MenuItem
+																		Header="_Deselect"
+																		cmds:ContextMenu.Command="PixiEditor.Selection.Clear" />
+																	<Separator />
+																	<MenuItem
+																		Header="_Cut"
+																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Cut" />
+																	<MenuItem
+																		Header="_Copy"
+																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Copy" />
+																	<MenuItem
+																		Header="_Paste"
+																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
+                                                                    </StackPanel>
+                                                                </Border>
+                                                                <ScrollViewer Margin="5" Grid.Column="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+                                                                    <ItemsControl ItemsSource="{Binding XamlAccesibleViewModel.BitmapManager.ActiveDocument.Palette}" AlternationCount="9999">
+                                                                        <ItemsControl.ItemsPanel>
+                                                                            <ItemsPanelTemplate>
+                                                                                <WrapPanel Orientation="Horizontal"
+                                  HorizontalAlignment="Left" VerticalAlignment="Top"/>
+                                                                            </ItemsPanelTemplate>
+                                                                        </ItemsControl.ItemsPanel>
+                                                                        <ItemsControl.ItemTemplate>
+                                                                            <DataTemplate>
+                                                                                <palettes:PaletteColor Cursor="Hand" CornerRadius="0" ToolTip="Click to select as main color." Width="22" Height="22" Color="{Binding}">
+                                                                                    <b:Interaction.Triggers>
+                                                                                        <b:EventTrigger EventName="MouseLeftButtonUp">
+                                                                                            <b:InvokeCommandAction
+                                     Command="{Binding DataContext.XamlAccesibleViewModel.ColorsSubViewModel.SelectColorCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"
+                                    CommandParameter="{Binding}" />
+                                                                                        </b:EventTrigger>
+                                                                                    </b:Interaction.Triggers>
+                                                                                </palettes:PaletteColor>
+                                                                            </DataTemplate>
+                                                                        </ItemsControl.ItemTemplate>
+                                                                    </ItemsControl>
+                                                                </ScrollViewer>
+                                                            </Grid>
+                                                        </Border>
+                                                    </ControlTemplate>
+                                                </ContextMenu.Template>
+                                            </ContextMenu>
+                                        </usercontrols:DrawingViewPort.ContextMenu>
+                                    </usercontrols:DrawingViewPort>
+                                </DataTemplate>
+                            </ui:DocumentsTemplateSelector.DocumentsViewTemplate>
+                        </ui:DocumentsTemplateSelector>
+                    </DockingManager.LayoutItemTemplateSelector>
+                    <avalondock:LayoutRoot x:Name="LayoutRoot">
+                        <LayoutPanel Orientation="Horizontal">
+                            <LayoutDocumentPane/>
+                            <LayoutAnchorablePaneGroup Orientation="Vertical" DockWidth="290">
 
-                                    <LayoutAnchorablePane
-                                        x:Name="colorPane">
-                                        <LayoutAnchorable
-                                            ContentId="colorPicker"
-                                            Title="Color Picker"
-                                            CanHide="False"
-                                            CanClose="False"
-                                            CanAutoHide="False"
-                                            x:Name="colorPickerPanel"
-                                            CanDockAsTabbedDocument="False"
-                                            CanFloat="True">
-                                            <usercontrols:SmallColorPicker
-                                                SelectedColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay, Converter={StaticResource SKColorToMediaColorConverter}}"
-                                                SecondaryColor="{Binding ColorsSubViewModel.SecondaryColor, Mode=TwoWay, Converter={StaticResource SKColorToMediaColorConverter}}"
-                                                Style="{StaticResource DefaultColorPickerStyle}"
-                                                x:Name="mainColorPicker">
-                                                <i:Interaction.Behaviors>
-                                                    <behaviours:GlobalShortcutFocusBehavior />
-                                                </i:Interaction.Behaviors>
-                                            </usercontrols:SmallColorPicker>
-                                        </LayoutAnchorable>
-                                        <LayoutAnchorable
-                                            ContentId="colorSliders"
-                                            Title="Color Sliders"
-                                            CanHide="False"
-                                            CanClose="False"
-                                            CanAutoHide="False"
-                                            x:Name="colorSlidersPanel"
-                                            CanDockAsTabbedDocument="False"
-                                            CanFloat="True">
-                                            <colorpicker:ColorSliders
-                                                Style="{StaticResource DefaultColorPickerStyle}"
-                                                ColorState="{Binding ElementName=mainColorPicker, Path=ColorState, Delay=10, Mode=TwoWay}">
-                                                <i:Interaction.Behaviors>
-                                                    <behaviours:GlobalShortcutFocusBehavior />
-                                                </i:Interaction.Behaviors>
-                                            </colorpicker:ColorSliders>
-                                        </LayoutAnchorable>
-                                        <avalondock:LayoutAnchorable
-                                            ContentId="swatches"
-                                            Title="Swatches"
-                                            CanHide="False"
-                                            CanClose="False"
-                                            CanAutoHide="False"
-                                            CanDockAsTabbedDocument="False"
-                                            CanFloat="True">
-                                            <usercontrols:SwatchesView
-                                                SelectSwatchCommand="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
-                                                RemoveSwatchCommand="{cmds:Command PixiEditor.Colors.RemoveSwatch, UseProvided=True}"
-                                                Swatches="{Binding BitmapManager.ActiveDocument.Swatches}" />
-                                        </avalondock:LayoutAnchorable>
-                                    </LayoutAnchorablePane>
-                                    <LayoutAnchorablePane>
+                                <LayoutAnchorablePane x:Name="colorPane">
+                                    <LayoutAnchorable ContentId="colorPicker" Title="Color Picker" CanHide="False"
+                                                      CanClose="False" CanAutoHide="False" x:Name="colorPickerPanel"
+                                                      CanDockAsTabbedDocument="False" CanFloat="True">
+                                        <usercontrols:SmallColorPicker SelectedColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay, Converter={StaticResource SKColorToMediaColorConverter}}"
+                                                                         SecondaryColor="{Binding ColorsSubViewModel.SecondaryColor, Mode=TwoWay, Converter={StaticResource SKColorToMediaColorConverter}}" 
+                                                                         Style="{StaticResource DefaultColorPickerStyle}" x:Name="mainColorPicker">
+                                            <i:Interaction.Behaviors>
+                                                <behaviours:GlobalShortcutFocusBehavior/>
+                                            </i:Interaction.Behaviors>
+                                        </usercontrols:SmallColorPicker>
+                                    </LayoutAnchorable>
+                                    <LayoutAnchorable ContentId="colorSliders" Title="Color Sliders" CanHide="False"
+                                                      CanClose="False" CanAutoHide="False" x:Name="colorSlidersPanel"
+                                                      CanDockAsTabbedDocument="False" CanFloat="True">
+                                        <colorpicker:ColorSliders Style="{StaticResource DefaultColorPickerStyle}" 
+                                                                  ColorState="{Binding ElementName=mainColorPicker, Path=ColorState, Delay=10, Mode=TwoWay}">
+                                            <i:Interaction.Behaviors>
+                                                <behaviours:GlobalShortcutFocusBehavior/>
+                                            </i:Interaction.Behaviors>
+                                        </colorpicker:ColorSliders>
+                                    </LayoutAnchorable>
+                                    <avalondock:LayoutAnchorable ContentId="palette" Title="Palette" CanHide="False"
+                                                                 CanClose="False" CanAutoHide="False" x:Name="paletteAnchorable"
+                                                                 CanDockAsTabbedDocument="False" CanFloat="True">
+                                        <Grid>
+                                            <palettes:CompactPaletteViewer IsEnabled="{Binding DocumentSubViewModel.Owner.BitmapManager.ActiveDocument,
+                                        Converter={converters:NotNullToBoolConverter}}"
+                                                                           SelectColorCommand="{Binding ColorsSubViewModel.SelectColorCommand}"
+                                                                           Colors="{Binding BitmapManager.ActiveDocument.Palette}" 
+                                                                           Visibility="{Binding RelativeSource={RelativeSource Mode=Self}, 
+                                                Path=ActualWidth, Converter={converters:PaletteViewerWidthToVisibilityConverter}}"/>
+                                            <palettes:PaletteViewer IsEnabled="{Binding DocumentSubViewModel.Owner.BitmapManager.ActiveDocument,
+                                        Converter={converters:NotNullToBoolConverter}}" Colors="{Binding BitmapManager.ActiveDocument.Palette}"
+                                                              Swatches="{Binding BitmapManager.ActiveDocument.Swatches}"
+                                                                    SelectColorCommand="{Binding ColorsSubViewModel.SelectColorCommand}"
+                                                              HintColor="{Binding Path=ColorsSubViewModel.PrimaryColor,
+                                                Converter={converters:SKColorToMediaColorConverter}}"
+                                                                DataSources="{Binding ColorsSubViewModel.PaletteDataSources}"
+                                                                FileParsers="{Binding ColorsSubViewModel.PaletteParsers}"
+                                                                    Visibility="{Binding RelativeSource={RelativeSource Mode=Self}, 
+                                                Path=ActualWidth, Converter={converters:PaletteViewerWidthToVisibilityConverter},
+                                                ConverterParameter=Hidden}"
+                                                                ImportPaletteCommand="{Binding ColorsSubViewModel.ImportPaletteCommand}"
+                                                                           ReplaceColorsCommand="{Binding ColorsSubViewModel.ReplaceColorsCommand}"/>
+                                        </Grid>
+                                    </avalondock:LayoutAnchorable>
+                                    <avalondock:LayoutAnchorable
+										ContentId="swatches"
+										Title="Swatches"
+										CanHide="False"
+										CanClose="False"
+										CanAutoHide="False"
+										CanDockAsTabbedDocument="False"
+										CanFloat="True">
+										<usercontrols:SwatchesView
+											SelectSwatchCommand="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
+											RemoveSwatchCommand="{cmds:Command PixiEditor.Colors.RemoveSwatch, UseProvided=True}"
+											Swatches="{Binding BitmapManager.ActiveDocument.Swatches}" />
+									</avalondock:LayoutAnchorable>
+                                </LayoutAnchorablePane>
+                                <LayoutAnchorablePane>
                                         <LayoutAnchorable
                                             ContentId="layers"
                                             Title="Layers"
@@ -705,14 +698,14 @@
                                                 PrimaryColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay, Converter={StaticResource SKColorToMediaColorConverter}}" />
                                         </LayoutAnchorable>
                                     </LayoutAnchorablePane>
-                                </LayoutAnchorablePaneGroup>
-                            </LayoutPanel>
-                        </avalondock:LayoutRoot>
-                    </DockingManager>
-                </Grid>
+                            </LayoutAnchorablePaneGroup>
+                        </LayoutPanel>
+                    </avalondock:LayoutRoot>
+                </DockingManager>
             </Grid>
+        </Grid>
 
-            <Border
+        <Border
                 Grid.Row="2"
                 Grid.Column="0"
                 Background="{StaticResource AccentColor}"
@@ -825,4 +818,4 @@
             Height="700"
             MaxWidth="920" />
     </Grid>
-</Window>
+</Window>

Some files were not shown because too many files changed in this diff