Jelajahi Sumber

Merge remote-tracking branch 'origin/master' into palettes

flabbet 3 tahun lalu
induk
melakukan
5afd05fc83
100 mengubah file dengan 1919 tambahan dan 794 penghapusan
  1. 23 4
      .github/ISSUE_TEMPLATE/report_a_bug.md
  2. 7 7
      CONTRIBUTING.md
  3. 5 2
      PixiEditor/App.xaml
  4. 36 1
      PixiEditor/App.xaml.cs
  5. 1 1
      PixiEditor/Exceptions/CorruptedFileException.cs
  6. 1 1
      PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  7. 2 2
      PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs
  8. 43 26
      PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  9. 37 0
      PixiEditor/Helpers/Converters/EnumBooleanConverter.cs
  10. 29 0
      PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  11. 3 1
      PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
  12. 24 22
      PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
  13. 6 2
      PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  14. 1 1
      PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  15. 27 0
      PixiEditor/Helpers/Converters/WidthToBitmapScalingModeConverter.cs
  16. 22 0
      PixiEditor/Helpers/Converters/ZoomLevelToBitmapScalingModeConverter.cs
  17. 63 20
      PixiEditor/Helpers/CrashHelper.cs
  18. 8 2
      PixiEditor/Helpers/Extensions/Int32RectEx.cs
  19. 4 4
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  20. 13 0
      PixiEditor/Helpers/Extensions/SKRectIEx.cs
  21. 19 0
      PixiEditor/Helpers/ProcessHelpers.cs
  22. 20 0
      PixiEditor/Helpers/SizeCalculator.cs
  23. 88 0
      PixiEditor/Helpers/SupportedFilesHelper.cs
  24. 14 0
      PixiEditor/Models/Constants.cs
  25. 1 0
      PixiEditor/Models/Controllers/BitmapManager.cs
  26. 60 56
      PixiEditor/Models/Controllers/ClipboardController.cs
  27. 48 3
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  28. 9 2
      PixiEditor/Models/Controllers/SurfaceRenderer.cs
  29. 1 1
      PixiEditor/Models/Controllers/UndoManager.cs
  30. 192 0
      PixiEditor/Models/DataHolders/CrashReport.cs
  31. 2 0
      PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
  32. 30 13
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  33. 47 12
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  34. 1 5
      PixiEditor/Models/DataHolders/Document/Document.Preview.cs
  35. 80 24
      PixiEditor/Models/DataHolders/Document/Document.cs
  36. 20 0
      PixiEditor/Models/DataHolders/PixelSize.cs
  37. 18 16
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  38. 3 3
      PixiEditor/Models/DataHolders/Selection.cs
  39. 5 4
      PixiEditor/Models/DataHolders/Surface.cs
  40. 2 3
      PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  41. 21 3
      PixiEditor/Models/Dialogs/ExportFileDialog.cs
  42. 2 4
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  43. 3 16
      PixiEditor/Models/Dialogs/NoticeDialog.cs
  44. 21 23
      PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
  45. 1 1
      PixiEditor/Models/Enums/FileType.cs
  46. 4 0
      PixiEditor/Models/Enums/SizeUnit.cs
  47. 49 21
      PixiEditor/Models/IO/Exporter.cs
  48. 55 0
      PixiEditor/Models/IO/FileTypeDialogData.cs
  49. 52 0
      PixiEditor/Models/IO/FileTypeDialogDataSet.cs
  50. 0 20
      PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
  51. 2 2
      PixiEditor/Models/IO/Importer.cs
  52. 0 22
      PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs
  53. 5 9
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  54. 18 7
      PixiEditor/Models/Layers/Layer.cs
  55. 4 4
      PixiEditor/Models/Layers/LayerHelper.cs
  56. 24 8
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  57. 1 0
      PixiEditor/Models/Tools/Tool.cs
  58. 1 1
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  59. 5 3
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  60. 1 1
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  61. 1 1
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  62. 1 1
      PixiEditor/Models/Tools/Tools/FloodFillTool.cs
  63. 5 3
      PixiEditor/Models/Tools/Tools/LineTool.cs
  64. 2 2
      PixiEditor/Models/Tools/Tools/MagicWandTool.cs
  65. 2 2
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  66. 2 7
      PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  67. 1 1
      PixiEditor/Models/Tools/Tools/PenTool.cs
  68. 6 3
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  69. 1 1
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  70. 1 1
      PixiEditor/Models/Tools/Tools/ZoomTool.cs
  71. 107 81
      PixiEditor/Models/Undo/StorageBasedChange.cs
  72. 5 3
      PixiEditor/PixiEditor.csproj
  73. 2 2
      PixiEditor/Properties/AssemblyInfo.cs
  74. 4 4
      PixiEditor/Styles/DarkCheckboxStyle.xaml
  75. 15 2
      PixiEditor/Styles/LabelStyles.xaml
  76. 26 0
      PixiEditor/Styles/PixiListBoxItemStyle.xaml
  77. 32 0
      PixiEditor/Styles/RadioButtonStyle.xaml
  78. 4 1
      PixiEditor/Styles/ThemeColors.xaml
  79. 13 2
      PixiEditor/Styles/ThemeStyle.xaml
  80. 62 0
      PixiEditor/ViewModels/CrashReportViewModel.cs
  81. 9 76
      PixiEditor/ViewModels/ImportFilePopupViewModel.cs
  82. 28 50
      PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  83. 1 24
      PixiEditor/ViewModels/SettingsWindowViewModel.cs
  84. 2 1
      PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs
  85. 5 12
      PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  86. 4 4
      PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs
  87. 3 2
      PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs
  88. 16 14
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  89. 32 6
      PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  90. 64 19
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  91. 2 8
      PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  92. 14 0
      PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  93. 4 6
      PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  94. 6 11
      PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  95. 4 3
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  96. 31 29
      PixiEditor/ViewModels/ViewModelMain.cs
  97. 27 23
      PixiEditor/Views/Dialogs/ConfirmationPopup.xaml
  98. 56 0
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml
  99. 9 6
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs
  100. 26 0
      PixiEditor/Views/Dialogs/DialogTitleBar.xaml

+ 23 - 4
.github/ISSUE_TEMPLATE/report_a_bug.md

@@ -4,13 +4,32 @@ about: Report a bug, not working feature or anything related
 ---
 
 **Describe as detailed as possible when it happens**
+
 A clear and concise description of what the problem is. Ex. Holding CTRL+Z and P at the same time, causes program to crash  
 
-**Describe what you tried to do in order to fix it**
-A clear and concise description of what you tried to do to fix the problem (if possible).
+**Add reproduction steps**
+
+If you are able to, include steps to reproduce bug
+
+Example:
+1. Create new file with size 64x64
+2. Draw line anywhere
+3. Center content
+4. PixiEditor crashes
+
+**Expected behaviour**
+
+What should happen?
+
+**Include related files,**
+
+If bug makes PixiEditor crash, include crash report. If it is possible, include screenshots and videos. 
+
+**System information**
+
+Windows version: 11/10/8/7
 
-**Include screenshots of error**
-If it is possible, include screenshots, videos etc.
 
 **Additional context**
+
 Add any other context here.

+ 7 - 7
CONTRIBUTING.md

@@ -1,21 +1,21 @@
 # Contributing
 
-Hey! Thanks for being interested in project! It means a lot. But, before contributing please read this guide :) 
+Hey! Thanks for being interested in the project! It means a lot. But, before contributing please read this guide :) 
 
 When contributing to this repository, please first discuss the change you wish to make via issue,
 email, or any other method with the owners of this repository before making a change. 
 
 ## Issues
 
-If you want to report a bug, follow steps below, if you want to request a feature, check [this](https://github.com/flabbet/PixiEditor/blob/master/.github/ISSUE_TEMPLATE/feature_request.md)
+If you want to report a bug, follow the steps below, if you want to request a feature, check [this](https://github.com/flabbet/PixiEditor/blob/master/.github/ISSUE_TEMPLATE/feature_request.md)
 
 * First of all, check if the issue is on the [list](https://github.com/flabbet/PixiEditor/issues) and/or [board](https://github.com/flabbet/PixiEditor/projects), if yes, upvote it.
 
-* If not, report an issue [here](https://github.com/flabbet/PixiEditor/issues) like that:
- 1. Clear as short as possible title
- 2. Describe issue as detailed as possible
- 3. Include screenshots if possible.
+* If not, report an issue [here](https://github.com/flabbet/PixiEditor/issues) while following these guidelines:
+ 1. Keep the title short and straightforward.
+ 2. Describe the issue as detailed as possible
+ 3. Include screenshots if you can.
 
  ## Pull Requests
 
- Before pull request, read [this](https://github.com/flabbet/PixiEditor/blob/master/PULL_REQUEST_TEMPLATE.md)
+ Before submitting a pull request, read [this](https://github.com/flabbet/PixiEditor/blob/master/PULL_REQUEST_TEMPLATE.md)

+ 5 - 2
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>
@@ -17,6 +18,7 @@
                 <ResourceDictionary Source="Styles/DarkCheckboxStyle.xaml" />
                 <ResourceDictionary Source="Styles/ListSwitchButtonStyle.xaml" />
                 <ResourceDictionary Source="Styles/LabelStyles.xaml" />
+                <ResourceDictionary Source="Styles/PixiListBoxItemStyle.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Menu/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/OverlayButtons.xaml" />
@@ -25,8 +27,9 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
+                <ResourceDictionary Source="Styles/RadioButtonStyle.xaml" />
                 <ResourceDictionary Source="pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>
-</Application>
+</Application>

+ 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;
+        }
     }
 }

+ 1 - 1
PixiEditor/Exceptions/CorruptedFileException.cs

@@ -6,7 +6,7 @@ namespace PixiEditor.Exceptions
     public class CorruptedFileException : Exception
     {
         public CorruptedFileException()
-            : base("Selected file is invalid or corrupted.")
+            : base("The file you've chosen might be corrupted.")
         {
         }
 

+ 1 - 1
PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -20,7 +20,7 @@ namespace PixiEditor.Helpers.Behaviours
         private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
         {
             AssociatedObject.Focus();
-            ShortcutController.BlockShortcutExecution = false;
+            ShortcutController.UnblockShortcutExecutionAll();
         }
     }
 }

+ 2 - 2
PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs

@@ -27,12 +27,12 @@ namespace PixiEditor.Helpers.Behaviours
 
         private void AssociatedObject_LostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
         {
-            ShortcutController.BlockShortcutExecution = false;
+            ShortcutController.UnblockShortcutExecution("GlobalShortcutFocusBehavior");
         }
 
         private void AssociatedObject_GotKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
         {
-            ShortcutController.BlockShortcutExecution = true;
+            ShortcutController.BlockShortcutExection("GlobalShortcutFocusBehavior");
         }
     }
 }

+ 43 - 26
PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -1,5 +1,4 @@
-using System.Text.RegularExpressions;
-using System.Windows;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Interactivity;
@@ -8,27 +7,42 @@ namespace PixiEditor.Helpers.Behaviours
 {
     internal class TextBoxFocusBehavior : Behavior<TextBox>
     {
-        // Using a DependencyProperty as the backing store for FillSize.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty SelectOnFocusProperty =
+        public static readonly DependencyProperty SelectOnMouseClickProperty =
             DependencyProperty.Register(
-                nameof(SelectOnFocus),
+                nameof(SelectOnMouseClick),
                 typeof(bool),
                 typeof(TextBoxFocusBehavior),
-                new PropertyMetadata(true));
+                new PropertyMetadata(false));
 
-        public static readonly DependencyProperty NextControlProperty =
-            DependencyProperty.Register(nameof(NextControl), typeof(FrameworkElement), typeof(TextBoxFocusBehavior));
+        public static readonly DependencyProperty ConfirmOnEnterProperty =
+            DependencyProperty.Register(
+                nameof(ConfirmOnEnter),
+                typeof(bool),
+                typeof(TextBoxFocusBehavior),
+                new PropertyMetadata(false));
 
-        public FrameworkElement NextControl
+        public static readonly DependencyProperty DeselectOnFocusLossProperty =
+            DependencyProperty.Register(
+                nameof(DeselectOnFocusLoss),
+                typeof(bool),
+                typeof(TextBoxFocusBehavior),
+                new PropertyMetadata(false));
+
+        public bool SelectOnMouseClick
         {
-            get => (FrameworkElement)GetValue(NextControlProperty);
-            set => SetValue(NextControlProperty, value);
+            get => (bool)GetValue(SelectOnMouseClickProperty);
+            set => SetValue(SelectOnMouseClickProperty, value);
         }
 
-        public bool SelectOnFocus
+        public bool ConfirmOnEnter
+        {
+            get => (bool)GetValue(ConfirmOnEnterProperty);
+            set => SetValue(ConfirmOnEnterProperty, value);
+        }
+        public bool DeselectOnFocusLoss
         {
-            get => (bool)GetValue(SelectOnFocusProperty);
-            set => SetValue(SelectOnFocusProperty, value);
+            get => (bool)GetValue(DeselectOnFocusLossProperty);
+            set => SetValue(DeselectOnFocusLossProperty, value);
         }
 
         protected override void OnAttached()
@@ -36,6 +50,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnAttached();
             AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp += AssociatedObject_KeyUp;
         }
@@ -45,6 +60,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnDetaching();
             AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
         }
@@ -52,10 +68,8 @@ namespace PixiEditor.Helpers.Behaviours
         // Converts number to proper format if enter is clicked and moves focus to next object
         private void AssociatedObject_KeyUp(object sender, KeyEventArgs e)
         {
-            if (e.Key != Key.Enter)
-            {
+            if (e.Key != Key.Enter || !ConfirmOnEnter)
                 return;
-            }
 
             RemoveFocus();
         }
