Browse Source

Rewrote how FileTypes work

flabbet 1 year ago
parent
commit
50ba595a8e

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

@@ -594,4 +594,13 @@
   "SEND": "Send report",
   "SEND": "Send report",
   "OPEN_DOCKABLE_MENU": "Open Tab",
   "OPEN_DOCKABLE_MENU": "Open Tab",
   "TIMELINE_TITLE": "Timeline",
   "TIMELINE_TITLE": "Timeline",
+  "EXPORT_IMAGE_HEADER": "Image",
+  "EXPORT_ANIMATION_HEADER": "Animation",
+  "EXPORT_SPRITESHEET_HEADER": "Spritesheet",
+  "PIXI_FILE": "PixiEditor Files",
+  "PNG_FILE": "PNG Images",
+  "JPEG_FILE": "JPEG Images",
+  "GIF_FILE": "GIFs",
+  "BMP_FILE": "BMP Images",
+  "IMAGE_FILES": "Image Files",
 }
 }

+ 8 - 10
src/PixiEditor.AvaloniaUI/Helpers/Converters/FileExtensionToColorConverter.cs

@@ -15,16 +15,14 @@ internal class FileExtensionToColorConverter :
     static FileExtensionToColorConverter()
     static FileExtensionToColorConverter()
     {
     {
         extensionsToBrushes = new Dictionary<string, SolidColorBrush>();
         extensionsToBrushes = new Dictionary<string, SolidColorBrush>();
-        AssignFormatToBrush(FileType.Unset, UnknownBrush);
-        AssignFormatToBrush(FileType.Pixi, ColorBrush(226, 1, 45));
-        AssignFormatToBrush(FileType.Png, ColorBrush(56, 108, 254));
-        AssignFormatToBrush(FileType.Jpeg, ColorBrush(36, 179, 66));
-        AssignFormatToBrush(FileType.Bmp, ColorBrush(255, 140, 0));
-        AssignFormatToBrush(FileType.Gif, ColorBrush(180, 0, 255));
-    }
-    static void AssignFormatToBrush(FileType format, SolidColorBrush brush)
-    {
-        SupportedFilesHelper.GetFileTypeDialogData(format).Extensions.ForEach(i => extensionsToBrushes[i] = brush);
+
+        foreach (var fileTypes in SupportedFilesHelper.FileTypes)
+        {
+            foreach (var ext in fileTypes.Extensions)
+            {
+                extensionsToBrushes[ext] = fileTypes.EditorColor;   
+            }
+        }
     }
     }
 
 
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>

+ 0 - 19
src/PixiEditor.AvaloniaUI/Helpers/Extensions/FileTypeExtensions.cs

@@ -1,19 +0,0 @@
-using PixiEditor.AvaloniaUI.Models.Files;
-using PixiEditor.DrawingApi.Core.Surface;
-
-namespace PixiEditor.AvaloniaUI.Helpers.Extensions;
-
-public static class FileTypeExtensions
-{
-    public static EncodedImageFormat ToEncodedImageFormat(this FileType fileType)
-    {
-        return fileType switch
-        {
-            FileType.Png => EncodedImageFormat.Png,
-            FileType.Jpeg => EncodedImageFormat.Jpeg,
-            FileType.Bmp => EncodedImageFormat.Bmp,
-            FileType.Gif => EncodedImageFormat.Gif,
-            _ => EncodedImageFormat.Unknown
-        };
-    }
-}

+ 7 - 0
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -6,6 +6,7 @@ using PixiEditor.AnimationRenderer.FFmpeg;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.ExtensionServices;
 using PixiEditor.AvaloniaUI.Models.ExtensionServices;
+using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Handlers.Tools;
 using PixiEditor.AvaloniaUI.Models.Handlers.Tools;
 using PixiEditor.AvaloniaUI.Models.IO.PaletteParsers;
 using PixiEditor.AvaloniaUI.Models.IO.PaletteParsers;
@@ -102,6 +103,12 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IBrightnessToolHandler, BrightnessToolViewModel>()
             .AddSingleton<IBrightnessToolHandler, BrightnessToolViewModel>()
             .AddSingleton<IToolHandler, BrightnessToolViewModel>(x => (BrightnessToolViewModel)x.GetService<IBrightnessToolHandler>())
             .AddSingleton<IToolHandler, BrightnessToolViewModel>(x => (BrightnessToolViewModel)x.GetService<IBrightnessToolHandler>())
             .AddSingleton<IToolHandler, ZoomToolViewModel>()
             .AddSingleton<IToolHandler, ZoomToolViewModel>()
