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

Merge remote-tracking branch 'origin/master' into rect-based-undo-fixes

flabbet 3 жил өмнө
parent
commit
d735d5966f
79 өөрчлөгдсөн 1701 нэмэгдсэн , 329 устгасан
  1. 3 1
      PixiEditor/App.xaml
  2. 36 1
      PixiEditor/App.xaml.cs
  3. 37 0
      PixiEditor/Helpers/Converters/EnumBooleanConverter.cs
  4. 29 0
      PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  5. 24 22
      PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
  6. 1 1
      PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  7. 63 20
      PixiEditor/Helpers/CrashHelper.cs
  8. 19 0
      PixiEditor/Helpers/ProcessHelpers.cs
  9. 20 0
      PixiEditor/Helpers/SizeCalculator.cs
  10. 88 0
      PixiEditor/Helpers/SupportedFilesHelper.cs
  11. 14 0
      PixiEditor/Models/Constants.cs
  12. 25 1
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  13. 192 0
      PixiEditor/Models/DataHolders/CrashReport.cs
  14. 27 14
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  15. 12 10
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  16. 2 2
      PixiEditor/Models/DataHolders/Surface.cs
  17. 19 1
      PixiEditor/Models/Dialogs/ExportFileDialog.cs
  18. 2 4
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  19. 21 23
      PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
  20. 1 1
      PixiEditor/Models/Enums/FileType.cs
  21. 4 0
      PixiEditor/Models/Enums/SizeUnit.cs
  22. 44 13
      PixiEditor/Models/IO/Exporter.cs
  23. 55 0
      PixiEditor/Models/IO/FileTypeDialogData.cs
  24. 52 0
      PixiEditor/Models/IO/FileTypeDialogDataSet.cs
  25. 2 2
      PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
  26. 2 2
      PixiEditor/Models/IO/Importer.cs
  27. 1 0
      PixiEditor/Models/Tools/Tool.cs
  28. 1 1
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  29. 1 1
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  30. 1 1
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  31. 1 1
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  32. 1 1
      PixiEditor/Models/Tools/Tools/FloodFillTool.cs
  33. 1 1
      PixiEditor/Models/Tools/Tools/LineTool.cs
  34. 1 1
      PixiEditor/Models/Tools/Tools/MagicWandTool.cs
  35. 1 1
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  36. 2 7
      PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  37. 1 1
      PixiEditor/Models/Tools/Tools/PenTool.cs
  38. 1 1
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  39. 1 1
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  40. 1 1
      PixiEditor/Models/Tools/Tools/ZoomTool.cs
  41. 1 1
      PixiEditor/Models/Undo/StorageBasedChange.cs
  42. 3 1
      PixiEditor/PixiEditor.csproj
  43. 2 2
      PixiEditor/Styles/DarkCheckboxStyle.xaml
  44. 32 0
      PixiEditor/Styles/RadioButtonStyle.xaml
  45. 2 0
      PixiEditor/Styles/ThemeColors.xaml
  46. 4 0
      PixiEditor/Styles/ThemeStyle.xaml
  47. 62 0
      PixiEditor/ViewModels/CrashReportViewModel.cs
  48. 3 2
      PixiEditor/ViewModels/ImportFilePopupViewModel.cs
  49. 22 3
      PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  50. 5 12
      PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  51. 13 12
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  52. 28 4
      PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  53. 2 8
      PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  54. 11 0
      PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  55. 5 11
      PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  56. 4 3
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  57. 3 1
      PixiEditor/ViewModels/ViewModelMain.cs
  58. 55 0
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml
  59. 29 0
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs
  60. 9 1
      PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs
  61. 18 9
      PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml
  62. 2 25
      PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml.cs
  63. 12 7
      PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml
  64. 1 21
      PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs
  65. 46 0
      PixiEditor/Views/Dialogs/ResizeablePopup.cs
  66. 48 0
      PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml
  67. 101 0
      PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs
  68. 4 3
      PixiEditor/Views/MainWindow.xaml
  69. 23 1
      PixiEditor/Views/MainWindow.xaml.cs
  70. 5 13
      PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml.cs
  71. 7 3
      PixiEditor/Views/UserControls/SizeInput.xaml
  72. 23 7
      PixiEditor/Views/UserControls/SizeInput.xaml.cs
  73. 93 31
      PixiEditor/Views/UserControls/SizePicker.xaml
  74. 99 10
      PixiEditor/Views/UserControls/SizePicker.xaml.cs
  75. 1 1
      PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml
  76. 38 0
      PixiEditorTests/HelpersTests/ConvertersTests/FileExtensionToColorConverterTests.cs
  77. 29 0
      PixiEditorTests/HelpersTests/SizeCalculatorTest.cs
  78. 45 0
      PixiEditorTests/HelpersTests/SupportedFilesHelperTests.cs
  79. 2 0
      PixiEditorTests/ModelsTests/IO/ImporterTests.cs

+ 3 - 1
PixiEditor/App.xaml

@@ -1,7 +1,8 @@
 <Application x:Class="PixiEditor.App"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             StartupUri="Views/MainWindow.xaml">
+             >
+    <!--StartupUri="Views/MainWindow.xaml"-->
     <Application.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
@@ -26,6 +27,7 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
+                <ResourceDictionary Source="Styles/RadioButtonStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

+ 36 - 1
PixiEditor/App.xaml.cs

@@ -1,7 +1,12 @@
-using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
+using PixiEditor.Views.Dialogs;
+using System;
+using System.Diagnostics;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Windows;
 
 namespace PixiEditor
@@ -11,6 +16,23 @@ namespace PixiEditor
     /// </summary>
     public partial class App : Application
     {
+        protected override void OnStartup(StartupEventArgs e)
+        {
+            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();
+        }
+
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
         {
             base.OnSessionEnding(e);
@@ -21,5 +43,18 @@ namespace PixiEditor
                 e.Cancel = confirmation != ConfirmationType.Yes;
             }
         }
+
+        private bool ParseArgument(string pattern, string args, out Group[] groups)
+        {
+            Match match = Regex.Match(args, pattern, RegexOptions.IgnoreCase);
+            groups = null;
+
+            if (match.Success)
+            {
+                groups = match.Groups.Values.ToArray();
+            }
+
+            return match.Success;
+        }
     }
 }

+ 37 - 0
PixiEditor/Helpers/Converters/EnumBooleanConverter.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class EnumBooleanConverter : SingleInstanceConverter<EnumBooleanConverter>
+    {
+        #region IValueConverter Members
+        public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            string parameterString = parameter as string;
+            if (parameterString == null)
+                return DependencyProperty.UnsetValue;
+
+            if (Enum.IsDefined(value.GetType(), value) == false)
+                return DependencyProperty.UnsetValue;
+
+            object parameterValue = Enum.Parse(value.GetType(), parameterString);
+
+            return parameterValue.Equals(value);
+        }
+
+        public override object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            string parameterString = parameter as string;
+            if (parameterString == null)
+                return DependencyProperty.UnsetValue;
+
+            return Enum.Parse(targetType, parameterString);
+        }
+        #endregion
+    }
+}

+ 29 - 0
PixiEditor/Helpers/Converters/EnumToStringConverter.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Models.Enums;
+using System;
+
+namespace PixiEditor.Helpers.Converters
+{
+  internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConverter>
+  {
+    public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+    {
+      try
+      {
+        var type = value.GetType();
+        if (type == typeof(SizeUnit))
+        {
+          var valueCasted = (SizeUnit)value;
+          if (valueCasted == SizeUnit.Percentage)
+            return "%";
+          
+          return "px";
+        }
+        return Enum.GetName((value.GetType()), value);
+      }
+      catch
+      {
+        return string.Empty;
+      }
+    }
+  }
+}

+ 24 - 22
PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs

@@ -1,38 +1,40 @@
-using System;
+using PixiEditor.Models;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Drawing.Imaging;
 using System.Globalization;
 using System.Windows.Media;
+using PixiEditor.Models.Enums;
 
 namespace PixiEditor.Helpers.Converters
 {
     public class FileExtensionToColorConverter :
         SingleInstanceConverter<FileExtensionToColorConverter>
     {
-        private static readonly SolidColorBrush PixiBrush = ColorBrush(226, 1, 45);
-
-        private static readonly SolidColorBrush PngBrush = ColorBrush(56, 108, 254);
-
-        private static readonly SolidColorBrush JpgBrush = ColorBrush(36, 179, 66);
-
-        private static readonly SolidColorBrush UnknownBrush = ColorBrush(100, 100, 100);
+        private static readonly Dictionary<string, SolidColorBrush> extensionsToBrushes;
+        public static readonly SolidColorBrush UnknownBrush = ColorBrush(100, 100, 100);
 
+        static FileExtensionToColorConverter()
+        {
+            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);
+        }
+        
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
             string extension = (string)value;
 
-            if (extension == ".pixi")
-            {
-                return PixiBrush;
-            }
-            else if (extension == ".png")
-            {
-                return PngBrush;
-            }
-            else if (extension is ".jpg" or ".jpeg")
-            {
-                return JpgBrush;
-            }
-
-            return UnknownBrush;
+            return extensionsToBrushes.ContainsKey(extension) ? extensionsToBrushes[extension] : UnknownBrush;
         }
 
         private static SolidColorBrush ColorBrush(byte r, byte g, byte b)

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

@@ -31,4 +31,4 @@ namespace PixiEditor.Helpers.Converters
             return int.Parse(match.Groups[0].ValueSpan);
         }
     }
-}
+}

+ 63 - 20
PixiEditor/Helpers/CrashHelper.cs

@@ -1,25 +1,76 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+using ByteSizeLib;
+using Hardware.Info;
+using PixiEditor.Models.DataHolders;
+using System;
+using System.Globalization;
 using System.Text;
-using System.Threading.Tasks;
 
 namespace PixiEditor.Helpers
 {
-    public static class CrashHelper
+    public class CrashHelper
     {
-        public static void SaveCrashInfo(Exception e)
+        private readonly IHardwareInfo hwInfo;
+
+        public static void SaveCrashInfo(Exception exception)
+        {
+            CrashReport report = CrashReport.Generate(exception);
+            report.TrySave();
+            report.RestartToCrashReport();
+        }
+
+        public CrashHelper()
+        {
+            hwInfo = new HardwareInfo();
+        }
+
+        public void GetCPUInformation(StringBuilder builder)
+        {
+            builder.AppendLine("CPU:");
+            hwInfo.RefreshCPUList(false);
+
+            foreach (var processor in hwInfo.CpuList)
+            {
+                builder
+                    .AppendLine($"  Name: {processor.Name}")
+                    .AppendLine($"  Speed: {(processor.CurrentClockSpeed / 1000f).ToString("F2", CultureInfo.InvariantCulture)} GHz")
+                    .AppendLine($"  Max Speed: {(processor.MaxClockSpeed / 1000f).ToString("F2", CultureInfo.InvariantCulture)} GHz")
+                    .AppendLine();
+            }
+        }
+
+        public void GetGPUInformation(StringBuilder builder)
         {
-            StringBuilder builder = new System.Text.StringBuilder();
-            DateTime currentTime = DateTime.Now;
+            builder.AppendLine("GPU:");
+            hwInfo.RefreshVideoControllerList();
 
+            foreach (var gpu in hwInfo.VideoControllerList)
+            {
+                builder
+                    .AppendLine($"  Name: {gpu.Name}")
+                    .AppendLine($"  Driver: {gpu.DriverVersion}")
+                    .AppendLine();
+            }
+        }
+
+        public void GetMemoryInformation(StringBuilder builder)
+        {
+            builder.AppendLine("Memory:");
+            hwInfo.RefreshMemoryStatus();
+
+            var memInfo = hwInfo.MemoryStatus;
+
+            builder
+                .AppendLine($"  Available: {new ByteSize(memInfo.AvailablePhysical).ToString("", CultureInfo.InvariantCulture)}")
+                .AppendLine($"  Total: {new ByteSize(memInfo.TotalPhysical).ToString("", CultureInfo.InvariantCulture)}");
+        }
+
+        public static void AddExceptionMessage(StringBuilder builder, Exception e)
+        {
             builder
-                .Append($"PixiEditor crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n\n")
-                .Append("-------Crash message-------\n")
+                .AppendLine("\n-------Crash message-------")
                 .Append(e.GetType().ToString())
                 .Append(": ")
-                .Append(e.Message);
+                .AppendLine(e.Message);
             {
                 var innerException = e.InnerException;
                 while (innerException != null)
@@ -46,14 +97,6 @@ namespace PixiEditor.Helpers
                     innerException = innerException.InnerException;
                 }
             }
-
-            string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.txt";
-            string path = Path.Combine(
-                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-                "PixiEditor",
-                "crash_logs");
-            Directory.CreateDirectory(path);
-            File.WriteAllText(Path.Combine(path, filename), builder.ToString());
         }
     }
 }

