Browse Source

Merge pull request #346 from PixiEditor/master

Development release V0.1.7.1-dev
Krzysztof Krysiński 3 years ago
parent
commit
ae20ed15af
100 changed files with 2115 additions and 1317 deletions
  1. 5 2
      PixiEditor/App.xaml
  2. 36 1
      PixiEditor/App.xaml.cs
  3. 1 1
      PixiEditor/Exceptions/CorruptedFileException.cs
  4. 43 26
      PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  5. 37 0
      PixiEditor/Helpers/Converters/EnumBooleanConverter.cs
  6. 29 0
      PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  7. 3 1
      PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
  8. 24 22
      PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
  9. 6 2
      PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  10. 1 1
      PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  11. 63 20
      PixiEditor/Helpers/CrashHelper.cs
  12. 8 2
      PixiEditor/Helpers/Extensions/Int32RectEx.cs
  13. 13 0
      PixiEditor/Helpers/Extensions/SKRectIEx.cs
  14. 19 0
      PixiEditor/Helpers/ProcessHelpers.cs
  15. 20 0
      PixiEditor/Helpers/SizeCalculator.cs
  16. 88 0
      PixiEditor/Helpers/SupportedFilesHelper.cs
  17. 14 0
      PixiEditor/Models/Constants.cs
  18. 2 3
      PixiEditor/Models/Controllers/ClipboardController.cs
  19. 25 1
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  20. 192 0
      PixiEditor/Models/DataHolders/CrashReport.cs
  21. 27 6
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  22. 12 10
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  23. 5 4
      PixiEditor/Models/DataHolders/Surface.cs
  24. 2 3
      PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  25. 21 3
      PixiEditor/Models/Dialogs/ExportFileDialog.cs
  26. 2 4
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  27. 3 16
      PixiEditor/Models/Dialogs/NoticeDialog.cs
  28. 21 23
      PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
  29. 1 1
      PixiEditor/Models/Enums/FileType.cs
  30. 4 0
      PixiEditor/Models/Enums/SizeUnit.cs
  31. 45 21
      PixiEditor/Models/IO/Exporter.cs
  32. 55 0
      PixiEditor/Models/IO/FileTypeDialogData.cs
  33. 52 0
      PixiEditor/Models/IO/FileTypeDialogDataSet.cs
  34. 2 2
      PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
  35. 2 2
      PixiEditor/Models/IO/Importer.cs
  36. 3 0
      PixiEditor/Models/Layers/Layer.cs
  37. 19 18
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  38. 1 0
      PixiEditor/Models/Tools/Tool.cs
  39. 1 1
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  40. 1 1
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  41. 1 1
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  42. 1 1
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  43. 1 1
      PixiEditor/Models/Tools/Tools/FloodFillTool.cs
  44. 1 1
      PixiEditor/Models/Tools/Tools/LineTool.cs
  45. 1 1
      PixiEditor/Models/Tools/Tools/MagicWandTool.cs
  46. 2 2
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  47. 2 7
      PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  48. 1 1
      PixiEditor/Models/Tools/Tools/PenTool.cs
  49. 1 1
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  50. 1 1
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  51. 1 1
      PixiEditor/Models/Tools/Tools/ZoomTool.cs
  52. 133 422
      PixiEditor/Models/Undo/StorageBasedChange.cs
  53. 3 3
      PixiEditor/Models/Undo/UndoLayer.cs
  54. 5 3
      PixiEditor/PixiEditor.csproj
  55. 2 2
      PixiEditor/Properties/AssemblyInfo.cs
  56. 4 4
      PixiEditor/Styles/DarkCheckboxStyle.xaml
  57. 15 2
      PixiEditor/Styles/LabelStyles.xaml
  58. 26 0
      PixiEditor/Styles/PixiListBoxItemStyle.xaml
  59. 32 0
      PixiEditor/Styles/RadioButtonStyle.xaml
  60. 4 1
      PixiEditor/Styles/ThemeColors.xaml
  61. 13 2
      PixiEditor/Styles/ThemeStyle.xaml
  62. 62 0
      PixiEditor/ViewModels/CrashReportViewModel.cs
  63. 9 76
      PixiEditor/ViewModels/ImportFilePopupViewModel.cs
  64. 28 50
      PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  65. 1 24
      PixiEditor/ViewModels/SettingsWindowViewModel.cs
  66. 5 12
      PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  67. 4 4
      PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs
  68. 3 2
      PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs
  69. 15 14
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  70. 28 4
      PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  71. 59 18
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  72. 2 8
      PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  73. 11 0
      PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  74. 4 6
      PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  75. 5 11
      PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  76. 4 3
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  77. 30 28
      PixiEditor/ViewModels/ViewModelMain.cs
  78. 27 23
      PixiEditor/Views/Dialogs/ConfirmationPopup.xaml
  79. 56 0
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml
  80. 9 6
      PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs
  81. 26 0
      PixiEditor/Views/Dialogs/DialogTitleBar.xaml
  82. 32 0
      PixiEditor/Views/Dialogs/DialogTitleBar.xaml.cs
  83. 42 0
      PixiEditor/Views/Dialogs/ExportFilePopup.xaml
  84. 18 12
      PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs
  85. 2 2
      PixiEditor/Views/Dialogs/HelloTherePopup.xaml
  86. 22 27
      PixiEditor/Views/Dialogs/ImportFilePopup.xaml
  87. 4 6
      PixiEditor/Views/Dialogs/ImportFilePopup.xaml.cs
  88. 22 26
      PixiEditor/Views/Dialogs/NewFilePopup.xaml
  89. 9 4
      PixiEditor/Views/Dialogs/NewFilePopup.xaml.cs
  90. 19 26
      PixiEditor/Views/Dialogs/NoticePopup.xaml
  91. 0 35
      PixiEditor/Views/Dialogs/PopupTemplate.xaml
  92. 39 31
      PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml
  93. 3 25
      PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml.cs
  94. 29 27
      PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml
  95. 2 21
      PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs
  96. 46 0
      PixiEditor/Views/Dialogs/ResizeablePopup.cs
  97. 0 51
      PixiEditor/Views/Dialogs/SaveFilePopup.xaml
  98. 50 0
      PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml
  99. 101 0
      PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs
  100. 129 81
      PixiEditor/Views/Dialogs/SettingsWindow.xaml

+ 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,7 +27,8 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
+                <ResourceDictionary Source="Styles/RadioButtonStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>
-</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.")
         {
         }
 

+ 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)
@@ -78,13 +85,14 @@ namespace PixiEditor.Helpers.Behaviours
             }
 
             FocusManager.SetFocusedElement(scope, parent);
+            Keyboard.ClearFocus();
         }
 
         private void AssociatedObjectGotKeyboardFocus(
             object sender,
             KeyboardFocusChangedEventArgs e)
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick || e.KeyboardDevice.IsKeyDown(Key.Tab))
                 AssociatedObject.SelectAll();
         }
 
@@ -92,12 +100,21 @@ 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);
+        }
+
         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);
         }
     }
-}
+}

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

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

+ 2 - 3
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -138,7 +138,6 @@ namespace PixiEditor.Models.Controllers
         /// <summary>
         ///     Gets image from clipboard, supported PNG, Dib and Bitmap.
         /// </summary>
-        /// <returns>WriteableBitmap.</returns>
         private static IEnumerable<Layer> GetLayersFromClipboard()
         {
             DataObject data = ClipboardHelper.TryGetDataObject();
@@ -290,7 +289,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);
@@ -315,7 +314,7 @@ namespace PixiEditor.Models.Controllers
 
             while (i < document.Layers.Count)
             {
-                document.RemoveLayer(i, true);
+                document.RemoveLayer(i, false);
             }
         }
 

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

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

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

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

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

@@ -7,6 +7,7 @@ using SkiaSharp;
 using System;
 using System.Linq;
 using System.Windows;