@@ -63,13 +77,6 @@ namespace PixiEditor.Helpers.Behaviours
         private void RemoveFocus()
         {
             DependencyObject scope = FocusManager.GetFocusScope(AssociatedObject);
-
-            if (NextControl != null)
-            {
-                FocusManager.SetFocusedElement(scope, NextControl);
-                return;
-            }
-
             FrameworkElement parent = (FrameworkElement)AssociatedObject.Parent;
 
             while (parent != null && parent is IInputElement element && !element.Focusable)
@@ -84,7 +91,7 @@ namespace PixiEditor.Helpers.Behaviours
             object sender,
             KeyboardFocusChangedEventArgs e)
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick || e.KeyboardDevice.IsKeyDown(Key.Tab))
                 AssociatedObject.SelectAll();
         }
 
@@ -92,12 +99,22 @@ namespace PixiEditor.Helpers.Behaviours
             object sender,
             MouseEventArgs e)
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick)
                 AssociatedObject.SelectAll();
         }
 
+        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
+        {
+            if (DeselectOnFocusLoss)
+                AssociatedObject.Select(0, 0);
+            RemoveFocus();
+        }
+
         private void AssociatedObjectPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
         {
+            if (!SelectOnMouseClick)
+                return;
+
             if (!AssociatedObject.IsKeyboardFocusWithin)
             {
                 AssociatedObject.Focus();

+ 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;
+      }
+    }
+  }
+}

+ 3 - 1
PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs

@@ -9,7 +9,9 @@ namespace PixiEditor.Helpers.Converters
     {
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
+            if (value == null)
+                return false;
             return value.Equals(parameter) ? Visibility.Visible : Visibility.Collapsed;
         }
     }
-}
+}

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

+ 6 - 2
PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -11,7 +11,11 @@ namespace PixiEditor.Helpers.Converters
         {
             if (value is Key key)
             {
-                return InputKeyHelpers.GetCharFromKey(key);
+                return key switch
+                {
+                    Key.Space => "Space",
+                    _ => InputKeyHelpers.GetCharFromKey(key),
+                };
             }
             else if (value is ModifierKeys)
             {
@@ -23,4 +27,4 @@ namespace PixiEditor.Helpers.Converters
             }
         }
     }
-}
+}

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

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

+ 27 - 0
PixiEditor/Helpers/Converters/WidthToBitmapScalingModeConverter.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Converters
+{
+    internal class WidthToBitmapScalingModeConverter : SingleInstanceMultiValueConverter<WidthToBitmapScalingModeConverter>
+    {
+        public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            int? pixelWidth = values[0] as int?;
+            double? actualWidth = values[1] as double?;
+            if (pixelWidth == null || actualWidth == null)
+                return DependencyProperty.UnsetValue;
+            double zoomLevel = actualWidth.Value / pixelWidth.Value;
+            if (zoomLevel < 1)
+                return BitmapScalingMode.HighQuality;
+            return BitmapScalingMode.NearestNeighbor;
+        }
+
+        public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 22 - 0
PixiEditor/Helpers/Converters/ZoomLevelToBitmapScalingModeConverter.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Globalization;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Converters
+{
+    internal class ZoomLevelToBitmapScalingModeConverter : SingleInstanceConverter<ZoomLevelToBitmapScalingModeConverter>
+    {
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            double zoomLevel = (double)value;
+            if (zoomLevel < 1)
+                return BitmapScalingMode.HighQuality;
+            return BitmapScalingMode.NearestNeighbor;
+        }
+
+        public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 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());
         }
     }
 }

+ 8 - 2
PixiEditor/Helpers/Extensions/Int32RectEx.cs

@@ -1,9 +1,10 @@
-using System;
+using SkiaSharp;
+using System;
 using System.Windows;
 
 namespace PixiEditor.Helpers.Extensions
 {
-    static class Int32RectEx
+    public static class Int32RectEx
     {
         public static Int32Rect Intersect(this Int32Rect rect, Int32Rect other)
         {
@@ -50,5 +51,10 @@ namespace PixiEditor.Helpers.Extensions
 
             return new Int32Rect(minX1, minY1, width, height);
         }
+
+        public static SKRectI ToSKRectI(this Int32Rect rect)
+        {
+            return new SKRectI(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height);
+        }
     }
 }

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

@@ -35,19 +35,19 @@ namespace PixiEditor.Helpers.Extensions
             Models.DataHolders.ObservableCollection<Layer> layers = new();
             foreach (SerializableLayer slayer in document)
             {
-                layers.Add(slayer.ToLayer());
+                layers.Add(slayer.ToLayer(document.Width, document.Height));
             }
 
             return layers;
         }
 
-        public static Layer ToLayer(this SerializableLayer layer)
+        public static Layer ToLayer(this SerializableLayer layer, int maxWidth, int maxHeight)
         {
-            return new Layer(layer.Name, new Surface(layer.ToSKImage()))
+            return new Layer(layer.Name, new Surface(layer.ToSKImage()), maxWidth, maxHeight)
             {
                 Opacity = layer.Opacity,
                 IsVisible = layer.IsVisible,
-                Offset = new(layer.OffsetX, layer.OffsetY, 0, 0)
+                Offset = new(layer.OffsetX, layer.OffsetY, 0, 0),
             };
         }
 

+ 13 - 0
PixiEditor/Helpers/Extensions/SKRectIEx.cs

@@ -0,0 +1,13 @@
+using SkiaSharp;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class SKRectIEx
+    {
+        public static Int32Rect ToInt32Rect(this SKRectI rect)
+        {
+            return new Int32Rect(rect.Left, rect.Top, rect.Width, rect.Height);
+        }
+    }
+}

+ 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 = 128;
+        public const int MaxPreviewHeight = 128;
+
+        public const int MaxCanvasSize = 9999;
+
+        public const string NativeExtensionNoDot = "pixi";
+        public const string NativeExtension = "." + NativeExtensionNoDot;
+    }
+}

+ 1 - 0
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -34,6 +34,7 @@ namespace PixiEditor.Models.Controllers
                 activeDocument?.UpdatePreviewImage();
                 Document oldDoc = activeDocument;
                 activeDocument = value;
+                activeDocument?.UpdatePreviewImage();
                 RaisePropertyChanged(nameof(ActiveDocument));
                 ActiveWindow = value;
                 DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));

+ 60 - 56
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -15,6 +15,7 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.IO;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
@@ -111,35 +112,66 @@ namespace PixiEditor.Models.Controllers
         /// <summary>
         ///     Pastes image from clipboard into new layer.
         /// </summary>
-        public static void PasteFromClipboard()
+        public static void PasteFromClipboard(Document document)
         {
-            IEnumerable<Layer> layers;
+            Layer[] layers;
             try
             {
-                layers = GetLayersFromClipboard();
+                layers = GetLayersFromClipboard(document).ToArray();
             }
             catch
             {
                 return;
             }
 
-            Document activeDocument = ViewModelMain.Current.BitmapManager.ActiveDocument;
-            int startIndex = activeDocument.Layers.Count;
+            int resizedCount = 0;
+
+            Guid[] guids = layers.Select(x => x.GuidValue).ToArray();
+
+            var undoArgs = new object[] { guids, document, new PixelSize(document.Width, document.Height) };
 
             foreach (var layer in layers)
             {
-                activeDocument.Layers.Add(layer);
+                document.Layers.Add(layer);
+
+                if (layer.Width > document.Width || layer.Height > document.Height)
+                {
+                    ResizeToLayer(document, layer);
+                    resizedCount++;
+                }
+            }
+
+            StorageBasedChange change = new StorageBasedChange(document, layers, false);
+
+            document.UndoManager.AddUndoChange(change.ToChange(RemoveLayersProcess, undoArgs,
+                RestoreLayersProcess, new object[] { document }, "Paste from clipboard"));
+        }
+
+        private static void RemoveLayersProcess(object[] parameters)
+        {
+            if (parameters.Length > 2 && parameters[1] is Document document && parameters[2] is PixelSize size) 
+            {
+                document.RemoveLayersProcess(parameters);
+                document.ResizeCanvas(size.Width, size.Height, Enums.AnchorPoint.Left | Enums.AnchorPoint.Top, false);
             }
+        }
 
-            activeDocument.UndoManager.AddUndoChange(
-                new Change(RemoveLayersProcess, new object[] { startIndex }, AddLayersProcess, new object[] { layers }) { DisposeProcess = DisposeProcess });
+        private static void RestoreLayersProcess(Layer[] layers, UndoLayer[] data, object[] parameters)
+        {
+            if (parameters.Length > 0 && parameters[0] is Document document)
+            {
+                document.RestoreLayersProcess(layers, data);
+                foreach (var layer in layers)
+                {
+                    ResizeToLayer(document, layer);
+                }
+            }
         }
 
         /// <summary>
         ///     Gets image from clipboard, supported PNG, Dib and Bitmap.
         /// </summary>
-        /// <returns>WriteableBitmap.</returns>
-        private static IEnumerable<Layer> GetLayersFromClipboard()
+        private static IEnumerable<Layer> GetLayersFromClipboard(Document document)
         {
             DataObject data = ClipboardHelper.TryGetDataObject();
             if (data == null)
@@ -173,7 +205,7 @@ namespace PixiEditor.Models.Controllers
             else */
             if (TryFromSingleImage(data, out Surface singleImage))
             {
-                yield return new Layer("Image", singleImage);
+                yield return new Layer("Image", singleImage, document.Width, document.Height);
             }
             else if (data.GetDataPresent(DataFormats.FileDrop))
             {
@@ -188,13 +220,13 @@ namespace PixiEditor.Models.Controllers
 
                     try
                     {
-                        layer = new(Path.GetFileName(path), Importer.ImportSurface(path));
+                        layer = new(Path.GetFileName(path), Importer.ImportSurface(path), document.Width, document.Height);
                     }
                     catch (CorruptedFileException)
                     {
                     }
 
-                    yield return layer ?? new($"Corrupt {path}");
+                    yield return layer ?? new($"Corrupt {path}", document.Width, document.Height);
                 }
             }
             else
@@ -209,18 +241,24 @@ namespace PixiEditor.Models.Controllers
             if (dao == null)
                 return false;
 
-            var files = dao.GetFileDropList();
-
-            if (files != null)
+            try
             {
-                foreach (var file in files)
+                var files = dao.GetFileDropList();
+                if (files != null)
                 {
-                    if (Importer.IsSupportedFile(file))
+                    foreach (var file in files)
                     {
-                        return true;
+                        if (Importer.IsSupportedFile(file))
+                        {
+                            return true;
+                        }
                     }
                 }
             }
+            catch(COMException)
+            {
+                return false;
+            }
 
             return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
                    dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop) ||
@@ -290,7 +328,7 @@ namespace PixiEditor.Models.Controllers
                     FormatConvertedBitmap newFormat = new FormatConvertedBitmap();
                     newFormat.BeginInit();
                     newFormat.Source = source;
-                    newFormat.DestinationFormat = PixelFormats.Rgba64;
+                    newFormat.DestinationFormat = PixelFormats.Bgra32;
                     newFormat.EndInit();
 
                     result = new Surface(newFormat);
@@ -304,43 +342,9 @@ namespace PixiEditor.Models.Controllers
             return false;
         }
 
-        private static void RemoveLayersProcess(object[] parameters)
+        private static void ResizeToLayer(Document document, Layer layer)
         {
-            if (parameters.Length == 0 || parameters[0] is not int i)
-            {
-                return;
-            }
-
-            Document document = ViewModelMain.Current.BitmapManager.ActiveDocument;
-
-            while (i < document.Layers.Count)
-            {
-                document.RemoveLayer(i, true);
-            }
-        }
-
-        private static void AddLayersProcess(object[] parameters)
-        {
-            if (parameters.Length == 0 || parameters[0] is not IEnumerable<Layer> layers)
-            {
-                return;
-            }
-
-            foreach (var layer in layers)
-            {
-                ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Add(layer);
-            }
-        }
-
-        private static void DisposeProcess(object[] rev, object[] proc)
-        {
-            if (proc[0] is IEnumerable<Layer> layers)
-            {
-                foreach (var layer in layers)
-                {
-                    layer.LayerBitmap.Dispose();
-                }
-            }
+            document.ResizeCanvas(Math.Max(document.Width, layer.Width), Math.Max(document.Height, layer.Height), Enums.AnchorPoint.Left | Enums.AnchorPoint.Top, false);
         }
     }
 }

+ 48 - 3
PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs

@@ -1,5 +1,8 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
+using System.Windows.Documents;
 using System.Windows.Input;
 
 namespace PixiEditor.Models.Controllers.Shortcuts
@@ -11,15 +14,57 @@ namespace PixiEditor.Models.Controllers.Shortcuts
             ShortcutGroups = new ObservableCollection<ShortcutGroup>(shortcutGroups);
         }
 
-        public static bool BlockShortcutExecution { get; set; }
+        public static bool ShortcutExecutionBlocked => _shortcutExecutionBlockers.Count > 0;
+
+        private static List<string> _shortcutExecutionBlockers = new List<string>();
 
         public ObservableCollection<ShortcutGroup> ShortcutGroups { get; init; }
 
         public Shortcut LastShortcut { get; private set; }
 