+ 19 - 0
PixiEditor/Helpers/ProcessHelpers.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Diagnostics;
+
+namespace PixiEditor.Helpers
+{
+    public static class ProcessHelpers
+    {
+        public static void ShellExecute(string url)
+        {
+            Process.Start(new ProcessStartInfo
+            {
+                FileName = url,
+                UseShellExecute = true
+            });
+        }
+
+        public static void ShellExecuteEV(string path) => ShellExecute(Environment.ExpandEnvironmentVariables(path));
+    }
+}

+ 20 - 0
PixiEditor/Helpers/SizeCalculator.cs

@@ -0,0 +1,20 @@
+using System;
+
+namespace PixiEditor.Helpers
+{
+    public static class SizeCalculator
+    {
+        public static System.Drawing.Size CalcAbsoluteFromPercentage(float percentage, System.Drawing.Size currentSize)
+        {
+            float percFactor = percentage / 100f;
+            float newWidth = currentSize.Width * percFactor;
+            float newHeight = currentSize.Height * percFactor;
+            return new System.Drawing.Size((int)MathF.Round(newWidth), (int)MathF.Round(newHeight));
+        }
+
+        public static int CalcPercentageFromAbsolute(int initAbsoluteSize, int currentAbsoluteSize)
+        {
+            return (int)((float)currentAbsoluteSize * 100) / initAbsoluteSize;
+        }
+    }
+}

+ 88 - 0
PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -0,0 +1,88 @@
+using PixiEditor.Models;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.IO;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace PixiEditor.Helpers
+{
+    public class SupportedFilesHelper
+    {
+        static Dictionary<FileType, FileTypeDialogData> fileTypeDialogsData;
+        static List<FileTypeDialogData> allFileTypeDialogsData;
+        public static string[] AllSupportedExtensions { get; private set; }
+        public static string[] PrimaryExtensions { get; private set; }
+
+        static SupportedFilesHelper()
+        {
+            fileTypeDialogsData = new Dictionary<FileType, FileTypeDialogData>();
+            allFileTypeDialogsData = new List<FileTypeDialogData>();
+
+            var allFormats = Enum.GetValues(typeof(FileType)).Cast<FileType>().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();
+        }
+
+        public static bool IsSupportedFile(string path)
+        {
+            var ext = Path.GetExtension(path.ToLower());
+            return IsExtensionSupported(ext);
+        }
+
+        public static bool IsExtensionSupported(string fileExtension)
+        {
+            return AllSupportedExtensions.Contains(fileExtension);
+        }
+        public static FileType 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;
+        }
+
+        public static List<FileTypeDialogData> GetAllSupportedFileTypes(bool includePixi)
+        {
+            var allExts = fileTypeDialogsData.Values.ToList();
+            if (!includePixi)
+                allExts.RemoveAll(item => item.FileType == FileType.Pixi);
+            return allExts;
+        }
+
+        public static string BuildSaveFilter(bool includePixi)
+        {
+            var allSupportedExtensions = GetAllSupportedFileTypes(includePixi);
+            var filter = string.Join("|", allSupportedExtensions.Select(i => i.SaveFilter));
+
+            return filter;
+        }
+
+        public static string BuildOpenFilter()
+        {
+            var any = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Any).GetFormattedTypes();
+            var pixi = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Pixi).GetFormattedTypes();
+            var images = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Images).GetFormattedTypes();
+
+            var filter = any + "|" + pixi + "|" + images;
+            return filter;
+        }
+    }
+}

+ 14 - 0
PixiEditor/Models/Constants.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Models
+{
+    internal class Constants
+    {
+        public const int DefaultCanvasSize = 64;
+        public const int MaxPreviewWidth = 2048;
+        public const int MaxPreviewHeight = 2048;
+
+        public const int MaxCanvasSize = 9999;
+
+        public const string NativeExtensionNoDot = "pixi";
+        public const string NativeExtension = "." + NativeExtensionNoDot;
+    }
+}

+ 25 - 1
PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs

@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows.Input;
 
@@ -17,6 +18,29 @@ namespace PixiEditor.Models.Controllers.Shortcuts
 
         public Shortcut LastShortcut { get; private set; }
 
+        public const Key MoveViewportToolTransientChangeKey = Key.Space;
+
+        public Shortcut GetToolShortcut<T>()
+        {
+            return GetToolShortcut(typeof(T));
+        }
+
+        public Shortcut GetToolShortcut(Type type)
+        {
+            return ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().Where(i => i.CommandParameter is Type nextType && nextType == type).SingleOrDefault();
+        }
+
+        public Key GetToolShortcutKey<T>()
+        {
+            return GetToolShortcutKey(typeof(T));
+        }
+
+        public Key GetToolShortcutKey(Type type)
+        {
+            var sh = GetToolShortcut(type);
+            return sh != null ? sh.ShortcutKey : Key.None;
+        }
+
         public void KeyPressed(Key key, ModifierKeys modifiers)
         {
             if (!BlockShortcutExecution)

+ 192 - 0
PixiEditor/Models/DataHolders/CrashReport.cs

@@ -0,0 +1,192 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Parser;
+using PixiEditor.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public class CrashReport : IDisposable
+    {
+        public static CrashReport Generate(Exception exception)
+        {
+            StringBuilder builder = new();
+            DateTime currentTime = DateTime.Now;
+
+            builder
+                .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString()} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
+                .AppendLine("-----System Information----")
+                .AppendLine("General:")
+                .AppendLine($"  OS: {Environment.OSVersion.VersionString}")
+                .AppendLine();
+
+            CrashHelper helper = new();
+
+            try
+            {
+                helper.GetCPUInformation(builder);
+            }
+            catch (Exception cpuE)
+            {
+                builder.AppendLine($"Error ({cpuE.GetType().FullName}: {cpuE.Message}) while gathering CPU information, skipping...");
+            }
+
+            try
+            {
+                helper.GetGPUInformation(builder);
+            }
+            catch (Exception gpuE)
+            {
+                builder.AppendLine($"Error ({gpuE.GetType().FullName}: {gpuE.Message}) while gathering GPU information, skipping...");
+            }
+
+            try
+            {
+                helper.GetMemoryInformation(builder);
+            }
+            catch (Exception memE)
+            {
+                builder.AppendLine($"Error ({memE.GetType().FullName}: {memE.Message}) while gathering memory information, skipping...");
+            }
+
+            CrashHelper.AddExceptionMessage(builder, exception);
+
+            string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.zip";
+            string path = Path.Combine(
+                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                "PixiEditor",
+                "crash_logs");
+            Directory.CreateDirectory(path);
+
+            CrashReport report = new();
+            report.FilePath = Path.Combine(path, filename);
+            report.ReportText = builder.ToString();
+
+            return report;
+        }
+
+        public static CrashReport Parse(string path)
+        {
+            CrashReport report = new();
+            report.FilePath = path;
+
+            report.ZipFile = System.IO.Compression.ZipFile.Open(path, ZipArchiveMode.Read);
+            report.ExtractReport();
+
+            return report;
+        }
+
+        public string FilePath { get; set; }
+
+        public string ReportText { get; set; }
+
+        private ZipArchive ZipFile { get; set; }
+
+        public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
+
+        public IEnumerable<Document> RecoverDocuments()
+        {
+            foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
+            {
+                using Stream stream = entry.Open();
+
+                Document document;
+
+                try
+                {
+                    document = PixiParser.Deserialize(stream).ToDocument();
+                    document.ChangesSaved = false;
+                }
+                catch
+                {
+                    continue;
+                }
+
+                yield return document;
+            }
+        }
+
+        public void Dispose()
+        {
+            ZipFile.Dispose();
+        }
+
+        public void RestartToCrashReport()
+        {
+            Process process = new();
+
+            process.StartInfo = new()
+            {
+                FileName = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "exe"),
+                Arguments = $"--crash \"{Path.GetFullPath(FilePath)}\""
+            };
+
+            process.Start();
+        }
+
+        public bool TrySave()
+        {
+            try
+            {
+                Save();
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        public void Save()
+        {
+            using FileStream zipStream = new(FilePath, FileMode.Create, FileAccess.Write);
+            using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
+
+            using (Stream reportStream = archive.CreateEntry("report.txt").Open())
+            {
+                reportStream.Write(Encoding.UTF8.GetBytes(ReportText));
+            }
+
+            foreach (Document document in ViewModelMain.Current.BitmapManager.Documents)
+            {
+                try
+                {
+                    string documentPath =
+                        $"{(string.IsNullOrWhiteSpace(document.DocumentFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.DocumentFilePath))}-{document.OpenedUTC}.pixi";
+
+                    byte[] serialized = PixiParser.Serialize(document.ToSerializable());
+
+                    using Stream documentStream = archive.CreateEntry($"Documents/{documentPath}").Open();
+                    documentStream.Write(serialized);
+                }
+                catch
+                { }
+            }
+        }
+
+        private void ExtractReport()
+        {
+            ZipArchiveEntry entry = ZipFile.GetEntry("report.txt");
+            using Stream stream = entry.Open();
+
+            byte[] encodedReport = new byte[entry.Length];
+            stream.Read(encodedReport);
+
+            ReportText = Encoding.UTF8.GetString(encodedReport);
+        }
+
+        public class CrashReportUserMessage
+        {
+            public string Message { get; set; }
+
+            public string Mail { get; set; }
+        }
+    }
+}

+ 27 - 14
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -32,23 +32,18 @@ namespace PixiEditor.Models.DataHolders
             int offsetX = GetOffsetXForAnchor(Width, width, anchor);
             int offsetY = GetOffsetYForAnchor(Height, height, anchor);
 
-            int widthDiff = Math.Abs(width - oldWidth);
-            int heightDiff = Math.Abs(height - oldHeight);
-
-            PixelSize diff = new PixelSize(widthDiff, heightDiff);
-
-            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
-            PixelSize[] oldRects = Layers.Select(x => x.GetSize()).ToArray();
-            PixelSize[] newRects = Layers.Select(x => x.GetSize() + diff).ToArray();
             Thickness[] newOffsets = Layers.Select(x => new Thickness(offsetX + x.OffsetX, offsetY + x.OffsetY, 0, 0))
                 .ToArray();
 
-            object[] processArgs = { newOffsets, width, height, newRects };
-            object[] reverseProcessArgs = { oldOffsets, Width, Height, oldRects };
+            object[] processArgs = { newOffsets, width, height };
+            object[] reverseProcessArgs = { Width, Height };
+
+            StorageBasedChange change = new(this, Layers);
 
             ResizeCanvas(newOffsets, width, height);
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
+
+            UndoManager.AddUndoChange(change.ToChange(
+                RestoreDocumentLayersProcess,
                 reverseProcessArgs,
                 ResizeCanvasProcess,
                 processArgs,
@@ -145,7 +140,7 @@ namespace PixiEditor.Models.DataHolders
                         int diff = documentCenter.X - newOffsetX;
                         newOffsetX = layer.OffsetX + (diff * 2);
                     }
-                    else if(flip == FlipType.Vertical)
+                    else if (flip == FlipType.Vertical)
                     {
                         newOffsetY += layerCenter.Y;
                         int diff = documentCenter.Y - newOffsetY;
@@ -213,8 +208,13 @@ namespace PixiEditor.Models.DataHolders
 
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         {
+            int oldWidth = Width;
+            int oldHeight = Height;
             Width = (int)args[0];
             Height = (int)args[1];
+            DocumentSizeChanged?.Invoke(
+                this,
+                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, Width, Height));
             Layers.Clear();
             Layers.AddRange(layers);
         }
@@ -227,9 +227,22 @@ namespace PixiEditor.Models.DataHolders
         /// <param name="newHeight">New canvas height.</param>
         private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
         {
+            Int32Rect newCanvasRect = new(0, 0, newWidth, newHeight);
             for (int i = 0; i < Layers.Count; i++)
             {
-                Layers[i].Offset = offset[i];
+                Layer layer = Layers[i];
+                Thickness newOffset = offset[i];
+                Int32Rect newRect = new((int)newOffset.Left, (int)newOffset.Top, layer.Width, layer.Height);
+                Int32Rect newLayerRect = newRect.Intersect(newCanvasRect);
+                Surface newBitmap = new(newLayerRect.Width, newLayerRect.Height);
+                var oldBitmap = layer.LayerBitmap;
+                using var snapshot = oldBitmap.SkiaSurface.Snapshot();
+                newBitmap.SkiaSurface.Canvas.DrawImage(snapshot, newRect.X - newLayerRect.X, newRect.Y - newLayerRect.Y, Surface.ReplacingPaint);
+
+                layer.LayerBitmap = newBitmap;
+                oldBitmap.Dispose();
+
+                Layers[i].Offset = new Thickness(newLayerRect.X, newLayerRect.Y, 0, 0);
                 Layers[i].MaxWidth = newWidth;
                 Layers[i].MaxHeight = newHeight;
             }

+ 12 - 10
PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -43,11 +43,8 @@ namespace PixiEditor.Models.DataHolders
                 {
                     return "? (Corrupt)";
                 }
-
                 string extension = Path.GetExtension(filePath).ToLower();
-                return extension is not (".pixi" or ".png" or ".jpg" or ".jpeg")
-                    ? $"? ({extension})"
-                    : extension;
+                return SupportedFilesHelper.IsExtensionSupported(extension) ? extension : $"? ({extension})";
             }
         }
 