+using Windows.Graphics;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -31,16 +32,18 @@ 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 };
+
+            StorageBasedChange change = new(this, Layers);
 
             ResizeCanvas(newOffsets, width, height);
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
+
+            UndoManager.AddUndoChange(change.ToChange(
+                RestoreDocumentLayersProcess,
                 reverseProcessArgs,
                 ResizeCanvasProcess,
                 processArgs,
@@ -137,7 +140,7 @@ namespace PixiEditor.Models.DataHolders
                         int diff = documentCenter.X - newOffsetX;
                         newOffsetX = layer.OffsetX + (diff * 2);
                     }
-                    else if(flip == FlipType.Vertical)
+                    else if (flip == FlipType.Vertical)
                     {
                         newOffsetY += layerCenter.Y;
                         int diff = documentCenter.Y - newOffsetY;
@@ -205,8 +208,13 @@ namespace PixiEditor.Models.DataHolders
 
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         {
+            int oldWidth = Width;
+            int oldHeight = Height;
             Width = (int)args[0];
             Height = (int)args[1];
+            DocumentSizeChanged?.Invoke(
+                this,
+                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, Width, Height));
             Layers.Clear();
             Layers.AddRange(layers);
         }
@@ -219,9 +227,22 @@ namespace PixiEditor.Models.DataHolders
         /// <param name="newHeight">New canvas height.</param>
         private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
         {
+            Int32Rect newCanvasRect = new(0, 0, newWidth, newHeight);
             for (int i = 0; i < Layers.Count; i++)
             {
-                Layers[i].Offset = offset[i];
+                Layer layer = Layers[i];
+                Thickness newOffset = offset[i];
+                Int32Rect newRect = new((int)newOffset.Left, (int)newOffset.Top, layer.Width, layer.Height);
+                Int32Rect newLayerRect = newRect.Intersect(newCanvasRect);
+                Surface newBitmap = new(newLayerRect.Width, newLayerRect.Height);
+                var oldBitmap = layer.LayerBitmap;
+                using var snapshot = oldBitmap.SkiaSurface.Snapshot();
+                newBitmap.SkiaSurface.Canvas.DrawImage(snapshot, newRect.X - newLayerRect.X, newRect.Y - newLayerRect.Y, Surface.ReplacingPaint);
+
+                layer.LayerBitmap = newBitmap;
+                oldBitmap.Dispose();
+
+                Layers[i].Offset = new Thickness(newLayerRect.X, newLayerRect.Y, 0, 0);
                 Layers[i].MaxWidth = newWidth;
                 Layers[i].MaxHeight = newHeight;
             }

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

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

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

+ 45 - 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,35 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         {
-            Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            if (Path.GetExtension(path) != Constants.NativeExtension)
+            {
+                var chosenFormat = ParseImageFormat(Path.GetExtension(path));
+                var bitmap = document.Renderer.FinalBitmap;
+                SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+            }
+            else
+            {
+                Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            }
+
             return path;
         }
 
+        public static FileType ParseImageFormat(string extension)
+        {
+            return SupportedFilesHelper.ParseImageFormat(extension);
+        }
+
+        static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
+
+        static Exporter()
+        {
+            encodersFactory[FileType.Png] = () => new PngBitmapEncoder();
+            encodersFactory[FileType.Jpeg] = () => new JpegBitmapEncoder();
+            encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder(); 
+            encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
+        }
+
         /// <summary>
         ///     Creates ExportFileDialog to get width, height and path of file.
         /// </summary>
@@ -55,22 +86,15 @@ namespace PixiEditor.Models.IO
         /// <param name="fileDimensions">Size of file.</param>
         public static void Export(WriteableBitmap bitmap, Size fileDimensions)
         {
-            ExportFileDialog info = new ExportFileDialog(fileDimensions);
+          ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
-            // If OK on dialog has been clicked
-            if (info.ShowDialog())
-            {
-                // 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;
-                }
-
-                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 +125,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));
+        }
+    }
+}

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

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

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

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

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

@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Windows;
+using Windows.Graphics;
 
 namespace PixiEditor.Models.Layers
 {
@@ -208,6 +209,8 @@ namespace PixiEditor.Models.Layers
 
         public bool IsReset { get; private set; }
 
+        public Int32Rect TightBounds => GetContentDimensions();
+
         public event EventHandler<Int32Rect> LayerBitmapChanged;
 
         public void InvokeLayerBitmapChange()

+ 19 - 18
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -1,9 +1,11 @@
-using PixiEditor.Models.DataHolders;
+using System;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
 using SkiaSharp;
 using System.Collections.Generic;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 namespace PixiEditor.Models.Tools
 {
@@ -52,26 +54,25 @@ namespace PixiEditor.Models.Tools
         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)
-            //{
-            //    finalRect = SKRectI.Create(doc.ActiveLayer.OffsetX, doc.ActiveLayer.OffsetY, doc.ActiveLayer.Width, doc.ActiveLayer.Height);
-            //}
+            if (toolSessionRect.IsEmpty)
+            {
+                finalRect = SKRectI.Create(doc.ActiveLayer.OffsetX, doc.ActiveLayer.OffsetY, doc.ActiveLayer.Width, doc.ActiveLayer.Height);
+            }
 
-            //Commented, because rect based undo is still a little buggy
-            //if (UseDocumentRectForUndo)
-            //{
-            //    finalRect = SKRectI.Create(0, 0, doc.Width, doc.Height);
-            //}
+            if (UseDocumentRectForUndo)
+            {
+                finalRect = SKRectI.Create(0, 0, doc.Width, doc.Height);
+            }
 
-            _change = new StorageBasedChange(doc, new[] { doc.ActiveLayer });
+            _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;
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 133 - 422
PixiEditor/Models/Undo/StorageBasedChange.cs

@@ -1,11 +1,15 @@
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
+using SkiaSharp;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text;
+using System.Windows;
 
 namespace PixiEditor.Models.Undo
 {
@@ -14,45 +18,83 @@ namespace PixiEditor.Models.Undo
     /// </summary>
     public class StorageBasedChange : IDisposable
     {
-        public static string DefaultUndoChangeLocation => Path.Join(Path.GetTempPath(), "PixiEditor", "UndoStack");
+        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;
+        private List<Guid> layersToStore = new List<Guid>();
         public Document Document { get; }
 
         public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
         {
             Document = doc;
-            layersToStore = layers.Select(x => x.GuidValue).ToList();
-            UndoChangeLocation = DefaultUndoChangeLocation;
-            GenerateUndoLayers();
-            if (saveOnStartup)
-            {
-                SaveLayersOnDevice();
-            }
+            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;
-            layersToStore = layers.Select(x => x.GuidValue).ToList();
-            UndoChangeLocation = undoChangeLocation;
-            GenerateUndoLayers();
+            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();
             }
         }
 
-        public void Dispose()
+        private void Initialize(IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup, bool useDocumentSize = false)
         {
-            var layers = LoadLayersFromDevice();
-            foreach (var layer in layers)
-                layer.LayerBitmap.Dispose();
+            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);
+            }
+
+            UndoChangeLocation = undoChangeLocation;
+            GenerateUndoLayers(layerChunks);
+            if (saveOnStartup)
+            {
+                SaveLayersOnDevice();
+            }
         }
 
         public void SaveLayersOnDevice()
@@ -64,7 +106,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 +129,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>
@@ -86,16 +155,17 @@ namespace PixiEditor.Models.Undo
                 var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName);
                 layers[i] = new Layer(storedLayer.Name, bitmap)
                 {
-                    Offset = new System.Windows.Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
+                    Width = storedLayer.Width,
+                    Height = storedLayer.Height,
+                    Offset = new 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,
-                    Height = storedLayer.Height,
                     LayerHighlightColor = storedLayer.LayerHighlightColor
                 };
+
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
 
                 File.Delete(StoredLayers[i].StoredPngLayerName);
@@ -139,14 +209,19 @@ namespace PixiEditor.Models.Undo
         /// <param name="undoRedoProcess">Process that is invoked on redo and undo.</param>
         /// <param name="processArgs">Custom parameters for undo and redo process.</param>
         /// <param name="description">Undo change description.</param>
-        /// <returns>UndoManager ready Change instance.</returns>
+        /// <returns>UndoManager ready 'Change' instance.</returns>
         public Change ToChange(Action<Layer[], UndoLayer[], object[]> undoRedoProcess, object[] processArgs, string description = "")
         {
             Action<object[]> finalProcess = processParameters =>
             {
-
                 Layer[] layers = LoadLayersFromDevice();
-                GenerateUndoLayers();
+                LayerChunk[] chunks = new LayerChunk[layers.Length];
+                for (int i = 0; i < layers.Length; i++)
+                {
+                    chunks[i] = new LayerChunk(layers[i], StoredLayers[i].SerializedRect);
+                }
+
+                GenerateUndoLayers(chunks);
 
                 SaveLayersOnDevice();
 
@@ -243,7 +318,7 @@ namespace PixiEditor.Models.Undo
         /// <summary>
         /// Generates UndoLayer[] StoredLayers data.
         /// </summary>
-        private void GenerateUndoLayers()
+        private void GenerateUndoLayers(LayerChunk[] chunks)
         {
             StoredLayers = new UndoLayer[layersToStore.Count];
             int i = 0;
@@ -255,16 +330,15 @@ namespace PixiEditor.Models.Undo
                     throw new ArgumentException("Provided document doesn't contain selected layer");
                 }
 
-                layer.ClipCanvas();
-
                 int index = Document.Layers.IndexOf(layer);
-                string pngName = layer.Name + Guid.NewGuid().ToString();
+                string fileName = layer.Name + Guid.NewGuid();
                 StoredLayers[i] = new UndoLayer(
                     Path.Join(
                         UndoChangeLocation,
-                        Convert.ToBase64String(Encoding.UTF8.GetBytes(pngName)) + ".png"),
+                        Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName)) + ".undoimg"),
                     layer,
-                    index);
+                    index,
+                    chunks[i].AbsoluteChunkRect);
                 i++;
             }
         }
