Browse Source

Merge pull request #515 from PixiEditor/new-palette-formats

New palette formats parsers
Krzysztof Krysiński 2 years ago
parent
commit
6eeb98ebb6

+ 9 - 0
src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs

@@ -208,5 +208,14 @@ namespace PixiEditor.DrawingApi.Core.ColorsImpl
           return false;
       }
     }
+
+    /// <summary>
+    ///     Returns hex string representation of the color.
+    /// </summary>
+    /// <returns>Color string in format: AARRGGBB</returns>
+    public string? ToHex()
+    {
+        return this == Empty ? null : $"{this._colorValue:X8}";
+    }
   }
 }

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -426,6 +426,8 @@
     "BROWSE_PALETTES": "Browse palettes",
     "LOAD_PALETTE": "Load palette",
     "SAVE_PALETTE": "Save palette",
+    "DISCARD_PALETTE": "Discard palette",
+    "DISCARD_PALETTE_CONFIRMATION": "Are you sure you want to discard current palette? This cannot be undone.",
     "FAVORITES": "Favorites",
     "ADD_FROM_CURRENT_PALETTE": "Add from current palette",
     "OPEN_PALETTES_DIR_TOOLTIP": "Open palettes directory in explorer",

+ 7 - 2
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -4,8 +4,8 @@ using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataProviders;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.IO.ClsFile;
-using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.IO.PaletteParsers;
+using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -64,6 +64,11 @@ internal static class ServiceCollectionHelpers
         // Palette Parsers
         .AddSingleton<PaletteFileParser, JascFileParser>()
         .AddSingleton<PaletteFileParser, ClsFileParser>()
+        .AddSingleton<PaletteFileParser, PngPaletteParser>()
+        .AddSingleton<PaletteFileParser, PaintNetTxtParser>()
+        .AddSingleton<PaletteFileParser, HexPaletteParser>()
+        .AddSingleton<PaletteFileParser, GimpGplParser>()
+        .AddSingleton<PaletteFileParser, PixiPaletteParser>()
         // Palette data sources
         .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();
 }

+ 1 - 1
src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders.Palettes;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.UserPreferences;
 
 namespace PixiEditor.Models.DataProviders;

+ 1 - 0
src/PixiEditor/Models/IO/PaletteFileData.cs

@@ -7,6 +7,7 @@ internal class PaletteFileData
     public string Title { get; set; }
     public Color[] Colors { get; set; }
     public bool IsCorrupted { get; set; } = false;