+        public const Key MoveViewportToolTransientChangeKey = Key.Space;
+
+        public static void BlockShortcutExection(string blocker)
+        {
+            if (_shortcutExecutionBlockers.Contains(blocker)) return;
+            _shortcutExecutionBlockers.Add(blocker);
+        }
+
+        public static void UnblockShortcutExecution(string blocker)
+        {
+            if (!_shortcutExecutionBlockers.Contains(blocker)) return;
+            _shortcutExecutionBlockers.Remove(blocker);
+        }
+
+        public static void UnblockShortcutExecutionAll()
+        {
+            _shortcutExecutionBlockers.Clear();
+        }
+
+        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)
+            if (!ShortcutExecutionBlocked)
             {
                 Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
                 if (shortcuts.Length < 1)

+ 9 - 2
PixiEditor/Models/Controllers/SurfaceRenderer.cs

@@ -12,6 +12,7 @@ namespace PixiEditor.Models.Controllers
         public SKSurface BackingSurface { get; private set; }
         public WriteableBitmap FinalBitmap { get; private set; }
         private SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private SKPaint HighQualityResizePaint { get; } = new SKPaint() { FilterQuality = SKFilterQuality.High };
         public SurfaceRenderer(int width, int height)
         {
             FinalBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null);
@@ -23,15 +24,21 @@ namespace PixiEditor.Models.Controllers
         {
             BackingSurface.Dispose();
             BlendingPaint.Dispose();
+            HighQualityResizePaint.Dispose();
         }
 
         public void Draw(Surface otherSurface, byte opacity)
+        {
+            Draw(otherSurface, opacity, new SKRectI(0, 0, otherSurface.Width, otherSurface.Height));
+        }
+
+        public void Draw(Surface otherSurface, byte opacity, SKRectI drawRect)
         {
             BackingSurface.Canvas.Clear();
             FinalBitmap.Lock();
             BlendingPaint.Color = new SKColor(255, 255, 255, opacity);
-            using (var snapshot = otherSurface.SkiaSurface.Snapshot())
-                BackingSurface.Canvas.DrawImage(snapshot, new SKRect(0, 0, FinalBitmap.PixelWidth, FinalBitmap.PixelHeight));
+            using (var snapshot = otherSurface.SkiaSurface.Snapshot(drawRect))
+                BackingSurface.Canvas.DrawImage(snapshot, new SKRect(0, 0, FinalBitmap.PixelWidth, FinalBitmap.PixelHeight), HighQualityResizePaint);
             FinalBitmap.AddDirtyRect(new Int32Rect(0, 0, FinalBitmap.PixelWidth, FinalBitmap.PixelHeight));
             FinalBitmap.Unlock();
         }

+ 1 - 1
PixiEditor/Models/Controllers/UndoManager.cs

@@ -56,7 +56,7 @@ namespace PixiEditor.Models.Controllers
             {
                 foreach (var redo in RedoStack)
                 {
-                    //redo.Dispose();
+                    redo.Dispose();
                 }
                 RedoStack.Clear();
             }

+ 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; }
+        }
+    }
+}

+ 2 - 0
PixiEditor/Models/DataHolders/Document/Document.Constructors.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
 using System;
 using System.Linq;
@@ -31,6 +32,7 @@ namespace PixiEditor.Models.DataHolders
             LayerStructure.LayerStructureChanged += LayerStructure_LayerStructureChanged;
             DocumentSizeChanged += (sender, args) =>
             {
+                ActiveSelection = new Selection(Array.Empty<Coordinates>(), new PixelSize(args.NewWidth, args.NewHeight));
                 Renderer.Resize(args.NewWidth, args.NewHeight);
                 GeneratePreviewLayer();
             };

+ 30 - 13
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -12,6 +12,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Windows;
+using Windows.Graphics;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -221,9 +222,7 @@ namespace PixiEditor.Models.DataHolders
             if (width <= 0 || height <= 0)
                 throw new ArgumentException("Dimensions must be greater than 0");
 
-            layer = bitmap == null ? new Layer(name, width, height) : new Layer(name, bitmap);
-            layer.MaxHeight = Height;
-            layer.MaxWidth = Width;
+            layer = bitmap == null ? new Layer(name, width, height, Width, Height) : new Layer(name, bitmap, Width, Height);
 
             Layers.Add(layer);
 
@@ -441,7 +440,7 @@ namespace PixiEditor.Models.DataHolders
 
             var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].GuidValue);
 
-            Layer placeholderLayer = new("_placeholder");
+            Layer placeholderLayer = new("_placeholder", Width, Height);
             Layers.Insert(index, placeholderLayer);
             LayerStructure.AssignParent(placeholderLayer.GuidValue, groupParent?.GroupGuid);
 
@@ -449,6 +448,8 @@ namespace PixiEditor.Models.DataHolders
             {
                 Layer firstLayer = mergedLayer;
                 Layer secondLayer = layersToMerge[i + 1];
+                firstLayer.ClipCanvas();
+                secondLayer.ClipCanvas();
                 mergedLayer = firstLayer.MergeWith(secondLayer, name, Width, Height);
                 RemoveLayer(layersToMerge[i], false);
             }
@@ -471,7 +472,7 @@ namespace PixiEditor.Models.DataHolders
                 throw new ArgumentException("Not enough layers were provided to merge. Minimum amount is 2");
             }
 
-            IEnumerable<Layer> undoArgs = layersToMerge;
+            Layer[] undoArgs = layersToMerge;
 
             var oldLayerStructure = LayerStructure.CloneGroups();
 
@@ -602,7 +603,7 @@ namespace PixiEditor.Models.DataHolders
                 for (int i = 0; i < layers.Length; i++)
                 {
                     Layer layer = layers[i];
-                    layer.IsActive = true;
+                    layer.IsActive = data[i].IsActive;
                     Layers.Insert(data[i].LayerIndex, layer);
                 }
 
@@ -614,20 +615,36 @@ namespace PixiEditor.Models.DataHolders
         /// <summary>
         ///     Moves offsets of layers by specified vector.
         /// </summary>
-        private void MoveOffsets(IEnumerable<Layer> layers, Coordinates moveVector)
+        private void MoveOffsets(IList<Layer> layers, IList<Int32Rect> bounds, Coordinates moveVector)
         {
-            foreach (Layer layer in layers)
+            for (int i = 0; i < layers.Count; i++)
             {
+                Layer layer = layers[i];
+                Int32Rect bound = bounds[i];
                 Thickness offset = layer.Offset;
                 layer.Offset = new Thickness(offset.Left + moveVector.X, offset.Top + moveVector.Y, 0, 0);
+                if (!bound.IsEmpty && layer.Bounds != bound)
+                {
+                    layer.DynamicResizeAbsolute(bound);
+                }
+                else
+                {
+                    layer.ClipCanvas();
+                }
             }
         }
 
         private void MoveOffsetsProcess(object[] arguments)
         {
-            if (arguments.Length > 0 && arguments[0] is IEnumerable<Layer> layers && arguments[1] is Coordinates vector)
+            if (arguments.Length > 0 && arguments[0] is List<Guid> guids && arguments[1] is List<Int32Rect> bounds && arguments[2] is Coordinates vector)
             {
-                MoveOffsets(layers, vector);
+                List<Layer> layers = new List<Layer>(guids.Count);
+                foreach (Guid guid in guids)
+                {
+                    layers.Add(Layers.First(x => x.GuidValue == guid));
+                }
+
+                MoveOffsets(layers, bounds, vector);
             }
             else
             {
@@ -712,7 +729,7 @@ namespace PixiEditor.Models.DataHolders
             Renderer.ForceRerender();
         }
 
-        private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
+        public void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
         {
             for (int i = 0; i < layers.Length; i++)
             {
@@ -726,7 +743,7 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        private void RemoveLayerProcess(object[] parameters)
+        public void RemoveLayerProcess(object[] parameters)
         {
             if (parameters is { Length: > 0 } && parameters[0] is Guid layerGuid)
             {
@@ -837,7 +854,7 @@ namespace PixiEditor.Models.DataHolders
             return sucess;
         }
 
-        private void RemoveLayersProcess(object[] parameters)
+        public void RemoveLayersProcess(object[] parameters)
         {
             if (parameters != null && parameters.Length > 0 && parameters[0] is IEnumerable<Guid> layerGuids)
             {

+ 47 - 12
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -23,7 +23,7 @@ namespace PixiEditor.Models.DataHolders
         ///     Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
         ///     vertical.
         /// </param>
-        public void ResizeCanvas(int width, int height, AnchorPoint anchor)
+        public void ResizeCanvas(int width, int height, AnchorPoint anchor, bool addToUndo = true)
         {
             int oldWidth = Width;
             int oldHeight = Height;
@@ -31,20 +31,29 @@ namespace PixiEditor.Models.DataHolders
             int offsetX = GetOffsetXForAnchor(Width, width, anchor);
             int offsetY = GetOffsetYForAnchor(Height, height, anchor);
 
-            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
             Thickness[] newOffsets = Layers.Select(x => new Thickness(offsetX + x.OffsetX, offsetY + x.OffsetY, 0, 0))
                 .ToArray();
 
             object[] processArgs = { newOffsets, width, height };
-            object[] reverseProcessArgs = { oldOffsets, Width, Height };
+            object[] reverseProcessArgs = { Width, Height };
+
+            if (addToUndo) 
+            { 
+                StorageBasedChange change = new(this, Layers);
+                ResizeCanvas(newOffsets, width, height);
+
+                UndoManager.AddUndoChange(change.ToChange(
+                    RestoreDocumentLayersProcess,
+                    reverseProcessArgs,
+                    ResizeCanvasProcess,
+                    processArgs,
+                    "Resize canvas"));
+            }
+            else
+            {
+                ResizeCanvas(newOffsets, width, height);
+            }
 
-            ResizeCanvas(newOffsets, width, height);
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
-                reverseProcessArgs,
-                ResizeCanvasProcess,
-                processArgs,
-                "Resize canvas"));
             DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
         }
 
@@ -137,7 +146,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;
@@ -205,8 +214,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);
         }
@@ -219,11 +233,32 @@ 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];
                 Layers[i].MaxWidth = newWidth;
                 Layers[i].MaxHeight = newHeight;
+                if (layer.IsReset)
+                    continue;
+
+                Thickness newOffset = offset[i];
+                Int32Rect newRect = new((int)newOffset.Left, (int)newOffset.Top, layer.Width, layer.Height);
+                Int32Rect newLayerRect = newRect.Intersect(newCanvasRect);
+                if (!newLayerRect.HasArea)
+                {
+                    layer.Reset();
+                    continue;
+                }
+                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);
             }
 
             Width = newWidth;

+ 1 - 5
PixiEditor/Models/DataHolders/Document/Document.Preview.cs

@@ -43,11 +43,7 @@ namespace PixiEditor.Models.DataHolders
 
         public void GeneratePreviewLayer()
         {
-            PreviewLayer = new Layer("_previewLayer")
-            {
-                MaxWidth = Width,
-                MaxHeight = Height
-            };
+            PreviewLayer = new Layer("_previewLayer", Width, Height);
         }
     }
 }