+            // File types
+            .AddSingleton<IoFileType, PixiFileType>()
+            .AddSingleton<IoFileType, PngFileType>()
+            .AddSingleton<IoFileType, JpegFileType>()
+            .AddSingleton<IoFileType, BmpFileType>()
+            .AddSingleton<IoFileType, GifFileType>()
             // Palette Parsers
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

+ 24 - 46
src/PixiEditor.AvaloniaUI/Helpers/SupportedFilesHelper.cs

@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage;
 using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Models.IO;
 
 
@@ -9,48 +6,31 @@ namespace PixiEditor.AvaloniaUI.Helpers;
 
 
 internal class SupportedFilesHelper
 internal class SupportedFilesHelper
 {
 {
-    static Dictionary<FileType, FileTypeDialogData> fileTypeDialogsData;
-    static List<FileTypeDialogData> allFileTypeDialogsData;
     public static string[] AllSupportedExtensions { get; private set; }
     public static string[] AllSupportedExtensions { get; private set; }
     public static string[] PrimaryExtensions { get; private set; }
     public static string[] PrimaryExtensions { get; private set; }
-
-    static SupportedFilesHelper()
+    
+    public static List<IoFileType> FileTypes { get; private set; }
+    
+    public static void InitFileTypes(IEnumerable<IoFileType> fileTypes)
     {
     {
-        fileTypeDialogsData = new Dictionary<FileType, FileTypeDialogData>();
-        allFileTypeDialogsData = new List<FileTypeDialogData>();
-
-        var allFormats = Enum.GetValues(typeof(FileType)).Cast<FileType>().ToList();
+        FileTypes = fileTypes.ToList();
 
 
-        foreach (var format in allFormats)
-        {
-            var fileTypeDialogData = new FileTypeDialogData(format);
-            if (format != FileType.Unset)
-                fileTypeDialogsData[format] = fileTypeDialogData;
-
-            allFileTypeDialogsData.Add(fileTypeDialogData);
-        }
-
-        AllSupportedExtensions = fileTypeDialogsData.SelectMany(i => i.Value.Extensions).ToArray();
-        PrimaryExtensions = fileTypeDialogsData.Select(i => i.Value.PrimaryExtension).ToArray();
-    }
-
-    public static FileTypeDialogData GetFileTypeDialogData(FileType type)
-    {
-        return allFileTypeDialogsData.Where(i => i.FileType == type).Single();
+        AllSupportedExtensions = FileTypes.SelectMany(i => i.Extensions).ToArray();
+        PrimaryExtensions = FileTypes.Select(i => i.PrimaryExtension).ToArray();
     }
     }
 
 
-    public static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
+    public static string FixFileExtension(string pathWithOrWithoutExtension, IoFileType requestedType)
     {
     {
-        if (requestedType == FileType.Unset)
+        if (requestedType == null)
             throw new ArgumentException("A valid filetype is required", nameof(requestedType));
             throw new ArgumentException("A valid filetype is required", nameof(requestedType));
 
 
-        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
-        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
+        var typeFromPath = ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
+        if (typeFromPath != null && typeFromPath == requestedType)
             return pathWithOrWithoutExtension;
             return pathWithOrWithoutExtension;
-        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
+        return AppendExtension(pathWithOrWithoutExtension, requestedType);
     }
     }
 
 
-    public static string AppendExtension(string path, FileTypeDialogData data)
+    public static string AppendExtension(string path, IoFileType data)
     {
     {
         string ext = data.Extensions.First();
         string ext = data.Extensions.First();
         string filename = Path.GetFileName(path);
         string filename = Path.GetFileName(path);
@@ -75,20 +55,18 @@ internal class SupportedFilesHelper
     {
     {
         return AllSupportedExtensions.Contains(fileExtension);
         return AllSupportedExtensions.Contains(fileExtension);
     }
     }
-    public static FileType ParseImageFormat(string extension)
+    public static IoFileType ParseImageFormat(string extension)
     {
     {
-        var allExts = fileTypeDialogsData.Values.ToList();
-        var fileData = allExts.Where(i => i.Extensions.Contains(extension)).SingleOrDefault();
-        if (fileData != null)
-            return fileData.FileType;
-        return FileType.Unset;
+        var allExts = FileTypes;
+        var fileData = allExts.SingleOrDefault(i => i.Extensions.Contains(extension));
+        return fileData;
     }
     }
 
 
-    public static List<FileTypeDialogData> GetAllSupportedFileTypes(bool includePixi)
+    public static List<IoFileType> GetAllSupportedFileTypes(bool includePixi)
     {
     {
-        var allExts = fileTypeDialogsData.Values.ToList();
+        var allExts = FileTypes.ToList();
         if (!includePixi)
         if (!includePixi)
-            allExts.RemoveAll(item => item.FileType == FileType.Pixi);
+            allExts.RemoveAll(item => item is PixiFileType);
         return allExts;
         return allExts;
     }
     }
 
 
@@ -100,15 +78,15 @@ internal class SupportedFilesHelper
         return filter;
         return filter;
     }
     }
 
 
-    public static FileType GetSaveFileType(bool includePixi, IStorageFile file)
+    public static IoFileType GetSaveFileType(bool includePixi, IStorageFile file)
     {
     {
         var allSupportedExtensions = GetAllSupportedFileTypes(includePixi);
         var allSupportedExtensions = GetAllSupportedFileTypes(includePixi);
 
 
         if (file is null)
         if (file is null)
-            return FileType.Unset;
+            return null;
 
 
         string extension = Path.GetExtension(file.Path.LocalPath);
         string extension = Path.GetExtension(file.Path.LocalPath);
-        return allSupportedExtensions.Single(i => i.Extensions.Contains(extension)).FileType;
+        return allSupportedExtensions.Single(i => i.Extensions.Contains(extension));
     }
     }
 
 
     public static List<FilePickerFileType> BuildOpenFilter()
     public static List<FilePickerFileType> BuildOpenFilter()

+ 15 - 0
src/PixiEditor.AvaloniaUI/Models/Files/BmpFileType.cs

@@ -0,0 +1,15 @@
+using Avalonia.Media;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal class BmpFileType : ImageFileType
+{
+    public static BmpFileType BmpFile { get; } = new BmpFileType();
+    public override string[] Extensions { get; } = new[] { ".bmp" };
+    public override string DisplayName => new LocalizedString("BMP_FILE");
+    public override EncodedImageFormat EncodedImageFormat { get; } = EncodedImageFormat.Bmp;
+
+    public override SolidColorBrush EditorColor { get; } = new SolidColorBrush(new Color(255, 255, 140, 0));
+}

+ 0 - 6
src/PixiEditor.AvaloniaUI/Models/Files/FileType.cs

@@ -1,6 +0,0 @@
-namespace PixiEditor.AvaloniaUI.Models.Files;
-
-public enum FileType
-{
-    Unset, Pixi, Png, Jpeg, Bmp, Gif
-}

+ 15 - 0
src/PixiEditor.AvaloniaUI/Models/Files/GifFileType.cs

@@ -0,0 +1,15 @@
+using Avalonia.Media;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal class GifFileType : ImageFileType
+{
+    public static GifFileType GifFile { get; } = new GifFileType();
+    public override string[] Extensions { get; } = new[] { ".gif" };
+    public override string DisplayName => new LocalizedString("GIF_FILE");
+    public override EncodedImageFormat EncodedImageFormat { get; } = EncodedImageFormat.Gif;
+    
+    public override SolidColorBrush EditorColor { get; } = new SolidColorBrush(new Color(255, 180, 0, 255));
+}

+ 69 - 0
src/PixiEditor.AvaloniaUI/Models/Files/ImageFileType.cs

@@ -0,0 +1,69 @@
+using System.Security;
+using ChunkyImageLib;
+using PixiEditor.AvaloniaUI.Models.IO;
+using PixiEditor.AvaloniaUI.Models.IO.FileEncoders;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal abstract class ImageFileType : IoFileType
+{
+    public abstract EncodedImageFormat EncodedImageFormat { get; }
+
+    public override SaveResult TrySave(string pathWithExtension, DocumentViewModel document, VecI? exportSize = null)
+    {
+        var maybeBitmap = document.TryRenderWholeImage();
+        if (maybeBitmap.IsT0)
+            return SaveResult.ConcurrencyError;
+        var bitmap = maybeBitmap.AsT1;
+
+        EncodedImageFormat mappedFormat = EncodedImageFormat;
+
+        if (mappedFormat == EncodedImageFormat.Unknown)
+        {
+            return SaveResult.UnknownError;
+        }
+
+        UniversalFileEncoder encoder = new(mappedFormat);
+        return TrySaveAs(encoder, pathWithExtension, bitmap, exportSize);
+    }
+    
+    /// <summary>
+    /// Saves image to PNG file. Messes with the passed bitmap.
+    /// </summary>
+    private static SaveResult TrySaveAs(IFileEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
+    {
+        try
+        {
+            if (exportSize is not null && exportSize != bitmap.Size)
+                bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
+
+            if (!encoder.SupportsTransparency)
+                bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
+
+            using var stream = new FileStream(savePath, FileMode.Create);
+            encoder.SaveAsync(stream, bitmap);
+        }
+        catch (SecurityException)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (UnauthorizedAccessException)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (IOException)
+        {
+            return SaveResult.IoError;
+        }
+        catch
+        {
+            return SaveResult.UnknownError;
+        }
+
+        return SaveResult.Success;
+    }
+}

+ 13 - 24
src/PixiEditor.AvaloniaUI/Models/IO/FileTypeDialogData.cs → src/PixiEditor.AvaloniaUI/Models/Files/IoFileType.cs

@@ -1,19 +1,18 @@
-using System.Collections.Generic;
-using System.Linq;
+using Avalonia.Media;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
-using PixiEditor.AvaloniaUI.Models.Files;
+using PixiEditor.AvaloniaUI.Models.IO;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.Numerics;
 
 
-namespace PixiEditor.AvaloniaUI.Models.IO;
+namespace PixiEditor.AvaloniaUI.Models.Files;
 
 
-internal class FileTypeDialogData
+internal abstract class IoFileType
 {
 {
-    public FileType FileType { get; set; }
-
     /// <summary>
     /// <summary>
     /// Gets or sets file type extensions e.g. {jpg,jpeg}
     /// Gets or sets file type extensions e.g. {jpg,jpeg}
     /// </summary>
     /// </summary>
-    public List<string> Extensions { get; set; }
-
+    public abstract string[] Extensions { get; }
+    
     /// <summary>
     /// <summary>
     /// Gets file type's main extensions e.g. jpeg
     /// Gets file type's main extensions e.g. jpeg
     /// </summary>
     /// </summary>
@@ -22,21 +21,9 @@ internal class FileTypeDialogData
     /// <summary>
     /// <summary>
     /// Gets or sets name displayed before extension e.g. JPEG Files
     /// Gets or sets name displayed before extension e.g. JPEG Files
     /// </summary>
     /// </summary>
-    public string DisplayName { get; set; }
+    public abstract string DisplayName { get; }
 
 
-    public FileTypeDialogData(FileType fileType)
-    {
-        FileType = fileType;
-        Extensions = new List<string>();
-        Extensions.Add("." + FileType.ToString().ToLower());
-        if (FileType == FileType.Jpeg)
-            Extensions.Add(".jpg");
-
-        if (fileType == FileType.Pixi)
-            DisplayName = "PixiEditor Files";
-        else
-            DisplayName = FileType.ToString() + " Images";
-    }
+    public virtual SolidColorBrush EditorColor { get; } = new SolidColorBrush(Color.FromRgb(100, 100, 100));
 
 
     public FilePickerFileType SaveFilter
     public FilePickerFileType SaveFilter
     {
     {
@@ -50,9 +37,11 @@ internal class FileTypeDialogData
     {
     {
         get { return Extensions.Select(GetExtensionFormattedForDialog).ToList(); }
         get { return Extensions.Select(GetExtensionFormattedForDialog).ToList(); }
     }
     }
-
+    
     string GetExtensionFormattedForDialog(string extension)
     string GetExtensionFormattedForDialog(string extension)
     {
     {
         return "*" + extension;
         return "*" + extension;
     }
     }
+
+    public abstract SaveResult TrySave(string pathWithExtension, DocumentViewModel document, VecI? exportSize = null);
 }
 }

+ 16 - 0
src/PixiEditor.AvaloniaUI/Models/Files/JpegFileType.cs

@@ -0,0 +1,16 @@
+using Avalonia.Media;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal class JpegFileType : ImageFileType
+{
+    public static JpegFileType JpegFile { get; } = new JpegFileType();
+
+    public override string[] Extensions => new[] { ".jpeg", ".jpg" };
+    public override string DisplayName => new LocalizedString("JPEG_FILE");
+    public override EncodedImageFormat EncodedImageFormat { get; } = EncodedImageFormat.Jpeg;
+
+    public override SolidColorBrush EditorColor { get; } = new SolidColorBrush(new Color(255, 36, 179, 66));
+}

+ 38 - 0
src/PixiEditor.AvaloniaUI/Models/Files/PixiFileType.cs

@@ -0,0 +1,38 @@
+using Avalonia.Media;
+using PixiEditor.AvaloniaUI.Models.IO;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal class PixiFileType : IoFileType
+{
+    public static PixiFileType PixiFile { get; } = new PixiFileType();
+    public override string DisplayName => new LocalizedString("PIXI_FILE");
+    public override string[] Extensions => new[] { ".pixi" };
+
+    public override SolidColorBrush EditorColor { get;  } = new SolidColorBrush(new Color(255, 226, 1, 45));
+
+    public override SaveResult TrySave(string pathWithExtension, DocumentViewModel document, VecI? exportSize = null)
+    {
+        try
+        {
+            Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
+        }
+        catch (UnauthorizedAccessException e)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (IOException)
+        {
+            return SaveResult.IoError;
+        }
+        catch
+        {
+            return SaveResult.UnknownError;
+        }
+
+        return SaveResult.Success;
+    }
+}

+ 16 - 0
src/PixiEditor.AvaloniaUI/Models/Files/PngFileType.cs

@@ -0,0 +1,16 @@
+using Avalonia.Media;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Models.Files;
+
+internal class PngFileType : ImageFileType
+{
+    public static PngFileType PngFile { get; } = new PngFileType();
+
+    public override string DisplayName => new LocalizedString("PNG_FILE");
+    public override EncodedImageFormat EncodedImageFormat { get; } = EncodedImageFormat.Png;
+    public override string[] Extensions => new[] { ".png" };
+
+    public override SolidColorBrush EditorColor { get; } = new SolidColorBrush(new Color(255, 56, 108, 254));
+}

+ 5 - 91
src/PixiEditor.AvaloniaUI/Models/IO/Exporter.cs

@@ -1,20 +1,12 @@
-using System.IO;
-using System.IO.Compression;
+using System.IO.Compression;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
-using System.Security;
-using System.Threading.Tasks;
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers;
-using PixiEditor.AvaloniaUI.Helpers.Extensions;
 using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.Files;
-using PixiEditor.AvaloniaUI.Models.IO.FileEncoders;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
-using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
@@ -92,7 +84,7 @@ internal class Exporter
     /// <summary>
     /// <summary>
     /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// </summary>
     /// </summary>
-    public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, FileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
+    public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, IoFileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
     {
     {
         finalPath = SupportedFilesHelper.FixFileExtension(pathFromDialog, fileTypeFromDialog);
         finalPath = SupportedFilesHelper.FixFileExtension(pathFromDialog, fileTypeFromDialog);
         var saveResult = TrySave(document, finalPath, exportSize);
         var saveResult = TrySave(document, finalPath, exportSize);
@@ -113,30 +105,10 @@ internal class Exporter
 
 
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
 
-        if (typeFromPath == FileType.Pixi)
-        {
-            return TrySaveAsPixi(document, pathWithExtension);
-        }
-        
-        var maybeBitmap = document.TryRenderWholeImage();
-        if (maybeBitmap.IsT0)
-            return SaveResult.ConcurrencyError;
-        var bitmap = maybeBitmap.AsT1;
-
-        EncodedImageFormat mappedFormat = typeFromPath.ToEncodedImageFormat();
-
-        if (mappedFormat == EncodedImageFormat.Unknown)
-        {
+        if (typeFromPath is null)
             return SaveResult.UnknownError;
             return SaveResult.UnknownError;
-        }
-
-        UniversalFileEncoder encoder = new(mappedFormat);
-
-        return TrySaveAs(encoder, pathWithExtension, bitmap, exportSize);
-    }
-
-    static Exporter()
-    {
+        
+        return typeFromPath.TrySave(pathWithExtension, document, exportSize);
     }
     }
 
 
     public static void SaveAsGZippedBytes(string path, Surface surface)
     public static void SaveAsGZippedBytes(string path, Surface surface)
@@ -166,62 +138,4 @@ internal class Exporter
         using GZipStream compressedStream = new GZipStream(outputStream, CompressionLevel.Fastest);
         using GZipStream compressedStream = new GZipStream(outputStream, CompressionLevel.Fastest);
         compressedStream.Write(bytes);
         compressedStream.Write(bytes);
     }
     }
-
-    /// <summary>
-    /// Saves image to PNG file. Messes with the passed bitmap.
-    /// </summary>
-    private static SaveResult TrySaveAs(IFileEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
-    {
-        try
-        {
-            if (exportSize is not null && exportSize != bitmap.Size)
-                bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
-
-            if (!encoder.SupportsTransparency)
-                bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
-
-            using var stream = new FileStream(savePath, FileMode.Create);
-            encoder.SaveAsync(stream, bitmap);
-        }
-        catch (SecurityException)
-        {
-            return SaveResult.SecurityError;
-        }
-        catch (UnauthorizedAccessException)
-        {
-            return SaveResult.SecurityError;
-        }
-        catch (IOException)
-        {
-            return SaveResult.IoError;
-        }
-        catch
-        {
-            return SaveResult.UnknownError;
-        }
-
-        return SaveResult.Success;
-    }
-    
-    private static SaveResult TrySaveAsPixi(DocumentViewModel document, string pathWithExtension)
-    {
-        try
-        {
-            Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
-        }
-        catch (UnauthorizedAccessException e)
-        {
-            return SaveResult.SecurityError;
-        }
-        catch (IOException)
-        {
-            return SaveResult.IoError;
-        }
-        catch
-        {
-            return SaveResult.UnknownError;
-        }
-
-        return SaveResult.Success;
-    }
 }
 }

+ 10 - 9
src/PixiEditor.AvaloniaUI/Models/IO/FileTypeDialogDataSet.cs

@@ -3,44 +3,45 @@ using System.Linq;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.Files;
+using PixiEditor.Extensions.Common.Localization;
 
 
 namespace PixiEditor.AvaloniaUI.Models.IO;
 namespace PixiEditor.AvaloniaUI.Models.IO;
 
 
 internal class FileTypeDialogDataSet
 internal class FileTypeDialogDataSet
 {
 {
     public enum SetKind { Any, Pixi, Images }
     public enum SetKind { Any, Pixi, Images }
-    IEnumerable<FileTypeDialogData> fileTypes;
+    IEnumerable<IoFileType> fileTypes;
     string displayName;
     string displayName;
 
 
-    public FileTypeDialogDataSet(SetKind kind, IEnumerable<FileTypeDialogData> fileTypes = null)
+    public FileTypeDialogDataSet(SetKind kind, IEnumerable<IoFileType> fileTypes = null)
     {
     {
         if (fileTypes == null)
         if (fileTypes == null)
             fileTypes = SupportedFilesHelper.GetAllSupportedFileTypes(true);
             fileTypes = SupportedFilesHelper.GetAllSupportedFileTypes(true);
         var allSupportedExtensions = fileTypes;
         var allSupportedExtensions = fileTypes;
         if (kind == SetKind.Any)
         if (kind == SetKind.Any)
         {
         {
-            Init("Any", allSupportedExtensions);
+            Init(new LocalizedString("ANY"), allSupportedExtensions);
         }
         }
         else if (kind == SetKind.Pixi)
         else if (kind == SetKind.Pixi)
         {
         {
-            Init("PixiEditor Files", new[] { new FileTypeDialogData(FileType.Pixi) });
+            Init(new LocalizedString("PIXI_FILE"), new[] { PixiFileType.PixiFile });
         }
         }
         else if (kind == SetKind.Images)
         else if (kind == SetKind.Images)
         {
         {
-            Init("Image Files", allSupportedExtensions, FileType.Pixi);
+            Init(new LocalizedString("IMAGE_FILES"), allSupportedExtensions, PixiFileType.PixiFile);
         }
         }
     }
     }
 
 
-    public FileTypeDialogDataSet(string displayName, IEnumerable<FileTypeDialogData> fileTypes, FileType? fileTypeToSkip = null)
+    public FileTypeDialogDataSet(string displayName, IEnumerable<IoFileType> fileTypes, IoFileType? fileTypeToSkip = null)
     {
     {
         Init(displayName, fileTypes, fileTypeToSkip);
         Init(displayName, fileTypes, fileTypeToSkip);
     }
     }
 
 
-    private void Init(string displayName, IEnumerable<FileTypeDialogData> fileTypes, FileType? fileTypeToSkip = null)
+    private void Init(string displayName, IEnumerable<IoFileType> fileTypes, IoFileType? fileTypeToSkip = null)
     {
     {
         var copy = fileTypes.ToList();
         var copy = fileTypes.ToList();
-        if (fileTypeToSkip.HasValue)
-            copy.RemoveAll(i => i.FileType == fileTypeToSkip.Value);
+        if (fileTypeToSkip != null)
+            copy.RemoveAll(i => i == fileTypeToSkip);
         this.fileTypes = copy;
         this.fileTypes = copy;
 
 
         this.displayName = displayName;
         this.displayName = displayName;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -81,7 +81,7 @@
                                     DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
                                     DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
                                     DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
                                     DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
                                     SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
                                     SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
-                                    ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=Value, Mode=TwoWay}" />
+                                    ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=PixiFile, Mode=TwoWay}" />
                             </Interaction.Behaviors>
                             </Interaction.Behaviors>
                         </animations:TimelineSlider>
                         </animations:TimelineSlider>
 
 

+ 5 - 1
src/PixiEditor.AvaloniaUI/ViewModels/ViewModelMain.cs

@@ -3,11 +3,13 @@ using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using CommunityToolkit.Mvvm.Input;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers.Collections;
 using PixiEditor.AvaloniaUI.Helpers.Collections;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
+using PixiEditor.AvaloniaUI.Models.Files;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Menu;
 using PixiEditor.AvaloniaUI.ViewModels.Menu;
@@ -114,6 +116,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
 
         Preferences = services.GetRequiredService<IPreferences>();
         Preferences = services.GetRequiredService<IPreferences>();
         Preferences.Init();
         Preferences.Init();
+        
+        SupportedFilesHelper.InitFileTypes(services.GetServices<IoFileType>());
 
 
         CommandController = services.GetService<CommandController>();
         CommandController = services.GetService<CommandController>();
 
 
@@ -163,7 +167,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         SearchSubViewModel = services.GetService<SearchViewModel>();
         SearchSubViewModel = services.GetService<SearchViewModel>();
         
         
         AnimationsSubViewModel = services.GetService<AnimationsViewModel>();
         AnimationsSubViewModel = services.GetService<AnimationsViewModel>();
-
+        
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
 
 
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFileDialog.cs

@@ -9,7 +9,7 @@ namespace PixiEditor.AvaloniaUI.Views.Dialogs;
 
 
 internal class ExportFileDialog : CustomDialog
 internal class ExportFileDialog : CustomDialog
 {
 {
-    FileType _chosenFormat;
+    IoFileType _chosenFormat;
 
 
     private int fileHeight;
     private int fileHeight;
 
 
@@ -61,7 +61,7 @@ internal class ExportFileDialog : CustomDialog
         }
         }
     }
     }
 
 
-    public FileType ChosenFormat
+    public IoFileType ChosenFormat
     {
     {
         get => _chosenFormat;
         get => _chosenFormat;
         set
         set

+ 22 - 21
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml

@@ -8,33 +8,34 @@
         SizeToContent="WidthAndHeight"
         SizeToContent="WidthAndHeight"
         Name="saveFilePopup"
         Name="saveFilePopup"
         x:Class="PixiEditor.AvaloniaUI.Views.Dialogs.ExportFilePopup"
         x:Class="PixiEditor.AvaloniaUI.Views.Dialogs.ExportFilePopup"
+        x:ClassModifier="internal"
         Title="EXPORT_IMAGE">
         Title="EXPORT_IMAGE">
     <DockPanel Background="{DynamicResource ThemeBackgroundBrush}">
     <DockPanel Background="{DynamicResource ThemeBackgroundBrush}">
         <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" IsDefault="True"
         <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" IsDefault="True"
                 Margin="15" ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
                 Margin="15" ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
 
 
         <Border HorizontalAlignment="Center" Margin="15,30,15,0" Background="{DynamicResource ThemeBackgroundBrush1}"
         <Border HorizontalAlignment="Center" Margin="15,30,15,0" Background="{DynamicResource ThemeBackgroundBrush1}"
-                VerticalAlignment="Stretch" CornerRadius="10">
-            <Grid MinHeight="205" MinWidth="240">
-                <Grid.RowDefinitions>
-                    <RowDefinition/>
-                    <RowDefinition Height="Auto"/>
-                </Grid.RowDefinitions>
-                <input:SizePicker Margin="0,15,0,0"
-                                         x:Name="sizePicker"
-                                         IsSizeUnitSelectionVisible="True"
-                                         VerticalAlignment="Top"
-                                         ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
-                                         ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
-                    <TextBlock Grid.Row="1" Margin="5,0,5,10" VerticalAlignment="Bottom" Classes="hyperlink" TextWrapping="Wrap"
-                               Width="220" TextAlignment="Center" Text="{Binding SizeHint, Mode=OneTime, ElementName=saveFilePopup}">
-                        <Interaction.Behaviors>
-                            <EventTriggerBehavior EventName="PointerPressed">
-                                <InvokeCommandAction Command="{Binding SetBestPercentageCommand, ElementName=saveFilePopup}"/>
-                            </EventTriggerBehavior>
-                        </Interaction.Behaviors>
-                    </TextBlock>
-            </Grid>
+                    VerticalAlignment="Stretch" CornerRadius="10">
+                    <Grid MinHeight="205" MinWidth="240">
+                        <Grid.RowDefinitions>
+                            <RowDefinition/>
+                            <RowDefinition Height="Auto"/>
+                        </Grid.RowDefinitions>
+                        <input:SizePicker Margin="0,15,0,0"
+                                          x:Name="sizePicker"
+                                          IsSizeUnitSelectionVisible="True"
+                                          VerticalAlignment="Top"
+                                          ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
+                                          ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
+                        <TextBlock Grid.Row="1" Margin="5,0,5,10" VerticalAlignment="Bottom" Classes="hyperlink" TextWrapping="Wrap"
+                                   Width="220" TextAlignment="Center" Text="{Binding SizeHint, Mode=OneTime, ElementName=saveFilePopup}">
+                            <Interaction.Behaviors>
+                                <EventTriggerBehavior EventName="PointerPressed">
+                                    <InvokeCommandAction Command="{Binding SetBestPercentageCommand, ElementName=saveFilePopup}"/>
+                                </EventTriggerBehavior>
+                            </Interaction.Behaviors>
+                        </TextBlock>
+                    </Grid>
         </Border>
         </Border>
     </DockPanel>
     </DockPanel>
 </dialogs:PixiEditorPopup>
 </dialogs:PixiEditorPopup>

+ 6 - 6
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -8,7 +8,7 @@ using PixiEditor.Extensions.Common.Localization;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Dialogs;
 namespace PixiEditor.AvaloniaUI.Views.Dialogs;
 
 
-public partial class ExportFilePopup : PixiEditorPopup
+internal partial class ExportFilePopup : PixiEditorPopup
 {
 {
     public int SaveWidth
     public int SaveWidth
     {
     {
@@ -29,9 +29,9 @@ public partial class ExportFilePopup : PixiEditorPopup
         set => SetValue(SavePathProperty, value);
         set => SetValue(SavePathProperty, value);
     }
     }
 
 
-    public FileType SaveFormat
+    public IoFileType SaveFormat
     {
     {
-        get => (FileType)GetValue(SaveFormatProperty);
+        get => (IoFileType)GetValue(SaveFormatProperty);
         set => SetValue(SaveFormatProperty, value);
         set => SetValue(SaveFormatProperty, value);
     }
     }
 
 
@@ -47,8 +47,8 @@ public partial class ExportFilePopup : PixiEditorPopup
     public static readonly StyledProperty<string?> SavePathProperty =
     public static readonly StyledProperty<string?> SavePathProperty =
         AvaloniaProperty.Register<ExportFilePopup, string?>(nameof(SavePath), "");
         AvaloniaProperty.Register<ExportFilePopup, string?>(nameof(SavePath), "");
 
 
-    public static readonly StyledProperty<FileType> SaveFormatProperty =
-        AvaloniaProperty.Register<ExportFilePopup, FileType>(nameof(SaveFormat), FileType.Png);
+    public static readonly StyledProperty<IoFileType> SaveFormatProperty =
+        AvaloniaProperty.Register<ExportFilePopup, IoFileType>(nameof(SaveFormat), new PngFileType());
 
 
     public static readonly StyledProperty<AsyncRelayCommand> ExportCommandProperty =
     public static readonly StyledProperty<AsyncRelayCommand> ExportCommandProperty =
         AvaloniaProperty.Register<ExportFilePopup, AsyncRelayCommand>(
         AvaloniaProperty.Register<ExportFilePopup, AsyncRelayCommand>(
@@ -122,7 +122,7 @@ public partial class ExportFilePopup : PixiEditorPopup
             if (string.IsNullOrEmpty(file.Name) == false)
             if (string.IsNullOrEmpty(file.Name) == false)
             {
             {
                 SaveFormat = SupportedFilesHelper.GetSaveFileType(false, file);
                 SaveFormat = SupportedFilesHelper.GetSaveFileType(false, file);
-                if (SaveFormat == FileType.Unset)
+                if (SaveFormat == null)
                 {
                 {
                     return null;
                     return null;
                 }
                 }

+ 8 - 4
src/PixiEditor.Extensions/UI/Translator.cs

@@ -202,14 +202,18 @@ public class Translator : Control
         {
         {
             window.Bind(Window.TitleProperty, valueObservable);
             window.Bind(Window.TitleProperty, valueObservable);
         }
         }
-        else if (d is ContentControl contentControl)
-        {
-            contentControl.Bind(ContentControl.ContentProperty, valueObservable);
-        }
         else if (d is HeaderedSelectingItemsControl menuItem)
         else if (d is HeaderedSelectingItemsControl menuItem)
         {
         {
             menuItem.Bind(HeaderedSelectingItemsControl.HeaderProperty, valueObservable);
             menuItem.Bind(HeaderedSelectingItemsControl.HeaderProperty, valueObservable);
         }
         }
+        else if (d is HeaderedContentControl headeredContentControl)
+        {
+            headeredContentControl.Bind(HeaderedContentControl.HeaderProperty, valueObservable);
+        }
+        else if (d is ContentControl contentControl)
+        {
+            contentControl.Bind(ContentControl.ContentProperty, valueObservable);
+        }
 #if DEBUG
 #if DEBUG
         else
         else
         {
         {