+    public static PaletteFileData Corrupted => new ("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
 
     public PaletteFileData(Color[] colors)
     {

+ 14 - 2
src/PixiEditor/Models/IO/PaletteFileParser.cs

@@ -1,9 +1,21 @@
-namespace PixiEditor.Models.IO;
+using System.IO;
+
+namespace PixiEditor.Models.IO;
 
 internal abstract class PaletteFileParser
 {
     public abstract Task<PaletteFileData> Parse(string path);
-    public abstract Task Save(string path, PaletteFileData data);
+    public abstract Task<bool> Save(string path, PaletteFileData data);
     public abstract string FileName { get; }
     public abstract string[] SupportedFileExtensions { get; }
+
+    public virtual bool CanSave => true;
+
+    protected static async Task<string[]> ReadTextLines(string path)
+    {
+        using var stream = File.OpenText(path);
+        string fileContent = await stream.ReadToEndAsync();
+        string[] lines = fileContent.Split('\n');
+        return lines;
+    }
 }

+ 2 - 2
src/PixiEditor/Models/IO/ClsFile/ClsFileParser.cs → src/PixiEditor/Models/IO/PaletteParsers/ClsFileParser.cs

@@ -2,7 +2,7 @@
 using CLSEncoderDecoder;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 
-namespace PixiEditor.Models.IO.ClsFile;
+namespace PixiEditor.Models.IO.PaletteParsers;
 
 internal class ClsFileParser : PaletteFileParser
 {
@@ -21,7 +21,7 @@ internal class ClsFileParser : PaletteFileParser
             }
             catch
             {
-                return new PaletteFileData("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
+                return PaletteFileData.Corrupted;
             }
 
             PaletteFileData data = new(

+ 91 - 0
src/PixiEditor/Models/IO/PaletteParsers/GimpGplParser.cs

@@ -0,0 +1,91 @@
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class GimpGplParser : PaletteFileParser
+{
+    public override string FileName { get; } = "GIMP Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".gpl" };
+
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        lines = lines.Where(x => !x.StartsWith("#") && !String.Equals(x.Trim(), "GIMP Palette", StringComparison.CurrentCultureIgnoreCase)).ToArray();
+
+        if(lines.Length == 0) return PaletteFileData.Corrupted;
+
+        List<Color> colors = new();
+        char[] separators = new[] { '\t', ' ' };
+        foreach (var colorLine in lines)
+        {
+            var colorParts = colorLine.Split(separators, StringSplitOptions.RemoveEmptyEntries);
+
+            if (colorParts.Length < 3)
+            {
+                continue;
+            }
+
+            if(colorParts.Length < 3) continue;
+
+            bool parsed = false;
+
+            parsed = byte.TryParse(colorParts[0], out byte r);
+            if(!parsed) continue;
+
+            parsed = byte.TryParse(colorParts[1], out byte g);
+            if(!parsed) continue;
+
+            parsed = byte.TryParse(colorParts[2], out byte b);
+            if(!parsed) continue;
+
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if (colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new();
+        string name = string.IsNullOrEmpty(data.Title) ? Path.GetFileNameWithoutExtension(path) : data.Title;
+        sb.AppendLine("GIMP Palette");
+        sb.AppendLine($"#Name: {name}");
+        sb.AppendLine($"#Colors {data.Colors.Length}");
+        sb.AppendLine("#Made with PixiEditor");
+        sb.AppendLine("#");
+        foreach (var color in data.Colors)
+        {
+            string hex = $"{color.R:X}{color.G:X}{color.B:X}";
+            sb.AppendLine($"{color.R}\t{color.G}\t{color.B}\t{hex}");
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 66 - 0
src/PixiEditor/Models/IO/PaletteParsers/HexPaletteParser.cs

@@ -0,0 +1,66 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class HexPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Hex Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".hex" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        List<Color> colors = new();
+        foreach (var colorLine in lines)
+        {
+            if (colorLine.Length < 6)
+                continue;
+
+            byte r = byte.Parse(colorLine.Substring(0, 2), NumberStyles.HexNumber);
+            byte g = byte.Parse(colorLine.Substring(2, 2), NumberStyles.HexNumber);
+            byte b = byte.Parse(colorLine.Substring(4, 2), NumberStyles.HexNumber);
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if (colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new();
+        foreach (var color in data.Colors)
+        {
+            string hex = $"{color.R:X}{color.G:X}{color.B:X}";
+            sb.AppendLine(hex);
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 1 - 1
src/PixiEditor/Models/IO/JascPalFile/JascFileException.cs → src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileException.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.IO.JascPalFile;
+namespace PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 
 internal class JascFileException : Exception
 {

+ 4 - 7
src/PixiEditor/Models/IO/JascPalFile/JascFileParser.cs → src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileParser.cs

@@ -1,7 +1,7 @@
 using System.IO;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 
-namespace PixiEditor.Models.IO.JascPalFile;
+namespace PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 
 /// <summary>
 ///     This class is responsible for parsing JASC-PAL files. Which holds the color palette data.
@@ -14,10 +14,7 @@ internal class JascFileParser : PaletteFileParser
 
     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[] lines = await ReadTextLines(path);
         string name = Path.GetFileNameWithoutExtension(path);
         string fileType = lines[0];
         string magicBytes = lines[1];
@@ -60,11 +57,11 @@ internal class JascFileParser : PaletteFileParser
         }
         catch
         {
-            return new PaletteFileData("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
+            return PaletteFileData.Corrupted;
         }
     }
 
-    public override async Task Save(string path, PaletteFileData data) => await SaveFile(path, data);
+    public override async Task<bool> Save(string path, PaletteFileData data) => await SaveFile(path, data);
 
     private static bool ValidateFile(string fileType, string magicBytes)
     {

+ 76 - 0
src/PixiEditor/Models/IO/PaletteParsers/PaintNetTxtParser.cs

@@ -0,0 +1,76 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Helpers;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+// https://www.getpaint.net/doc/latest/WorkingWithPalettes.html
+
+internal class PaintNetTxtParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Paint.NET Palette";
+    public override string[] SupportedFileExtensions { get; } = new string[] { ".txt" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private static async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        lines = lines.Where(x => !x.StartsWith(";")).ToArray();
+
+        List<Color> colors = new();
+        for (int i = 0; i < lines.Length; i++)
+        {
+            // Color format aarrggbb
+            string colorLine = lines[i];
+            if(colorLine.Length < 8)
+                continue;
+
+            byte a = byte.Parse(colorLine.Substring(0, 2), NumberStyles.HexNumber);
+            byte r = byte.Parse(colorLine.Substring(2, 2), NumberStyles.HexNumber);
+            byte g = byte.Parse(colorLine.Substring(4, 2), NumberStyles.HexNumber);
+            byte b = byte.Parse(colorLine.Substring(6, 2), NumberStyles.HexNumber);
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if(colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.AppendLine("; Paint.NET Palette File");
+        sb.AppendLine($"; Made using PixiEditor {VersionHelpers.GetCurrentAssemblyVersion().ToString()}");
+        sb.AppendLine($"; {data.Colors.Length} colors");
+        foreach (Color color in data.Colors)
+        {
+            sb.AppendLine(color.ToHex());
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 39 - 0
src/PixiEditor/Models/IO/PaletteParsers/PixiPaletteParser.cs

@@ -0,0 +1,39 @@
+using System.IO;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Parser;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class PixiPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Palette from PixiEditor .pixi";
+    public override string[] SupportedFileExtensions { get; } = { ".pixi" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var file = await PixiParser.DeserializeAsync(path);
+        if(file.Palette == null) return PaletteFileData.Corrupted;
+
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        return new PaletteFileData(name, file.Palette.Select(x => new Color(x.R, x.G, x.B, x.A)).ToArray());
+    }
+
+    public override bool CanSave => false;
+
+    public override Task<bool> Save(string path, PaletteFileData data)
+    {
+        throw new SavingNotSupportedException("Saving palette as .pixi directly is not supported.");
+    }
+}

+ 123 - 0
src/PixiEditor/Models/IO/PaletteParsers/PngPaletteParser.cs

@@ -0,0 +1,123 @@
+using System.IO;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class PngPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "PNG Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".png" };
+
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+           try
+           {
+               return await ParseFile(path);
+           }
+           catch
+           {
+               return PaletteFileData.Corrupted;
+           }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        return await Task.Run(() =>
+        {
+            PngBitmapDecoder decoder = new PngBitmapDecoder(new Uri(path), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
+
+            BitmapFrame frame = decoder.Frames[0];
+
+            Color[] colors = ExtractFromBitmap(frame);
+
+            PaletteFileData data = new(
+                Path.GetFileNameWithoutExtension(path), colors);
+
+            return data;
+        });
+    }
+
+    private Color[] ExtractFromBitmap(BitmapFrame frame)
+    {
+        if (frame.Palette is not null && frame.Palette.Colors.Count > 0)
+        {
+            return ExtractFromBitmapPalette(frame.Palette);
+        }
+
+        return ExtractFromBitmapSource(frame);
+    }
+
+    private Color[] ExtractFromBitmapSource(BitmapFrame frame)
+    {
+        if (frame.PixelWidth == 0 || frame.PixelHeight == 0)
+        {
+            return Array.Empty<Color>();
+        }
+
+        List<Color> colors = new();
+
+        byte[] pixels = new byte[frame.PixelWidth * frame.PixelHeight * 4];
+        frame.CopyPixels(pixels, frame.PixelWidth * 4, 0);
+        int pixelCount = pixels.Length / 4;
+        for (int i = 0; i < pixelCount; i++)
+        {
+            var color = GetColorFromBytes(pixels, i);
+            if (!colors.Contains(color))
+            {
+                colors.Add(color);
+            }
+        }
+
+        return colors.ToArray();
+    }
+
+    private Color GetColorFromBytes(byte[] pixels, int i)
+    {
+        return new Color(pixels[i * 4 + 2], pixels[i * 4 + 1], pixels[i * 4]);
+    }
+
+    private Color[] ExtractFromBitmapPalette(BitmapPalette palette)
+    {
+        if (palette.Colors == null || palette.Colors.Count == 0)
+        {
+            return Array.Empty<Color>();
+        }
+
+        return palette.Colors.Select(color => color.ToColor()).ToArray();
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        try
+        {
+            await SaveFile(path, data);
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+
+    private async Task SaveFile(string path, PaletteFileData data)
+    {
+        await Task.Run(() =>
+        {
+            WriteableBitmap bitmap = new(data.Colors.Length, 1, 96, 96, PixelFormats.Bgra32, null);
+            bitmap.Lock();
+            for (int i = 0; i < data.Colors.Length; i++)
+            {
+                Color color = data.Colors[i];
+                bitmap.SetPixel(i, 0, color.ToOpaqueMediaColor());
+            }
+
+            bitmap.Unlock();
+            PngBitmapEncoder encoder = new();
+            encoder.Frames.Add(BitmapFrame.Create(bitmap));
+            using FileStream stream = new(path, FileMode.Create);
+            encoder.Save(stream);
+        });
+    }
+}

+ 8 - 0
src/PixiEditor/Models/IO/PaletteParsers/SavingNotSupportedException.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+public class SavingNotSupportedException : Exception
+{
+    public SavingNotSupportedException(string message) : base(message)
+    {
+    }
+}

+ 6 - 6
src/PixiEditor/PixiEditor.csproj

@@ -227,14 +227,14 @@
 	</ItemGroup>
 	<ItemGroup>
 		<PackageReference Include="CLSEncoderDecoder" Version="1.0.0" />
-		<PackageReference Include="Dirkster.AvalonDock" Version="4.70.3" />
+		<PackageReference Include="Dirkster.AvalonDock" Version="4.72.0" />
 		<PackageReference Include="ByteSize" Version="2.1.1" />
-		<PackageReference Include="DiscordRichPresence" Version="1.1.1.14" />
-		<PackageReference Include="Hardware.Info" Version="10.1.0" />
-		<PackageReference Include="MessagePack" Version="2.4.35" />
+		<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
+		<PackageReference Include="Hardware.Info" Version="11.0.0" />
+		<PackageReference Include="MessagePack" Version="2.5.108" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
-		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
-		<PackageReference Include="OneOf" Version="3.0.223" />
+		<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+		<PackageReference Include="OneOf" Version="3.0.243" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
 		<PackageReference Include="PixiEditor.Parser" Version="3.3.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="3.0.0" />

+ 8 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -4,7 +4,7 @@ using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Localization;
-using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.XAML;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders.Palettes;
@@ -16,6 +16,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Views.Dialogs;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
+using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
@@ -319,6 +320,12 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         PrimaryColor = color;
     }
 
+    [Command.Internal("PixiEditor.CloseContextMenu")]
+    public void CloseContextMenu(System.Windows.Controls.ContextMenu menu)
+    {
+        menu.IsOpen = false;
+    }
+
     public void SetupPaletteParsers(IServiceProvider services)
     {
         PaletteParsers = new WpfObservableRangeCollection<PaletteFileParser>(services.GetServices<PaletteFileParser>());

+ 6 - 0
src/PixiEditor/Views/MainWindow.xaml

@@ -650,6 +650,12 @@
                                                                                                     Command="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
                                                                                                     CommandParameter="{Binding}" />
                                                                                             </b:EventTrigger>
+                                                                                            <b:EventTrigger EventName="MouseLeftButtonUp">
+                                                                                                <b:InvokeCommandAction
+                                                                                                    Command="{cmds:Command PixiEditor.CloseContextMenu, UseProvided=True}"
+                                                                                                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
+                                                                                                     AncestorType={x:Type ContextMenu}}}" />
+                                                                                            </b:EventTrigger>
                                                                                         </b:Interaction.Triggers>
                                                                                     </palettes:PaletteColor>
                                                                                 </DataTemplate>

+ 11 - 3
src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml

@@ -28,7 +28,7 @@
                                             Swatches="{Binding ElementName=paletteControl, Path=Swatches}"
                                             HintColor="{Binding ElementName=paletteControl, Path=HintColor}"
                                             Colors="{Binding ElementName=paletteControl, Path=Colors}"/>
-                    <StackPanel Margin="0, 0, 5, 0" HorizontalAlignment="Right" Width="85" VerticalAlignment="Center" Orientation="Horizontal">
+                    <StackPanel Margin="0, 0, 5, 0" HorizontalAlignment="Right" Width="110" VerticalAlignment="Center" Orientation="Horizontal">
                         <Button Margin="0, 0, 5, 0" Style="{StaticResource ToolButtonStyle}" Click="BrowsePalettes_Click"
                 Cursor="Hand" Height="24" Width="24" views:Translator.TooltipKey="BROWSE_PALETTES">
                             <Button.Background>
@@ -41,12 +41,20 @@
                                 <ImageBrush ImageSource="/Images/Folder.png"/>
                             </Button.Background>
                         </Button>
-                        <Button Height="24" Width="24" Margin="0" Style="{StaticResource ToolButtonStyle}" 
-                Cursor="Hand" views:Translator.TooltipKey="SAVE_PALETTE" Click="SavePalette_OnClick">
+                        <Button Height="24" Width="24" Margin="0, 0, 2.5, 0" Style="{StaticResource ToolButtonStyle}"
+                                IsEnabled="{Binding ElementName=paletteControl, Path=Colors.Count}"
+                                Cursor="Hand" views:Translator.TooltipKey="SAVE_PALETTE" Click="SavePalette_OnClick">
                             <Button.Background>
                                 <ImageBrush ImageSource="/Images/Save.png"/>
                             </Button.Background>
                         </Button>
+                        <Button Height="24" Width="24" Margin="0, 0, 5, 0" Style="{StaticResource ToolButtonStyle}"
+                                IsEnabled="{Binding ElementName=paletteControl, Path=Colors.Count}"
+                                Cursor="Hand" views:Translator.TooltipKey="DISCARD_PALETTE" Click="DiscardPalette_OnClick">
+                            <Button.Background>
+                                <ImageBrush ImageSource="/Images/Trash.png"/>
+                            </Button.Background>
+                        </Button>
                     </StackPanel>
                 </DockPanel>
             </StackPanel>

+ 22 - 2
src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml.cs

@@ -7,6 +7,8 @@ using Microsoft.Win32;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Views.Dialogs;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
@@ -131,14 +133,23 @@ internal partial class PaletteViewer : UserControl
     {
         SaveFileDialog saveFileDialog = new SaveFileDialog
         {
-            Filter = PaletteHelpers.GetFilter(FileParsers, false)
+            Filter = PaletteHelpers.GetFilter(FileParsers.Where(x => x.CanSave).ToList(), false)
         };
 
         if (saveFileDialog.ShowDialog() == true)
         {
             string fileName = saveFileDialog.FileName;
             var foundParser = FileParsers.First(x => x.SupportedFileExtensions.Contains(Path.GetExtension(fileName)));
-            await foundParser.Save(fileName, new PaletteFileData(Colors.ToArray()));
+            if (Colors == null || Colors.Count == 0)
+            {
+                NoticeDialog.Show("NO_COLORS_TO_SAVE", "ERROR");
+                return;
+            }
+            bool saved = await foundParser.Save(fileName, new PaletteFileData(Colors.ToArray()));
+            if (!saved)
+            {
+                NoticeDialog.Show("COULD_NOT_SAVE_PALETTE", "ERROR");
+            }
         }
     }
 
@@ -166,6 +177,7 @@ internal partial class PaletteViewer : UserControl
             return;
         }
 
+        e.Handled = true;
         await ImportPalette(filePath);
         dragDropGrid.Visibility = Visibility.Hidden;
     }
@@ -230,4 +242,12 @@ internal partial class PaletteViewer : UserControl
             SelectColorCommand.Execute(origin.CommandParameter);
         }
     }
+
+    private void DiscardPalette_OnClick(object sender, RoutedEventArgs e)
+    {
+        if(ConfirmationDialog.Show("DISCARD_PALETTE_CONFIRMATION", "DISCARD_PALETTE") == ConfirmationType.Yes)
+        {
+            Colors.Clear();
+        }
+    }
 }