+ 80 - 24
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -61,7 +61,7 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        private Selection selection = new Selection(Array.Empty<Coordinates>());
+        private Selection selection;
 
         public Selection ActiveSelection
         {
@@ -115,16 +115,19 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void ClipCanvas()
         {
-            DoubleCoords points = GetEdgePoints(Layers);
-            int smallestX = points.Coords1.X;
-            int smallestY = points.Coords1.Y;
-            int biggestX = points.Coords2.X;
-            int biggestY = points.Coords2.Y;
+            DoubleCoords? maybePoints = GetEdgePoints(Layers);
 
-            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
+            if (maybePoints == null)
             {
+                //all layers are empty
                 return;
             }
+            DoubleCoords points = maybePoints.Value;
+
+            int smallestX = points.Coords1.X;
+            int smallestY = points.Coords1.Y;
+            int biggestX = points.Coords2.X;
+            int biggestY = points.Coords2.Y;
 
             int width = biggestX - smallestX;
             int height = biggestY - smallestY;
@@ -134,15 +137,15 @@ namespace PixiEditor.Models.DataHolders
             int oldWidth = Width;
             int oldHeight = Height;
 
-            MoveOffsets(Layers, moveVector);
+            StorageBasedChange change = new StorageBasedChange(this, Layers);
 
-            object[] reverseArguments = { oldOffsets, oldWidth, oldHeight };
-            object[] processArguments = { Layers.Select(x => x.Offset).ToArray(), width, height };
+            object[] reverseArguments = { oldWidth, oldHeight };
+            object[] processArguments = { Layers.Select(x => new Thickness(x.OffsetX - smallestX, x.OffsetY - smallestY, 0, 0)).ToArray(), width, height };
 
             ResizeCanvasProcess(processArguments);
 
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
+            UndoManager.AddUndoChange(change.ToChange(
+                RestoreDocumentLayersProcess,
                 reverseArguments,
                 ResizeCanvasProcess,
                 processArguments,
@@ -154,37 +157,41 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void CenterContent()
         {
-            var layersToCenter = Layers.Where(x => x.IsActive && LayerStructureUtils.GetFinalLayerIsVisible(x, LayerStructure));
-            if (!layersToCenter.Any())
+            var layersToCenter = Layers.Where(x => x.IsActive && LayerStructureUtils.GetFinalLayerIsVisible(x, LayerStructure)).ToList();
+            if (layersToCenter.Count == 0)
             {
                 return;
             }
 
-            DoubleCoords points = GetEdgePoints(layersToCenter);
+            List<Int32Rect> oldBounds = layersToCenter.Select(x => x.Bounds).ToList();
+
+            DoubleCoords? maybePoints = ClipLayersAndGetEdgePoints(layersToCenter);
+            if (maybePoints == null)
+                return;
+            DoubleCoords points = maybePoints.Value;
 
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int biggestX = points.Coords2.X;
             int biggestY = points.Coords2.Y;
 
-            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
-            {
-                return;
-            }
-
             Coordinates contentCenter = CoordinatesCalculator.GetCenterPoint(points.Coords1, points.Coords2);
             Coordinates documentCenter = CoordinatesCalculator.GetCenterPoint(
                 new Coordinates(0, 0),
                 new Coordinates(Width, Height));
             Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
 
-            MoveOffsets(layersToCenter, moveVector);
+            List<Int32Rect> emptyBounds = Enumerable.Repeat(Int32Rect.Empty, layersToCenter.Count).ToList();
+
+            MoveOffsets(layersToCenter, emptyBounds, moveVector);
+
+            List <Guid> guids = layersToCenter.Select(x => x.GuidValue).ToList();
             UndoManager.AddUndoChange(
                 new Change(
                     MoveOffsetsProcess,
-                    new object[] { layersToCenter, new Coordinates(-moveVector.X, -moveVector.Y) },
+                    new object[] { guids, oldBounds, new Coordinates(-moveVector.X, -moveVector.Y) },
                     MoveOffsetsProcess,
-                    new object[] { layersToCenter, moveVector },
+                    new object[] { guids, emptyBounds, moveVector },
                     "Center content"));
         }
 
@@ -242,7 +249,47 @@ namespace PixiEditor.Models.DataHolders
             return 0;
         }
 
-        private DoubleCoords GetEdgePoints(IEnumerable<Layer> layers)
+        private DoubleCoords? GetEdgePoints(IEnumerable<Layer> layers)
+        {
+            if (Layers.Count == 0)
+                throw new ArgumentException("Not enough layers");
+
+            int smallestX = int.MaxValue;
+            int smallestY = int.MaxValue;
+            int biggestX = int.MinValue;
+            int biggestY = int.MinValue;
+
+            bool allLayersSkipped = true;
+
+            foreach (Layer layer in layers)
+            {
+                Int32Rect bounds = layer.TightBounds;
+                if (layer.IsReset || !bounds.HasArea)
+                    continue;
+                allLayersSkipped = false;
+
+                if (layer.OffsetX + bounds.X < smallestX)
+                    smallestX = layer.OffsetX + bounds.X;
+
+                if (layer.OffsetX + bounds.X + bounds.Width > biggestX)
+                    biggestX = layer.OffsetX + bounds.X + bounds.Width;
+
+                if (layer.OffsetY + bounds.Y < smallestY)
+                    smallestY = layer.OffsetY + bounds.Y;
+
+                if (layer.OffsetY + bounds.Y + bounds.Height > biggestY)
+                    biggestY = layer.OffsetY + bounds.Y + bounds.Height;
+            }
+
+            if (allLayersSkipped)
+                return null;
+
+            return new DoubleCoords(
+                new Coordinates(smallestX, smallestY),
+                new Coordinates(biggestX, biggestY));
+        }
+
+        private DoubleCoords? ClipLayersAndGetEdgePoints(IEnumerable<Layer> layers)
         {
             if (Layers.Count == 0)
             {
@@ -254,9 +301,15 @@ namespace PixiEditor.Models.DataHolders
             int biggestX = int.MinValue;
             int biggestY = int.MinValue;
 
+            bool allLayersSkipped = true;
+
             foreach (Layer layer in layers)
             {
                 layer.ClipCanvas();
+                if (layer.IsReset)
+                    continue;
+                allLayersSkipped = false;
+
                 if (layer.OffsetX < smallestX)
                 {
                     smallestX = layer.OffsetX;
@@ -278,6 +331,9 @@ namespace PixiEditor.Models.DataHolders
                 }
             }
 
+            if (allLayersSkipped)
+                return null;
+
             return new DoubleCoords(
                 new Coordinates(smallestX, smallestY),
                 new Coordinates(biggestX, biggestY));

+ 20 - 0
PixiEditor/Models/DataHolders/PixelSize.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public struct PixelSize
+    {
+        public int Width { get; set; }
+        public int Height { get; set; }
+
+        public PixelSize(int width, int height)
+        {
+            Width = width;
+            Height = height;
+        }
+    }
+}

+ 18 - 16
PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -3,6 +3,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Skia;
+using System;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
@@ -43,11 +44,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})";
             }
         }
 
@@ -91,9 +89,9 @@ namespace PixiEditor.Models.DataHolders
                               .Where(x => x.Opacity > 0.8)
                               .Select(x => (x.ToSKImage(), new Coordinates(x.OffsetX, x.OffsetY))));
 
-                return surface.ToWriteableBitmap();
+                return DownscaleToMaxSize(surface.ToWriteableBitmap());
             }
-            else if (FileExtension is ".png" or ".jpg" or ".jpeg")
+            else if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
             {
                 WriteableBitmap bitmap = null;
 
@@ -104,22 +102,26 @@ namespace PixiEditor.Models.DataHolders
                 catch
                 {
                     corrupt = true;
+                    return null;
                 }
 
-                const int MaxWidthInPixels = 2048;
-                const int MaxHeightInPixels = 2048;
-                ImageFileMaxSizeChecker imageFileMaxSizeChecker = new ImageFileMaxSizeChecker()
-                {
-                    MaxAllowedWidthInPixels = MaxWidthInPixels,
-                    MaxAllowedHeightInPixels = MaxHeightInPixels,
-                };
+                if (bitmap == null) //prevent crash
+                    return null;
 
-                return imageFileMaxSizeChecker.IsFileUnderMaxSize(bitmap) ?
-                    bitmap
-                    : bitmap.Resize(width: MaxWidthInPixels, height: MaxHeightInPixels, WriteableBitmapExtensions.Interpolation.Bilinear);
+                return DownscaleToMaxSize(bitmap);
             }
 
             return null;
         }
+
+        private WriteableBitmap DownscaleToMaxSize(WriteableBitmap bitmap)
+        {
+            if (bitmap.PixelWidth > Constants.MaxPreviewWidth || bitmap.PixelHeight > Constants.MaxPreviewHeight)
+            {
+                double factor = Math.Min(Constants.MaxPreviewWidth / (double)bitmap.PixelWidth, Constants.MaxPreviewHeight / (double)bitmap.PixelHeight);
+                return bitmap.Resize((int)(bitmap.PixelWidth * factor), (int)(bitmap.PixelHeight * factor), WriteableBitmapExtensions.Interpolation.Bilinear);
+            }
+            return bitmap;
+        }
     }
 }

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

@@ -17,10 +17,10 @@ namespace PixiEditor.Models.DataHolders
         private readonly SKColor selectionBlue;
         private Layer selectionLayer;
 
-        public Selection(Coordinates[] selectedPoints)
+        public Selection(Coordinates[] selectedPoints, PixelSize maxSize)
         {
-            SelectedPoints = new System.Collections.ObjectModel.ObservableCollection<Coordinates>(selectedPoints);
-            SelectionLayer = new Layer("_selectionLayer");
+            SelectedPoints = new ObservableCollection<Coordinates>(selectedPoints);
+            SelectionLayer = new Layer("_selectionLayer", maxSize.Width, maxSize.Height);
             selectionBlue = new SKColor(142, 202, 255, 255);
         }
 

+ 5 - 4
PixiEditor/Models/DataHolders/Surface.cs

@@ -64,8 +64,9 @@ namespace PixiEditor.Models.DataHolders
             if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
                 throw new ArgumentException("Surface dimensions must be non-zero");
 
-            byte[] pixels = new byte[original.PixelWidth * original.PixelHeight * 4];
-            original.CopyPixels(pixels, original.PixelWidth * 4, 0);
+            int stride = (original.PixelWidth * original.Format.BitsPerPixel + 7) / 8;
+            byte[] pixels = new byte[stride * original.PixelHeight];
+            original.CopyPixels(pixels, stride, 0);
 
             Width = original.PixelWidth;
             Height = original.PixelHeight;
@@ -117,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)

+ 2 - 3
PixiEditor/Models/Dialogs/ConfirmationDialog.cs

@@ -11,8 +11,7 @@ namespace PixiEditor.Models.Dialogs
         {
             ConfirmationPopup popup = new ConfirmationPopup
             {
-                Body = message,
-                Topmost = true
+                Body = message
             };
             if (popup.ShowDialog().GetValueOrDefault())
             {
@@ -38,4 +37,4 @@ namespace PixiEditor.Models.Dialogs
             return ConfirmationType.Canceled;
         }
     }
-}
+}

+ 21 - 3
PixiEditor/Models/Dialogs/ExportFileDialog.cs

@@ -1,10 +1,14 @@
-using System.Windows;
+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,9 +60,22 @@ namespace PixiEditor.Models.Dialogs
             }
         }
 
+        public FileType ChosenFormat
+        {
+            get => _chosenFormat;
+            set
+            {
+                if (_chosenFormat != value)
+                {
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
+                }
+            }
+        }
+
         public override bool ShowDialog()
         {
-            SaveFilePopup popup = new SaveFilePopup
+            ExportFilePopup popup = new ExportFilePopup
             {
                 SaveWidth = FileWidth,
                 SaveHeight = FileHeight
@@ -69,9 +86,10 @@ 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
         {

+ 3 - 16
PixiEditor/Models/Dialogs/NoticeDialog.cs

@@ -4,28 +4,15 @@ namespace PixiEditor.Models.Dialogs
 {
     public static class NoticeDialog
     {
-        public static void Show(string message)
-        {
-            NoticePopup popup = new ()
-            {
-                Body = message,
-                Title = string.Empty,
-                Topmost = true
-            };
-
-            popup.ShowDialog();
-        }
-
         public static void Show(string message, string title)
         {
-            NoticePopup popup = new ()
+            NoticePopup popup = new()
             {
                 Body = message,
-                Title = title,
-                Topmost = true
+                Title = title
             };
 
             popup.ShowDialog();
         }
     }
-}
+}

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

+ 49 - 21
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,39 @@ 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 if(Directory.Exists(Path.GetDirectoryName(path)))
+            {
+                Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            }
+            else
+            {
+                SaveAsEditableFileWithDialog(document, out 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,22 +90,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);
-
-            // If OK on dialog has been clicked
-            if (info.ShowDialog())
-            {
-                // If sizes are incorrect
-                if (info.FileWidth < bitmap.Width || info.FileHeight < bitmap.Height)
-                {
-                    MessageBox.Show("Incorrect height or width value", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
-                    return;
-                }
+          ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
-                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));
@@ -101,25 +129,25 @@ 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);
                 }
             }
             catch (Exception err)
             {
-                MessageBox.Show(err.ToString(), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+                NoticeDialog.Show(err.ToString(), "Error");
             }
         }
     }

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

+ 0 - 20
PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs

@@ -1,20 +0,0 @@
-using System.Windows.Media.Imaging;
-
-namespace PixiEditor.Models.IO
-{
-    internal class ImageFileMaxSizeChecker
-    {
-        public int MaxAllowedWidthInPixels { get; init; } = 2048;
-        public int MaxAllowedHeightInPixels { get; init; } = 2048;
-
-        public ImageFileMaxSizeChecker()
-        {
-        }
-
-        public bool IsFileUnderMaxSize(WriteableBitmap fileToCheck)
-        {
-            return fileToCheck.PixelWidth <= MaxAllowedWidthInPixels
-                && fileToCheck.PixelHeight <= MaxAllowedHeightInPixels;
-        }
-    }
-}

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

+ 0 - 22
PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs

@@ -1,22 +0,0 @@
-using PixiEditor.Parser;
-
-namespace PixiEditor.Models.IO
-{
-    internal class PixiFileMaxSizeChecker
-    {
-        public int MaxAllowedWidthInPixels { get; init; } = 1080;
-        public int MaxAllowedHeightInPixels { get; init; } = 1080;
-        public int MaxAllowedLayerCount { get; init; } = 5;
-
-        public PixiFileMaxSizeChecker()
-        {
-        }
-
-        public bool IsFileUnderMaxSize(SerializableDocument fileToCheck)
-        {
-            return fileToCheck.Width <= MaxAllowedWidthInPixels
-                && fileToCheck.Height <= MaxAllowedHeightInPixels
-                && fileToCheck.Layers.Count <= MaxAllowedLayerCount;
-        }
-    }
-}

+ 5 - 9
PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -185,10 +185,7 @@ namespace PixiEditor.Models.ImageManipulation
                 throw new ArgumentException("There were not the same amount of bitmaps and offsets", nameof(layerBitmaps));
             }
 
-            using Surface previewSurface = new Surface(maxPreviewWidth, maxPreviewHeight);
-            return previewSurface.ToWriteableBitmap();
-            /*
-            WriteableBitmap previewBitmap = BitmapFactory.New(width, height);
+            using Surface previewSurface = new Surface(width, height);
 
             var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
             var offsetsXEnumerator = offsetsX.GetEnumerator();
@@ -199,19 +196,18 @@ namespace PixiEditor.Models.ImageManipulation
                 offsetsXEnumerator.MoveNext();
                 offsetsYEnumerator.MoveNext();
 
-                var bitmap = layerBitmapsEnumerator.Current;
+                var bitmap = layerBitmapsEnumerator.Current.SkiaSurface.Snapshot();
                 var offsetX = offsetsXEnumerator.Current;
                 var offsetY = offsetsYEnumerator.Current;
 
-                previewBitmap.Blit(
-                    new Rect(offsetX, offsetY, bitmap.Width, bitmap.Height),
+                previewSurface.SkiaSurface.Canvas.DrawImage(
                     bitmap,
-                    new Rect(0, 0, bitmap.Width, bitmap.Height));
+                    offsetX, offsetY, Surface.BlendingPaint);
             }
 
             int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
             int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
-            return previewBitmap.Redesize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);*/
+            return previewSurface.ResizeNearestNeighbor(newWidth, newHeight).ToWriteableBitmap();
         }
     }
 }

+ 18 - 7
PixiEditor/Models/Layers/Layer.cs

@@ -31,32 +31,38 @@ namespace PixiEditor.Models.Layers
 
         private string layerHighlightColor = "#666666";
 
-        public Layer(string name)
+        public Layer(string name, int maxWidth, int maxHeight)
         {
             Name = name;
             LayerBitmap = new Surface(1, 1);
             IsReset = true;
             Width = 1;
             Height = 1;
+            MaxWidth = maxWidth;
+            MaxHeight = maxHeight;
             GuidValue = Guid.NewGuid();
         }
 