@@ -93,7 +90,7 @@ namespace PixiEditor.Models.DataHolders
 
                 return surface.ToWriteableBitmap();
             }
-            else if (FileExtension is ".png" or ".jpg" or ".jpeg")
+            else if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
             {
                 WriteableBitmap bitmap = null;
 
@@ -104,19 +101,24 @@ namespace PixiEditor.Models.DataHolders
                 catch
                 {
                     corrupt = true;
+                    return null;
                 }
+                
+                if (bitmap == null)//prevent crash
+                    return null;
 
-                const int MaxWidthInPixels = 2048;
-                const int MaxHeightInPixels = 2048;
                 ImageFileMaxSizeChecker imageFileMaxSizeChecker = new ImageFileMaxSizeChecker()
                 {
-                    MaxAllowedWidthInPixels = MaxWidthInPixels,
-                    MaxAllowedHeightInPixels = MaxHeightInPixels,
+                    MaxAllowedWidthInPixels = Constants.MaxPreviewWidth,
+                    MaxAllowedHeightInPixels = Constants.MaxPreviewHeight,
                 };
 
+                if (bitmap == null)
+                    return null;
+
                 return imageFileMaxSizeChecker.IsFileUnderMaxSize(bitmap) ?
                     bitmap
-                    : bitmap.Resize(width: MaxWidthInPixels, height: MaxHeightInPixels, WriteableBitmapExtensions.Interpolation.Bilinear);
+                    : bitmap.Resize(width: Constants.MaxPreviewWidth, height: Constants.MaxPreviewHeight, WriteableBitmapExtensions.Interpolation.Bilinear);
             }
 
             return null;

+ 2 - 2
PixiEditor/Models/DataHolders/Surface.cs

@@ -118,8 +118,8 @@ namespace PixiEditor.Models.DataHolders
         public unsafe SKColor GetSRGBPixel(int x, int y)
         {
             Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
-            SKColor color = (SKColor)new SKColorF((float)ptr[0], (float)ptr[1], (float)ptr[2], (float)ptr[3]);
-            return SKPMColor.UnPreMultiply(new SKPMColor((uint)color));
+            float a = (float)ptr[3];
+            return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
         }
 
         public void SetSRGBPixel(int x, int y, SKColor color)

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

@@ -1,10 +1,14 @@
-using PixiEditor.Views;
+using PixiEditor.Models.Enums;
+using PixiEditor.Views;
+using System.Drawing.Imaging;
 using System.Windows;
 
 namespace PixiEditor.Models.Dialogs
 {
     public class ExportFileDialog : CustomDialog
     {
+        FileType _chosenFormat;
+
         private int fileHeight;
 
         private string filePath;
@@ -56,6 +60,19 @@ namespace PixiEditor.Models.Dialogs
             }
         }
 
+        public FileType ChosenFormat
+        {
+            get => _chosenFormat;
+            set
+            {
+                if (_chosenFormat != value)
+                {
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
+                }
+            }
+        }
+
         public override bool ShowDialog()
         {
             ExportFilePopup popup = new ExportFilePopup
@@ -69,6 +86,7 @@ namespace PixiEditor.Models.Dialogs
                 FileWidth = popup.SaveWidth;
                 FileHeight = popup.SaveHeight;
                 FilePath = popup.SavePath;
+                ChosenFormat = popup.SaveFormat;
             }
 
             return (bool)popup.DialogResult;

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

@@ -5,11 +5,9 @@ namespace PixiEditor.Models.Dialogs
 {
     public class NewFileDialog : CustomDialog
     {
-        public const int defaultSize = 64;
+        private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
 
-        private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", defaultSize);
-
-        private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", defaultSize);
+        private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
 
         public int Width
         {

+ 21 - 23
PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs

@@ -50,41 +50,39 @@ namespace PixiEditor.Models.Dialogs
             return OpenResizeCanvas ? ShowResizeCanvasDialog() : ShowResizeDocumentCanvas();
         }
 
-        private bool ShowResizeDocumentCanvas()
-        {
-            ResizeDocumentPopup popup = new ResizeDocumentPopup
+        bool ShowDialog<T>()
+            where T : ResizeablePopup, new()
+        {
+            var popup = new T()
             {
-                NewHeight = Height,
-                NewWidth = Width
+                NewAbsoluteHeight = Height,
+                NewAbsoluteWidth = Width,
+                NewPercentageSize = 100,
+                NewSelectedUnit = SizeUnit.Pixel
             };
 
             popup.ShowDialog();
             if (popup.DialogResult == true)
             {
-                Width = popup.NewWidth;
-                Height = popup.NewHeight;
+                Width = popup.NewAbsoluteWidth;
+                Height = popup.NewAbsoluteHeight;
+                if (popup is ResizeCanvasPopup resizeCanvas)
+                {
+                    ResizeAnchor = resizeCanvas.SelectedAnchorPoint;
+                }
             }
 
             return (bool)popup.DialogResult;
         }
 
-        private bool ShowResizeCanvasDialog()
+        private bool ShowResizeDocumentCanvas()
         {
-            ResizeCanvasPopup popup = new ResizeCanvasPopup
-            {
-                NewHeight = Height,
-                NewWidth = Width
-            };
-
-            popup.ShowDialog();
-            if (popup.DialogResult == true)
-            {
-                Width = popup.NewWidth;
-                Height = popup.NewHeight;
-                ResizeAnchor = popup.SelectedAnchorPoint;
-            }
+            return ShowDialog<ResizeDocumentPopup>();
+        }
 
-            return (bool)popup.DialogResult;
+        private bool ShowResizeCanvasDialog()
+        {
+            return ShowDialog<ResizeCanvasPopup>();
         }
     }
-}
+}

+ 1 - 1
PixiEditor/Models/Enums/FileType.cs

@@ -2,6 +2,6 @@
 {
     public enum FileType
     {
-        Png = 0
+        Unset, Pixi, Png, Jpeg, Bmp, Gif
     }
 }

+ 4 - 0
PixiEditor/Models/Enums/SizeUnit.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.Models.Enums
+{
+  public enum SizeUnit { Pixel, Percentage }
+}

+ 44 - 13
PixiEditor/Models/IO/Exporter.cs

@@ -1,11 +1,17 @@
 using Microsoft.Win32;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