@@ -273,408 +347,45 @@ namespace PixiEditor.Models.Undo
         {
             if (args.Length > 0 && args[0] is Document document)
             {
-                var ls = document.LayerStructure.CloneGroups();
-
                 for (int i = 0; i < layers.Length; i++)
                 {
                     Layer layer = layers[i];
+                    UndoLayer layerData = data[i];
+                    var foundLayer = document.Layers.FirstOrDefault(x => x.GuidValue == layerData.LayerGuid);
 
-                    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)
+                    if (layerData.IsActive)
                     {
-                        document.SetMainActiveLayer(data[i].LayerIndex);
+                        document.SetMainActiveLayer(layerData.LayerIndex);
                     }
                 }
+            }
+        }
 
-                document.BuildLayerStructureProcess(new object[] { ls });
+        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());
+        }
+
+        public void Dispose()
+        {
+            for (int i = 0; i < StoredLayers.Length; i++)
+            {
+                if (File.Exists(StoredLayers[i].StoredPngLayerName))
+                    File.Delete(StoredLayers[i].StoredPngLayerName);
             }
         }
     }
 }
-
-//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 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);
-//            }
-
-//            UndoChangeLocation = undoChangeLocation;
-//            GenerateUndoLayers(layerChunks);
-//            if (saveOnStartup)
-//            {
-//                SaveLayersOnDevice();
-//            }
-//        }
-
-//        public void SaveLayersOnDevice()
-//        {
-//            int i = 0;
-//            foreach (var layerGuid in layersToStore)
-//            {
-//                Layer layer = Document.Layers.First(x => x.GuidValue == layerGuid);
-//                UndoLayer storedLayer = StoredLayers[i];
-//                if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName)))
-//                {
-//                    // 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();
-//                    Surface targetSizeSurface = new Surface(finalRect.Width, finalRect.Height);
-
-//                    targetSizeSurface.SkiaSurface.Canvas.DrawImage(image, finalRect, SKRect.Create(0, 0, finalRect.Width, finalRect.Height), Surface.ReplacingPaint);
-
-//                    Exporter.SaveAsGZippedBytes(storedLayer.StoredPngLayerName, targetSizeSurface);
-//                }
-
-//                i++;
-//            }
-
-//            layersToStore = new List<Guid>();
-//        }
-
-//        /// <summary>
-//        /// Loads saved layers from disk.
-//        /// </summary>
-//        /// <returns>Array of saved layers.</returns>
-//        public Layer[] LoadLayersFromDevice()
-//        {
-//            Layer[] layers = new Layer[StoredLayers.Length];
-//            for (int i = 0; i < StoredLayers.Length; i++)
-//            {
-//                UndoLayer storedLayer = StoredLayers[i];
-//                var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName);
-//                layers[i] = new Layer(storedLayer.Name, bitmap)
-//                {
-//                    Width = storedLayer.Width,
-//                    Height = storedLayer.Height,
-//                    Offset = new Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
-//                    Opacity = storedLayer.Opacity,
-//                    MaxWidth = storedLayer.MaxWidth,
-//                    MaxHeight = storedLayer.MaxHeight,
-//                    IsVisible = storedLayer.IsVisible,
-//                    IsActive = storedLayer.IsActive,
-//                    LayerHighlightColor = storedLayer.LayerHighlightColor
-//                };
-
-//                layers[i].ChangeGuid(storedLayer.LayerGuid);
-
-//                File.Delete(StoredLayers[i].StoredPngLayerName);
-//            }
-
-//            layersToStore = layers.Select(x => x.GuidValue).ToList();
-//            return layers;
-//        }
-
-//        /// <summary>
-//        ///     Creates UndoManager ready Change instance, where undo process loads layers from device, and redo saves them.
-//        /// </summary>
-//        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
-//        /// <param name="processArgs">Custom parameters for undo process.</param>
-//        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
-//        /// <param name="redoProcessParameters">Parameters for redo process.</param>
-//        /// <param name="description">Undo change description.</param>
-//        /// <returns>UndoManager ready Change instance.</returns>
-//        public Change ToChange(Action<Layer[], UndoLayer[], object[]> undoProcess, object[] processArgs, Action<object[]> redoProcess, object[] redoProcessParameters, string description = "")
-//        {
-//            Action<object[]> finalUndoProcess = processParameters =>
-//            {
-//                Layer[] layers = LoadLayersFromDevice();
-//                undoProcess(layers, StoredLayers, processParameters);
-//            };
-
-//            Action<object[]> finalRedoProcess = parameters =>
-//            {
-//                SaveLayersOnDevice();
-//                redoProcess(parameters);
-//            };
-
-//            var change = new Change(finalUndoProcess, processArgs, finalRedoProcess, redoProcessParameters, description);
-//            change.DisposeProcess = (_, _) => Dispose();
-//            return change;
-//        }
-
-//        /// <summary>
-//        ///     Creates UndoManager ready Change instance, where undo and redo is the same, before process images are loaded from disk and current ones are saved.
-//        /// </summary>
-//        /// <param name="undoRedoProcess">Process that is invoked on redo and undo.</param>
-//        /// <param name="processArgs">Custom parameters for undo and redo process.</param>
-//        /// <param name="description">Undo change description.</param>
-//        /// <returns>UndoManager ready 'Change' instance.</returns>
-//        public Change ToChange(Action<Layer[], UndoLayer[], object[]> undoRedoProcess, object[] processArgs, string description = "")
-//        {
-//            Action<object[]> finalProcess = processParameters =>
-//            {
-//                Layer[] layers = LoadLayersFromDevice();
-//                LayerChunk[] chunks = new LayerChunk[layers.Length];
-//                for (int i = 0; i < layers.Length; i++)
-//                {
-//                    chunks[i] = new LayerChunk(layers[i], StoredLayers[i].SerializedRect);
-//                }
-
-//                GenerateUndoLayers(chunks);
-
-//                SaveLayersOnDevice();
-
-//                undoRedoProcess(layers, StoredLayers, processParameters);
-//            };
-
-//            var change = new Change(finalProcess, processArgs, finalProcess, processArgs, description);
-//            change.DisposeProcess = (_, _) => Dispose();
-//            return change;
-//        }
-
-//        /// <summary>
-//        ///     Creates UndoManager ready Change instance, where undo process loads layers from device, and redo saves them.
-//        /// </summary>
-//        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
-//        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
-//        /// <param name="redoProcessParameters">Parameters for redo process.</param>
-//        /// <param name="description">Undo change description.</param>
-//        /// <returns>UndoManager ready Change instance.</returns>
-//        public Change ToChange(Action<Layer[], UndoLayer[]> undoProcess, Action<object[]> redoProcess, object[] redoProcessParameters, string description = "")
-//        {
-//            Action<object[]> finalUndoProcess = _ =>
-//            {
-//                Layer[] layers = LoadLayersFromDevice();
-//                undoProcess(layers, StoredLayers);
-//            };
-
-//            Action<object[]> finalRedoProcess = parameters =>
-//            {
-//                SaveLayersOnDevice();
-//                redoProcess(parameters);
-//            };
-
-//            var change = new Change(finalUndoProcess, null, finalRedoProcess, redoProcessParameters, description);
-//            change.DisposeProcess = (_, _) => Dispose();
-//            return change;
-//        }
-
-//        /// <summary>
-//        ///     Creates UndoManager ready Change instance, where undo process saves layers on device, and redo loads them.
-//        /// </summary>
-//        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
-//        /// <param name="undoProcessParameters">Parameters for undo process.</param>
-//        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
-//        /// <param name="description">Undo change description.</param>
-//        /// <returns>UndoManager ready Change instance.</returns>
-//        public Change ToChange(Action<object[]> undoProcess, object[] undoProcessParameters, Action<Layer[], UndoLayer[]> redoProcess, string description = "")
-//        {
-//            Action<object[]> finalUndoProcess = parameters =>
-//            {
-//                SaveLayersOnDevice();
-//                undoProcess(parameters);
-//            };
-
-//            Action<object[]> finalRedoProcess = parameters =>
-//            {
-//                Layer[] layers = LoadLayersFromDevice();
-//                redoProcess(layers, StoredLayers);
-//            };
-
-//            var change = new Change(finalUndoProcess, undoProcessParameters, finalRedoProcess, null, description);
-//            change.DisposeProcess = (_, _) => Dispose();
-//            return change;
-//        }
-
-//        /// <summary>
-//        ///     Creates UndoManager ready Change instance, where undo process saves layers on device, and redo loads them.
-//        /// </summary>
-//        /// <param name="undoProcess">Method that is invoked on undo, with loaded layers parameter and UndoLayer array data.</param>
-//        /// <param name="undoProcessParameters">Parameters for undo process.</param>
-//        /// <param name="redoProcess">Method that is invoked on redo with custom object array parameters.</param>
-//        /// <param name="redoProcessArgs">Parameters for redo process.</param>
-//        /// <param name="description">Undo change description.</param>
-//        /// <returns>UndoManager ready Change instance.</returns>
-//        public Change ToChange(Action<object[]> undoProcess, object[] undoProcessParameters, Action<Layer[], UndoLayer[], object[]> redoProcess, object[] redoProcessArgs, string description = "")
-//        {
-//            Action<object[]> finalUndoProcess = parameters =>
-//            {
-//                SaveLayersOnDevice();
-//                undoProcess(parameters);
-//            };
-
-//            Action<object[]> finalRedoProcess = parameters =>
-//            {
-//                Layer[] layers = LoadLayersFromDevice();
-//                redoProcess(layers, StoredLayers, parameters);
-//            };
-
-//            var change = new Change(finalUndoProcess, undoProcessParameters, finalRedoProcess, redoProcessArgs, description);
-//            change.DisposeProcess = (_, _) => Dispose();
-//            return change;
-//        }
-
-//        /// <summary>
-//        /// Generates UndoLayer[] StoredLayers data.
-//        /// </summary>
-//        private void GenerateUndoLayers(LayerChunk[] chunks)
-//        {
-//            StoredLayers = new UndoLayer[layersToStore.Count];
-//            int i = 0;
-//            foreach (var layerGuid in layersToStore)
-//            {
-//                Layer layer = Document.Layers.First(x => x.GuidValue == layerGuid);
-//                if (!Document.Layers.Contains(layer))
-//                {
-//                    throw new ArgumentException("Provided document doesn't contain selected layer");
-//                }
-
-//                int index = Document.Layers.IndexOf(layer);
-//                string fileName = layer.Name + Guid.NewGuid();
-//                StoredLayers[i] = new UndoLayer(
-//                    Path.Join(
-//                        UndoChangeLocation,
-//                        Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName)) + ".undoimg"),
-//                    layer,
-//                    index,
-//                    chunks[i].AbsoluteChunkRect);
-//                i++;
-//            }
-//        }
-
-//        public static void BasicUndoProcess(Layer[] layers, UndoLayer[] data, object[] args)
-//        {
-//            if (args.Length > 0 && args[0] is Document document)
-//            {
-//                for (int i = 0; i < layers.Length; i++)
-//                {
-//                    Layer layer = layers[i];
-//                    UndoLayer layerData = data[i];
-//                    var foundLayer = document.Layers.FirstOrDefault(x => x.GuidValue == layerData.LayerGuid);
-
-//                    if (foundLayer != null)
-//                    {
-//                        ApplyChunkToLayer(foundLayer, layerData, layer.LayerBitmap);
-//                    }
-//                    else
-//                    {
-//                        document.RemoveLayer(layerData.LayerIndex, false);
-//                        document.Layers.Insert(layerData.LayerIndex, layer);
-//                    }
-
-//                    if (layerData.IsActive)
-//                    {
-//                        document.SetMainActiveLayer(layerData.LayerIndex);
-//                    }
-//                }
-//            }
-//        }
-
-//        private static void ApplyChunkToLayer(Layer layer, UndoLayer layerData, Surface chunk)
-//        {
-//            bool widthBigger = layer.Width < chunk.Width;
-//            bool heightBigger = layer.Height < chunk.Height;
-//            int targetWidth = widthBigger ? chunk.Width : layer.Width;
-//            int targetHeight = heightBigger ? chunk.Height : layer.Height;
-
-//            int offsetDiffX = layerData.OffsetX - layer.OffsetX;
-//            int offsetDiffY = layerData.OffsetY - layer.OffsetY;
-
-//            int targetOffsetX = layerData.OffsetX == 0 && widthBigger ? layerData.SerializedRect.Left : layerData.OffsetX;
-//            int targetOffsetY = layerData.OffsetY == 0 && heightBigger ? layerData.SerializedRect.Top : layerData.OffsetY;
-
-//            Surface targetSizeSurface = new Surface(targetWidth, targetHeight);
-//            using var foundLayerSnapshot = layer.LayerBitmap.SkiaSurface.Snapshot();
-//            targetSizeSurface.SkiaSurface.Canvas.DrawImage(
-//                foundLayerSnapshot,
-//                SKRect.Create(offsetDiffX, offsetDiffY, layer.Width, layer.Height),
-//                SKRect.Create(0, 0, targetWidth, targetHeight),
-//                Surface.ReplacingPaint);
-
-//            layer.Offset = new Thickness(targetOffsetX, targetOffsetY, 0, 0);
-
-//            SKRect finalRect = SKRect.Create(
-//                layerData.SerializedRect.Left - layer.OffsetX,
-//                layerData.SerializedRect.Top - layer.OffsetY,
-//                layerData.SerializedRect.Width,
-//                layerData.SerializedRect.Height);
-
-//            using var snapshot = chunk.SkiaSurface.Snapshot();
-
-//            targetSizeSurface.SkiaSurface.Canvas.DrawImage(
-//                snapshot,
-//                finalRect,
-//                Surface.ReplacingPaint);
-
-//            layer.LayerBitmap = targetSizeSurface;
-//        }
-
-//        public void Dispose()
-//        {
-//            var layers = LoadLayersFromDevice();
-//            foreach (var layer in layers)
-//                layer.LayerBitmap.Dispose();
-//        }
-//    }
-//}