-        public Layer(string name, int width, int height)
+        public Layer(string name, int width, int height, int maxWidth, int maxHeight)
         {
             Name = name;
             LayerBitmap = new Surface(width, height);
             IsReset = true;
             Width = width;
             Height = height;
+            MaxWidth = maxWidth;
+            MaxHeight = maxHeight;
             GuidValue = Guid.NewGuid();
         }
 
-        public Layer(string name, Surface layerBitmap)
+        public Layer(string name, Surface layerBitmap, int maxWidth, int maxHeight)
         {
             Name = name;
             LayerBitmap = layerBitmap;
             Width = layerBitmap.Width;
             Height = layerBitmap.Height;
+            MaxWidth = maxWidth;
+            MaxHeight = maxHeight;
             GuidValue = Guid.NewGuid();
         }
 
@@ -208,6 +214,9 @@ namespace PixiEditor.Models.Layers
 
         public bool IsReset { get; private set; }
 
+        public Int32Rect TightBounds => GetContentDimensions();
+        public Int32Rect Bounds => new Int32Rect(OffsetX, OffsetY, Width, Height);
+
         public event EventHandler<Int32Rect> LayerBitmapChanged;
 
         public void InvokeLayerBitmapChange()
@@ -243,12 +252,10 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         public Layer Clone(bool generateNewGuid = false)
         {
-            return new Layer(Name, new Surface(LayerBitmap))
+            return new Layer(Name, new Surface(LayerBitmap), MaxWidth, MaxHeight)
             {
                 IsVisible = IsVisible,
                 Offset = Offset,
-                MaxHeight = MaxHeight,
-                MaxWidth = MaxWidth,
                 Opacity = Opacity,
                 IsActive = IsActive,
                 IsRenaming = IsRenaming,
@@ -488,7 +495,11 @@ namespace PixiEditor.Models.Layers
         public void ClipCanvas()
         {
             var dimensions = GetContentDimensions();
-            if (dimensions == Int32Rect.Empty) return;
+            if (dimensions == Int32Rect.Empty)
+            {
+                Reset();
+                return;
+            }
 
             ResizeCanvas(0, 0, dimensions.X, dimensions.Y, dimensions.Width, dimensions.Height);
             Offset = new Thickness(OffsetX + dimensions.X, OffsetY + dimensions.Y, 0, 0);

+ 4 - 4
PixiEditor/Models/Layers/LayerHelper.cs

@@ -57,7 +57,7 @@ namespace PixiEditor.Models.Layers
             }
         }
 
-        public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, Vector documentsSize)
+        public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, PixelSize documentSize)
         {
             Int32Rect thisRect = new(thisLayer.OffsetX, thisLayer.OffsetY, thisLayer.Width, thisLayer.Height);
             Int32Rect otherRect = new(otherLayer.OffsetX, otherLayer.OffsetY, otherLayer.Width, otherLayer.Height);
@@ -66,9 +66,9 @@ namespace PixiEditor.Models.Layers
 
             Surface mergedBitmap = BitmapUtils.CombineLayers(combined, new Layer[] { thisLayer, otherLayer });
 
-            Layer mergedLayer = new Layer(newName, mergedBitmap)
+            Layer mergedLayer = new Layer(newName, mergedBitmap, documentSize.Width, documentSize.Height)
             {
-                Offset = new Thickness(combined.X, combined.Y, 0, 0)
+                Offset = new Thickness(combined.X, combined.Y, 0, 0),
             };
 
             return mergedLayer;
@@ -76,7 +76,7 @@ namespace PixiEditor.Models.Layers
 
         public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, int documentWidth, int documentHeight)
         {
-            return MergeWith(thisLayer, otherLayer, newName, new Vector(documentWidth, documentHeight));
+            return MergeWith(thisLayer, otherLayer, newName, new PixelSize(documentWidth, documentHeight));
         }
     }
 }

+ 24 - 8
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -17,6 +17,9 @@ namespace PixiEditor.Models.Tools
 
         public bool UseDocumentRectForUndo { get; set; } = false;
 
+        private SKRectI _rectReportedByTool;
+        private bool _customRectReported = false;
+
         private StorageBasedChange _change;
 
         public abstract void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color);
@@ -49,16 +52,22 @@ namespace PixiEditor.Models.Tools
             _change = null;
         }
 
+        protected void ReportCustomSessionRect(SKRectI rect)
+        {
+            _rectReportedByTool = rect;
+            _customRectReported = true;
+        }
+
         private void InitializeStorageBasedChange(SKRectI toolSessionRect)
         {
             Document doc = ViewModels.ViewModelMain.Current.BitmapManager.ActiveDocument;
-            //var toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize");
-            //SKRectI finalRect = toolSessionRect;
-            //if (toolSize != null)
-            //{
-            //    int halfSize = (int)Math.Ceiling(toolSize.Value / 2f);
-            //    finalRect.Inflate(halfSize, halfSize);
-            //}
+            var toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize");
+            SKRectI finalRect = toolSessionRect;
+            if (toolSize != null && toolSize.Value > 1)
+            {
+                int halfSize = (int)Math.Ceiling(toolSize.Value / 2f);
+                finalRect.Inflate(halfSize, halfSize);
+            }
 
             //if (toolSessionRect.IsEmpty)
             //{
@@ -71,7 +80,14 @@ namespace PixiEditor.Models.Tools
             //    finalRect = SKRectI.Create(0, 0, doc.Width, doc.Height);
             //}
 
-            _change = new StorageBasedChange(doc, new[] { doc.ActiveLayer });
+            if (_customRectReported)
+            {
+                _customRectReported = false;
+                finalRect = _rectReportedByTool;
+                _rectReportedByTool = SKRectI.Empty;
+            }
+
+            _change = new StorageBasedChange(doc, new[] { new LayerChunk(doc.ActiveLayer, finalRect) });
         }
     }
 }

+ 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;
 

+ 5 - 3
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)
         {
@@ -41,10 +41,11 @@ namespace PixiEditor.Models.Tools.Tools
                 CoordinatesHelper.GetSquareCoordiantes(recordedMouseMovement) :
                 (recordedMouseMovement[0], recordedMouseMovement[^1]);
 
-            DrawEllipseFromCoordinates(previewLayer, start, end, color, fill, thickness, hasFillColor);
+            var dirtyRect = DrawEllipseFromCoordinates(previewLayer, start, end, color, fill, thickness, hasFillColor);
+            ReportCustomSessionRect(SKRectI.Create(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
         }
 
-        public static void DrawEllipseFromCoordinates(Layer layer, Coordinates first, Coordinates second,
+        public static Int32Rect DrawEllipseFromCoordinates(Layer layer, Coordinates first, Coordinates second,
             SKColor color, SKColor fillColor, int thickness, bool hasFillColor)
         {
             DoubleCoords corners = CalculateCoordinatesForShapeRotation(first, second);
@@ -71,6 +72,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
 
             layer.InvokeLayerBitmapChange(dirtyRect);
+            return dirtyRect;
         }
 
         public static void DrawEllipseFill(Layer layer, SKColor color, List<Coordinates> outlineCoordinates)

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

+ 5 - 3
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)
         {
@@ -45,10 +45,11 @@ namespace PixiEditor.Models.Tools.Tools
             if (Session.IsShiftDown)
                 (start, end) = CoordinatesHelper.GetSquareOrLineCoordinates(recordedMouseMovement);
 
-            DrawLine(previewLayer, start, end, color, thickness, SKBlendMode.Src);
+            var dirtyRect = DrawLine(previewLayer, start, end, color, thickness, SKBlendMode.Src);
+            ReportCustomSessionRect(SKRectI.Create(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
         }
 
-        public void DrawLine(
+        public Int32Rect DrawLine(
             Layer layer, Coordinates start, Coordinates end, SKColor color, int thickness, SKBlendMode blendMode,
             SKStrokeCap strokeCap = SKStrokeCap.Butt)
         {
@@ -90,6 +91,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
 
             layer.InvokeLayerBitmapChange(dirtyRect);
+            return dirtyRect;
         }
 
         private void DrawBresenhamLine(Layer layer, int x1, int y1, int x2, int y2, SKPaint paint)

+ 2 - 2
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;
 
@@ -88,7 +88,7 @@ namespace PixiEditor.Models.Tools.Tools
             cachedDocument ??= new Layer("_CombinedLayers", BitmapUtils.CombineLayers(
                 new Int32Rect(0, 0, document.Width, document.Height),
                 document.Layers,
-                document.LayerStructure));
+                document.LayerStructure), document.Width, document.Height);
         }
     }
 }

+ 2 - 2
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;
 
@@ -69,7 +69,7 @@ namespace PixiEditor.Models.Tools.Tools
                 affectedLayers = doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray();
             }
 
-            change = new StorageBasedChange(doc, affectedLayers, true);
+            change = new StorageBasedChange(doc, affectedLayers, true, true);
 
             Layer selLayer = selection.SelectionLayer;
             moveStartRect = anySelection ?

+ 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;
 

+ 6 - 3
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;
 
@@ -38,10 +38,11 @@ namespace PixiEditor.Models.Tools.Tools
                 var temp = Toolbar.GetSetting<ColorSetting>("FillColor").Value;
                 fillColor = new SKColor(temp.R, temp.G, temp.B, temp.A);
             }
-            CreateRectangle(previewLayer, color, fillColor, recordedMouseMovement, thickness);
+            var dirtyRect = CreateRectangle(previewLayer, color, fillColor, recordedMouseMovement, thickness);
+            ReportCustomSessionRect(SKRectI.Create(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
         }
 
-        private void CreateRectangle(Layer layer, SKColor color, SKColor? fillColor, IReadOnlyList<Coordinates> coordinates, int thickness)
+        private Int32Rect CreateRectangle(Layer layer, SKColor color, SKColor? fillColor, IReadOnlyList<Coordinates> coordinates, int thickness)
         {
             var (start, end) = Session.IsShiftDown ? CoordinatesHelper.GetSquareCoordiantes(coordinates) : (coordinates[0], coordinates[^1]);
 
@@ -75,7 +76,9 @@ namespace PixiEditor.Models.Tools.Tools
                 paint.Color = color;
                 layer.LayerBitmap.SkiaSurface.Canvas.DrawRect(x, y, w, h, paint);
             }
+
             layer.InvokeLayerBitmapChange(dirtyRect);
+            return dirtyRect;
         }
     }
 }

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

+ 107 - 81
PixiEditor/Models/Undo/StorageBasedChange.cs

@@ -1,8 +1,10 @@
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text;
@@ -26,7 +28,33 @@ namespace PixiEditor.Models.Undo
         public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
         {
             Document = doc;
-            layersToStore = layers.Select(x => x.GuidValue).ToList();
+            Initialize(layers, DefaultUndoChangeLocation, saveOnStartup);
+        }
+
+        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool useDocumentSize, bool saveOnStartup)
+        {
+            Document = doc;
+            Initialize(layers, DefaultUndoChangeLocation, saveOnStartup, useDocumentSize);
+        }
+
+        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup = true)
+        {
+            Document = doc;
+            Initialize(layers, undoChangeLocation, saveOnStartup);
+        }
+
+        public StorageBasedChange(Document doc, IEnumerable<LayerChunk> chunks, bool saveOnStartup = true)
+        {
+            Document = doc;
+            var chunkData = chunks as LayerChunk[] ?? chunks.ToArray();
+            LayerChunk[] layerChunks = new LayerChunk[chunkData.Length];
+            for (var i = 0; i < chunkData.Length; i++)
+            {
+                var chunk = chunkData[i];
+                layerChunks[i] = chunk;
+                layersToStore.Add(chunk.Layer.GuidValue);
+            }
+
             UndoChangeLocation = DefaultUndoChangeLocation;
             GenerateUndoLayers();
             if (saveOnStartup)
@@ -35,12 +63,29 @@ namespace PixiEditor.Models.Undo
             }
         }
 
-        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup = true)
+        private void Initialize(IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup, bool useDocumentSize = false)
         {
-            Document = doc;
-            layersToStore = layers.Select(x => x.GuidValue).ToList();
-            UndoChangeLocation = undoChangeLocation;
-            GenerateUndoLayers();
+            var layersArray = layers as Layer[] ?? layers.ToArray();
+            LayerChunk[] layerChunks = new LayerChunk[layersArray.Length];
+            for (var i = 0; i < layersArray.Length; i++)
+            {
+                var layer = layersArray[i];
+                int width = layer.Width;
+                int height = layer.Height;
+                int offsetX = layer.OffsetX;
+                int offsetY = layer.OffsetY;
+
+                if (useDocumentSize)
+                {
+                    width = layer.MaxWidth;
+                    height = layer.MaxHeight;
+                    offsetX = 0;
+                    offsetY = 0;
+                }
+
+                layerChunks[i] = new LayerChunk(layer, SKRectI.Create(offsetX, offsetY, width, height));
+                layersToStore.Add(layer.GuidValue);
+            }
 
             if (saveOnStartup)
             {
@@ -64,7 +109,21 @@ namespace PixiEditor.Models.Undo
                 UndoLayer storedLayer = StoredLayers[i];
                 if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName)))
                 {
-                    Exporter.SaveAsGZippedBytes(storedLayer.StoredPngLayerName, layer.LayerBitmap);
+                    // Calculate absolute rect to relative rect
+                    SKRectI finalRect = SKRectI.Create(
+                        storedLayer.SerializedRect.Left - layer.OffsetX,
+                        storedLayer.SerializedRect.Top - layer.OffsetY,
+                        storedLayer.SerializedRect.Width,
+                        storedLayer.SerializedRect.Height);
+
+                    using var image = layer.LayerBitmap.SkiaSurface.Snapshot();
+                    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);
+
+                    //DebugSavePng(targetSizeSurface, storedLayer);
+
+                    Exporter.SaveAsGZippedBytes(storedLayer.StoredPngLayerName, targetSizeSurface);
                 }
 
                 i++;