+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;
@@ -23,8 +29,8 @@ namespace PixiEditor.Models.IO
         {
             SaveFileDialog dialog = new SaveFileDialog
             {
-                Filter = "PixiEditor Files | *.pixi",
-                DefaultExt = "pixi"
+                Filter = SupportedFilesHelper.BuildSaveFilter(true),
+                FilterIndex = 0
             };
             if ((bool)dialog.ShowDialog())
             {
@@ -44,10 +50,35 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         {
-            Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            if (Path.GetExtension(path) != Constants.NativeExtension)
+            {
+                var chosenFormat = ParseImageFormat(Path.GetExtension(path));
+                var bitmap = document.Renderer.FinalBitmap;
+                SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+            }
+            else
+            {
+                Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            }
+
             return path;
         }
 
+        public static FileType ParseImageFormat(string extension)
+        {
+            return SupportedFilesHelper.ParseImageFormat(extension);
+        }
+
+        static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
+
+        static Exporter()
+        {
+            encodersFactory[FileType.Png] = () => new PngBitmapEncoder();
+            encodersFactory[FileType.Jpeg] = () => new JpegBitmapEncoder();
+            encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder(); 
+            encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
+        }
+
         /// <summary>
         ///     Creates ExportFileDialog to get width, height and path of file.
         /// </summary>
@@ -55,15 +86,15 @@ 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())
-            {
-                SaveAsPng(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)
         {
             SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
@@ -94,18 +125,18 @@ namespace PixiEditor.Models.IO
         /// <summary>
         ///     Saves image to PNG file.
         /// </summary>
+        /// <param name="encoder">encoder to do the job.</param>
         /// <param name="savePath">Save file path.</param>
         /// <param name="exportWidth">File width.</param>
         /// <param name="exportHeight">File height.</param>
         /// <param name="bitmap">Bitmap to save.</param>
-        public static void SaveAsPng(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+        private static void SaveAs(BitmapEncoder encoder, string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
         {
             try
             {
                 bitmap = bitmap.Resize(exportWidth, exportHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
-                using (FileStream stream = new FileStream(savePath, FileMode.Create))
+                using (var stream = new FileStream(savePath, FileMode.Create))
                 {
-                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                     encoder.Frames.Add(BitmapFrame.Create(bitmap));
                     encoder.Save(stream);
                 }

+ 55 - 0
PixiEditor/Models/IO/FileTypeDialogData.cs

@@ -0,0 +1,55 @@
+using PixiEditor.Models.Enums;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PixiEditor.Models.IO
+{
+    public class FileTypeDialogData
+    {
+        public FileType FileType { get; set; }
+
+        /// <summary>
+        /// Gets or sets file type extensions e.g. {jpg,jpeg}
+        /// </summary>
+        public List<string> Extensions { get; set; }
+
+        /// <summary>
+        /// Gets file type's main extensions e.g. jpeg
+        /// </summary>
+        public string PrimaryExtension { get => Extensions.FirstOrDefault(); }
+
+        /// <summary>
+        /// Gets or sets name displayed before extension e.g. JPEG Files
+        /// </summary>
+        public string DisplayName { get; set; }
+
+        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 string SaveFilter
+        {
+            get { return DisplayName + "|" + GetExtensionFormattedForDialog(PrimaryExtension); }
+        }
+
+        public string ExtensionsFormattedForDialog
+        {
+            get { return string.Join(";", Extensions.Select(i => GetExtensionFormattedForDialog(i))); }
+        }
+
+        string GetExtensionFormattedForDialog(string extension)
+        {
+            return "*" + extension;
+        }
+    }
+}

+ 52 - 0
PixiEditor/Models/IO/FileTypeDialogDataSet.cs

@@ -0,0 +1,52 @@
+using PixiEditor.Helpers;
+using PixiEditor.Models.Enums;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PixiEditor.Models.IO
+{
+    public class FileTypeDialogDataSet
+    {
+        public enum SetKind { Any, Pixi, Images }
+        IEnumerable<FileTypeDialogData> fileTypes;
+        string displayName;
+
+        public FileTypeDialogDataSet(SetKind kind, IEnumerable<FileTypeDialogData> fileTypes = null)
+        {
+            if (fileTypes == null)
+                fileTypes = SupportedFilesHelper.GetAllSupportedFileTypes(true);
+            var allSupportedExtensions = fileTypes;
+            if (kind == SetKind.Any)
+            {
+                Init("Any", allSupportedExtensions);
+            }
+            else if (kind == SetKind.Pixi)
+            {
+                Init("PixiEditor Files", new[] { new FileTypeDialogData(FileType.Pixi) });
+            }
+            else if (kind == SetKind.Images)
+            {
+                Init("Image Files", allSupportedExtensions, FileType.Pixi);
+            }
+        }
+        public FileTypeDialogDataSet(string displayName, IEnumerable<FileTypeDialogData> fileTypes, FileType? fileTypeToSkip = null)
+        {
+            Init(displayName, fileTypes, fileTypeToSkip);
+        }
+
+        private void Init(string displayName, IEnumerable<FileTypeDialogData> fileTypes, FileType? fileTypeToSkip = null)
+        {
+            var copy = fileTypes.ToList();
+            if (fileTypeToSkip.HasValue)
+                copy.RemoveAll(i => i.FileType == fileTypeToSkip.Value);
+            this.fileTypes = copy;
+
+            this.displayName = displayName;
+        }
+
+        public string GetFormattedTypes()
+        {
+            return displayName + " |" + string.Join(";", this.fileTypes.Select(i => i.ExtensionsFormattedForDialog));
+        }
+    }
+}

+ 2 - 2
PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs

@@ -4,8 +4,8 @@ namespace PixiEditor.Models.IO
 {
     internal class ImageFileMaxSizeChecker
     {
-        public int MaxAllowedWidthInPixels { get; init; } = 2048;
-        public int MaxAllowedHeightInPixels { get; init; } = 2048;
+        public int MaxAllowedWidthInPixels { get; init; } = Constants.MaxPreviewWidth;
+        public int MaxAllowedHeightInPixels { get; init; } = Constants.MaxPreviewHeight;
 
         public ImageFileMaxSizeChecker()
         {

+ 2 - 2
PixiEditor/Models/IO/Importer.cs

@@ -7,6 +7,7 @@ using SkiaSharp;
 using System;
 using System.IO;
 using System.IO.Compression;
+using System.Linq;
 using System.Runtime.InteropServices;
 using System.Windows.Media.Imaging;
 
@@ -87,8 +88,7 @@ namespace PixiEditor.Models.IO
 
         public static bool IsSupportedFile(string path)
         {
-            path = path.ToLower();
-            return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
+            return SupportedFilesHelper.IsSupportedFile(path);
         }
 
         public static Surface LoadFromGZippedBytes(string path)

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

@@ -10,6 +10,7 @@ namespace PixiEditor.Models.Tools
 {
     public abstract class Tool : NotifyableObject
     {
+        public Key ShortcutKey { get; set; }
         public virtual string ToolName => GetType().Name.Replace("Tool", string.Empty);
 
         public virtual string DisplayName => ToolName.AddSpacesBeforeUppercaseLetters();

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

@@ -28,7 +28,7 @@ namespace PixiEditor.Models.Tools.Tools
             Toolbar = new BrightnessToolToolbar(CorrectionFactor);
         }
 
-        public override string Tooltip => "Makes pixels brighter or darker (U). Hold Ctrl to make pixels darker.";
+        public override string Tooltip => $"Makes pixels brighter or darker ({ShortcutKey}). Hold Ctrl to make pixels darker.";
 
         public BrightnessMode Mode { get; set; } = BrightnessMode.Default;
 

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

@@ -20,7 +20,7 @@ namespace PixiEditor.Models.Tools.Tools
             ActionDisplay = defaultActionDisplay;
         }
 
-        public override string Tooltip => "Draws circle on canvas (C). Hold Shift to draw even circle.";
+        public override string Tooltip => $"Draws circle on canvas ({ShortcutKey}). Hold Shift to draw even circle.";
 
         public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
         {

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

@@ -28,7 +28,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public override bool RequiresPreciseMouseData => true;
 
-        public override string Tooltip => "Picks the primary color from the canvas. (O)";
+        public override string Tooltip => $"Picks the primary color from the canvas. ({ShortcutKey})";
 
         public override void Use(IReadOnlyList<Coordinates> recordedMouseMovement)
         {

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

@@ -18,7 +18,7 @@ namespace PixiEditor.Models.Tools.Tools
             Toolbar = new BasicToolbar();
             pen = new PenTool(bitmapManager);
         }

-        public override string Tooltip => "Erasers color from pixel. (E)";
+        public override string Tooltip => $"Erasers color from pixel. ({ShortcutKey})";
 
         public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
         {

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

@@ -20,7 +20,7 @@ namespace PixiEditor.Models.Tools.Tools
             UseDocumentRectForUndo = true;
         }
 
-        public override string Tooltip => "Fills area with color. (G)";
+        public override string Tooltip => $"Fills area with color. ({ShortcutKey})";
 
         public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
         {

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

@@ -25,7 +25,7 @@ namespace PixiEditor.Models.Tools.Tools
             Toolbar = new BasicToolbar();
         }
 
-        public override string Tooltip => "Draws line on canvas (L). Hold Shift to draw even line.";
+        public override string Tooltip => $"Draws line on canvas ({ShortcutKey}). Hold Shift to draw even line.";
 
         public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
         {

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

@@ -24,7 +24,7 @@ namespace PixiEditor.Models.Tools.Tools
         private IEnumerable<Coordinates> oldSelection;
         private List<Coordinates> newSelection = new List<Coordinates>();
 
-        public override string Tooltip => "Magic Wand (W). Flood's the selection";
+        public override string Tooltip => $"Magic Wand ({ShortcutKey}). Flood's the selection";
 
         private Layer cachedDocument;
 

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

@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Tools.Tools
             BitmapManager = bitmapManager;
         }
 
-        public override string Tooltip => "Moves selected pixels (V). Hold Ctrl to move all layers.";
+        public override string Tooltip => $"Moves selected pixels ({ShortcutKey}). Hold Ctrl to move all layers.";
 
         public override bool HideHighlight => true;
 

+ 2 - 7
PixiEditor/Models/Tools/Tools/MoveViewportTool.cs

@@ -1,5 +1,4 @@
 using PixiEditor.Models.Position;
-using PixiEditor.ViewModels.SubViewModels.Main;
 using System.Collections.Generic;
 using System.Windows.Input;
 
@@ -7,18 +6,14 @@ namespace PixiEditor.Models.Tools.Tools
 {
     public class MoveViewportTool : ReadonlyTool
     {
-        private ToolsViewModel ToolsViewModel { get; }
-
-        public MoveViewportTool(ToolsViewModel toolsViewModel)
+        public MoveViewportTool()
         {
             Cursor = Cursors.SizeAll;
             ActionDisplay = "Click and move to pan viewport.";
-
-            ToolsViewModel = toolsViewModel;
         }
 
         public override bool HideHighlight => true;
-        public override string Tooltip => "Move viewport. (Space)";
+        public override string Tooltip => $"Move viewport. ({ShortcutKey})"; 
 
         public override void Use(IReadOnlyList<Coordinates> pixels)
         {

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

@@ -49,7 +49,7 @@ namespace PixiEditor.Models.Tools.Tools
             };
         }
 
-        public override string Tooltip => "Standard brush. (B)";
+        public override string Tooltip => $"Standard brush. ({ShortcutKey})";
 
         public bool AutomaticallyResizeCanvas { get; set; } = true;
 

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

@@ -17,7 +17,7 @@ namespace PixiEditor.Models.Tools.Tools
             ActionDisplay = defaultActionDisplay;
         }
 
-        public override string Tooltip => "Draws rectangle on canvas (R). Hold Shift to draw a square.";
+        public override string Tooltip => $"Draws rectangle on canvas ({ShortcutKey}). Hold Shift to draw a square.";
 
         public bool Filled { get; set; } = false;
 

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

@@ -36,7 +36,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public SelectionType SelectionType { get; set; } = SelectionType.Add;
 
-        public override string Tooltip => "Selects area. (M)";
+        public override string Tooltip => $"Selects area. ({ShortcutKey})";
 
         public override void BeforeUse()
         {

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

@@ -18,7 +18,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public override bool HideHighlight => true;
 
-        public override string Tooltip => "Zooms viewport (Z). Click to zoom in, hold alt and click to zoom out.";
+        public override string Tooltip => $"Zooms viewport ({ShortcutKey}). Click to zoom in, hold alt and click to zoom out.";
 
         public override void OnKeyDown(Key key)
         {

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

@@ -113,7 +113,7 @@ namespace PixiEditor.Models.Undo
                         storedLayer.SerializedRect.Height);
 
                     using var image = layer.LayerBitmap.SkiaSurface.Snapshot();
-                    Surface targetSizeSurface = new Surface(finalRect.Width, finalRect.Height);
+                    using Surface targetSizeSurface = new Surface(finalRect.Width, finalRect.Height);
 
                     targetSizeSurface.SkiaSurface.Canvas.DrawImage(image, finalRect, SKRect.Create(0, 0, finalRect.Width, finalRect.Height), Surface.ReplacingPaint);
 

+ 3 - 1
PixiEditor/PixiEditor.csproj

@@ -183,18 +183,20 @@
 	</ItemGroup>
 	<ItemGroup>
 		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.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="MessagePack" Version="2.3.85" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
-		<PackageReference Include="PixiEditor.ColorPicker" Version="3.1.0" />
+		<PackageReference Include="PixiEditor.ColorPicker" Version="3.2.0" />
 		<PackageReference Include="PixiEditor.Parser" Version="2.0.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.0.0.1" />
 		<PackageReference Include="SkiaSharp" Version="2.80.3" />

+ 2 - 2
PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -13,7 +13,7 @@
                         <BulletDecorator.Bullet>
                             <Border x:Name="Border" Width="18" Height="18" CornerRadius="2" Background="#FF1B1B1B"
                                     BorderThickness="1">
-                                <Path Width="9" Height="9" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="#FF0077C9" StrokeThickness="1.5" Data="M 0 4 L 3 8 8 0" />
+                                <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>
                         </BulletDecorator.Bullet>
                         <ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True"/>
@@ -21,7 +21,7 @@
                     <ControlTemplate.Triggers>
                         <Trigger Property="IsMouseOver" Value="False">
                             <Setter TargetName="Border" Property="Background" Value="#252525" />
-                            <Setter TargetName="Border" Property="BorderBrush" Value="#3F3F46"/>
+                            <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource UIElementBorder}"/>
                         </Trigger>
                         <Trigger Property="IsChecked" Value="false">
                             <Setter TargetName="CheckMark" Property="Visibility" Value="Collapsed"/>

+ 32 - 0
PixiEditor/Styles/RadioButtonStyle.xaml

@@ -0,0 +1,32 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Style TargetType="{x:Type RadioButton}" >
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type RadioButton}">
+                    <BulletDecorator Background="Transparent" Cursor="Hand">
+                        <BulletDecorator.Bullet>
+                            <Grid Height="16" Width="16">
+                                <!--Define size of the Bullet-->
+                                <!--The two borders-->
+                                <Border Name="RadioOuter" Background="#202020" BorderBrush="{StaticResource UIElementBorder}" BorderThickness="1" CornerRadius="10" />
+                                <Border CornerRadius="10" Margin="4" Name="RadioMark" Background="{StaticResource UIElementBlue}" Visibility="Hidden" />
+                            </Grid>
+                        </BulletDecorator.Bullet>
+                        <!--Text element-->
+                        <TextBlock Margin="3,1,0,0" Foreground="White" FontSize="12">
+                        <ContentPresenter />
+                        </TextBlock>
+                    </BulletDecorator>
+                    <!--If item is checked, trigger the visibility of the mark-->
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsChecked" Value="true">
+                            <!--If item is checked, trigger the visibility of the mark and change the color of the selected bullet into a darker gray for better highlighting-->
+                            <Setter TargetName="RadioMark" Property="Visibility" Value="Visible"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+</ResourceDictionary>

+ 2 - 0
PixiEditor/Styles/ThemeColors.xaml

@@ -8,4 +8,6 @@
     <SolidColorBrush x:Key="BrighterAccentColor" Color="#3F3F46" />
     <SolidColorBrush x:Key="AlmostLightModeAccentColor" Color="#4F4F4F" />
     <SolidColorBrush x:Key="SelectionColor" Color="#999" />
+    <SolidColorBrush x:Key="UIElementBlue" Color="#FF0077C9"/>
+    <SolidColorBrush x:Key="UIElementBorder" Color="#3F3F46" />
 </ResourceDictionary>

+ 4 - 0
PixiEditor/Styles/ThemeStyle.xaml

@@ -9,6 +9,10 @@
         <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
     </Style>
 
+    <Style TargetType="{x:Type Border}">
+        <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
+    </Style>
+
     <Style TargetType="Button" x:Key="BaseDarkButton">
         <Setter Property="Background" Value="#404040" />
         <Setter Property="Foreground" Value="White" />

+ 62 - 0
PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -0,0 +1,62 @@
+using GalaSoft.MvvmLight.CommandWpf;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Views.Dialogs;
+using System.Diagnostics;
+using System.Windows;
+
+namespace PixiEditor.ViewModels
+{
+    public class CrashReportViewModel : ViewModelBase
+    {
+        private bool hasRecoveredDocuments = true;
+
+        public CrashReport CrashReport { get; }
+
+        public string ReportText { get; }
+
+        public int DocumentCount { get; }
+
+        public RelayCommand OpenSendCrashReportCommand { get; }
+
+        public RelayCommand RecoverDocumentsCommand { get; }
+
+        public RelayCommand AttachDebuggerCommand { get; }
+
+        public bool IsDebugBuild { get; set; }
+
+        public CrashReportViewModel(CrashReport report)
+        {
+            SetIsDebug();
+
+            CrashReport = report;
+            ReportText = report.ReportText;
+            DocumentCount = report.GetDocumentCount();
+            OpenSendCrashReportCommand = new(() => new SendCrashReportWindow(CrashReport).Show());
+            RecoverDocumentsCommand = new(RecoverDocuments, () => hasRecoveredDocuments, false);
+            AttachDebuggerCommand = new(AttachDebugger);
+        }
+
+        public void RecoverDocuments()
+        {
+            MainWindow window = MainWindow.CreateWithDocuments(CrashReport.RecoverDocuments());
+
+            Application.Current.MainWindow = window;
+            window.Show();
+            hasRecoveredDocuments = false;
+        }
+
+        [Conditional("DEBUG")]
+        private void SetIsDebug()
+        {
+            IsDebugBuild = true;
+        }
+
+        private void AttachDebugger()
+        {
+            if (!Debugger.Launch())
+            {
+                MessageBox.Show("Starting debugger failed", "Starting debugger failed", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+    }
+}

+ 3 - 2
PixiEditor/ViewModels/ImportFilePopupViewModel.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
+using PixiEditor.Models.IO;
 using System;
 using System.IO;
 using System.Windows;
@@ -69,12 +70,12 @@ namespace PixiEditor.ViewModels
 
         private void CheckForPath(string path)
         {
-            if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpeg") || path.EndsWith(".jpg")))
+            if (SupportedFilesHelper.IsSupportedFile(path))
             {
                 try
                 {
                     filePath = path;
-                    BitmapImage bitmap = new BitmapImage(new Uri(path));
+                    var bitmap = new BitmapImage(new Uri(path));
                     ImportHeight = bitmap.PixelHeight;
                     ImportWidth = bitmap.PixelWidth;
                 }

+ 22 - 3
PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -1,5 +1,8 @@
 using Microsoft.Win32;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.IO;
+using System.IO;
 using System.Windows;
 
 namespace PixiEditor.ViewModels
@@ -7,6 +10,7 @@ namespace PixiEditor.ViewModels
     internal class SaveFilePopupViewModel : ViewModelBase
     {
         private string _filePath;
+        private FileType _chosenFormat;
 
         public SaveFilePopupViewModel()
         {
@@ -27,11 +31,24 @@ namespace PixiEditor.ViewModels
                 if (_filePath != value)
                 {
                     _filePath = value;
-                    RaisePropertyChanged("FilePath");
+                    RaisePropertyChanged(nameof(FilePath));
                 }
             }
         }
 
+        public FileType ChosenFormat 
+        { 
+            get => _chosenFormat;
+            set
+            {
+                if (_chosenFormat != value)
+                {
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
+                }
+            }
+        }
+                
         /// <summary>
         ///     Command that handles Path choosing to save file
         /// </summary>
@@ -41,13 +58,14 @@ namespace PixiEditor.ViewModels
             {
                 Title = "Export path",
                 CheckPathExists = true,
-                DefaultExt = "PNG Image (.png) | *.png",
-                Filter = "PNG Image (.png) | *.png"
+                Filter = SupportedFilesHelper.BuildSaveFilter(false),
+                FilterIndex = 0
             };
             if (path.ShowDialog() == true)
             {
                 if (string.IsNullOrEmpty(path.FileName) == false)
                 {
+                    ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName));
                     return path.FileName;
                 }
             }
@@ -71,6 +89,7 @@ namespace PixiEditor.ViewModels
             if (path == null)
                 return;
             FilePath = path;
+            
             ((Window)parameter).DialogResult = true;
             CloseButton(parameter);
         }

+ 5 - 12
PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -1,6 +1,5 @@
 using PixiEditor.Helpers;
 using System;
-using System.Diagnostics;
 using System.IO;
 using System.Reflection;
 
@@ -12,30 +11,24 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand OpenInstallLocationCommand { get; set; }
 
+        public RelayCommand CrashCommand { get; set; }
+
         public DebugViewModel(ViewModelMain owner)
             : base(owner)
         {
             OpenFolderCommand = new RelayCommand(OpenFolder);
             OpenInstallLocationCommand = new RelayCommand(OpenInstallLocation);
+            CrashCommand = new RelayCommand(_ => throw new InvalidOperationException("Debug Crash"));
         }
 
         public static void OpenFolder(object parameter)
         {
-            OpenShellExecute((string)parameter);
+            ProcessHelpers.ShellExecuteEV(parameter as string);
         }
 
         public static void OpenInstallLocation(object parameter)
         {
-            OpenShellExecute(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
-        }
-
-        private static void OpenShellExecute(string path)
-        {
-            ProcessStartInfo startInfo = new (Environment.ExpandEnvironmentVariables(path));
-
-            startInfo.UseShellExecute = true;
-
-            Process.Start(startInfo);
+            ProcessHelpers.ShellExecuteEV(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
         }
     }
 }

+ 13 - 12
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -2,6 +2,7 @@
 using Newtonsoft.Json.Linq;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
+using PixiEditor.Models;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
@@ -10,6 +11,7 @@ using PixiEditor.Parser;
 using PixiEditor.Views.Dialogs;
 using System;
 using System.Collections.Generic;
+using System.Drawing.Imaging;
 using System.IO;
 using System.Linq;
 using System.Windows;
@@ -186,12 +188,13 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         {
-            var lastArg = Environment.GetCommandLineArgs().Last();
-            if (Importer.IsSupportedFile(lastArg) && File.Exists(lastArg))
+            var args = Environment.GetCommandLineArgs();
+            var file = args.Last();
+            if (Importer.IsSupportedFile(file) && File.Exists(file))
             {
-                Open(lastArg);
+                Open(file);
             }
-            else
+            else if (Owner.BitmapManager.Documents.Count == 0 || !args.Contains("--crash"))
             {
                 if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 {
@@ -199,16 +202,15 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 }
             }
         }
-
+                
         private void Open(object property)
         {
+            var filter = SupportedFilesHelper.BuildOpenFilter();
+
             OpenFileDialog dialog = new OpenFileDialog
             {
-                Filter =
-                "Any|*.pixi;*.png;*.jpg;*.jpeg;|" +
-                "PixiEditor Files | *.pixi|" +
-                "Image Files|*.png;*.jpg;*.jpeg;",
-                DefaultExt = "pixi"
+                Filter = filter,
+                FilterIndex = 0
             };
 
             if ((bool)dialog.ShowDialog())
@@ -244,8 +246,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew";
             if (paramIsAsNew ||
-                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath) ||
-                !Owner.BitmapManager.ActiveDocument.DocumentFilePath.EndsWith(".pixi"))
+                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) 
             {
                 Owner.BitmapManager.ActiveDocument.SaveWithDialog();
             }

+ 28 - 4
PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs

@@ -24,7 +24,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         private bool restoreToolOnKeyUp = false;
 
         private MouseInputFilter filter = new();
-
+    
         public IoViewModel(ViewModelMain owner)
             : base(owner)
         {
@@ -54,6 +54,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             {
                 Owner.BitmapManager.InputTarget.OnKeyDown(key);
             }
+
+            if (args.Key == ShortcutController.MoveViewportToolTransientChangeKey)
+                ChangeMoveViewportToolState(true);
         }
 
         private void ProcessShortcutDown(bool isRepeat, Key key)
@@ -79,6 +82,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
             if (Owner.BitmapManager.ActiveDocument != null)
                 Owner.BitmapManager.InputTarget.OnKeyUp(key);
+
+            if (args.Key == ShortcutController.MoveViewportToolTransientChangeKey)
+            {
+                ChangeMoveViewportToolState(false);     
+            }
         }
 
         private void ProcessShortcutUp(Key key)
@@ -107,7 +115,24 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void OnPreviewMiddleMouseButton(object sender)
         {
-            Owner.ToolsSubViewModel.SetActiveTool<MoveViewportTool>();
+            ChangeMoveViewportToolState(true);
+        }
+
+        void ChangeMoveViewportToolState(bool setOn)
+        {
+            if (setOn)
+            {
+                var moveViewportToolIsActive = Owner.ToolsSubViewModel.ActiveTool is MoveViewportTool;
+                if (!moveViewportToolIsActive)
+                {
+                    Owner.ToolsSubViewModel.SetActiveTool<MoveViewportTool>();
+                    Owner.ToolsSubViewModel.MoveToolIsTransient = true;
+                }
+            }
+            else if (Owner.ToolsSubViewModel.LastActionTool != null && Owner.ToolsSubViewModel.MoveToolIsTransient)
+            {
+                Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool);
+            }
         }
 
         private void OnMouseMove(object sender, EventArgs args)
@@ -128,8 +153,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else if (button == MouseButton.Middle)
             {
-                if (Owner.ToolsSubViewModel.LastActionTool != null)
-                    Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool);
+                ChangeMoveViewportToolState(false);
             }
         }
     }

+ 2 - 8
PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -40,18 +40,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void OpenHyperlink(object parameter)
         {
-            if (parameter == null)
+            if (parameter is not string s)
             {
                 return;
             }
 
-            var url = (string)parameter;
-            var processInfo = new ProcessStartInfo()
-            {
-                FileName = url,
-                UseShellExecute = true
-            };
-            Process.Start(processInfo);
+            ProcessHelpers.ShellExecute(s);
         }
 
         private void OpenShortcutWindow(object parameter)

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

@@ -23,6 +23,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public Tool LastActionTool { get; private set; }
 
+        public bool MoveToolIsTransient { get; set; }
+
         public Cursor ToolCursor
         {
             get => toolCursor;
@@ -74,6 +76,14 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
         }
 
+        public void SetupToolsTooltipShortcuts(IServiceProvider services)
+        {
+            foreach (var tool in ToolSet)
+            {
+                tool.ShortcutKey = Owner.ShortcutController.GetToolShortcutKey(tool.GetType());
+            }
+        }
+
         public void SetActiveTool<T>()
             where T : Tool
         {
@@ -82,6 +92,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void SetActiveTool(Tool tool)
         {
+            MoveToolIsTransient = false;
             if (ActiveTool != null)
             {
                 activeTool.IsActive = false;

+ 5 - 11
PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -1,5 +1,5 @@
 using AvalonDock.Layout;
-using PixiEditor.Helpers;
+using GalaSoft.MvvmLight.CommandWpf;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -7,9 +7,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
     public class WindowViewModel : SubViewModel<ViewModelMain>, ISettableOwner<ViewModelMain>
     {
-        public MainWindow MainWindow { get; private set; }
-
-        public RelayCommand ShowAvalonDockWindowCommand { get; set; }
+        public RelayCommand<string> ShowAvalonDockWindowCommand { get; set; }
 
         public WindowViewModel()
             : this(null)
@@ -19,9 +17,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public WindowViewModel(ViewModelMain owner)
             : base(owner)
         {
-            ShowAvalonDockWindowCommand = new RelayCommand(ShowAvalonDockWindow);
-
-            MainWindow = (MainWindow)System.Windows.Application.Current?.MainWindow;
+            ShowAvalonDockWindowCommand = new(ShowAvalonDockWindow);
         }
 
         public void SetOwner(ViewModelMain owner)
@@ -29,11 +25,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner = owner;
         }
 
-        private void ShowAvalonDockWindow(object parameter)
+        private void ShowAvalonDockWindow(string id)
         {
-            string id = (string)parameter;
-
-            var anchorables = new List<LayoutAnchorable>(MainWindow.LayoutRoot.Manager.Layout
+            var anchorables = new List<LayoutAnchorable>(MainWindow.Current.LayoutRoot.Manager.Layout
                     .Descendents()
                     .OfType<LayoutAnchorable>());
 

+ 4 - 3
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Dialogs;
+using PixiEditor.Models;
+using PixiEditor.Models.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
 {
@@ -12,7 +13,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
             set => RaiseAndUpdatePreference(ref showStartupWindow, value);
         }
 
-        private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", NewFileDialog.defaultSize);
+        private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
 
         public int DefaultNewFileWidth
         {
@@ -25,7 +26,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
             }
         }
 
-        private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", NewFileDialog.defaultSize);
+        private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
 
         public int DefaultNewFileHeight
         {

+ 3 - 1
PixiEditor/ViewModels/ViewModelMain.cs

@@ -174,7 +174,7 @@ namespace PixiEditor.ViewModels
                         CreateToolShortcut<MoveTool>(Key.V, "Move selection"),
                         CreateToolShortcut<SelectTool>(Key.M, "Select"),
                         CreateToolShortcut<ZoomTool>(Key.Z, "Zoom"),
-                        CreateToolShortcut<MoveViewportTool>(Key.Space, "Move viewport"),
+                        CreateToolShortcut<MoveViewportTool>(Key.H, "Move viewport"),
                         CreateToolShortcut<MagicWandTool>(Key.W, "Magic wand"),
                         new Shortcut(Key.OemPlus, ViewportSubViewModel.ZoomCommand, "Zoom in", 1),
                         new Shortcut(Key.OemMinus, ViewportSubViewModel.ZoomCommand, "Zoom out", -1),
@@ -218,6 +218,8 @@ namespace PixiEditor.ViewModels
                         new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open shortcuts window", true)));
 
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
+
+            ToolsSubViewModel?.SetupToolsTooltipShortcuts(services);
         }
 
         /// <summary>

+ 55 - 0
PixiEditor/Views/Dialogs/CrashReportDialog.xaml

@@ -0,0 +1,55 @@
+<Window x:Class="PixiEditor.Views.Dialogs.CrashReportDialog"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:vm="clr-namespace:PixiEditor.ViewModels"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        d:DataContext="{d:DesignInstance vm:CrashReportViewModel}"
+        mc:Ignorable="d"
+        Background="{StaticResource AccentColor}" Foreground="White"
+        Title="PixiEditor has crashed!" WindowStyle="None"
+        MinWidth="450" MinHeight="195"
+        Width="480" Height="180">
+
+    <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>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition/>
+        </Grid.RowDefinitions>
+        <dial:DialogTitleBar TitleText="PixiEditor has crashed!" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+        <Grid Grid.Row="1" Margin="30,30,30,0" >
+            <StackPanel>
+                <Grid Background="{StaticResource MainColor}">
+                    <StackPanel Margin="7" VerticalAlignment="Center">
+                        <TextBlock Text="{Binding DocumentCount, StringFormat={}{0} file(s) can be recovered}"
+                       d:Text="2 file(s) can be recovered"/>
+                        <TextBlock TextWrapping="Wrap">You can help the developers fixing this bug by sending a crash report that was generated</TextBlock>
+                    </StackPanel>
+                </Grid>
+
+                <WrapPanel Margin="0,20,0,5" Orientation="Horizontal" HorizontalAlignment="Center">
+                    <Button Command="{Binding OpenSendCrashReportCommand}"
+                        Width="120"
+                        Style="{StaticResource DarkRoundButton}">Send report</Button>
+                    <Button Margin="5,0,5,0" Width="120"
+                        Command="{Binding RecoverDocumentsCommand}"
+                        Style="{StaticResource DarkRoundButton}">Recover files</Button>
+                    <Button Visibility="{Binding IsDebugBuild, Converter={BoolToVisibilityConverter}}"
+                    Style="{StaticResource DarkRoundButton}" Width="170"
+                    Command="{Binding AttachDebuggerCommand}">(Re)Attach debugger</Button>
+                </WrapPanel>
+            </StackPanel>
+        </Grid>
+    </Grid>
+</Window>

+ 29 - 0
PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Models.DataHolders;
+using PixiEditor.ViewModels;
+using System.Windows;
+using System.Windows.Input;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for CrashReportDialog.xaml
+    /// </summary>
+    public partial class CrashReportDialog : Window
+    {
+        public CrashReportDialog(CrashReport report)
+        {
+            DataContext = new CrashReportViewModel(report);
+            InitializeComponent();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+    }
+}

+ 9 - 1
PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs

@@ -1,4 +1,6 @@
-using PixiEditor.ViewModels;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels;
+using System.Drawing.Imaging;
 using System.Windows;
 using System.Windows.Input;
 
@@ -52,5 +54,11 @@ namespace PixiEditor.Views
             get => dataContext.FilePath;
             set => dataContext.FilePath = value;
         }
+
+        public FileType SaveFormat 
+        {
+            get => dataContext.ChosenFormat;
+            set => dataContext.ChosenFormat = value;
+        }
     }
 }

+ 18 - 9
PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.Views.ResizeCanvasPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeCanvasPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -7,9 +7,13 @@
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
-        mc:Ignorable="d" Name="window"
+        xmlns:base="clr-namespace:PixiEditor.Views"
+        mc:Ignorable="d" 
+        x:Name="window"
         Title="ResizeCanvasPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="350" Width="300" MinHeight="350" MinWidth="300" WindowStyle="None">
+        Height="420" Width="320" MinHeight="420" MinWidth="320" 
+        WindowStyle="None"
+        >
 
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
@@ -33,12 +37,17 @@
                 Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
 
         <StackPanel HorizontalAlignment="Center" Margin="0,30,0,0" Background="{StaticResource MainColor}"
-                    VerticalAlignment="Top" Grid.Row="1" Width="230" Height="225">
-            <local:SizePicker Margin="0,18,0,0" Width="200"
+                    VerticalAlignment="Top" Grid.Row="1" Width="250" Height="290">
+            <local:SizePicker Margin="0,8,0,0" 
+                              Width="240"
+                              Height="170"
                               x:Name="sizePicker"
-                              PreserveAspectRatio="False"
-                              ChosenHeight="{Binding NewHeight, Mode=TwoWay, ElementName=window}"
-                              ChosenWidth="{Binding NewWidth, Mode=TwoWay, ElementName=window}" />
+                              ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+                              ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+                              ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+                              SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+                              SizeUnitSelectionVisibility="Visible"
+                              />
             <Separator Margin="10,5,10,0" Background="{StaticResource AccentColor}" Height="1" />
             <DockPanel>
                 <Label Content="Anchor point:" Foreground="White" Margin="25,5,0,0" HorizontalAlignment="Left"
@@ -49,4 +58,4 @@
             </DockPanel>
         </StackPanel>
     </DockPanel>
-</Window>
+</base:ResizeablePopup>

+ 2 - 25
PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml.cs

@@ -7,21 +7,14 @@ namespace PixiEditor.Views
     /// <summary>
     ///     Interaction logic for ResizeCanvasPopup.xaml
     /// </summary>
-    public partial class ResizeCanvasPopup : Window
+    public partial class ResizeCanvasPopup : ResizeablePopup
     {
         // Using a DependencyProperty as the backing store for SelectedAnchorPoint.  This enables animation, styling, binding, etc...
         public static readonly DependencyProperty SelectedAnchorPointProperty =
             DependencyProperty.Register("SelectedAnchorPoint", typeof(AnchorPoint), typeof(ResizeCanvasPopup),
                 new PropertyMetadata(AnchorPoint.Top | AnchorPoint.Left));
 
-        // Using a DependencyProperty as the backing store for NewHeight.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty NewHeightProperty =
-            DependencyProperty.Register("NewHeight", typeof(int), typeof(ResizeCanvasPopup), new PropertyMetadata(0));
-
-        // Using a DependencyProperty as the backing store for NewWidth.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty NewWidthProperty =
-            DependencyProperty.Register("NewWidth", typeof(int), typeof(ResizeCanvasPopup), new PropertyMetadata(0));
-
+        
         public ResizeCanvasPopup()
         {
             InitializeComponent();
@@ -29,28 +22,12 @@ namespace PixiEditor.Views
             Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
-
         public AnchorPoint SelectedAnchorPoint
         {
             get => (AnchorPoint)GetValue(SelectedAnchorPointProperty);
             set => SetValue(SelectedAnchorPointProperty, value);
         }
 
-
-        public int NewHeight
-        {
-            get => (int)GetValue(NewHeightProperty);
-            set => SetValue(NewHeightProperty, value);
-        }
-
-
-        public int NewWidth
-        {
-            get => (int)GetValue(NewWidthProperty);
-            set => SetValue(NewWidthProperty, value);
-        }
-
-
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;

+ 12 - 7
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.Views.ResizeDocumentPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeDocumentPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -7,9 +7,10 @@
         xmlns:local="clr-namespace:PixiEditor.Views" 
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
-        mc:Ignorable="d" Name="window"
+        mc:Ignorable="d" x:Name="window"
         Title="ResizeDocumentPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="250" Width="300" MaxHeight="250" MaxWidth="300"
+        Height="305" Width="310" MinHeight="305" MinWidth="310"
+        xmlns:base="clr-namespace:PixiEditor.Views"
         WindowStyle="None">
 
     <WindowChrome.WindowChrome>
@@ -33,11 +34,15 @@
         <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" Margin="15"
                 Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
 
-        <local:SizePicker HorizontalAlignment="Center" Width="230" Height="125" Margin="0,30,0,0"
+        <local:SizePicker HorizontalAlignment="Center" Width="240" Height="180" Margin="0,30,0,0"
             x:Name="sizePicker"
             PreserveAspectRatio="True"
-            ChosenHeight="{Binding Path=NewHeight, Mode=TwoWay, ElementName=window}"
-            ChosenWidth="{Binding Path=NewWidth, Mode=TwoWay, ElementName=window}" />
+            ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+            ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+            ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+            SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+            SizeUnitSelectionVisibility="Visible"
+            />
 
     </DockPanel>
-</Window>
+</base:ResizeablePopup>

+ 1 - 21
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs

@@ -6,14 +6,8 @@ namespace PixiEditor.Views
     /// <summary>
     ///     Interaction logic for ResizeDocumentPopup.xaml
     /// </summary>
-    public partial class ResizeDocumentPopup : Window
+    public partial class ResizeDocumentPopup : ResizeablePopup
     {
-        public static readonly DependencyProperty NewHeightProperty =
-            DependencyProperty.Register("NewHeight", typeof(int), typeof(ResizeDocumentPopup), new PropertyMetadata(0));
-
-        public static readonly DependencyProperty NewWidthProperty =
-            DependencyProperty.Register("NewWidth", typeof(int), typeof(ResizeDocumentPopup), new PropertyMetadata(0));
-
         public ResizeDocumentPopup()
         {
             InitializeComponent();
@@ -22,20 +16,6 @@ namespace PixiEditor.Views
             Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
-        public int NewHeight
-        {
-            get => (int)GetValue(NewHeightProperty);
-            set => SetValue(NewHeightProperty, value);
-        }
-
-
-        public int NewWidth
-        {
-            get => (int)GetValue(NewWidthProperty);
-            set => SetValue(NewWidthProperty, value);
-        }
-
-
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;

+ 46 - 0
PixiEditor/Views/Dialogs/ResizeablePopup.cs

@@ -0,0 +1,46 @@
+using PixiEditor.Models.Enums;
+using System.Windows;
+
+namespace PixiEditor.Views
+{
+    public class ResizeablePopup : Window
+    {
+        public static readonly DependencyProperty NewPercentageSizeProperty =
+            DependencyProperty.Register(nameof(NewPercentageSize), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        public static readonly DependencyProperty NewSelectedUnitProperty =
+            DependencyProperty.Register(nameof(NewSelectedUnit), typeof(SizeUnit), typeof(SizePicker), new PropertyMetadata(SizeUnit.Pixel));
+
+        // Using a DependencyProperty as the backing store for NewAbsoluteHeight.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty NewAbsoluteHeightProperty =
+            DependencyProperty.Register(nameof(NewAbsoluteHeight), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        // Using a DependencyProperty as the backing store for NewAbsoluteWidth.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty NewAbsoluteWidthProperty =
+            DependencyProperty.Register(nameof(NewAbsoluteWidth), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        public int NewPercentageSize
+        {
+            get => (int)GetValue(NewPercentageSizeProperty);
+            set => SetValue(NewPercentageSizeProperty, value);
+        }
+
+        public SizeUnit NewSelectedUnit
+        {
+            get => (SizeUnit)GetValue(NewSelectedUnitProperty);
+            set => SetValue(NewSelectedUnitProperty, value);
+        }
+
+        public int NewAbsoluteHeight
+        {
+            get => (int)GetValue(NewAbsoluteHeightProperty);
+            set => SetValue(NewAbsoluteHeightProperty, value);
+        }
+
+        public int NewAbsoluteWidth
+        {
+            get => (int)GetValue(NewAbsoluteWidthProperty);
+            set => SetValue(NewAbsoluteWidthProperty, value);
+        }
+    }
+}

+ 48 - 0
PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml

@@ -0,0 +1,48 @@
+<Window x:Class="PixiEditor.Views.Dialogs.SendCrashReportWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d"
+        Background="{StaticResource AccentColor}" Foreground="White"
+        Title="Send crash report"
+        WindowStyle="None"
+        Height="170" Width="340">
+    <Window.Resources>
+        <Style TargetType="TextBlock">
+            <Setter Property="HorizontalAlignment" Value="Center"/>
+        </Style>
+        <Style TargetType="Button" BasedOn="{StaticResource DarkRoundButton}">
+            <Setter Property="Margin" Value="5"/>
+            <Setter Property="Width" Value="100"/>
+        </Style>
+    </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>
+
+    <StackPanel>
+        <dial:DialogTitleBar TitleText="Send crash report" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+        <StackPanel Margin="10">
+            <TextBlock>You can find the report here:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Width="140" Click="OpenInExplorer">Open in Explorer</Button>
+            </StackPanel>
+            <TextBlock TextAlignment="Center">You can report your crash report here:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Click="OpenHyperlink" Tag="github">GitHub</Button>
+                <Button Click="OpenHyperlink" Tag="discord">Discord</Button>
+                <Button Click="OpenHyperlink" Tag="email">E-Mail</Button>
+            </StackPanel>
+            <TextBlock TextWrapping="Wrap" FontSize="8">The report might contain the doucments that were opened when the crash happened</TextBlock>
+        </StackPanel>
+    </StackPanel>
+</Window>

+ 101 - 0
PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs

@@ -0,0 +1,101 @@
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using System;
+using System.IO;
+using System.Text;
+using System.Web;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for SendCrashReportWindow.xaml
+    /// </summary>
+    public partial class SendCrashReportWindow : Window
+    {
+        const string DiscordInviteLink = "https://discord.gg/eh8gx6vNEp";
+
+        private readonly CrashReport report;
+
+        public SendCrashReportWindow(CrashReport report)
+        {
+            this.report = report;
+            InitializeComponent();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        private void OpenInExplorer(object sender, RoutedEventArgs e)
+        {
+            string tempPath = Path.Combine(
+                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                "PixiEditor",
+                "crash_logs",
+                "to-copy");
+
+            DirectoryInfo info = Directory.CreateDirectory(tempPath);
+
+            foreach (var file in info.EnumerateFiles())
+            {
+                file.Delete();
+            }
+
+            File.Copy(report.FilePath, Path.Combine(tempPath, Path.GetFileName(report.FilePath)), true);
+
+            ProcessHelpers.ShellExecute(tempPath);
+        }
+
+        private void OpenHyperlink(object sender, RoutedEventArgs e)
+        {
+            var button = sender as Button;
+            var tag = button.Tag as string;
+
+            string body = HttpUtility.UrlEncode($"** IMPORTANT: Drop the \"{Path.GetFileName(report.FilePath)}\" file in here **");
+
+            var result = tag switch
+            {
+                "github" => GetGitHubLink(),
+                "discord" => DiscordInviteLink,
+                "email" => GetMailtoLink(),
+                _ => throw new NotImplementedException()
+            };
+
+            OpenInExplorer(null, null);
+            ProcessHelpers.ShellExecute(result);
+
+            string GetGitHubLink()
+            {
+                StringBuilder builder = new();
+
+                builder.Append("https://github.com/PixiEditor/PixiEditor/issues/new?title=");
+                builder.Append(HttpUtility.UrlEncode($"Crash Report"));
+                builder.Append("&body=");
+                builder.Append(body);
+
+                return builder.ToString();
+            }
+
+            string GetMailtoLink()
+            {
+                StringBuilder builder = new();
+
+                builder.Append("mailto:[email protected]?subject=");
+                builder.Append(HttpUtility.UrlEncode($"Crash Report"));
+                builder.Append("&body=");
+                builder.Append(body);
+
+                return builder.ToString();
+            }
+        }
+    }
+}

+ 4 - 3
PixiEditor/Views/MainWindow.xaml

@@ -177,11 +177,12 @@
                               CommandParameter="https://pixieditor.net/docs/Third-party-licenses"/>
                 </MenuItem>
                 <MenuItem Header="_Debug" Visibility="{Binding IsDebug, Converter={StaticResource BoolToVisibilityConverter}}">
-                    <MenuItem Header="_Open Local App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
+                    <MenuItem Header="Open _Local App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
                               CommandParameter="%LocalAppData%/PixiEditor"/>
-                    <MenuItem Header="_Open Roaming App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
+                    <MenuItem Header="Open _Roaming App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
                               CommandParameter="%AppData%/PixiEditor"/>
-                    <MenuItem Header="_Open Install Location"  Command="{Binding DebugSubViewModel.OpenInstallLocationCommand}"/>
+                    <MenuItem Header="Open _Install Location"  Command="{Binding DebugSubViewModel.OpenInstallLocationCommand}"/>
+                    <MenuItem Header="_Crash"  Command="{Binding DebugSubViewModel.CrashCommand}"/>
                 </MenuItem>
             </Menu>
             <StackPanel DockPanel.Dock="Right" VerticalAlignment="Top" Orientation="Horizontal" Margin="0,-5,-5,0"

+ 23 - 1
PixiEditor/Views/MainWindow.xaml.cs

@@ -1,10 +1,12 @@
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.Views.Dialogs;
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
@@ -24,11 +26,15 @@ namespace PixiEditor
 
         private readonly IPreferences preferences;
 
+        private readonly IServiceProvider services;
+
+        public static MainWindow Current { get; private set; }
+
         public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
 
         public MainWindow()
         {
-            IServiceProvider services = new ServiceCollection()
+            services = new ServiceCollection()
                 .AddPixiEditor()
                 .BuildServiceProvider();
 
@@ -63,6 +69,22 @@ namespace PixiEditor
             OnReleaseBuild();
         }
 
+        public static MainWindow CreateWithDocuments(IEnumerable<Document> documents)
+        {
+            MainWindow window = new();
+
+            BitmapManager bitmapManager = window.services.GetRequiredService<BitmapManager>();
+
+            foreach (Document document in documents)
+            {
+                bitmapManager.Documents.Add(document);
+            }
+
+            bitmapManager.ActiveDocument = bitmapManager.Documents.FirstOrDefault();
+
+            return window;
+        }
+
         protected override void OnClosing(CancelEventArgs e)
         {
             DataContext.CloseWindow(e);

+ 5 - 13
PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml.cs

@@ -1,20 +1,12 @@
 using Microsoft.Win32;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
 using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
+using static PixiEditor.Helpers.SupportedFilesHelper;
 
 namespace PixiEditor.Views.UserControls.Layers
 {
@@ -51,12 +43,12 @@ namespace PixiEditor.Views.UserControls.Layers
 
         private string OpenFilePicker()
         {
-
+            var imagesFilter = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Images).GetFormattedTypes();
             OpenFileDialog dialog = new OpenFileDialog
             {
                 Title = "Reference layer path",
                 CheckPathExists = true,
-                Filter = "Image Files|*.png;*.jpeg;*.jpg|PNG Files|*.png|JPG Files|*.jpeg;*.jpg"
+                Filter = imagesFilter
             };
 
             return (bool)dialog.ShowDialog() ? dialog.FileName : null;

+ 7 - 3
PixiEditor/Views/UserControls/SizeInput.xaml

@@ -41,7 +41,9 @@
                      Margin="0,0,5,0" VerticalAlignment="Center"
                      x:Name="textBox"
                      Text="{Binding Size, ElementName=uc, Converter={converters:ToolSizeToIntConverter}}"
-                     d:Text="22">
+                     d:Text="22"
+                     MaxLength = "6"
+                     >
                 <i:Interaction.Behaviors>
                     <behaviors:GlobalShortcutFocusBehavior/>
                     <behaviors:TextBoxFocusBehavior 
@@ -52,8 +54,10 @@
             </TextBox>
             <Grid Grid.Column="1" Background="{Binding BorderBrush, ElementName=border}"
                   d:Background="{StaticResource BrighterAccentColor}"/>
-            <TextBlock Text="px" TextAlignment="Right"
-                       Grid.Column="2" Margin="5,0" VerticalAlignment="Center"/>
+            <TextBlock Text="{Binding Unit, ElementName=uc, Converter={converters:EnumToStringConverter}}" TextAlignment="Right"
+                       Grid.Column="2" Margin="5,0" VerticalAlignment="Center" d:Text="px"
+                       
+                       />
         </Grid>
     </Border>
 </UserControl>

+ 23 - 7
PixiEditor/Views/UserControls/SizeInput.xaml.cs

@@ -1,4 +1,6 @@
-using System.Windows;
+using PixiEditor.Models.Enums;
+using System;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 
@@ -18,6 +20,19 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty BehaveLikeSmallEmbeddedFieldProperty =
             DependencyProperty.Register(nameof(BehaveLikeSmallEmbeddedField), typeof(bool), typeof(SizeInput), new PropertyMetadata(true));
 
+        public static readonly DependencyProperty UnitProperty =
+            DependencyProperty.Register(nameof(Unit), typeof(SizeUnit), typeof(SizeInput), new PropertyMetadata(SizeUnit.Pixel));
+
+        public SizeInput()
+        {
+            InitializeComponent();
+        }
+
+        private void SizeInput_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
+        {
+            textBox.Focus();
+        }
+
         public int Size
         {
             get => (int)GetValue(SizeProperty);
@@ -35,12 +50,7 @@ namespace PixiEditor.Views
             get => (bool)GetValue(BehaveLikeSmallEmbeddedFieldProperty);
             set => SetValue(BehaveLikeSmallEmbeddedFieldProperty, value);
         }
-
-        public SizeInput()
-        {
-            InitializeComponent();
-        }
-
+                
         public void FocusAndSelect()
         {
             textBox.Focus();
@@ -62,6 +72,12 @@ namespace PixiEditor.Views
             e.Handled = true;
         }
 
+        public SizeUnit Unit
+        {
+          get => (SizeUnit)GetValue(UnitProperty);
+          set => SetValue(UnitProperty, value);
+        }
+
         private static void InputSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
         {
             int newValue = (int)e.NewValue;

+ 93 - 31
PixiEditor/Views/UserControls/SizePicker.xaml

@@ -5,8 +5,10 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
              xmlns:local="clr-namespace:PixiEditor.Views"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:enums="clr-namespace:PixiEditor.Models.Enums"
              mc:Ignorable="d"
-             d:DesignHeight="150" d:DesignWidth="300" Name="uc">
+             d:DesignHeight="200" d:DesignWidth="240" Name="uc">
     <i:Interaction.Triggers>
         <i:EventTrigger EventName="Loaded">
             <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=LoadedCommand}"/>
@@ -19,47 +21,106 @@
             <Setter Property="BehaveLikeSmallEmbeddedField" Value="False"/>
             <Setter Property="FontSize" Value="12"/>
             <Setter Property="Margin" Value="10,0,0,0"/>
-            <Setter Property="Width" Value="100"/>
+            <Setter Property="Width" Value="80"/>
             <Setter Property="Height" Value="25"/>
         </Style>
     </UserControl.Resources>
     <Border Background="{StaticResource MainColor}" VerticalAlignment="Stretch">
-        <Grid Height="90" HorizontalAlignment="Center">
-            <Grid.ColumnDefinitions>
-                <ColumnDefinition Width="40"/>
-                <ColumnDefinition/>
-            </Grid.ColumnDefinitions>
-            <Grid.RowDefinitions>
-                <RowDefinition />
-                <RowDefinition />
-                <RowDefinition />
-            </Grid.RowDefinitions>
+        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
+            <Grid Height="60" HorizontalAlignment="Center" DockPanel.Dock="Top"
+                  Visibility="{Binding SizeUnitSelectionVisibility, ElementName=uc}">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="100"/>
+                    <ColumnDefinition/>
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="30"/>
+                    <RowDefinition Height="30"/>
+                </Grid.RowDefinitions>
 
-            <TextBlock Grid.Column="0" Grid.Row="0" Foreground="Snow" Text="Width:" VerticalAlignment="Center" HorizontalAlignment="Left" />
-            <local:SizeInput Grid.Column="1" Grid.Row="0"
+                <RadioButton Grid.Row="0" Grid.Column="0"
+                             x:Name="PercentageRb" 
+                             Foreground="White" 
+                             FontSize="12"
+                             GroupName="Unit"
+                             Checked="PercentageRb_Checked"   
+                             VerticalAlignment="Center"
+                             IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters:EnumBooleanConverter}, 
+                                              ConverterParameter=Percentage
+                                              }">Percentage:</RadioButton>
+                <local:SizeInput Grid.Column="1" Grid.Row="0" 
+                                     VerticalAlignment="Center"
+                                     x:Name="PercentageSizePicker"
+                                     IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                                     Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
+                                     Unit="Percentage"
+                                     Margin="-10,0,0,0"
+                                     MaxSize="999900"
+                                     Width="80">
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger EventName="LostFocus">
+                            <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=PercentageLostFocusCommand}"/>
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </local:SizeInput>
+
+                <RadioButton Grid.Row="1" Grid.Column="0"  
+                             x:Name="AbsoluteRb" 
+                             Foreground="White" 
+                             FontSize="12"
+                             GroupName="Unit"
+                             Checked="AbsoluteRb_Checked"
+                             VerticalAlignment="Center"
+                             IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters:EnumBooleanConverter}, 
+                                              ConverterParameter=Pixel}"
+                              >Absolute:</RadioButton>
+
+            </Grid>
+
+            <Grid Height="90" HorizontalAlignment="Center" DockPanel.Dock="Top">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="40"/>
+                    <ColumnDefinition/>
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition />
+                    <RowDefinition />
+                    <RowDefinition />
+                </Grid.RowDefinitions>
+
+                <TextBlock Grid.Column="0" Grid.Row="0" Foreground="Snow" Text="Width:" VerticalAlignment="Center" HorizontalAlignment="Left" />
+                <local:SizeInput Grid.Column="1" Grid.Row="0"
                              x:Name="WidthPicker"
+                             Width="80"
                              IsEnabled="{Binding EditingEnabled, ElementName=uc}"
-                             Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}">
-                <i:Interaction.Triggers>
-                    <i:EventTrigger EventName="LostFocus">
-                        <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=WidthLostFocusCommand}"/>
-                    </i:EventTrigger>
-                </i:Interaction.Triggers>
-            </local:SizeInput>
+                             Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}"
+                             Margin="50,0,0,0"
+                             >
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger EventName="LostFocus">
+                            <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=WidthLostFocusCommand}"/>
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </local:SizeInput>
 
-            <TextBlock Grid.Column="0" Grid.Row="1" Foreground="Snow" Text="Height:" VerticalAlignment="Center" HorizontalAlignment="Left"/>
-            <local:SizeInput Grid.Column="1" Grid.Row="1"
+                <TextBlock Grid.Column="0" Grid.Row="1" Foreground="Snow" Text="Height:" VerticalAlignment="Center" HorizontalAlignment="Left"/>
+                <local:SizeInput Grid.Column="1" Grid.Row="1"
                              x:Name="HeightPicker" 
                              IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                             Margin="50,0,0,0"
                              Size="{Binding ChosenHeight, ElementName=uc, Mode=TwoWay}">
-                <i:Interaction.Triggers>
-                    <i:EventTrigger EventName="LostFocus">
-                        <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=HeightLostFocusCommand}"/>
-                    </i:EventTrigger>
-                </i:Interaction.Triggers>
-            </local:SizeInput>
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger EventName="LostFocus">
+                            <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=HeightLostFocusCommand}"/>
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </local:SizeInput>
 
-            <CheckBox 
+                <CheckBox 
                   Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"
                   Name="aspectRatio" 
                   IsChecked="{Binding ElementName=uc, Path=PreserveAspectRatio}"
@@ -67,6 +128,7 @@
                   Foreground="White" 
                   HorizontalAlignment="Left" 
                   VerticalAlignment="Center" />
-        </Grid>
+            </Grid>
+        </StackPanel>
     </Border>
 </UserControl>

+ 99 - 10
PixiEditor/Views/UserControls/SizePicker.xaml.cs

@@ -1,4 +1,6 @@
 using PixiEditor.Helpers;
+using PixiEditor.Models;
+using PixiEditor.Models.Enums;
 using System;
 using System.Windows;
 using System.Windows.Controls;
@@ -19,6 +21,17 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty ChosenHeightProperty =
             DependencyProperty.Register(nameof(ChosenHeight), typeof(int), typeof(SizePicker), new PropertyMetadata(1));
 
+        public static readonly DependencyProperty ChosenPercentageSizeProperty =
+            DependencyProperty.Register(nameof(ChosenPercentageSize), typeof(float), typeof(SizePicker), new PropertyMetadata(100f));
+
+        public static readonly DependencyProperty SelectedUnitProperty =
+            DependencyProperty.Register(nameof(SelectedUnit), typeof(SizeUnit), typeof(SizePicker), new PropertyMetadata(SizeUnit.Pixel));
+
+        public static readonly DependencyProperty SizeUnitSelectionVisibilityProperty =
+            DependencyProperty.Register(nameof(SizeUnitSelectionVisibility), typeof(Visibility), typeof(SizePicker), new PropertyMetadata(Visibility.Collapsed));
+
+        System.Drawing.Size? initSize = null;
+
         public bool EditingEnabled
         {
             get => (bool)GetValue(EditingEnabledProperty);
@@ -37,6 +50,24 @@ namespace PixiEditor.Views
             set => SetValue(ChosenHeightProperty, value);
         }
 
+        public float ChosenPercentageSize
+        {
+            get => (float)GetValue(ChosenPercentageSizeProperty);
+            set => SetValue(ChosenPercentageSizeProperty, value);
+        }
+
+        public SizeUnit SelectedUnit
+        {
+            get => (SizeUnit)GetValue(SelectedUnitProperty);
+            set => SetValue(SelectedUnitProperty, value);
+        }
+
+        public Visibility SizeUnitSelectionVisibility
+        {
+            get => (Visibility)GetValue(SizeUnitSelectionVisibilityProperty);
+            set => SetValue(SizeUnitSelectionVisibilityProperty, value);
+        }
+
         public bool PreserveAspectRatio
         {
             get => (bool)GetValue(PreserveAspectRatioProperty);
@@ -46,46 +77,104 @@ namespace PixiEditor.Views
         public RelayCommand LoadedCommand { get; private set; }
         public RelayCommand WidthLostFocusCommand { get; private set; }
         public RelayCommand HeightLostFocusCommand { get; private set; }
+        public RelayCommand PercentageLostFocusCommand { get; private set; }
 
-        private bool initialValuesLoaded = false;
-        private int initW;
-        private int initH;
         public SizePicker()
         {
             LoadedCommand = new(AfterLoaded);
             WidthLostFocusCommand = new(WidthLostFocus);
             HeightLostFocusCommand = new(HeightLostFocus);
+            PercentageLostFocusCommand = new(PercentageLostFocus);
             InitializeComponent();
         }
 
         public void FocusWidthPicker()
         {
-            WidthPicker.FocusAndSelect();
+            PercentageSizePicker.FocusAndSelect();
         }
 
         private void AfterLoaded(object parameter)
         {
-            initW = ChosenWidth;
-            initH = ChosenHeight;
-            initialValuesLoaded = true;
+            initSize = new System.Drawing.Size(ChosenWidth, ChosenHeight);
+            EnableSizeEditors();
         }
 
         private void WidthLostFocus(object param) => OnSizeUpdate(true);
         private void HeightLostFocus(object param) => OnSizeUpdate(false);
 
+        private void PercentageLostFocus(object param)
+        {
+            if (!initSize.HasValue)
+                return;
+
+            float targetPercentage = GetTargetPercentage(initSize.Value, ChosenPercentageSize);
+            var newSize = SizeCalculator.CalcAbsoluteFromPercentage(targetPercentage, initSize.Value);
+
+            //this shouldn't ever be necessary but just in case
+            newSize.Width = Math.Clamp(newSize.Width, 1, Constants.MaxCanvasSize);
+            newSize.Height = Math.Clamp(newSize.Height, 1, Constants.MaxCanvasSize);
+
+            ChosenPercentageSize = targetPercentage;
+            ChosenWidth = newSize.Width;
+            ChosenHeight = newSize.Height;
+        }
+
+        private static float GetTargetPercentage(System.Drawing.Size initSize, float desiredPercentage)
+        {
+            var potentialSize = SizeCalculator.CalcAbsoluteFromPercentage(desiredPercentage, initSize);
+            // all good
+            if (potentialSize.Width > 0 && potentialSize.Height > 0 && potentialSize.Width <= Constants.MaxCanvasSize && potentialSize.Height <= Constants.MaxCanvasSize)
+                return desiredPercentage;
+
+            // canvas too small
+            if (potentialSize.Width <= 0 || potentialSize.Height <= 0)
+            {
+                if (potentialSize.Width < potentialSize.Height)
+                    return 100f / initSize.Width;
+                else
+                    return 100f / initSize.Height;
+            }
+
+            // canvas too big
+            if (potentialSize.Width > potentialSize.Height)
+                return Constants.MaxCanvasSize * 100f / initSize.Width;
+            else
+                return Constants.MaxCanvasSize * 100f / initSize.Height;
+        }
+
         private void OnSizeUpdate(bool widthUpdated)
         {
-            if (!initialValuesLoaded || !PreserveAspectRatio)
+            if (!initSize.HasValue || !PreserveAspectRatio)
                 return;
 
             if (widthUpdated)
             {
-                ChosenHeight = Math.Clamp(ChosenWidth * initH / initW, 1, HeightPicker.MaxSize);
+                ChosenHeight = Math.Clamp(ChosenWidth * initSize.Value.Height / initSize.Value.Width, 1, HeightPicker.MaxSize);
             }
             else
             {
-                ChosenWidth = Math.Clamp(ChosenHeight * initW / initH, 1, WidthPicker.MaxSize);
+                ChosenWidth = Math.Clamp(ChosenHeight * initSize.Value.Width / initSize.Value.Height, 1, WidthPicker.MaxSize);
             }
         }
+
+        private void PercentageRb_Checked(object sender, RoutedEventArgs e)
+        {
+            EnableSizeEditors();
+        }
+
+        private void AbsoluteRb_Checked(object sender, RoutedEventArgs e)
+        {
+            EnableSizeEditors();
+        }
+
+        private void EnableSizeEditors()
+        {
+            if (PercentageSizePicker != null)
+                PercentageSizePicker.IsEnabled = EditingEnabled && PercentageRb.IsChecked.Value;
+            if (WidthPicker != null)
+                WidthPicker.IsEnabled = EditingEnabled && !PercentageRb.IsChecked.Value;
+            if (HeightPicker != null)
+                HeightPicker.IsEnabled = EditingEnabled && !PercentageRb.IsChecked.Value;
+        }
     }
 }

+ 1 - 1
PixiEditor/Views/UserControls/ToolSettingColorPicker.xaml

@@ -9,7 +9,7 @@
              d:Background="{StaticResource AccentColor}">
     <Grid>
         <StackPanel Orientation="Horizontal">
-            <colorpicker:PortableColorPicker x:Name="ColorPicker" SelectedColor="{Binding SelectedColor, ElementName=uc,Mode=TwoWay}"/>
+            <colorpicker:PortableColorPicker Width="40" Height="20" x:Name="ColorPicker" SelectedColor="{Binding SelectedColor, ElementName=uc,Mode=TwoWay}"/>
             <Button Command="{Binding CopyMainColorCommand, ElementName=uc}" Style="{StaticResource DarkRoundButton}" FontSize="12" Width="100" Margin="5,0,0,0">Copy Main Color</Button>
         </StackPanel>
     </Grid>

+ 38 - 0
PixiEditorTests/HelpersTests/ConvertersTests/FileExtensionToColorConverterTests.cs

@@ -0,0 +1,38 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Converters;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Media;
+using Xunit;
+
+namespace PixiEditorTests.HelpersTests.ConvertersTests
+{
+    public class FileExtensionToColorConverterTests
+    {
+        private static SolidColorBrush GetTypedColor(string ext)
+        {
+            var converter = new FileExtensionToColorConverter();
+            object value = converter.Convert(ext, typeof(int), null, CultureInfo.CurrentCulture);
+            Assert.IsType<SolidColorBrush>(value);
+            return value as SolidColorBrush;
+        }
+
+        [Fact]
+        public void TestThatEachFormatHasColor()
+        {
+            SupportedFilesHelper.AllSupportedExtensions.ToList().ForEach(i =>
+            {
+                var typed = GetTypedColor(i);
+                Assert.NotEqual(FileExtensionToColorConverter.UnknownBrush, typed);
+            });
+        }
+               
+        [Fact]
+        public void TestThatUnsupportedFormatHasDefaultColor()
+        {
+            var converter = new FileExtensionToColorConverter();
+            var typed = GetTypedColor(".abc");
+            Assert.Equal(FileExtensionToColorConverter.UnknownBrush, typed);
+        }
+    }
+}

+ 29 - 0
PixiEditorTests/HelpersTests/SizeCalculatorTest.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Helpers;
+using Xunit;
+
+namespace PixiEditorTests.HelpersTests
+{
+    public class SizeCalculatorTest
+    {
+        [Theory]
+        [InlineData(50, 64, 64, 32, 32)]
+        [InlineData(100, 64, 64, 64, 64)]
+        [InlineData(200, 128, 128, 256, 256)]
+        public void TestCalculationOfAbsoluteFromPercentageWorks(int percent, int currentWidth, int currentHeight, int expectedWidth, int expectedHeight)
+        {
+            var newSize = SizeCalculator.CalcAbsoluteFromPercentage(percent, new System.Drawing.Size(currentWidth, currentHeight));
+            Assert.Equal(expectedWidth, newSize.Width);
+            Assert.Equal(expectedHeight, newSize.Height);
+        }
+
+        [Theory]
+        [InlineData(32, 64, 50)]
+        [InlineData(32, 32, 100)]
+        [InlineData(64, 32, 200)]
+        public void TestCalculationOfPercentageFromAbsoluteWorks(int currentSize, int initSize, int expectedPerc)
+        {
+            var perc = SizeCalculator.CalcPercentageFromAbsolute(initSize, currentSize);
+            Assert.Equal(perc, expectedPerc);
+        }
+    }
+}

+ 45 - 0
PixiEditorTests/HelpersTests/SupportedFilesHelperTests.cs

@@ -0,0 +1,45 @@
+using PixiEditor.Helpers;
+using Xunit;
+
+namespace PixiEditorTests.HelpersTests
+{
+    public class SupportedFilesHelperTests
+    {
+        [Fact]
+        public void TestAllExtensionsAreSupported()
+        {
+            var all = SupportedFilesHelper.AllSupportedExtensions;
+            Assert.Contains(all, i => i == ".pixi");
+            Assert.Contains(all, i => i == ".png");
+            Assert.Contains(all, i => i == ".jpg");
+            Assert.Contains(all, i => i == ".jpeg");
+            Assert.Contains(all, i => i == ".bmp");
+            Assert.Contains(all, i => i == ".gif");
+        }
+
+        [Fact]
+        public void TestBuildSaveFilter()
+        {
+            var filter = SupportedFilesHelper.BuildSaveFilter(true);
+            Assert.Equal("PixiEditor Files|*.pixi|Png Images|*.png|Jpeg Images|*.jpeg|Bmp Images|*.bmp|Gif Images|*.gif", filter);
+        }
+
+        [Fact]
+        public void TestBuildOpenFilter()
+        {
+            var filter = SupportedFilesHelper.BuildOpenFilter();
+            Assert.Equal("Any |*.pixi;*.png;*.jpeg;*.jpg;*.bmp;*.gif|PixiEditor Files |*.pixi|Image Files |*.png;*.jpeg;*.jpg;*.bmp;*.gif", filter);
+        }
+
+        [Fact]
+        public void TestIsSupportedFile()
+        {
+            Assert.True(SupportedFilesHelper.IsSupportedFile("foo.png"));
+            Assert.True(SupportedFilesHelper.IsSupportedFile("foo.bmp"));
+            Assert.True(SupportedFilesHelper.IsSupportedFile("foo.jpg"));
+            Assert.True(SupportedFilesHelper.IsSupportedFile("foo.jpeg"));
+
+            Assert.False(SupportedFilesHelper.IsSupportedFile("foo.abc"));
+        }
+    }
+}

+ 2 - 0
PixiEditorTests/ModelsTests/IO/ImporterTests.cs

@@ -26,6 +26,8 @@ namespace PixiEditorTests.ModelsTests.IO
         [InlineData("dub.jpeg")]
         [InlineData("-.JPEG")]
         [InlineData("dub.jpg")]
+        [InlineData("dub.gif")]
+        [InlineData("dub.bmp")]
         public void TestThatIsSupportedFile(string file)
         {
             Assert.True(Importer.IsSupportedFile(file));