+ 3 - 3
PixiEditor/Models/Undo/UndoLayer.cs

@@ -36,9 +36,9 @@ namespace PixiEditor.Models.Undo
 
         public float Opacity { get; set; }
 
-        //public SKRectI SerializedRect { get; set; }
+        public SKRectI SerializedRect { get; set; }
 
-        public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex/*, SKRectI serializedRect*/)
+        public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex, SKRectI serializedRect)
         {
             StoredPngLayerName = storedPngLayerName;
             LayerIndex = layerIndex;
@@ -54,7 +54,7 @@ namespace PixiEditor.Models.Undo
             IsActive = layer.IsActive;
             LayerGuid = layer.GuidValue;
             LayerHighlightColor = layer.LayerHighlightColor;
-            //SerializedRect = serializedRect;
+            SerializedRect = serializedRect;
         }
     }
 }

+ 5 - 3
PixiEditor/PixiEditor.csproj

@@ -182,21 +182,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="WriteableBitmapEx">

+ 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.6.8")]
-[assembly: AssemblyFileVersion("0.1.6.8")]
+[assembly: AssemblyVersion("0.1.7.1")]
+[assembly: AssemblyFileVersion("0.1.7.1")]

+ 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

@@ -7,4 +7,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>
@@ -41,6 +49,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">
@@ -143,6 +153,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>
@@ -261,4 +272,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;
-            }
         }
     }
-}
+}

+ 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

@@ -8,7 +8,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; }
 
@@ -61,7 +62,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);

+ 15 - 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;
             }
@@ -176,7 +178,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 +188,13 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         {
-            var lastArg = Environment.GetCommandLineArgs().Last();
-            if (Importer.IsSupportedFile(lastArg) && File.Exists(lastArg))
+            var args = Environment.GetCommandLineArgs();
+            var file = args.Last();
+            if (Importer.IsSupportedFile(file) && File.Exists(file))
             {
-                Open(lastArg);
+                Open(file);
             }
-            else
+            else if (Owner.BitmapManager.Documents.Count == 0 || !args.Contains("--crash"))
             {
                 if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 {
@@ -199,16 +202,15 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                 }
             }
         }
-
+                
         private void Open(object property)
         {
+            var filter = SupportedFilesHelper.BuildOpenFilter();
+
             OpenFileDialog dialog = new OpenFileDialog
             {
-                Filter =
-                "Any|*.pixi;*.png;*.jpg;*.jpeg;|" +
-                "PixiEditor Files | *.pixi|" +
-                "Image Files|*.png;*.jpg;*.jpeg;",
-                DefaultExt = "pixi"
+                Filter = filter,
+                FilterIndex = 0
             };
 
             if ((bool)dialog.ShowDialog())
@@ -244,8 +246,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew";
             if (paramIsAsNew ||
-                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath) ||
-                !Owner.BitmapManager.ActiveDocument.DocumentFilePath.EndsWith(".pixi"))
+                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) 
             {
                 Owner.BitmapManager.ActiveDocument.SaveWithDialog();
             }

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

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

+ 59 - 18
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)
@@ -224,15 +259,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 +285,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)

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

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

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

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

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

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

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

+ 30 - 28
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)),
@@ -215,9 +215,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>
@@ -321,7 +323,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);

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

+ 32 - 0
PixiEditor/Views/Dialogs/DialogTitleBar.xaml.cs