@@ -73,6 +132,19 @@ namespace PixiEditor.Models.Undo
             layersToStore = new List<Guid>();
         }
 
+        [Conditional("DEBUG")]
+        private static void DebugSavePng(Surface surface, UndoLayer storedLayer)
+        {
+            //Debug png visualization
+            using var targetSizeImage = surface.SkiaSurface.Snapshot();
+            using (var data = targetSizeImage.Encode(SKEncodedImageFormat.Png, 100))
+            using (var stream = File.OpenWrite(storedLayer.StoredPngLayerName + ".png"))
+            {
+                // save the data to a stream
+                data.SaveTo(stream);
+            }
+        }
+
         /// <summary>
         /// Loads saved layers from disk.
         /// </summary>
@@ -84,12 +156,10 @@ namespace PixiEditor.Models.Undo
             {
                 UndoLayer storedLayer = StoredLayers[i];
                 var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName);
-                layers[i] = new Layer(storedLayer.Name, bitmap)
+                layers[i] = new Layer(storedLayer.Name, bitmap, storedLayer.MaxWidth, storedLayer.MaxHeight)
                 {
                     Offset = new System.Windows.Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
                     Opacity = storedLayer.Opacity,
-                    MaxWidth = storedLayer.MaxWidth,
-                    MaxHeight = storedLayer.MaxHeight,
                     IsVisible = storedLayer.IsVisible,
                     IsActive = storedLayer.IsActive,
                     Width = storedLayer.Width,
@@ -279,8 +349,15 @@ namespace PixiEditor.Models.Undo
                 {
                     Layer layer = layers[i];
 
-                    document.RemoveLayer(data[i].LayerIndex, false);
-                    document.Layers.Insert(data[i].LayerIndex, layer);
+                    if (foundLayer != null)
+                    {
+                        ApplyChunkToLayer(foundLayer, layerData.SerializedRect, layer.LayerBitmap);
+                    }
+                    else
+                    {
+                        document.RemoveLayer(layerData.LayerIndex, false);
+                        document.Layers.Insert(layerData.LayerIndex, layer);
+                    }
 
                     if (data[i].IsActive)
                     {
@@ -294,75 +371,24 @@ namespace PixiEditor.Models.Undo
     }
 }
 
-//using PixiEditor.Models.DataHolders;
-//using PixiEditor.Models.IO;
-//using PixiEditor.Models.Layers;
-//using SkiaSharp;
-//using System;
-//using System.Collections.Generic;
-//using System.IO;
-//using System.Linq;
-//using System.Text;
-//using System.Windows;
-
-//namespace PixiEditor.Models.Undo
-//{
-//    /// <summary>
-//    ///     A class that allows to save layers on disk and load them on Undo/Redo.
-//    /// </summary>
-//    public class StorageBasedChange : IDisposable
-//    {
-//        public static string DefaultUndoChangeLocation { get; } = Path.Join(Path.GetTempPath(), "PixiEditor", Guid.NewGuid().ToString(), "UndoStack");
-
-//        public string UndoChangeLocation { get; set; }
-
-//        public UndoLayer[] StoredLayers { get; set; }
-
-//        private List<Guid> layersToStore = new List<Guid>();
-//        public Document Document { get; }
-
-//        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
-//        {
-//            Document = doc;
-//            Initialize(layers, DefaultUndoChangeLocation, saveOnStartup);
-//        }
-
-//        public StorageBasedChange(Document doc, IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup = true)
-//        {
-//            Document = doc;
-//            Initialize(layers, undoChangeLocation, saveOnStartup);
-//        }
-
-//        public StorageBasedChange(Document doc, IEnumerable<LayerChunk> chunks, bool saveOnStartup = true)
-//        {
-//            Document = doc;
-//            var chunkData = chunks as LayerChunk[] ?? chunks.ToArray();
-//            LayerChunk[] layerChunks = new LayerChunk[chunkData.Length];
-//            for (var i = 0; i < chunkData.Length; i++)
-//            {
-//                var chunk = chunkData[i];
-//                layerChunks[i] = chunk;
-//                layersToStore.Add(chunk.Layer.GuidValue);
-//            }
-
-//            UndoChangeLocation = DefaultUndoChangeLocation;
-//            GenerateUndoLayers(layerChunks);
-//            if (saveOnStartup)
-//            {
-//                SaveLayersOnDevice();
-//            }
-//        }
+        private static void ApplyChunkToLayer(Layer layer, SKRectI rect, Surface chunk)
+        {
+            layer.DynamicResizeAbsolute(rect.ToInt32Rect());
+            using var snapshot = chunk.SkiaSurface.Snapshot();
+            layer.LayerBitmap.SkiaSurface.Canvas.DrawImage(snapshot, new SKPoint(rect.Left - layer.OffsetX, rect.Top - layer.OffsetY), Surface.ReplacingPaint);
+            layer.InvokeLayerBitmapChange(rect.ToInt32Rect());
+        }
 
-//        private void Initialize(IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup)
-//        {
-//            var layersArray = layers as Layer[] ?? layers.ToArray();
-//            LayerChunk[] layerChunks = new LayerChunk[layersArray.Length];
-//            for (var i = 0; i < layersArray.Length; i++)
-//            {
-//                var layer = layersArray[i];
-//                layerChunks[i] = new LayerChunk(layer, SKRectI.Create(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height));
-//                layersToStore.Add(layer.GuidValue);
-//            }
+        public void Dispose()
+        {
+            for (int i = 0; i < StoredLayers.Length; i++)
+            {
+                if (File.Exists(StoredLayers[i].StoredPngLayerName))
+                    File.Delete(StoredLayers[i].StoredPngLayerName);
+            }
+        }
+    }
+}
 
 //            UndoChangeLocation = undoChangeLocation;
 //            GenerateUndoLayers(layerChunks);

+ 5 - 3
PixiEditor/PixiEditor.csproj

@@ -186,21 +186,23 @@
 		</None>
 	</ItemGroup>
 	<ItemGroup>
-		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.0" />
+		<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" />
+		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.0.0.1" />
 		<PackageReference Include="SkiaSharp" Version="2.80.3" />
 		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />

+ 2 - 2
PixiEditor/Properties/AssemblyInfo.cs

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

+ 4 - 4
PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -11,9 +11,9 @@
                 <ControlTemplate TargetType="CheckBox">
                     <BulletDecorator Background="Transparent">
                         <BulletDecorator.Bullet>
-                            <Border x:Name="Border" Width="20" Height="20" CornerRadius="2" Background="#FF1B1B1B"
+                            <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="2" 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"/>
@@ -43,4 +43,4 @@
             </Setter.Value>
         </Setter>
     </Style>
-</ResourceDictionary>
+</ResourceDictionary>

+ 15 - 2
PixiEditor/Styles/LabelStyles.xaml

@@ -5,7 +5,20 @@
     <Style TargetType="Label" x:Key="BaseLabel">
         <Setter Property="Foreground" Value="White"/>
     </Style>
-    
+
+    <Style x:Key="SettingsHeader" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
+        <Setter Property="FontSize" Value="15"/>
+        <Setter Property="Padding" Value="0"/>
+        <Setter Property="VerticalAlignment" Value="Center"/>
+        <Setter Property="FontWeight" Value="DemiBold"/>
+    </Style>
+
+    <Style x:Key="SettingsText" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
+        <Setter Property="FontSize" Value="12"/>
+        <Setter Property="Padding" Value="0"/>
+        <Setter Property="VerticalAlignment" Value="Center"/>
+    </Style>
+
     <Style x:Key="Header1" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
         <Setter Property="FontSize" Value="36"/>
         <Setter Property="Margin" Value="20"/>
@@ -19,4 +32,4 @@
     <Style x:Key="Paragraph" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
         <Setter Property="Margin" Value="0 10 0 10"/>
     </Style>
-</ResourceDictionary>
+</ResourceDictionary>

+ 26 - 0
PixiEditor/Styles/PixiListBoxItemStyle.xaml

@@ -0,0 +1,26 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Style TargetType="ListBoxItem" x:Key="PixiListBoxItemStyle">
+        <Setter Property="OverridesDefaultStyle" Value="True"/>
+        <Setter Property="Foreground" Value="White"/>
+        <Setter Property="Background" Value="Transparent"/>
+        <Setter Property="FontSize" Value="15"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="ListBoxItem">
+                    <Border x:Name="Border" Padding="15,7" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
+                        <ContentPresenter Content="{TemplateBinding Content}"/>
+                    </Border>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsMouseOver" Value="True">
+                            <Setter Property="Background" Value="{StaticResource AlmostLightModeAccentColor}"/>
+                        </Trigger>
+                        <Trigger Property="IsSelected" Value="True">
+                            <Setter Property="Background" Value="#B00022"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+</ResourceDictionary>

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

+ 4 - 1
PixiEditor/Styles/ThemeColors.xaml

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

+ 13 - 2
PixiEditor/Styles/ThemeStyle.xaml

@@ -5,10 +5,18 @@
         <Setter Property="Foreground" Value="White" />
     </Style>
 
+    <Style TargetType="{x:Type Grid}">
+        <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" />
-        <Setter Property="FontSize" Value="22" />
+        <Setter Property="FontSize" Value="15" />
         <Setter Property="SnapsToDevicePixels" Value="True" />
         <Setter Property="Template">
             <Setter.Value>
@@ -52,6 +60,8 @@
     <Style TargetType="Button" x:Key="DarkRoundButton" BasedOn="{StaticResource BaseDarkButton}">
         <Setter Property="OverridesDefaultStyle" Value="True" />
         <Setter Property="Background" Value="#303030" />
+        <Setter Property="Height" Value="28"/>
+        <Setter Property="Width" Value="70"/>
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="Button">
@@ -154,6 +164,7 @@
     <Style TargetType="TextBox" x:Key="DarkTextBoxStyle">
         <Setter Property="BorderThickness" Value="1" />
         <Setter Property="Foreground" Value="Snow" />
+        <Setter Property="SelectionBrush" Value="{StaticResource SelectionColor}" />
 
         <Setter Property="Template">
             <Setter.Value>
@@ -272,4 +283,4 @@
             </Setter.Value>
         </Setter>
     </Style>
-</ResourceDictionary>
+</ResourceDictionary>

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

+ 9 - 76
PixiEditor/ViewModels/ImportFilePopupViewModel.cs

@@ -1,6 +1,6 @@
-using Microsoft.Win32;
-using PixiEditor.Exceptions;
+using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
+using PixiEditor.Models.IO;
 using System;
 using System.IO;
 using System.Windows;
@@ -15,53 +15,19 @@ namespace PixiEditor.ViewModels
         private int importHeight = 16;
 
         private int importWidth = 16;
-
-        private string pathButtonBorder = "#f08080";
-
-        private bool pathIsCorrect;
-
         public ImportFilePopupViewModel()
         {
             CloseButtonCommand = new RelayCommand(CloseWindow);
             DragMoveCommand = new RelayCommand(MoveWindow);
-            ChoosePathCommand = new RelayCommand(ChoosePath);
-            OkCommand = new RelayCommand(OkButton, CanClickOk);
+            OkCommand = new RelayCommand(OkButton);
         }
 
         public RelayCommand CloseButtonCommand { get; set; }
 
         public RelayCommand DragMoveCommand { get; set; }
 
-        public RelayCommand ChoosePathCommand { get; set; }
-
         public RelayCommand OkCommand { get; set; }
 
-        public string PathButtonBorder
-        {
-            get => pathButtonBorder;
-            set
-            {
-                if (pathButtonBorder != value)
-                {
-                    pathButtonBorder = value;
-                    RaisePropertyChanged("PathButtonBorder");
-                }
-            }
-        }
-
-        public bool PathIsCorrect
-        {
-            get => pathIsCorrect;
-            set
-            {
-                if (pathIsCorrect != value)
-                {
-                    pathIsCorrect = value;
-                    RaisePropertyChanged("PathIsCorrect");
-                }
-            }
-        }
-
         public string FilePath
         {
             get => filePath;
@@ -71,7 +37,7 @@ namespace PixiEditor.ViewModels
                 {
                     filePath = value;
                     CheckForPath(value);
-                    RaisePropertyChanged("FilePath");
+                    RaisePropertyChanged(nameof(FilePath));
                 }
             }
         }
@@ -84,7 +50,7 @@ namespace PixiEditor.ViewModels
                 if (importWidth != value)
                 {
                     importWidth = value;
-                    RaisePropertyChanged("ImportWidth");
+                    RaisePropertyChanged(nameof(ImportWidth));
                 }
             }
         }
@@ -97,47 +63,19 @@ namespace PixiEditor.ViewModels
                 if (importHeight != value)
                 {
                     importHeight = value;
-                    RaisePropertyChanged("ImportHeight");
-                }
-            }
-        }
-
-        /// <summary>
-        ///     Command that handles Path choosing to save file.
-        /// </summary>
-        /// <param name="parameter">Binding parameter.</param>
-        private void ChoosePath(object parameter)
-        {
-            OpenFileDialog path = new OpenFileDialog
-            {
-                Title = "Import path",
-                CheckPathExists = true,
-                Filter = "Image Files|*.png;*.jpeg;*.jpg"
-            };
-            if (path.ShowDialog() == true)
-            {
-                if (string.IsNullOrEmpty(path.FileName) == false)
-                {
-                    CheckForPath(path.FileName);
-                }
-                else
-                {
-                    PathButtonBorder = "#f08080";
-                    PathIsCorrect = false;
+                    RaisePropertyChanged(nameof(ImportHeight));
                 }
             }
         }
 
         private void CheckForPath(string path)
         {
-            if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpeg") || path.EndsWith(".jpg")))
+            if (SupportedFilesHelper.IsSupportedFile(path))
             {
                 try
                 {
-                    PathButtonBorder = "#b8f080";
-                    PathIsCorrect = true;
                     filePath = path;
-                    BitmapImage bitmap = new BitmapImage(new Uri(path));
+                    var bitmap = new BitmapImage(new Uri(path));
                     ImportHeight = bitmap.PixelHeight;
                     ImportWidth = bitmap.PixelWidth;
                 }
@@ -168,10 +106,5 @@ namespace PixiEditor.ViewModels
             ((Window)parameter).DialogResult = true;
             CloseButton(parameter);
         }
-
-        private bool CanClickOk(object property)
-        {
-            return PathIsCorrect;
-        }
     }
-}
+}

+ 28 - 50
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,91 +10,66 @@ namespace PixiEditor.ViewModels
     internal class SaveFilePopupViewModel : ViewModelBase
     {
         private string _filePath;
-
-
-        private string _pathButtonBorder = "#f08080";
-
-
-        private bool _pathIsCorrect;
+        private FileType _chosenFormat;
 
         public SaveFilePopupViewModel()
         {
             CloseButtonCommand = new RelayCommand(CloseWindow);
             DragMoveCommand = new RelayCommand(MoveWindow);
-            ChoosePathCommand = new RelayCommand(ChoosePath);
-            OkCommand = new RelayCommand(OkButton, CanClickOk);
+            OkCommand = new RelayCommand(OkButton);
         }
 
         public RelayCommand CloseButtonCommand { get; set; }
         public RelayCommand DragMoveCommand { get; set; }
-        public RelayCommand ChoosePathCommand { get; set; }
         public RelayCommand OkCommand { get; set; }
 
-        public string PathButtonBorder
-        {
-            get => _pathButtonBorder;
-            set
-            {
-                if (_pathButtonBorder != value)
-                {
-                    _pathButtonBorder = value;
-                    RaisePropertyChanged("PathButtonBorder");
-                }
-            }
-        }
-
-        public bool PathIsCorrect
+        public string FilePath
         {
-            get => _pathIsCorrect;
+            get => _filePath;
             set
             {
-                if (_pathIsCorrect != value)
+                if (_filePath != value)
                 {
-                    _pathIsCorrect = value;
-                    RaisePropertyChanged("PathIsCorrect");
+                    _filePath = value;
+                    RaisePropertyChanged(nameof(FilePath));
                 }
             }
         }
 
-        public string FilePath
-        {
-            get => _filePath;
+        public FileType ChosenFormat 
+        { 
+            get => _chosenFormat;
             set
             {
-                if (_filePath != value)
+                if (_chosenFormat != value)
                 {
-                    _filePath = value;
-                    RaisePropertyChanged("FilePath");
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
                 }
             }
         }
-
+                
         /// <summary>
         ///     Command that handles Path choosing to save file
         /// </summary>
-        private void ChoosePath(object parameter)
+        private string ChoosePath()
         {
             SaveFileDialog path = new SaveFileDialog
             {
                 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)
                 {
-                    PathButtonBorder = "#b8f080";
-                    PathIsCorrect = true;
-                    FilePath = path.FileName;
-                }
-                else
-                {
-                    PathButtonBorder = "#f08080";
-                    PathIsCorrect = false;
+                    ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName));
+                    return path.FileName;
                 }
             }
+            return null;
         }
 
         private void CloseWindow(object parameter)
@@ -107,13 +85,13 @@ namespace PixiEditor.ViewModels
 
         private void OkButton(object parameter)
         {
+            string path = ChoosePath();
+            if (path == null)
+                return;
+            FilePath = path;
+            
             ((Window)parameter).DialogResult = true;
             CloseButton(parameter);
         }
-
-        private bool CanClickOk(object property)
-        {
-            return PathIsCorrect;
-        }
     }
 }

+ 1 - 24
PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -10,20 +10,6 @@ namespace PixiEditor.ViewModels
 {
     public class SettingsWindowViewModel : ViewModelBase
     {
-        public RelayCommand SelectCategoryCommand { get; set; }
-
-        private string selectedCategory = "General";
-
-        public string SelectedCategory
-        {
-            get => selectedCategory;
-            set
-            {
-                selectedCategory = value;
-                RaisePropertyChanged(nameof(SelectedCategory));
-            }
-        }
-
         public bool ShowUpdateTab
         {
             get
@@ -41,15 +27,6 @@ namespace PixiEditor.ViewModels
         public SettingsWindowViewModel()
         {
             SettingsSubViewModel = new SettingsViewModel(this);
-            SelectCategoryCommand = new RelayCommand(SelectCategory);
-        }
-
-        private void SelectCategory(object parameter)
-        {
-            if (parameter is not null && parameter is string value)
-            {
-                SelectedCategory = value;
-            }
         }
     }
-}
+}

+ 2 - 1
PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -45,7 +45,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void Paste(object parameter)
         {
-            ClipboardController.PasteFromClipboard();
+            if (Owner.BitmapManager.ActiveDocument == null) return;
+            ClipboardController.PasteFromClipboard(Owner.BitmapManager.ActiveDocument);
         }
 
         private bool CanPaste(object property)

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

+ 4 - 4
PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs

@@ -1,8 +1,8 @@
-using System;
-using DiscordRPC;
+using DiscordRPC;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.UserPreferences;
+using System;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -158,7 +158,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 Assets = new Assets
                 {
                     LargeImageKey = "editorlogo",
-                    LargeImageText = "You discovered PixiEditor's logo",
+                    LargeImageText = "You've discovered PixiEditor's logo",
                     SmallImageKey = "github",
                     SmallImageText = "Download PixiEditor on GitHub (github.com/PixiEditor/PixiEditor)!"
                 },
@@ -210,4 +210,4 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Enabled = false;
         }
     }
-}
+}

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

@@ -10,7 +10,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
     public class DocumentViewModel : SubViewModel<ViewModelMain>
     {
-        public const string ConfirmationDialogMessage = "Document was modified. Do you want to save changes?";
+        public const string ConfirmationDialogTitle = "Unsaved changes";
+        public const string ConfirmationDialogMessage = "The document has been modified. Do you want to save changes?";
 
         public RelayCommand CenterContentCommand { get; set; }
         public RelayCommand ClipCanvasCommand { get; set; }
@@ -59,7 +60,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             if (!document.ChangesSaved)
             {
-                ConfirmationType result = ConfirmationDialog.Show(ConfirmationDialogMessage);
+                ConfirmationType result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
                 if (result == ConfirmationType.Yes)
                 {
                     Owner.FileSubViewModel.SaveDocument(false);

+ 16 - 14
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;
@@ -80,7 +82,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
             if (!File.Exists(path))
             {
-                NoticeDialog.Show("The file does no longer exist at that path");
+                NoticeDialog.Show("The file does not exist", "Failed to open the file");
                 RecentlyOpened.Remove(path);
                 return;
             }
@@ -146,6 +148,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 Owner.BitmapManager.ActiveDocument.AddNewLayer(
                     "Image",
                     Importer.ImportImage(dialog.FilePath, dialog.FileWidth, dialog.FileHeight));
+                Owner.BitmapManager.ActiveDocument.UpdatePreviewImage();
             }
         }
 
@@ -176,7 +179,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             catch (CorruptedFileException ex)
             {
-                NoticeDialog.Show(ex.Message, "Failed to open file.");
+                NoticeDialog.Show(ex.Message, "Failed to open the file");
             }
             catch (OldFileFormatException)
             {
@@ -186,12 +189,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 +203,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 +247,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();
             }

+ 32 - 6
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)
@@ -62,7 +65,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 Owner.ShortcutController.LastShortcut.Command == Owner.ToolsSubViewModel.SelectToolCommand)
             {
                 restoreToolOnKeyUp = true;
-                ShortcutController.BlockShortcutExecution = true;
+                ShortcutController.BlockShortcutExection("ShortcutDown");
             }
 
             Owner.ShortcutController.KeyPressed(key, Keyboard.Modifiers);
@@ -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)
@@ -88,7 +96,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             {
                 restoreToolOnKeyUp = false;
                 Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool);
-                ShortcutController.BlockShortcutExecution = false;
+                ShortcutController.UnblockShortcutExecution("ShortcutDown");
             }
         }
 
@@ -107,7 +115,26 @@ 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);
+                restoreToolOnKeyUp = false;
+                ShortcutController.UnblockShortcutExecution("ShortcutDown");
+            }
         }
 
         private void OnMouseMove(object sender, EventArgs args)
@@ -128,8 +155,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else if (button == MouseButton.Middle)
             {
-                if (Owner.ToolsSubViewModel.LastActionTool != null)
-                    Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool);
+                ChangeMoveViewportToolState(false);
             }
         }
     }

+ 64 - 19
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -1,7 +1,6 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
-using PixiEditor.Models.Undo;
 using PixiEditor.Views.UserControls.Layers;
 using System;
 using System.Linq;
@@ -48,7 +47,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
             NewGroupCommand = new RelayCommand(NewGroup, CanCreateNewLayer);
             CreateGroupFromActiveLayersCommand = new RelayCommand(CreateGroupFromActiveLayers, CanCreateGroupFromSelected);
-            DeleteLayersCommand = new RelayCommand(DeleteLayer, CanDeleteLayer);
+            DeleteLayersCommand = new RelayCommand(DeleteActiveLayers, CanDeleteActiveLayers);
             DuplicateLayerCommand = new RelayCommand(DuplicateLayer, CanDuplicateLayer);
             MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
             MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront);
@@ -57,7 +56,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
             MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
             RenameGroupCommand = new RelayCommand(RenameGroup);
-            DeleteGroupCommand = new RelayCommand(DeleteGroup);
+            DeleteGroupCommand = new RelayCommand(DeleteGroup, CanDeleteGroup);
             DeleteSelectedCommand = new RelayCommand(DeleteSelected, CanDeleteSelected);
             Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
         }
@@ -73,22 +72,37 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public bool CanDeleteSelected(object parameter)
         {
-            return (
-                (
-                    parameter is not null and (Layer or LayerGroup)) || (Owner.BitmapManager?.ActiveDocument?.ActiveLayer != null)
-                )
-                && Owner.BitmapManager.ActiveDocument != null;
+            bool paramIsLayerOrGroup = parameter is not null and (Layer or LayerGroup);
+            bool activeLayerExists = Owner.BitmapManager?.ActiveDocument?.ActiveLayer != null;
+            bool activeDocumentExists = Owner.BitmapManager.ActiveDocument != null;
+            bool allGood = (paramIsLayerOrGroup || activeLayerExists) && activeDocumentExists;
+            if (!allGood)
+                return false;
+
+            if (parameter is Layer or LayerStructureItemContainer)
+            {
+                return CanDeleteActiveLayers(null);
+            }
+            else if (parameter is LayerGroup group)
+            {
+                return CanDeleteGroup(group.GuidValue);
+            }
+            else if (parameter is LayerGroupControl groupControl)
+            {
+                return CanDeleteGroup(groupControl.GroupGuid);
+            }
+            else if (Owner.BitmapManager.ActiveDocument.ActiveLayer != null)
+            {
+                return CanDeleteActiveLayers(null);
+            }
+            return false;
         }
 
         public void DeleteSelected(object parameter)
         {
-            if (parameter is Layer layer)
-            {
-                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(layer));
-            }
-            else if (parameter is LayerStructureItemContainer container)
+            if (parameter is Layer or LayerStructureItemContainer)
             {
-                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(container.Layer));
+                DeleteActiveLayers(null);
             }
             else if (parameter is LayerGroup group)
             {
@@ -100,14 +114,35 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else if (Owner.BitmapManager.ActiveDocument.ActiveLayer != null)
             {
-                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(Owner.BitmapManager.ActiveDocument.ActiveLayer));
+                DeleteActiveLayers(null);
             }
         }
 
+        public bool CanDeleteGroup(object parameter)
+        {
+            if (parameter is not Guid guid)
+                return false;
+
+            var document = Owner.BitmapManager.ActiveDocument;
+            if (document == null)
+                return false;
+
+            var group = document.LayerStructure.GetGroupByGuid(guid);
+            if (group == null)
+                return false;
+
+            return document.LayerStructure.GetGroupLayers(group).Count < document.Layers.Count;
+        }
+
         public void DeleteGroup(object parameter)
         {
             if (parameter is Guid guid)
             {
+                foreach (var layer in Owner.BitmapManager.ActiveDocument?.Layers)
+                {
+                    layer.IsActive = false;
+                }
+
                 var group = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupByGuid(guid);
                 var layers = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupLayers(group);
                 foreach (var layer in layers)
@@ -182,7 +217,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             if (doc.Layers.Count > 1)
             {
                 doc.MoveLayerInStructure(doc.Layers[^1].GuidValue, lastActiveLayerGuid, true);
-                Guid? parent = parameter is Layer or LayerStructureItemContainer ? activeLayerParent?.GroupGuid : activeLayerParent.Parent?.GroupGuid;
+                Guid? parent = null;
+                if (activeLayerParent != null)
+                {
+                    parent = parameter is Layer or LayerStructureItemContainer ? activeLayerParent?.GroupGuid : activeLayerParent.Parent?.GroupGuid;
+                }
                 doc.LayerStructure.AssignParent(doc.ActiveLayerGuid, parent);
                 doc.AddLayerStructureToUndo(oldGroups);
                 doc.UndoManager.SquashUndoChanges(3, "Add New Layer");
@@ -224,15 +263,18 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
         }
 
-        public void DeleteLayer(object parameter)
+        public void DeleteActiveLayers(object unusedParameter)
         {
             var doc = Owner.BitmapManager.ActiveDocument;
             doc.RemoveActiveLayers();
         }
 
-        public bool CanDeleteLayer(object property)
+        public bool CanDeleteActiveLayers(object unusedParam)
         {
-            return Owner.BitmapManager.ActiveDocument != null && Owner.BitmapManager.ActiveDocument.Layers.Count > 1;
+            if (Owner.BitmapManager.ActiveDocument == null)
+                return false;
+            int activeLayerCount = Owner.BitmapManager.ActiveDocument.Layers.Where(layer => layer.IsActive).Count();
+            return Owner.BitmapManager.ActiveDocument.Layers.Count > activeLayerCount;
         }
 
         public void DuplicateLayer(object parameter)
@@ -247,6 +289,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void RenameLayer(object parameter)
         {
+            if (Owner.BitmapManager.ActiveDocument == null)
+                return;
+
             int? index = (int?)parameter;
 
             if (index == null)

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

+ 14 - 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,9 +92,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void SetActiveTool(Tool tool)
         {
+            MoveToolIsTransient = false;
             if (ActiveTool != null)
             {
                 activeTool.IsActive = false;
+                ActiveTool.Toolbar.SaveToolbarSettings();
             }
 
             LastActionTool = ActiveTool;
@@ -92,6 +104,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
             ActiveTool = tool;
 
+            ActiveTool.Toolbar.LoadSharedSettings();
+
             if (LastActionTool != ActiveTool)
                 SelectedToolChanged?.Invoke(this, new SelectedToolEventArgs(LastActionTool, ActiveTool));
 

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

@@ -127,11 +127,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             catch (Win32Exception)
             {
-                MessageBox.Show(
+                NoticeDialog.Show(
                     "Couldn't update without administrator rights.",
-                    "Insufficient permissions",
-                    MessageBoxButton.OK,
-                    MessageBoxImage.Error);
+                    "Insufficient permissions");
             }
         }
 
@@ -185,7 +183,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 }
                 catch (System.Net.Http.HttpRequestException)
                 {
-                    NoticeDialog.Show("Could not check if there's an update available");
+                    NoticeDialog.Show("Could not check if there is an update available", "Update check failed");
                 }
 
                 AskToInstall();
@@ -210,4 +208,4 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             return selectedChannel;
         }
     }