@@ -0,0 +1,32 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PixiEditor.Views.Dialogs
+{
+    public partial class DialogTitleBar : UserControl
+    {
+        public static readonly DependencyProperty TitleTextProperty =
+            DependencyProperty.Register(nameof(TitleText), typeof(string), typeof(DialogTitleBar), new PropertyMetadata(""));
+
+        public static readonly DependencyProperty CloseCommandProperty =
+            DependencyProperty.Register(nameof(CloseCommand), typeof(ICommand), typeof(DialogTitleBar), new PropertyMetadata(null));
+
+        public ICommand CloseCommand
+        {
+            get { return (ICommand)GetValue(CloseCommandProperty); }
+            set { SetValue(CloseCommandProperty, value); }
+        }
+
+        public string TitleText
+        {
+            get { return (string)GetValue(TitleTextProperty); }
+            set { SetValue(TitleTextProperty, value); }
+        }
+
+        public DialogTitleBar()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 42 - 0
PixiEditor/Views/Dialogs/ExportFilePopup.xaml

@@ -0,0 +1,42 @@
+<Window x:Class="PixiEditor.Views.ExportFilePopup"
+        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:local="clr-namespace:PixiEditor.Views"
+        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d" BorderBrush="Black" BorderThickness="1"
+        Title="SaveFilePopup" Height="250" Width="300" WindowStyle="None" MinHeight="250" MinWidth="300"
+        WindowStartupLocation="CenterScreen" Name="saveFilePopup">
+    <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>
+
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
+        <i:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
+
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Export image" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
+
+        <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" IsDefault="True"
+                    Margin="15" Style="{StaticResource DarkRoundButton}" Content="Export" Command="{Binding OkCommand}"
+                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
+
+        <local:SizePicker Width="230" Height="125" Margin="0,30,0,0"
+            x:Name="sizePicker"
+            ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
+            ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
+
+    </DockPanel>
+</Window>

+ 18 - 12
PixiEditor/Views/Dialogs/SaveFilePopup.xaml.cs → PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs

@@ -1,23 +1,22 @@
-using PixiEditor.ViewModels;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels;
+using System.Drawing.Imaging;
 using System.Windows;
-using System.Windows.Input;
-
+using System.Windows.Input;
+
 namespace PixiEditor.Views
 {
-    /// <summary>
-    ///     Interaction logic for SaveFilePopup.xaml
-    /// </summary>
-    public partial class SaveFilePopup : Window
+    public partial class ExportFilePopup : Window
     {
         public static readonly DependencyProperty SaveHeightProperty =
-            DependencyProperty.Register("SaveHeight", typeof(int), typeof(SaveFilePopup), new PropertyMetadata(32));
+            DependencyProperty.Register("SaveHeight", typeof(int), typeof(ExportFilePopup), new PropertyMetadata(32));
 
 
         public static readonly DependencyProperty SaveWidthProperty =
-            DependencyProperty.Register("SaveWidth", typeof(int), typeof(SaveFilePopup), new PropertyMetadata(32));
+            DependencyProperty.Register("SaveWidth", typeof(int), typeof(ExportFilePopup), new PropertyMetadata(32));
+
+        private readonly SaveFilePopupViewModel dataContext = new SaveFilePopupViewModel();
 
-        private readonly SaveFilePopupViewModel dataContext = new SaveFilePopupViewModel();
-
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;
@@ -28,11 +27,12 @@ namespace PixiEditor.Views
             SystemCommands.CloseWindow(this);
         }
 
-        public SaveFilePopup()
+        public ExportFilePopup()
         {
             InitializeComponent();
             Owner = Application.Current.MainWindow;
             DataContext = dataContext;
+            Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
 
@@ -54,5 +54,11 @@ namespace PixiEditor.Views
             get => dataContext.FilePath;
             set => dataContext.FilePath = value;
         }
+
+        public FileType SaveFormat 
+        {
+            get => dataContext.ChosenFormat;
+            set => dataContext.ChosenFormat = value;
+        }
     }
 }

+ 2 - 2
PixiEditor/Views/Dialogs/HelloTherePopup.xaml

@@ -8,7 +8,7 @@
         xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
         xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
         mc:Ignorable="d" ShowInTaskbar="False"
-        Title="Hello there!" Height="662" Width="632"
+        Title="Hello there!" Height="662" Width="632" MinHeight="500" MinWidth="500"
         d:DataContext="{d:DesignInstance local:HelloTherePopup}"
         WindowStyle="None" WindowStartupLocation="CenterScreen">
 
@@ -72,7 +72,7 @@
                     Command="{x:Static SystemCommands.CloseWindowCommand}" />
         </DockPanel>
 
-        <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
+        <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="3,0">
             <Grid Grid.Row="1" Margin="0,30,0,0">
                 <Grid.RowDefinitions>
                     <RowDefinition Height="90"/>

+ 22 - 27
PixiEditor/Views/Dialogs/ImportFilePopup.xaml

@@ -4,9 +4,16 @@
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:local="clr-namespace:PixiEditor.Views"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
         xmlns:vm="clr-namespace:PixiEditor.ViewModels"
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
+        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         mc:Ignorable="d" BorderBrush="Black" BorderThickness="1"
-        Title="ImportFilePopup" Topmost="True" ShowInTaskbar="False" Height="350" Width="300" WindowStyle="None" WindowStartupLocation="CenterScreen" Name="importFilePopup" MinHeight="350" MinWidth="300"
+        Title="ImportFilePopup" ShowInTaskbar="False" 
+        MinHeight="250" MinWidth="300" Height="250" Width="300" 
+        WindowStyle="None" 
+        WindowStartupLocation="CenterScreen" 
+        Name="importFilePopup"
         DataContext="{DynamicResource ImportFilePopupViewModel}">
     <Window.Resources>
         <vm:ImportFilePopupViewModel x:Key="ImportFilePopupViewModel" />
@@ -21,31 +28,19 @@
                         Executed="CommandBinding_Executed_Close" />
     </Window.CommandBindings>
 
-    <Grid Background="{StaticResource AccentColor}">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource AccentColor}">
+        <i:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
 
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <StackPanel Grid.Row="1">
-            <Label Height="40" Width="120" VerticalAlignment="Top" Content="Open" Foreground="Snow"
-                       HorizontalContentAlignment="Center" FontSize="24" Margin="0,10,0,0" />
-            <Button Grid.Row="1" BorderThickness="1" Foreground="Snow" Height="40" Width="160" Margin="0,30,0,0"
-                        Content="File Path" Background="#303030" BorderBrush="{Binding PathButtonBorder}"
-                        Command="{Binding ChoosePathCommand}" />
-            <StackPanel Background="{StaticResource MainColor}" Height="120" Width="225" Margin="0,30,0,0">
-                <local:SizePicker EditingEnabled="{Binding PathIsCorrect}"
-                                      ChosenWidth="{Binding ImportWidth, Mode=TwoWay}"
-                                      ChosenHeight="{Binding ImportHeight,Mode=TwoWay}" />
-            </StackPanel>
-        </StackPanel>
-        <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right"
-                    Margin="10" Style="{StaticResource DarkRoundButton}" Content="OK" Command="{Binding OkCommand}"
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Import image" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
+        <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" IsDefault="True"
+                    Margin="15" Style="{StaticResource DarkRoundButton}" Content="Import" Command="{Binding OkCommand}"
                     CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
-    </Grid>
-</Window>
+        <local:SizePicker x:Name="sizePicker" Width="230" Height="125" Margin="0,30,0,0"
+                                  ChosenWidth="{Binding ImportWidth, Mode=TwoWay}"
+                                  ChosenHeight="{Binding ImportHeight, Mode=TwoWay}" />
+
+    </DockPanel>
+</Window>

+ 4 - 6
PixiEditor/Views/Dialogs/ImportFilePopup.xaml.cs

@@ -1,12 +1,9 @@
-using System.Windows;
+using PixiEditor.ViewModels;
+using System.Windows;
 using System.Windows.Input;
-using PixiEditor.ViewModels;
 
 namespace PixiEditor.Views
 {
-    /// <summary>
-    ///     Interaction logic for ImportFilePopup.xaml
-    /// </summary>
     public partial class ImportFilePopup : Window
     {
         private readonly ImportFilePopupViewModel dc = new ImportFilePopupViewModel();
@@ -15,6 +12,7 @@ namespace PixiEditor.Views
         {
             InitializeComponent();
             DataContext = dc;
+            Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
 
@@ -48,4 +46,4 @@ namespace PixiEditor.Views
             SystemCommands.CloseWindow(this);
         }
     }
-}
+}

+ 22 - 26
PixiEditor/Views/Dialogs/NewFilePopup.xaml

@@ -6,9 +6,15 @@
         xmlns:local="clr-namespace:PixiEditor.Views"
         xmlns:vm="clr-namespace:PixiEditor.ViewModels"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
         mc:Ignorable="d"
-        d:DesignHeight="600" Topmost="True" ShowInTaskbar="False" d:DesignWidth="450"
-        DataContext="{DynamicResource NewFileMenuViewModel}" WindowStyle="None" WindowStartupLocation="CenterScreen" MinHeight="300" MinWidth="400" Height="600" Width="450" Name="newFilePopup" BorderBrush="Black" BorderThickness="1">
+        ShowInTaskbar="False"
+        DataContext="{DynamicResource NewFileMenuViewModel}" 
+        WindowStyle="None" 
+        WindowStartupLocation="CenterScreen" 
+        MinHeight="250" MinWidth="300" Height="250" Width="300" 
+        Name="newFilePopup" 
+        BorderBrush="Black" BorderThickness="1">
     <Window.Resources>
         <vm:NewFileMenuViewModel x:Key="NewFileMenuViewModel" />
     </Window.Resources>
@@ -22,34 +28,24 @@
                         Executed="CommandBinding_Executed_Close" />
     </Window.CommandBindings>
 
-    <Grid Background="{StaticResource AccentColor}" Focusable="True">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
 
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <Label Content="New File" Grid.Row="1" Margin="0,10,0,0" HorizontalAlignment="Center"
-                   VerticalAlignment="Top" Foreground="White" FontSize="24" />
-        <StackPanel HorizontalAlignment="Center" Margin="0,60,0,0" Background="{StaticResource MainColor}"
-                        VerticalAlignment="Top" Grid.Row="1" Width="350" Height="150">
-            <local:SizePicker Margin="0,20" HorizontalAlignment="Center" Height="110"
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Create a new image" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+
+        <Button DockPanel.Dock="Bottom" Width="70" Margin="0,15,0,15" HorizontalAlignment="Center"
+                IsDefault="True" Content="Create" x:Name="createButton"
+                Style="{StaticResource DarkRoundButton}" 
+                Command="{Binding OkCommand}"
+                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
+
+        <local:SizePicker HorizontalAlignment="Center" Width="230" Height="125" Margin="0,30,0,0"
+                              PreserveAspectRatio="False"
                               ChosenHeight="{Binding FileHeight, Mode=TwoWay, ElementName=newFilePopup}"
                               ChosenWidth="{Binding FileWidth, Mode=TwoWay, ElementName=newFilePopup}" 
-                              x:Name="sizePicker"
-                              NextControl="{Binding ElementName=createButton}"/>
-        </StackPanel>
-        <Button VerticalAlignment="Bottom" HorizontalAlignment="Right" FontSize="20" Height="30" Width="120"
-                Style="{StaticResource DarkRoundButton}" Content="Create" Margin="0,0,10,10" Grid.Row="1"
-                Command="{Binding OkCommand}"
-                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" 
-                x:Name="createButton"/>
-    </Grid>
+                              x:Name="sizePicker"/>
+    </DockPanel>
 </Window>

+ 9 - 4
PixiEditor/Views/Dialogs/NewFilePopup.xaml.cs

@@ -18,9 +18,14 @@ namespace PixiEditor.Views
         {
             InitializeComponent();
             Owner = Application.Current.MainWindow;
-            sizePicker.FocusWidthPicker();
-        }
-
+            Loaded += OnDialogShown;
+        }
+
+        private void OnDialogShown(object sender, RoutedEventArgs e)
+        {
+            sizePicker.FocusWidthPicker();
+        }
+
         public int FileHeight
         {
             get => (int)GetValue(FileHeightProperty);
@@ -43,4 +48,4 @@ namespace PixiEditor.Views
             SystemCommands.CloseWindow(this);
         }
     }
-}
+}

+ 19 - 26
PixiEditor/Views/Dialogs/NoticePopup.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"
+        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" WindowStyle="None"
-        Title="NoticePopup" Height="200" Width="500"
+        d:Title="Notice" Height="180" Width="400" MinHeight="180" MinWidth="400"
+        WindowStartupLocation="CenterScreen"
         x:Name="popup">
 
     <WindowChrome.WindowChrome>
@@ -13,32 +17,21 @@
                       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}" TextAlignment="Center"
-                   VerticalAlignment="Center" FontSize="18" Foreground="White"
-                       TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <TextBlock Text="{Binding Title, ElementName=popup}" 
-                       FontSize="18" Foreground="White"
-                       VerticalAlignment="Center" Margin="5,0,0,0"/>
-            <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 Height="30" Width="60"
-                    Click="OkButton_Close"
-                    Style="{StaticResource DarkRoundButton}" Content="Ok">
-            </Button>
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="{Binding ElementName=popup, Path=Title}" CloseCommand="{Binding DataContext.CancelCommand, ElementName=popup}" />
+
+        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,15">
+            <Button Width="70" IsDefault="True" Click="OkButton_Close" Style="{StaticResource DarkRoundButton}" Content="Close"/>
         </StackPanel>
-    </Grid>
+
+        <TextBlock 
+            Grid.Row="1" Text="{Binding Body, ElementName=popup}" TextAlignment="Center"
+            VerticalAlignment="Center" FontSize="15" Foreground="White" Margin="20,0" d:Text="The file does not exist"
+            TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
+    </DockPanel>
 </Window>

+ 0 - 35
PixiEditor/Views/Dialogs/PopupTemplate.xaml

@@ -1,35 +0,0 @@
-<Window x:Class="PixiEditor.Views.PopupTemplate"
-        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:local="clr-namespace:PixiEditor.Views"
-        mc:Ignorable="d" BorderBrush="Black" BorderThickness="1"
-        Title="ResizeDocumentPopup" WindowStartupLocation="CenterScreen" Height="200" Width="400" WindowStyle="None">
-
-    <WindowChrome.WindowChrome>
-        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
-                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
-    </WindowChrome.WindowChrome>
-
-    <Window.CommandBindings>
-        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
-                        Executed="CommandBinding_Executed_Close" />
-    </Window.CommandBindings>
-
-    <Grid Background="{StaticResource AccentColor}">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
-
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10"
-                Style="{StaticResource DarkRoundButton}" Content="OK" Command="{Binding OkCommand}"
-                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
-    </Grid>
-</Window>

+ 39 - 31
PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.Views.ResizeCanvasPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeCanvasPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -6,9 +6,14 @@
         xmlns:local="clr-namespace:PixiEditor.Views"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
-        mc:Ignorable="d" Name="window"
-        Title="ResizeCanvasPopup" Topmost="True" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="390" Width="400" WindowStyle="None">
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:base="clr-namespace:PixiEditor.Views"
+        mc:Ignorable="d" 
+        x:Name="window"
+        Title="ResizeCanvasPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
+        Height="420" Width="320" MinHeight="420" MinWidth="320" 
+        WindowStyle="None"
+        >
 
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
@@ -20,34 +25,37 @@
                         Executed="CommandBinding_Executed_Close" />
     </Window.CommandBindings>
 
-    <Grid Background="{StaticResource AccentColor}" Focusable="True">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
-        
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <Label Grid.Row="1" VerticalAlignment="Top" Foreground="White" FontSize="24" HorizontalAlignment="Center"
-               Content="Resize Canvas" />
-        <StackPanel HorizontalAlignment="Center" Margin="0,50,0,0" Background="{StaticResource MainColor}"
-                    VerticalAlignment="Top" Grid.Row="1" Width="300" Height="250">
-            <local:SizePicker Margin="0,10,0,0" Width="300" Height="110"
-                              ChosenHeight="{Binding NewHeight, Mode=TwoWay, ElementName=window}"
-                              ChosenWidth="{Binding NewWidth, Mode=TwoWay, ElementName=window}" />
-            <Separator Margin="10,20,10,0" Background="{StaticResource AccentColor}" Height="1" />
-            <Label Content="Anchor point:" Foreground="White" Margin="10,5,0,0" HorizontalAlignment="Left"
-                   FontSize="16" />
-            <local:AnchorPointPicker AnchorPoint="{Binding Path=SelectedAnchorPoint, Mode=TwoWay, ElementName=window}"
-                                     Width="78" Margin="45,-25,0,0" Height="78" />
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Resize canvas" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+
+        <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" Margin="15"
+                Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
+
+        <StackPanel HorizontalAlignment="Center" Margin="0,30,0,0" Background="{StaticResource MainColor}"
+                    VerticalAlignment="Top" Grid.Row="1" Width="250" Height="290">
+            <local:SizePicker Margin="0,8,0,0" 
+                              Width="240"
+                              Height="170"
+                              x:Name="sizePicker"
+                              ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+                              ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+                              ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+                              SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+                              SizeUnitSelectionVisibility="Visible"
+                              />
+            <Separator Margin="10,5,10,0" Background="{StaticResource AccentColor}" Height="1" />
+            <DockPanel>
+                <Label Content="Anchor point:" Foreground="White" Margin="25,5,0,0" HorizontalAlignment="Left"
+                       FontSize="12" />
+                <local:AnchorPointPicker AnchorPoint="{Binding Path=SelectedAnchorPoint, Mode=TwoWay, ElementName=window}"
+                                         HorizontalAlignment="Right"
+                                         Width="78" Margin="0,10,30,0" Height="78" />
+            </DockPanel>
         </StackPanel>
-        <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10"
-                Style="{StaticResource DarkRoundButton}" Content="OK" Click="Button_Click" />
-    </Grid>
-</Window>
+    </DockPanel>
+</base:ResizeablePopup>

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

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

+ 29 - 27
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml

@@ -1,13 +1,17 @@
-<Window x:Class="PixiEditor.Views.ResizeDocumentPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeDocumentPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
-        xmlns:local="clr-namespace:PixiEditor.Views" xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
-        mc:Ignorable="d" Name="window"
-        Title="ResizeDocumentPopup" Topmost="True" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="300" Width="400" WindowStyle="None">
+        xmlns:local="clr-namespace:PixiEditor.Views" 
+        xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d" x:Name="window"
+        Title="ResizeDocumentPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
+        Height="305" Width="310" MinHeight="305" MinWidth="310"
+        xmlns:base="clr-namespace:PixiEditor.Views"
+        WindowStyle="None">
 
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32" GlassFrameThickness="0.1"
@@ -19,28 +23,26 @@
                         Executed="CommandBinding_Executed_Close" />
     </Window.CommandBindings>
 
-    <Grid Background="{StaticResource AccentColor}" Focusable="True">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
-        
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <Label Grid.Row="1" VerticalAlignment="Top" Foreground="White" FontSize="24" HorizontalAlignment="Center"
-               Content="Resize document" />
-        <StackPanel HorizontalAlignment="Center" Margin="0,50,0,0" Background="{StaticResource MainColor}"
-                    VerticalAlignment="Top" Grid.Row="1" Width="350" Height="150">
-            <local:SizePicker Margin="0,20" ChosenHeight="{Binding Path=NewHeight, Mode=TwoWay, ElementName=window}"
-                              ChosenWidth="{Binding Path=NewWidth, Mode=TwoWay, ElementName=window}" />
-        </StackPanel>
-        <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10"
-                Style="{StaticResource DarkRoundButton}" Content="OK" Click="Button_Click" />
-    </Grid>
-</Window>
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Resize image" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
+
+        <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" Margin="15"
+                Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
+
+        <local:SizePicker HorizontalAlignment="Center" Width="240" Height="180" Margin="0,30,0,0"
+            x:Name="sizePicker"
+            PreserveAspectRatio="True"
+            ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+            ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+            ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+            SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+            SizeUnitSelectionVisibility="Visible"
+            />
+
+    </DockPanel>
+</base:ResizeablePopup>

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

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

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

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

+ 0 - 51
PixiEditor/Views/Dialogs/SaveFilePopup.xaml

@@ -1,51 +0,0 @@
-<Window x:Class="PixiEditor.Views.SaveFilePopup"
-        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:local="clr-namespace:PixiEditor.Views"
-        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
-        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
-        mc:Ignorable="d" BorderBrush="Black" BorderThickness="1"
-        Title="SaveFilePopup" Height="300" Width="400" WindowStyle="None" MinHeight="300" MinWidth="400"
-        WindowStartupLocation="CenterScreen" Name="saveFilePopup">
-    <WindowChrome.WindowChrome>
-        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
-                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
-    </WindowChrome.WindowChrome>
-
-    <Window.CommandBindings>
-        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
-                        Executed="CommandBinding_Executed_Close" />
-    </Window.CommandBindings>
-
-    <Grid Background="{StaticResource AccentColor}" Focusable="True">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
-        <i:Interaction.Behaviors>
-            <behaviours:ClearFocusOnClickBehavior/>
-        </i:Interaction.Behaviors>
-
-        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        
-        <TextBlock Grid.Row="1" Foreground="Snow" VerticalAlignment="Top" HorizontalAlignment="Center"
-                       Text="File settings" TextAlignment="Center" Margin="0,10,0,0" FontSize="24" />
-            <StackPanel Orientation="Vertical" Grid.Row="1" Margin="0,50,0,0">
-                <local:SizePicker Width="250" Height="120"
-                                  ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
-                                  ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
-                <Button Foreground="Snow" Height="40" Width="160" Margin="0,10,0,0" Content="Path"
-                        Background="{StaticResource MainColor}" BorderBrush="{Binding PathButtonBorder}"
-                        Command="{Binding ChoosePathCommand}" />
-            </StackPanel>
-        <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right"
-                    Margin="10" Style="{StaticResource DarkRoundButton}" Content="OK" Command="{Binding OkCommand}"
-                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
-    </Grid>
-</Window>

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

@@ -0,0 +1,50 @@
+<Window x:Class="PixiEditor.Views.Dialogs.SendCrashReportWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d"
+        Background="{StaticResource AccentColor}" Foreground="White"
+        Title="Send crash report"
+        WindowStyle="None"
+        WindowStartupLocation="CenterScreen"
+        MinHeight="195" MinWidth="340"
+        Height="195" Width="340">
+    <Window.Resources>
+        <Style TargetType="TextBlock">
+            <Setter Property="HorizontalAlignment" Value="Center"/>
+        </Style>
+        <Style TargetType="Button" BasedOn="{StaticResource DarkRoundButton}">
+            <Setter Property="Margin" Value="5"/>
+            <Setter Property="Width" Value="100"/>
+        </Style>
+    </Window.Resources>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <StackPanel>
+        <dial:DialogTitleBar TitleText="Send crash report" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+        <StackPanel Margin="10">
+            <TextBlock>You can find the report here:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Width="140" Click="OpenInExplorer">Open in Explorer</Button>
+            </StackPanel>
+            <TextBlock TextAlignment="Center">You can send your crash report using:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Click="OpenHyperlink" Tag="github">GitHub</Button>
+                <Button Click="OpenHyperlink" Tag="discord">Discord</Button>
+                <Button Click="OpenHyperlink" Tag="email">E-Mail</Button>
+            </StackPanel>
+            <TextBlock TextWrapping="Wrap" TextAlignment="Center">The report contains the documents that were opened when the crash happened, feel free to review it before sending.</TextBlock>
+        </StackPanel>
+    </StackPanel>
+</Window>

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

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

+ 129 - 81
PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -3,17 +3,27 @@
         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:local="clr-namespace:PixiEditor.Views.Dialogs" xmlns:viewmodels="clr-namespace:PixiEditor.ViewModels" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:views="clr-namespace:PixiEditor.Views" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls"
+        xmlns:local="clr-namespace:PixiEditor.Views.Dialogs" 
+        xmlns:sys="clr-namespace:System;assembly=System.Runtime"
+        xmlns:viewmodels="clr-namespace:PixiEditor.ViewModels" 
+        xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" 
+        xmlns:views="clr-namespace:PixiEditor.Views" 
+        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
+        xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
         mc:Ignorable="d"
         Title="Settings" Name="window" 
-        Height="600" Width="800"
-        MinHeight="350" MinWidth="600"
+        Height="500" Width="640"
+        MinHeight="500" MinWidth="640"
         WindowStyle="None" DataContext="{DynamicResource SettingsWindowViewModel}"
+        WindowStartupLocation="CenterScreen"
         BorderBrush="Black" BorderThickness="1">
     <Window.Resources>
         <viewmodels:SettingsWindowViewModel x:Key="SettingsWindowViewModel"/>
         <BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
     </Window.Resources>
+
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
@@ -24,88 +34,126 @@
                         Executed="CommandBinding_Executed_Close" />
     </Window.CommandBindings>
 
-    <Grid Background="{StaticResource MainColor}" Focusable="True">
-        <Grid.ColumnDefinitions>
-            <ColumnDefinition Width="200"/>
-            <ColumnDefinition Width="147*"/>
-        </Grid.ColumnDefinitions>
-        <Grid.RowDefinitions>
-            <RowDefinition Height="35" />
-            <RowDefinition />
-        </Grid.RowDefinitions>
+    <DockPanel Background="{StaticResource MainColor}" Focusable="True">
         <i:Interaction.Behaviors>
             <behaviours:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
-        
-        <DockPanel Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource MainColor}">
-            <Label Foreground="White" FontSize="16">Settings</Label>
-            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
-                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
-                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
-        </DockPanel>
-        <StackPanel Grid.Row="1" Grid.Column="0">
-            <Button Style="{StaticResource AccentDarkRoundButton}" Margin="10 5 10 5"
-                    Command="{Binding SelectCategoryCommand}" CommandParameter="General">General</Button>
-            <Button Style="{StaticResource AccentDarkRoundButton}" Margin="10 5 10 5" 
-                    Command="{Binding SelectCategoryCommand}" CommandParameter="Updates"
-                    Visibility="{Binding ShowUpdateTab, Converter={StaticResource BoolToVisibilityConverter}}">Updates</Button>
-            <Button Style="{StaticResource AccentDarkRoundButton}" Margin="10 5 10 5" 
-                    Command="{Binding SelectCategoryCommand}" CommandParameter="Discord">Discord</Button>
-        </StackPanel>
+
+        <dial:DialogTitleBar DockPanel.Dock="Top"
+            TitleText="Settings" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
+
+        <ListBox DockPanel.Dock="Left" SelectedIndex="0" x:Name="pages" 
+                 Background="Transparent" BorderThickness="0" Width="150" ItemContainerStyle="{StaticResource PixiListBoxItemStyle}">
+            <ListBox.ItemTemplate>
+                <DataTemplate>
+                    <TextBlock Margin="10 5 10 5" FontSize="15" Foreground="White" Text="{Binding}"/>
+                </DataTemplate>
+            </ListBox.ItemTemplate>
+            <ListBox.ItemsSource>
+                <x:Array Type="{x:Type sys:String}">
+                    <sys:String>General</sys:String>
+                    <sys:String>Discord</sys:String>
+                </x:Array>
+            </ListBox.ItemsSource>
+        </ListBox>
+
         <Grid Grid.Row="1" Grid.Column="1" Background="{StaticResource AccentColor}">
-            <Grid Visibility="{Binding SelectedCategory, Converter={converters:EqualityBoolToVisibilityConverter},
-            ConverterParameter='General'}">
-                <StackPanel Orientation="Vertical">
-                    <CheckBox Content="Show Document Preview in Taskbar" Margin="25,30,0,0"
-                                  IsChecked="{Binding SettingsSubViewModel.General.ImagePreviewInTaskbar}"/>
-                    <Label Content="File" Style="{StaticResource Header1}"/>
-                    <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox Content="Show Startup Window" 
-                                  IsChecked="{Binding SettingsSubViewModel.File.ShowStartupWindow}"/>
-                        <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
-                            <Label Content="Max Saved Opened Recently:" ToolTip="How many documents are shown under File > Recent. Default: 8" Style="{StaticResource BaseLabel}"/>
-                            <views:NumberInput Min="0" FontSize="16" Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently, Mode=TwoWay}" Width="40"/>
-                        </StackPanel>
-                        <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
-                        <StackPanel Orientation="Horizontal" Margin="40,0,0,0">
-                            <Label Content="Width:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileWidth, Mode=TwoWay}" 
-                                             Width="70" Height="25" MaxSize="9999"/>
-                            <Label Content="Height:" Style="{StaticResource BaseLabel}"/>
-                            <views:SizeInput FontSize="16" Size="{Binding SettingsSubViewModel.File.DefaultNewFileHeight, Mode=TwoWay}" 
-                                             Width="70" Height="25" MaxSize="9999"/>
-                        </StackPanel>
-                    </StackPanel>
-                </StackPanel>
-            </Grid>
-            <Grid Visibility="{Binding SelectedCategory, Converter={converters:EqualityBoolToVisibilityConverter},
-            ConverterParameter='Updates'}">
-                <StackPanel Orientation="Vertical">
-                    <Label Style="{StaticResource Header1}" Content="Auto-updates"/>
-                    <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox IsChecked="{Binding SettingsSubViewModel.Update.CheckUpdatesOnStartup}" Content="Check updates on startup"/>
-                        <StackPanel Orientation="Horizontal">
-                        <Label VerticalAlignment="Center" Content="Update source" Style="{StaticResource BaseLabel}"/>
-                            <ComboBox Width="110" Margin="0 10 0 10" HorizontalAlignment="Left" 
-                                      ItemsSource="{Binding SettingsSubViewModel.Update.UpdateChannels}"
-                                      SelectedValue="{Binding SettingsSubViewModel.Update.UpdateChannelName}"/>
-                        </StackPanel>
-                    </StackPanel>
-                </StackPanel>
-            </Grid>
-            <Grid Visibility="{Binding SelectedCategory, Converter={converters:EqualityBoolToVisibilityConverter},
-            ConverterParameter='Discord'}">
-                <StackPanel Orientation="Vertical">
-                    <Label Style="{StaticResource Header1}" Content="Rich Presence"/>
-                    <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox Margin="5" IsChecked="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" Content="Enabled"/>
-                        <CheckBox Margin="5" IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentName}" Content="Show Document Name"/>
-                        <CheckBox Margin="5" IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentSize}" Content="Show Document Size"/>
-                        <CheckBox Margin="5" IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" IsChecked="{Binding SettingsSubViewModel.Discord.ShowLayerCount}" Content="Show Layer Count"/>
-                        <usercontrols:DiscordRPPreview Margin="5" State="{Binding SettingsSubViewModel.Discord.StatePreview}" Detail="{Binding SettingsSubViewModel.Discord.DetailPreview}" Width="280" IsPlaying="{Binding SettingsSubViewModel.Discord.EnableRichPresence}"/>
-                    </StackPanel>
-                </StackPanel>
+            <Grid Visibility="{Binding SelectedItem, ElementName=pages, Converter={converters:EqualityBoolToVisibilityConverter},
+            ConverterParameter='General'}" Margin="15,10" Tag="27">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="15"/>
+                    <ColumnDefinition Width="230"/>
+                    <ColumnDefinition/>
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                </Grid.RowDefinitions>
+
+                <Label Grid.Row="0" Grid.ColumnSpan="2" Style="{StaticResource SettingsHeader}">Misc</Label>
+
+                <CheckBox Grid.Row="1" Grid.Column="1"
+                          VerticalAlignment="Center"
+                          IsChecked="{Binding SettingsSubViewModel.File.ShowStartupWindow}">Show startup window</CheckBox>
+
+                <CheckBox Grid.Row="2" Grid.Column="1"
+                          VerticalAlignment="Center"
+                          IsChecked="{Binding SettingsSubViewModel.General.ImagePreviewInTaskbar}">Show image preview in taskbar</CheckBox>
+
+                <Label Grid.Row="3" Grid.Column="1" Style="{StaticResource SettingsText}"
+                       ToolTip="How many documents are shown under File > Recent. Default: 8">Recent file list length</Label>
+                <views:NumberInput Grid.Row="3" Grid.Column="2" 
+                                   Min="0" FontSize="12" HorizontalAlignment="Left"
+                                   Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently, Mode=TwoWay}" Height="19" Width="40"/>
+
+                <Label Grid.Row="4" Grid.ColumnSpan="2"  Style="{StaticResource SettingsHeader}">Default new file size</Label>
+
+                <Label Grid.Row="5" Grid.Column="1" Style="{StaticResource SettingsText}">Width</Label>
+                <views:SizeInput Grid.Row="5" Grid.Column="2" 
+                                 Size="{Binding SettingsSubViewModel.File.DefaultNewFileWidth, Mode=TwoWay}" 
+                                 Width="70" Height="21" MaxSize="9999" HorizontalAlignment="Left"/>
+
+                <Label Grid.Row="6" Grid.Column="1" Style="{StaticResource SettingsText}">Height</Label>
+                <views:SizeInput Grid.Row="6" Grid.Column="2" 
+                                 Size="{Binding SettingsSubViewModel.File.DefaultNewFileHeight, Mode=TwoWay}" 
+                                 Width="70" Height="21" MaxSize="9999" HorizontalAlignment="Left"/>
+
+                <Label Grid.Row="7" Grid.ColumnSpan="2" Style="{StaticResource SettingsHeader}">Automatic updates</Label>
+
+                <CheckBox Grid.Row="8" Grid.Column="1" VerticalAlignment="Center"
+                    IsChecked="{Binding SettingsSubViewModel.Update.CheckUpdatesOnStartup}">Check updates on startup</CheckBox>
+
+                <Label Grid.Row="9" Grid.Column="1" Style="{StaticResource SettingsText}">Update stream</Label>
+                <ComboBox Grid.Row="9" Grid.Column="2" VerticalAlignment="Center"
+                    Width="110" Height="22" HorizontalAlignment="Left"
+                    ItemsSource="{Binding SettingsSubViewModel.Update.UpdateChannels}"
+                    SelectedValue="{Binding SettingsSubViewModel.Update.UpdateChannelName}"/>
             </Grid>
+
+            <StackPanel Visibility="{Binding SelectedItem, ElementName=pages, Converter={converters:EqualityBoolToVisibilityConverter},
+            ConverterParameter='Discord'}" Margin="15,10">
+                <Grid Tag="27">
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="15"/>
+                        <ColumnDefinition/>
+                    </Grid.ColumnDefinitions>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                        <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                        <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                        <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                        <RowDefinition Height="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}"/>
+                    </Grid.RowDefinitions>
+
+                    <Label Grid.Row="0" Grid.ColumnSpan="2" Style="{StaticResource SettingsHeader}">Rich Presence</Label>
+
+                    <CheckBox Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"
+                    IsChecked="{Binding SettingsSubViewModel.Discord.EnableRichPresence}">Enabled</CheckBox>
+                    <CheckBox Grid.Row="2" Grid.Column="1" VerticalAlignment="Center"
+                    IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" 
+                    IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentName}">Show image name</CheckBox>
+                    <CheckBox Grid.Row="3" Grid.Column="1" VerticalAlignment="Center"
+                    IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" 
+                    IsChecked="{Binding SettingsSubViewModel.Discord.ShowDocumentSize}">Show image size</CheckBox>
+                    <CheckBox Grid.Row="4" Grid.Column="1" VerticalAlignment="Center"
+                    IsEnabled="{Binding SettingsSubViewModel.Discord.EnableRichPresence}" 
+                    IsChecked="{Binding SettingsSubViewModel.Discord.ShowLayerCount}">Show layer count</CheckBox>
+                </Grid>
+                <usercontrols:DiscordRPPreview 
+                    Margin="15"
+                    Width="280"
+                    State="{Binding SettingsSubViewModel.Discord.StatePreview}" 
+                    Detail="{Binding SettingsSubViewModel.Discord.DetailPreview}" 
+                    IsPlaying="{Binding SettingsSubViewModel.Discord.EnableRichPresence}"/>
+            </StackPanel>
         </Grid>
-    </Grid>
+    </DockPanel>
 </Window>

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