-}
+}

+ 6 - 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,10 @@ 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
+            if (MainWindow.Current?.LayoutRoot?.Manager?.Layout == null) return;
+            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
         {

+ 31 - 29
PixiEditor/ViewModels/ViewModelMain.cs

@@ -163,45 +163,45 @@ namespace PixiEditor.ViewModels
             ShortcutController = new ShortcutController(
                     new ShortcutGroup(
                         "Tools",
-                        CreateToolShortcut<PenTool>(Key.B, "Select Pen Tool"),
-                        CreateToolShortcut<EraserTool>(Key.E, "Select Eraser Tool"),
-                        CreateToolShortcut<ColorPickerTool>(Key.O, "Select Color Picker Tool"),
-                        CreateToolShortcut<RectangleTool>(Key.R, "Select Rectangle Tool"),
-                        CreateToolShortcut<CircleTool>(Key.C, "Select Circle Tool"),
-                        CreateToolShortcut<LineTool>(Key.L, "Select Line Tool"),
-                        CreateToolShortcut<FloodFillTool>(Key.G, "Select Flood Fill Tool"),
-                        CreateToolShortcut<BrightnessTool>(Key.U, "Select Brightness Tool"),
-                        CreateToolShortcut<MoveTool>(Key.V, "Select Move Tool"),
-                        CreateToolShortcut<SelectTool>(Key.M, "Select Select Tool"),
-                        CreateToolShortcut<ZoomTool>(Key.Z, "Select Zoom Tool"),
-                        CreateToolShortcut<MoveViewportTool>(Key.Space, "Select Viewport Move Tool"),
-                        CreateToolShortcut<MagicWandTool>(Key.W, "Select Magic Wand Tool"),
+                        CreateToolShortcut<PenTool>(Key.B, "Pen"),
+                        CreateToolShortcut<EraserTool>(Key.E, "Eraser"),
+                        CreateToolShortcut<ColorPickerTool>(Key.O, "Color picker"),
+                        CreateToolShortcut<RectangleTool>(Key.R, "Rectangle"),
+                        CreateToolShortcut<CircleTool>(Key.C, "Ellipse"),
+                        CreateToolShortcut<LineTool>(Key.L, "Line"),
+                        CreateToolShortcut<FloodFillTool>(Key.G, "Flood fill"),
+                        CreateToolShortcut<BrightnessTool>(Key.U, "Brightness"),
+                        CreateToolShortcut<MoveTool>(Key.V, "Move selection"),
+                        CreateToolShortcut<SelectTool>(Key.M, "Select"),
+                        CreateToolShortcut<ZoomTool>(Key.Z, "Zoom"),
+                        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),
-                        new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Decrease Tool Size", -1),
-                        new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Increase Tool Size", 1)),
+                        new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Decrease tool size", -1),
+                        new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Increase tool size", 1)),
                     new ShortcutGroup(
                         "Editor",
-                        new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand, "Swap primary and secondary color"),
+                        new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand, "Swap primary and secondary colors"),
                         new Shortcut(Key.Y, UndoSubViewModel.RedoCommand, "Redo", modifier: ModifierKeys.Control),
                         new Shortcut(Key.Z, UndoSubViewModel.UndoCommand, "Undo", modifier: ModifierKeys.Control),
-                        new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, "Deselect all command", modifier: ModifierKeys.Control),
-                        new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, "Select all command", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, "Clear selection", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, "Select all", modifier: ModifierKeys.Control),
                         new Shortcut(Key.C, ClipboardSubViewModel.CopyCommand, "Copy", modifier: ModifierKeys.Control),
                         new Shortcut(Key.V, ClipboardSubViewModel.PasteCommand, "Paste", modifier: ModifierKeys.Control),
                         new Shortcut(Key.J, ClipboardSubViewModel.DuplicateCommand, "Duplicate", modifier: ModifierKeys.Control),
                         new Shortcut(Key.X, ClipboardSubViewModel.CutCommand, "Cut", modifier: ModifierKeys.Control),
-                        new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand, "Delete selected pixels"),
-                        new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, "Resize document", modifier: ModifierKeys.Control | ModifierKeys.Shift),
+                        new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand, "Clear selected area"),
+                        new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, "Resize image", modifier: ModifierKeys.Control | ModifierKeys.Shift),
                         new Shortcut(Key.C, DocumentSubViewModel.OpenResizePopupCommand, "Resize canvas", "canvas", ModifierKeys.Control | ModifierKeys.Shift),
-                        new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand, "Maximize")),
+                        new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand, "Maximize window")),
                     new ShortcutGroup(
                         "File",
-                        new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, "Open a Document", modifier: ModifierKeys.Control),
-                        new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, "Export as image", modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
-                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Document", modifier: ModifierKeys.Control),
-                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Document As New", "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
-                        new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, "Create new Document", modifier: ModifierKeys.Control)),
+                        new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, "Open image", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, "Export image", modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
+                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save as new", "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
+                        new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, "Create new image", modifier: ModifierKeys.Control)),
                     new ShortcutGroup(
                         "Layers",
                         new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, "Rename active layer", BitmapManager.ActiveDocument?.ActiveLayerGuid)),
@@ -226,9 +226,11 @@ namespace PixiEditor.ViewModels
             ShortcutController.ShortcutGroups.Add(
                     new ShortcutGroup(
                         "Misc",
-                        new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open the shortcut window", true)));
+                        new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open shortcuts window", true)));
 
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
+
+            ToolsSubViewModel?.SetupToolsTooltipShortcuts(services);
         }
 
         /// <summary>
@@ -332,7 +334,7 @@ namespace PixiEditor.ViewModels
 
             if (!BitmapManager.ActiveDocument.ChangesSaved)
             {
-                result = ConfirmationDialog.Show(DocumentViewModel.ConfirmationDialogMessage);
+                result = ConfirmationDialog.Show(DocumentViewModel.ConfirmationDialogMessage, DocumentViewModel.ConfirmationDialogTitle);
                 if (result == ConfirmationType.Yes)
                 {
                     FileSubViewModel.SaveDocument(false);
@@ -371,7 +373,7 @@ namespace PixiEditor.ViewModels
 
         private void ActiveDocument_DocumentSizeChanged(object sender, DocumentSizeChangedEventArgs e)
         {
-            BitmapManager.ActiveDocument.ActiveSelection = new Selection(Array.Empty<Coordinates>());
+            BitmapManager.ActiveDocument.ActiveSelection = new Selection(Array.Empty<Coordinates>(), new PixelSize(e.NewWidth, e.NewHeight));
             BitmapManager.ActiveDocument.ChangesSaved = false;
             BitmapManager.ActiveDocument.CenterViewportTrigger.Execute(this, new Size(BitmapManager.ActiveDocument.Width, BitmapManager.ActiveDocument.Height));
         }

+ 27 - 23
PixiEditor/Views/Dialogs/ConfirmationPopup.xaml

@@ -3,9 +3,13 @@
         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:system="clr-namespace:System;assembly=System.Runtime" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
-        mc:Ignorable="d"
-        Title="ConfirmationPopup" Name="popup" WindowStartupLocation="CenterScreen" Height="200" Width="500"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime" 
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
+        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d" d:Title="Unsaved changes"
+        Name="popup" WindowStartupLocation="CenterScreen" 
+        Height="180" Width="400" MinHeight="180" MinWidth="400"
         WindowStyle="None">
 
     <WindowChrome.WindowChrome>
@@ -13,40 +17,40 @@
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
     </WindowChrome.WindowChrome>
 
-    <Grid Background="{StaticResource AccentColor}" Focusable="True">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition Height="34*" />
-            <RowDefinition Height="21*" />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviours:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
-        <TextBlock Grid.Row="1" Text="{Binding Body, ElementName=popup}" HorizontalAlignment="Center"
-                   VerticalAlignment="Center" FontSize="18" Foreground="White" />
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{Binding DataContext.CancelCommand, ElementName=popup}" />
-        </DockPanel>
-        <StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Center"
-                    Margin="0,0,10,10">
-            <Button Margin="10,0,10,0" Height="30" Width="60"
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="{Binding ElementName=popup, Path=Title}" CloseCommand="{Binding DataContext.CancelCommand, ElementName=popup}" />
+
+        <StackPanel DockPanel.Dock="Bottom" Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center"
+                    Margin="0,0,10,15">
+            <Button Margin="10,0,10,0" Width="70" IsDefault="True"
                     Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}"
                     Style="{StaticResource DarkRoundButton}" Content="Yes">
                 <Button.CommandParameter>
                     <system:Boolean>True</system:Boolean>
                 </Button.CommandParameter>
             </Button>
-            <Button Height="30" Width="60"
+            <Button Width="70"
                     Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=popup}"
                     Style="{StaticResource DarkRoundButton}" Content="No">
                 <Button.CommandParameter>
                     <system:Boolean>False</system:Boolean>
                 </Button.CommandParameter>
             </Button>
-            <Button Margin="10,0,10,0" Height="30" Width="80" Style="{StaticResource DarkRoundButton}" Content="Cancel"
+            <Button Margin="10,0,10,0" Width="70" Style="{StaticResource DarkRoundButton}" Content="Cancel"
                     Command="{Binding DataContext.CancelCommand, ElementName=popup}" />
         </StackPanel>
-    </Grid>
-</Window>
+
+        <TextBlock Grid.Row="1" 
+                   Text="{Binding Body, ElementName=popup}" 
+                   HorizontalAlignment="Center" Margin="20,0" 
+                   TextWrapping="WrapWithOverflow" 
+                   TextTrimming="WordEllipsis"
+                   TextAlignment="Center"
+                   VerticalAlignment="Center" FontSize="15" Foreground="White" d:Text="The document has been modified. Do you want to save changes?"/>
+    </DockPanel>
+</Window>

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

@@ -0,0 +1,56 @@
+<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="480" MinHeight="195"
+        WindowStartupLocation="CenterScreen"
+        Width="480" Height="195">
+
+    <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 fix this bug by sending a crash report that was generated (you will still be able to recover the files).</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>

+ 9 - 6
PixiEditor/Views/Dialogs/PopupTemplate.xaml.cs → PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs

@@ -1,15 +1,18 @@
-using System.Windows;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.ViewModels;
+using System.Windows;
 using System.Windows.Input;
 
-namespace PixiEditor.Views
+namespace PixiEditor.Views.Dialogs
 {
     /// <summary>
-    ///     Interaction logic for PopupTemplate.xaml
+    /// Interaction logic for CrashReportDialog.xaml
     /// </summary>
-    public partial class PopupTemplate : Window
+    public partial class CrashReportDialog : Window
     {
-        public PopupTemplate()
+        public CrashReportDialog(CrashReport report)
         {
+            DataContext = new CrashReportViewModel(report);
             InitializeComponent();
         }
 
@@ -23,4 +26,4 @@ namespace PixiEditor.Views
             SystemCommands.CloseWindow(this);
         }
     }
-}
+}

+ 26 - 0
PixiEditor/Views/Dialogs/DialogTitleBar.xaml

@@ -0,0 +1,26 @@
+<UserControl x:Class="PixiEditor.Views.Dialogs.DialogTitleBar"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+             mc:Ignorable="d"
+             x:Name="uc"
+             Height="35" d:DesignWidth="300">
+    <Grid Grid.Row="0" Background="{StaticResource MainColor}">
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition/>
+            <ColumnDefinition/>
+        </Grid.ColumnDefinitions>
+        <TextBlock 
+            TextAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center" 
+            Text="{Binding ElementName=uc, Path=TitleText}"
+            Foreground="White" 
+            FontSize="15" 
+            Margin="5,0,0,0" 
+            Grid.Column="0" Grid.ColumnSpan="2"/>
+        <Button Grid.Column="1" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}" IsCancel="True"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{Binding ElementName=uc, Path=CloseCommand}" />
+    </Grid>
+</UserControl>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini