Browse Source

Merge pull request #396 from PixiEditor/master

Version 0.1.8
Krzysztof Krysiński 3 years ago
parent
commit
7d38ed61e2
100 changed files with 2286 additions and 2127 deletions
  1. 23 4
      .github/ISSUE_TEMPLATE/report_a_bug.md
  2. 7 7
      CONTRIBUTING.md
  3. 5 2
      PixiEditor/App.xaml
  4. 36 1
      PixiEditor/App.xaml.cs
  5. 0 19
      PixiEditor/Exceptions/ArrayLengthMismatchException.cs
  6. 1 1
      PixiEditor/Exceptions/CorruptedFileException.cs
  7. 0 106
      PixiEditor/Helpers/Behaviours/AllowableCharactersTextBoxBehavior.cs
  8. 9 2
      PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  9. 2 2
      PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs
  10. 0 72
      PixiEditor/Helpers/Behaviours/HintTextBehavior.cs
  11. 44 35
      PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  12. 0 15
      PixiEditor/Helpers/Converters/BoolToBrushConverter.cs
  13. 0 34
      PixiEditor/Helpers/Converters/BrushTuple.cs
  14. 37 0
      PixiEditor/Helpers/Converters/EnumBooleanConverter.cs
  15. 29 0
      PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  16. 3 1
      PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
  17. 24 22
      PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
  18. 0 27
      PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs
  19. 6 2
      PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  20. 0 27
      PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs
  21. 1 1
      PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  22. 27 0
      PixiEditor/Helpers/Converters/WidthToBitmapScalingModeConverter.cs
  23. 22 0
      PixiEditor/Helpers/Converters/ZoomLevelToBitmapScalingModeConverter.cs
  24. 63 20
      PixiEditor/Helpers/CrashHelper.cs
  25. 8 2
      PixiEditor/Helpers/Extensions/Int32RectHelper.cs
  26. 4 4
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  27. 0 11
      PixiEditor/Helpers/Extensions/PixiParserHelper.cs
  28. 13 0
      PixiEditor/Helpers/Extensions/SKRectIHelper.cs
  29. 19 0
      PixiEditor/Helpers/ProcessHelpers.cs
  30. 20 0
      PixiEditor/Helpers/SizeCalculator.cs
  31. 88 0
      PixiEditor/Helpers/SupportedFilesHelper.cs
  32. 0 15
      PixiEditor/Helpers/Validators/SizeValidationRule.cs
  33. 14 0
      PixiEditor/Models/Constants.cs
  34. 0 21
      PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs
  35. 1 0
      PixiEditor/Models/Controllers/BitmapManager.cs
  36. 1 1
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  37. 60 56
      PixiEditor/Models/Controllers/ClipboardController.cs
  38. 50 3
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  39. 9 2
      PixiEditor/Models/Controllers/SurfaceRenderer.cs
  40. 1 1
      PixiEditor/Models/Controllers/UndoManager.cs
  41. 1 23
      PixiEditor/Models/DataHolders/BitmapPixelChanges.cs
  42. 194 0
      PixiEditor/Models/DataHolders/CrashReport.cs
  43. 2 0
      PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
  44. 40 19
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  45. 49 12
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  46. 1 5
      PixiEditor/Models/DataHolders/Document/Document.Preview.cs
  47. 81 25
      PixiEditor/Models/DataHolders/Document/Document.cs
  48. 20 0
      PixiEditor/Models/DataHolders/PixelSize.cs
  49. 598 577
      PixiEditor/Models/DataHolders/RangeObservableCollection.cs
  50. 18 16
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  51. 2 2
      PixiEditor/Models/DataHolders/Selection.cs
  52. 5 4
      PixiEditor/Models/DataHolders/Surface.cs
  53. 100 89
      PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs
  54. 2 19
      PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  55. 21 3
      PixiEditor/Models/Dialogs/ExportFileDialog.cs
  56. 2 4
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  57. 3 16
      PixiEditor/Models/Dialogs/NoticeDialog.cs
  58. 23 25
      PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
  59. 0 8
      PixiEditor/Models/Enums/CapType.cs
  60. 1 1
      PixiEditor/Models/Enums/FileType.cs
  61. 4 0
      PixiEditor/Models/Enums/SizeUnit.cs
  62. 49 21
      PixiEditor/Models/IO/Exporter.cs
  63. 55 0
      PixiEditor/Models/IO/FileTypeDialogData.cs
  64. 52 0
      PixiEditor/Models/IO/FileTypeDialogDataSet.cs
  65. 0 20
      PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
  66. 2 2
      PixiEditor/Models/IO/Importer.cs
  67. 0 22
      PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs
  68. 5 36
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  69. 0 99
      PixiEditor/Models/ImageManipulation/Morphology.cs
  70. 0 31
      PixiEditor/Models/ImageManipulation/Transform.cs
  71. 18 7
      PixiEditor/Models/Layers/Layer.cs
  72. 4 4
      PixiEditor/Models/Layers/LayerHelper.cs
  73. 12 12
      PixiEditor/Models/Layers/LayerStructure.cs
  74. 0 19
      PixiEditor/Models/Position/MousePositionConverter.cs
  75. 38 21
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  76. 1 0
      PixiEditor/Models/Tools/Tool.cs
  77. 1 1
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  78. 5 3
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  79. 1 1
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  80. 1 1
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  81. 1 1
      PixiEditor/Models/Tools/Tools/FloodFillTool.cs
  82. 5 3
      PixiEditor/Models/Tools/Tools/LineTool.cs
  83. 2 2
      PixiEditor/Models/Tools/Tools/MagicWandTool.cs
  84. 2 2
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  85. 2 7
      PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  86. 1 1
      PixiEditor/Models/Tools/Tools/PenTool.cs
  87. 6 3
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  88. 1 1
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  89. 1 1
      PixiEditor/Models/Tools/Tools/ZoomTool.cs
  90. 134 425
      PixiEditor/Models/Undo/StorageBasedChange.cs
  91. 3 3
      PixiEditor/Models/Undo/UndoLayer.cs
  92. 6 4
      PixiEditor/PixiEditor.csproj
  93. 2 2
      PixiEditor/Properties/AssemblyInfo.cs
  94. 3 2
      PixiEditor/Styles/AvalonDock/Themes/Generic.xaml
  95. 4 4
      PixiEditor/Styles/DarkCheckboxStyle.xaml
  96. 1 0
      PixiEditor/Styles/ImageCheckBoxStyle.xaml
  97. 15 2
      PixiEditor/Styles/LabelStyles.xaml
  98. 1 0
      PixiEditor/Styles/ListSwitchButtonStyle.xaml
  99. 26 0
      PixiEditor/Styles/PixiListBoxItemStyle.xaml
  100. 32 0
      PixiEditor/Styles/RadioButtonStyle.xaml

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

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

+ 7 - 7
CONTRIBUTING.md

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

+ 5 - 2
PixiEditor/App.xaml

@@ -1,7 +1,8 @@
 <Application x:Class="PixiEditor.App"
 <Application x:Class="PixiEditor.App"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             StartupUri="Views/MainWindow.xaml">
+             >
+    <!--StartupUri="Views/MainWindow.xaml"-->
     <Application.Resources>
     <Application.Resources>
         <ResourceDictionary>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
             <ResourceDictionary.MergedDictionaries>
@@ -17,6 +18,7 @@
                 <ResourceDictionary Source="Styles/DarkCheckboxStyle.xaml" />
                 <ResourceDictionary Source="Styles/DarkCheckboxStyle.xaml" />
                 <ResourceDictionary Source="Styles/ListSwitchButtonStyle.xaml" />
                 <ResourceDictionary Source="Styles/ListSwitchButtonStyle.xaml" />
                 <ResourceDictionary Source="Styles/LabelStyles.xaml" />
                 <ResourceDictionary Source="Styles/LabelStyles.xaml" />
+                <ResourceDictionary Source="Styles/PixiListBoxItemStyle.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Menu/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Menu/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/OverlayButtons.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/OverlayButtons.xaml" />
@@ -25,7 +27,8 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
                 <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
+                <ResourceDictionary Source="Styles/RadioButtonStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
         </ResourceDictionary>
     </Application.Resources>
     </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.Models.Enums;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
+using PixiEditor.Views.Dialogs;
+using System;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows;
 
 
 namespace PixiEditor
 namespace PixiEditor
@@ -11,6 +16,23 @@ namespace PixiEditor
     /// </summary>
     /// </summary>
     public partial class App : Application
     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)
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
         {
         {
             base.OnSessionEnding(e);
             base.OnSessionEnding(e);
@@ -21,5 +43,18 @@ namespace PixiEditor
                 e.Cancel = confirmation != ConfirmationType.Yes;
                 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;
+        }
     }
     }
 }
 }

+ 0 - 19
PixiEditor/Exceptions/ArrayLengthMismatchException.cs

@@ -1,19 +0,0 @@
-using System;
-
-namespace PixiEditor.Exceptions
-{
-    public class ArrayLengthMismatchException : Exception
-    {
-        public const string DefaultMessage = "First array length doesn't match second array length";
-
-        public ArrayLengthMismatchException()
-            : base(DefaultMessage)
-        {
-        }
-
-        public ArrayLengthMismatchException(string message)
-            : base(message)
-        {
-        }
-    }
-}

+ 1 - 1
PixiEditor/Exceptions/CorruptedFileException.cs

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

+ 0 - 106
PixiEditor/Helpers/Behaviours/AllowableCharactersTextBoxBehavior.cs

@@ -1,106 +0,0 @@
-using System;
-using System.Text.RegularExpressions;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Interactivity;
-
-namespace PixiEditor.Helpers.Behaviours
-{
-    public class AllowableCharactersTextBoxBehavior : Behavior<TextBox>
-    {
-        public static readonly DependencyProperty RegularExpressionProperty =
-            DependencyProperty.Register(
-                "RegularExpression",
-                typeof(string),
-                typeof(AllowableCharactersTextBoxBehavior),
-                new FrameworkPropertyMetadata(".*"));
-
-        public static readonly DependencyProperty MaxLengthProperty =
-            DependencyProperty.Register(
-                "MaxLength",
-                typeof(int),
-                typeof(AllowableCharactersTextBoxBehavior),
-                new FrameworkPropertyMetadata(int.MinValue));
-
-        public string RegularExpression
-        {
-            get => (string)GetValue(RegularExpressionProperty);
-            set => SetValue(RegularExpressionProperty, value);
-        }
-
-        public int MaxLength
-        {
-            get => (int)GetValue(MaxLengthProperty);
-            set => SetValue(MaxLengthProperty, value);
-        }
-
-        protected override void OnAttached()
-        {
-            base.OnAttached();
-            AssociatedObject.PreviewTextInput += OnPreviewTextInput;
-            DataObject.AddPastingHandler(AssociatedObject, OnPaste);
-        }
-
-        protected override void OnDetaching()
-        {
-            base.OnDetaching();
-            AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
-            DataObject.RemovePastingHandler(AssociatedObject, OnPaste);
-        }
-
-        private void OnPaste(object sender, DataObjectPastingEventArgs e)
-        {
-            if (e.DataObject.GetDataPresent(DataFormats.Text))
-            {
-                string text = Convert.ToString(e.DataObject.GetData(DataFormats.Text));
-
-                if (!IsValid(text, true))
-                {
-                    e.CancelCommand();
-                }
-            }
-            else
-            {
-                e.CancelCommand();
-            }
-        }
-
-        private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
-        {
-            e.Handled = !IsValid(e.Text, false);
-        }
-
-        private bool IsValid(string newText, bool paste)
-        {
-            return !ExceedsMaxLength(newText, paste) && Regex.IsMatch(newText, RegularExpression);
-        }
-
-        private bool ExceedsMaxLength(string newText, bool paste)
-        {
-            if (MaxLength == 0)
-            {
-                return false;
-            }
-
-            return LengthOfModifiedText(newText, paste) > MaxLength;
-        }
-
-        private int LengthOfModifiedText(string newText, bool paste)
-        {
-            int countOfSelectedChars = AssociatedObject.SelectedText.Length;
-            int caretIndex = AssociatedObject.CaretIndex;
-            string text = AssociatedObject.Text;
-
-            if (countOfSelectedChars > 0 || paste)
-            {
-                text = text.Remove(caretIndex, countOfSelectedChars);
-                return text.Length + newText.Length;
-            }
-
-            bool insert = Keyboard.IsKeyToggled(Key.Insert);
-
-            return insert && caretIndex < text.Length ? text.Length : text.Length + newText.Length;
-        }
-    }
-}

+ 9 - 2
PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Controllers.Shortcuts;
 using PixiEditor.Models.Controllers.Shortcuts;
 using System.Windows;
 using System.Windows;
+using System.Windows.Input;
 using System.Windows.Interactivity;
 using System.Windows.Interactivity;
 
 
 namespace PixiEditor.Helpers.Behaviours
 namespace PixiEditor.Helpers.Behaviours
@@ -8,8 +9,14 @@ namespace PixiEditor.Helpers.Behaviours
     {
     {
         protected override void OnAttached()
         protected override void OnAttached()
         {
         {
-            AssociatedObject.MouseDown += AssociatedObject_MouseDown;
             base.OnAttached();
             base.OnAttached();
+            AssociatedObject.MouseDown += AssociatedObject_MouseDown;
+            AssociatedObject.LostKeyboardFocus += AssociatedObject_LostKeyboardFocus;
+        }
+
+        private void AssociatedObject_LostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
+        {
+            
         }
         }
 
 
         protected override void OnDetaching()
         protected override void OnDetaching()
@@ -20,7 +27,7 @@ namespace PixiEditor.Helpers.Behaviours
         private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
         private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
         {
         {
             AssociatedObject.Focus();
             AssociatedObject.Focus();
-            ShortcutController.BlockShortcutExecution = false;
+            ShortcutController.UnblockShortcutExecutionAll();
         }
         }
     }
     }
 }
 }

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

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

+ 0 - 72
PixiEditor/Helpers/Behaviours/HintTextBehavior.cs

@@ -1,72 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Interactivity;
-using System.Windows.Media;
-
-namespace PixiEditor.Helpers.Behaviours
-{
-    internal class HintTextBehavior : Behavior<TextBox>
-    {
-        // Using a DependencyProperty as the backing store for Hint.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty HintProperty =
-            DependencyProperty.Register(
-                "Hint",
-                typeof(string),
-                typeof(HintTextBehavior),
-                new PropertyMetadata(string.Empty));
-
-        private Brush textColor;
-
-        public string Hint
-        {
-            get => (string)GetValue(HintProperty);
-            set => SetValue(HintProperty, value);
-        }
-
-        protected override void OnAttached()
-        {
-            base.OnAttached();
-            AssociatedObject.GotFocus += AssociatedObject_GotFocus;
-            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
-            textColor = AssociatedObject.Foreground;
-            SetHint(true);
-        }
-
-        protected override void OnDetaching()
-        {
-            base.OnDetaching();
-            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
-            AssociatedObject.GotFocus -= AssociatedObject_GotFocus;
-        }
-
-        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
-        {
-            if (string.IsNullOrEmpty(AssociatedObject.Text))
-            {
-                SetHint(true);
-            }
-        }
-
-        private void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
-        {
-            if (AssociatedObject.Text == Hint)
-            {
-                SetHint(false);
-            }
-        }
-
-        private void SetHint(bool active)
-        {
-            if (active)
-            {
-                AssociatedObject.Foreground = (SolidColorBrush)new BrushConverter().ConvertFromString("#7B7B7B");
-                AssociatedObject.Text = Hint;
-            }
-            else
-            {
-                AssociatedObject.Text = string.Empty;
-                AssociatedObject.Foreground = textColor;
-            }
-        }
-    }
-}

+ 44 - 35
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.Controls;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Interactivity;
 using System.Windows.Interactivity;
@@ -8,27 +7,42 @@ namespace PixiEditor.Helpers.Behaviours
 {
 {
     internal class TextBoxFocusBehavior : Behavior<TextBox>
     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(
             DependencyProperty.Register(
-                nameof(SelectOnFocus),
+                nameof(SelectOnMouseClick),
                 typeof(bool),
                 typeof(bool),
                 typeof(TextBoxFocusBehavior),
                 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 static readonly DependencyProperty DeselectOnFocusLossProperty =
+            DependencyProperty.Register(
+                nameof(DeselectOnFocusLoss),
+                typeof(bool),
+                typeof(TextBoxFocusBehavior),
+                new PropertyMetadata(false));
 
 
-        public FrameworkElement NextControl
+        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(SelectOnFocusProperty);
-            set => SetValue(SelectOnFocusProperty, value);
+            get => (bool)GetValue(ConfirmOnEnterProperty);
+            set => SetValue(ConfirmOnEnterProperty, value);
+        }
+        public bool DeselectOnFocusLoss
+        {
+            get => (bool)GetValue(DeselectOnFocusLossProperty);
+            set => SetValue(DeselectOnFocusLossProperty, value);
         }
         }
 
 
         protected override void OnAttached()
         protected override void OnAttached()
@@ -36,6 +50,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnAttached();
             base.OnAttached();
             AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
             AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp += AssociatedObject_KeyUp;
             AssociatedObject.KeyUp += AssociatedObject_KeyUp;
         }
         }
@@ -45,6 +60,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnDetaching();
             base.OnDetaching();
             AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
             AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
             AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
         }
         }
@@ -52,39 +68,22 @@ namespace PixiEditor.Helpers.Behaviours
         // Converts number to proper format if enter is clicked and moves focus to next object
         // Converts number to proper format if enter is clicked and moves focus to next object
         private void AssociatedObject_KeyUp(object sender, KeyEventArgs e)
         private void AssociatedObject_KeyUp(object sender, KeyEventArgs e)
         {
         {
-            if (e.Key != Key.Enter)
-            {
+            if (e.Key != Key.Enter || !ConfirmOnEnter)
                 return;
                 return;
-            }
 
 
             RemoveFocus();
             RemoveFocus();
         }
         }
 
 
         private void RemoveFocus()
         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)
-            {
-                parent = (FrameworkElement)parent.Parent;
-            }
-
-            FocusManager.SetFocusedElement(scope, parent);
+            MainWindow.Current.mainGrid.Focus();
         }
         }
 
 
         private void AssociatedObjectGotKeyboardFocus(
         private void AssociatedObjectGotKeyboardFocus(
             object sender,
             object sender,
             KeyboardFocusChangedEventArgs e)
             KeyboardFocusChangedEventArgs e)
         {
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick || e.KeyboardDevice.IsKeyDown(Key.Tab))
                 AssociatedObject.SelectAll();
                 AssociatedObject.SelectAll();
         }
         }
 
 
@@ -92,12 +91,22 @@ namespace PixiEditor.Helpers.Behaviours
             object sender,
             object sender,
             MouseEventArgs e)
             MouseEventArgs e)
         {
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick)
                 AssociatedObject.SelectAll();
                 AssociatedObject.SelectAll();
         }
         }
 
 
+        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
+        {
+            if (DeselectOnFocusLoss)
+                AssociatedObject.Select(0, 0);
+            RemoveFocus();
+        }
+
         private void AssociatedObjectPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
         private void AssociatedObjectPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
         {
         {
+            if (!SelectOnMouseClick)
+                return;
+
             if (!AssociatedObject.IsKeyboardFocusWithin)
             if (!AssociatedObject.IsKeyboardFocusWithin)
             {
             {
                 AssociatedObject.Focus();
                 AssociatedObject.Focus();

+ 0 - 15
PixiEditor/Helpers/Converters/BoolToBrushConverter.cs

@@ -1,15 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace PixiEditor.Helpers.Converters
-{
-    public class BoolToBrushConverter
-        : SingleInstanceConverter<BoolToBrushConverter>
-    {
-        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            BrushTuple tuple = (BrushTuple)parameter;
-            return (bool)value ? tuple.FirstBrush : tuple.SecondBrush;
-        }
-    }
-}

+ 0 - 34
PixiEditor/Helpers/Converters/BrushTuple.cs

@@ -1,34 +0,0 @@
-using System;
-using System.Runtime.CompilerServices;
-using System.Windows.Media;
-
-namespace PixiEditor.Helpers.Converters
-{
-    public class BrushTuple : NotifyableObject, ITuple
-    {
-        public object this[int index] => index switch
-        {
-            0 => FirstBrush,
-            1 => SecondBrush,
-            _ => throw new ArgumentOutOfRangeException(nameof(index))
-        };
-
-        private Brush item1;
-
-        public Brush FirstBrush
-        {
-            get => item1;
-            set => SetProperty(ref item1, value);
-        }
-
-        private Brush item2;
-
-        public Brush SecondBrush
-        {
-            get => item2;
-            set => SetProperty(ref item2, value);
-        }
-
-        public int Length => 2;
-    }
-}

+ 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)
         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;
             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.Globalization;
 using System.Windows.Media;
 using System.Windows.Media;
+using PixiEditor.Models.Enums;
 
 
 namespace PixiEditor.Helpers.Converters
 namespace PixiEditor.Helpers.Converters
 {
 {
     public class FileExtensionToColorConverter :
     public class FileExtensionToColorConverter :
         SingleInstanceConverter<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)
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
         {
             string extension = (string)value;
             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)
         private static SolidColorBrush ColorBrush(byte r, byte g, byte b)

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

@@ -1,27 +0,0 @@
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.Layers;
-using PixiEditor.ViewModels;
-using System;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-using System.Windows.Markup;
-
-namespace PixiEditor.Helpers.Converters
-{
-    public class FinalIsVisibleToVisiblityConverter
-        : SingleInstanceMultiValueConverter<FinalIsVisibleToVisiblityConverter>
-    {
-        public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
-        {
-            BitmapManager bitmapManager = ViewModelMain.Current?.BitmapManager;
-
-            return
-                (values[0] is not Layer layer ||
-                bitmapManager.ActiveDocument is null ||
-                bitmapManager.ActiveDocument.GetFinalLayerIsVisible(layer))
-                    ? Visibility.Visible
-                    : (object)Visibility.Collapsed;
-        }
-    }
-}

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

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

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

@@ -1,27 +0,0 @@
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Layers.Utils;
-using PixiEditor.ViewModels;
-using System;
-using System.Globalization;
-
-namespace PixiEditor.Helpers.Converters
-{
-    public class LayerToFinalOpacityConverter
-        : SingleInstanceMultiValueConverter<LayerToFinalOpacityConverter>
-    {
-        public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
-        {
-            if (values.Length > 0 && values[0] is Layer layer && ViewModelMain.Current?.BitmapManager?.ActiveDocument != null)
-            {
-                return (double)LayerStructureUtils.GetFinalLayerOpacity(layer, ViewModelMain.Current.BitmapManager.ActiveDocument.LayerStructure);
-            }
-
-            return null;
-        }
-
-        public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
-        {
-            return null;
-        }
-    }
-}

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

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

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

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

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

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

+ 63 - 20
PixiEditor/Helpers/CrashHelper.cs

@@ -1,25 +1,76 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+using ByteSizeLib;
+using Hardware.Info;
+using PixiEditor.Models.DataHolders;
+using System;
+using System.Globalization;
 using System.Text;
 using System.Text;
-using System.Threading.Tasks;
 
 
 namespace PixiEditor.Helpers
 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
             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(e.GetType().ToString())
                 .Append(": ")
                 .Append(": ")
-                .Append(e.Message);
+                .AppendLine(e.Message);
             {
             {
                 var innerException = e.InnerException;
                 var innerException = e.InnerException;
                 while (innerException != null)
                 while (innerException != null)
@@ -46,14 +97,6 @@ namespace PixiEditor.Helpers
                     innerException = innerException.InnerException;
                     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 → PixiEditor/Helpers/Extensions/Int32RectHelper.cs

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

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

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

+ 0 - 11
PixiEditor/Helpers/Extensions/PixiParserHelper.cs

@@ -1,11 +0,0 @@
-using PixiEditor.Parser;
-using SkiaSharp;
-
-namespace PixiEditor.Helpers.Extensions
-{
-    public static class PixiParserHelper
-    {
-        public static SKRectI GetRect(this SerializableLayer layer) =>
-            SKRectI.Create(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height);
-    }
-}

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

@@ -0,0 +1,13 @@
+using SkiaSharp;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class SKRectIHelper
+    {
+        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;
+        }
+    }
+}

+ 0 - 15
PixiEditor/Helpers/Validators/SizeValidationRule.cs

@@ -1,15 +0,0 @@
-using System.Globalization;
-using System.Windows.Controls;
-
-namespace PixiEditor.Helpers.Validators
-{
-    public class SizeValidationRule : ValidationRule
-    {
-        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
-        {
-            int i = int.Parse(((string)value).Split(' ')[0]);
-
-            return new ValidationResult(i > 0, null); // Size is greater than 0
-        }
-    }
-}

+ 14 - 0
PixiEditor/Models/Constants.cs

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

+ 0 - 21
PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs

@@ -1,21 +0,0 @@
-using PixiEditor.Models.DataHolders;
-using System;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class BitmapChangedEventArgs : EventArgs
-    {
-        public BitmapChangedEventArgs(BitmapPixelChanges pixelsChanged, BitmapPixelChanges oldPixelsValues, Guid changedLayerGuid)
-        {
-            PixelsChanged = pixelsChanged;
-            OldPixelsValues = oldPixelsValues;
-            ChangedLayerGuid = changedLayerGuid;
-        }
-
-        public BitmapPixelChanges PixelsChanged { get; set; }
-
-        public BitmapPixelChanges OldPixelsValues { get; set; }
-
-        public Guid ChangedLayerGuid { get; set; }
-    }
-}

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

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

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

@@ -13,7 +13,7 @@ namespace PixiEditor.Models.Controllers
 {
 {
     public class BitmapOperationsUtility
     public class BitmapOperationsUtility
     {
     {
-        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
+        public event EventHandler BitmapChanged;
 
 
         public BitmapManager Manager { get; set; }
         public BitmapManager Manager { get; set; }
 
 

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

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

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

@@ -1,5 +1,10 @@
-using System.Collections.ObjectModel;
+using PixiEditor.Models.Tools;
+using PixiEditor.Models.Tools.Tools;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Linq;
+using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Input;
 
 
 namespace PixiEditor.Models.Controllers.Shortcuts
 namespace PixiEditor.Models.Controllers.Shortcuts
@@ -11,15 +16,57 @@ namespace PixiEditor.Models.Controllers.Shortcuts
             ShortcutGroups = new ObservableCollection<ShortcutGroup>(shortcutGroups);
             ShortcutGroups = new ObservableCollection<ShortcutGroup>(shortcutGroups);
         }
         }
 
 
-        public static bool BlockShortcutExecution { get; set; }
+        public static bool ShortcutExecutionBlocked => _shortcutExecutionBlockers.Count > 0;
+
+        private static List<string> _shortcutExecutionBlockers = new List<string>();
 
 
         public ObservableCollection<ShortcutGroup> ShortcutGroups { get; init; }
         public ObservableCollection<ShortcutGroup> ShortcutGroups { get; init; }
 
 
         public Shortcut LastShortcut { get; private set; }
         public Shortcut LastShortcut { get; private set; }
 
 
+        public Dictionary<Key, Tool> TransientShortcuts { get; set; } = new Dictionary<Key, Tool>();
+
+        public static void BlockShortcutExection(string blocker)
+        {
+            if (_shortcutExecutionBlockers.Contains(blocker)) return;
+            _shortcutExecutionBlockers.Add(blocker);
+        }
+
+        public static void UnblockShortcutExecution(string blocker)
+        {
+            if (!_shortcutExecutionBlockers.Contains(blocker)) return;
+            _shortcutExecutionBlockers.Remove(blocker);
+        }
+
+        public static void UnblockShortcutExecutionAll()
+        {
+            _shortcutExecutionBlockers.Clear();
+        }
+
+        public Shortcut GetToolShortcut<T>()
+        {
+            return GetToolShortcut(typeof(T));
+        }
+
+        public Shortcut GetToolShortcut(Type type)
+        {
+            return ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().Where(i => i.CommandParameter is Type nextType && nextType == type).SingleOrDefault();
+        }
+
+        public Key GetToolShortcutKey<T>()
+        {
+            return GetToolShortcutKey(typeof(T));
+        }
+
+        public Key GetToolShortcutKey(Type type)
+        {
+            var sh = GetToolShortcut(type);
+            return sh != null ? sh.ShortcutKey : Key.None;
+        }
+
         public void KeyPressed(Key key, ModifierKeys modifiers)
         public void KeyPressed(Key key, ModifierKeys modifiers)
         {
         {
-            if (!BlockShortcutExecution)
+            if (!ShortcutExecutionBlocked)
             {
             {
                 Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
                 Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
                 if (shortcuts.Length < 1)
                 if (shortcuts.Length < 1)

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

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

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

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

+ 1 - 23
PixiEditor/Models/DataHolders/BitmapPixelChanges.cs

@@ -1,5 +1,4 @@
-using PixiEditor.Exceptions;
-using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using SkiaSharp;
 using SkiaSharp;
 using System;
 using System;
@@ -69,27 +68,6 @@ namespace PixiEditor.Models.DataHolders
             return CombineOverride(new[] { changes1, changes2 });
             return CombineOverride(new[] { changes1, changes2 });
         }
         }
 
 
-        /// <summary>
-        ///     Builds BitmapPixelChanges using 2 same-length enumerables of coordinates and colors.
-        /// </summary>
-        public static BitmapPixelChanges FromArrays(IEnumerable<Coordinates> coordinates, IEnumerable<SKColor> color)
-        {
-            Coordinates[] coordinateArray = coordinates.ToArray();
-            SKColor[] colorArray = color.ToArray();
-            if (coordinateArray.Length != colorArray.Length)
-            {
-                throw new ArrayLengthMismatchException();
-            }
-
-            Dictionary<Coordinates, SKColor> dict = new Dictionary<Coordinates, SKColor>();
-            for (int i = 0; i < coordinateArray.Length; i++)
-            {
-                dict.Add(coordinateArray[i], colorArray[i]);
-            }
-
-            return new BitmapPixelChanges(dict);
-        }
-
         public BitmapPixelChanges WithoutTransparentPixels()
         public BitmapPixelChanges WithoutTransparentPixels()
         {
         {
             return new BitmapPixelChanges(ChangedPixels.Where(x => x.Value.Alpha > 0).ToDictionary(y => y.Key, y => y.Value));
             return new BitmapPixelChanges(ChangedPixels.Where(x => x.Value.Alpha > 0).ToDictionary(y => y.Key, y => y.Value));

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

@@ -0,0 +1,194 @@
+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 List<Document> RecoverDocuments()
+        {
+            List<Document> documents = new();
+            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;
+                }
+
+                documents.Add(document);
+            }
+
+            return documents;
+        }
+
+        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".Replace(':', '_');
+
+                    byte[] serialized = PixiParser.Serialize(document.ToSerializable());
+
+                    using Stream documentStream = archive.CreateEntry($"Documents/{documentPath}").Open();
+                    documentStream.Write(serialized);
+                }
+                catch { }
+            }
+        }
+
+        private void ExtractReport()
+        {
+            ZipArchiveEntry entry = ZipFile.GetEntry("report.txt");
+            using Stream stream = entry.Open();
+
+            byte[] encodedReport = new byte[entry.Length];
+            stream.Read(encodedReport);
+
+            ReportText = Encoding.UTF8.GetString(encodedReport);
+        }
+
+        public class CrashReportUserMessage
+        {
+            public string Message { get; set; }
+
+            public string Mail { get; set; }
+        }
+    }
+}

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

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

+ 40 - 19
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -12,6 +12,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows;
+using Windows.Graphics;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
@@ -221,9 +222,7 @@ namespace PixiEditor.Models.DataHolders
             if (width <= 0 || height <= 0)
             if (width <= 0 || height <= 0)
                 throw new ArgumentException("Dimensions must be greater than 0");
                 throw new ArgumentException("Dimensions must be greater than 0");
 
 
-            layer = bitmap == null ? new Layer(name, width, height) : new Layer(name, bitmap);
-            layer.MaxHeight = Height;
-            layer.MaxWidth = Width;
+            layer = bitmap == null ? new Layer(name, width, height, Width, Height) : new Layer(name, bitmap, Width, Height);
 
 
             Layers.Add(layer);
             Layers.Add(layer);
 
 
@@ -441,7 +440,7 @@ namespace PixiEditor.Models.DataHolders
 
 
             var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].GuidValue);
             var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].GuidValue);
 
 
-            Layer placeholderLayer = new("_placeholder");
+            Layer placeholderLayer = new("_placeholder", Width, Height);
             Layers.Insert(index, placeholderLayer);
             Layers.Insert(index, placeholderLayer);
             LayerStructure.AssignParent(placeholderLayer.GuidValue, groupParent?.GroupGuid);
             LayerStructure.AssignParent(placeholderLayer.GuidValue, groupParent?.GroupGuid);
 
 
@@ -449,6 +448,8 @@ namespace PixiEditor.Models.DataHolders
             {
             {
                 Layer firstLayer = mergedLayer;
                 Layer firstLayer = mergedLayer;
                 Layer secondLayer = layersToMerge[i + 1];
                 Layer secondLayer = layersToMerge[i + 1];
+                firstLayer.ClipCanvas();
+                secondLayer.ClipCanvas();
                 mergedLayer = firstLayer.MergeWith(secondLayer, name, Width, Height);
                 mergedLayer = firstLayer.MergeWith(secondLayer, name, Width, Height);
                 RemoveLayer(layersToMerge[i], false);
                 RemoveLayer(layersToMerge[i], false);
             }
             }
@@ -471,7 +472,7 @@ namespace PixiEditor.Models.DataHolders
                 throw new ArgumentException("Not enough layers were provided to merge. Minimum amount is 2");
                 throw new ArgumentException("Not enough layers were provided to merge. Minimum amount is 2");
             }
             }
 
 
-            IEnumerable<Layer> undoArgs = layersToMerge;
+            Layer[] undoArgs = layersToMerge;
 
 
             var oldLayerStructure = LayerStructure.CloneGroups();
             var oldLayerStructure = LayerStructure.CloneGroups();
 
 
@@ -491,6 +492,8 @@ namespace PixiEditor.Models.DataHolders
 
 
             UndoManager.SquashUndoChanges(2, "Undo merge layers", false);
             UndoManager.SquashUndoChanges(2, "Undo merge layers", false);
 
 
+            LayersChanged?.Invoke(this, new LayersChangedEventArgs(layer.GuidValue, LayerAction.Add));
+
             return layer;
             return layer;
         }
         }
 
 
@@ -534,13 +537,13 @@ namespace PixiEditor.Models.DataHolders
 
 
             var startGroup = LayerStructure.GetGroupByLayer(layerGuid);
             var startGroup = LayerStructure.GetGroupByLayer(layerGuid);
 
 
-            LayerStructure.PreMoveReassignBounds(new GroupData(startGroup?.GroupGuid), layerGuid);
+            LayerStructure.Unassign(new GroupData(startGroup?.GroupGuid), layerGuid);
 
 
             Layers.Move(Layers.IndexOf(Layers.First(x => x.GuidValue == layerGuid)), indexTo);
             Layers.Move(Layers.IndexOf(Layers.First(x => x.GuidValue == layerGuid)), indexTo);
 
 
             var newGroup = LayerStructure.GetGroupByLayer(layerAtOldIndex);
             var newGroup = LayerStructure.GetGroupByLayer(layerAtOldIndex);
 
 
-            LayerStructure.PostMoveReassignBounds(new GroupData(newGroup?.GroupGuid), layerGuid);
+            LayerStructure.Assign(new GroupData(newGroup?.GroupGuid), layerGuid);
 
 
             RaisePropertyChanged(nameof(LayerStructure));
             RaisePropertyChanged(nameof(LayerStructure));
         }
         }
@@ -590,6 +593,7 @@ namespace PixiEditor.Models.DataHolders
 
 
                 Layer layer = MergeLayers(layers, nameOfSecond, indexes[0]);
                 Layer layer = MergeLayers(layers, nameOfSecond, indexes[0]);
                 layer.ChangeGuid(mergedLayerGuid);
                 layer.ChangeGuid(mergedLayerGuid);
+                SetMainActiveLayer(Layers.IndexOf(layer));
             }
             }
         }
         }
 
 
@@ -602,7 +606,7 @@ namespace PixiEditor.Models.DataHolders
                 for (int i = 0; i < layers.Length; i++)
                 for (int i = 0; i < layers.Length; i++)
                 {
                 {
                     Layer layer = layers[i];
                     Layer layer = layers[i];
-                    layer.IsActive = true;
+                    layer.IsActive = data[i].IsActive;
                     Layers.Insert(data[i].LayerIndex, layer);
                     Layers.Insert(data[i].LayerIndex, layer);
                 }
                 }
 
 
@@ -614,20 +618,36 @@ namespace PixiEditor.Models.DataHolders
         /// <summary>
         /// <summary>
         ///     Moves offsets of layers by specified vector.
         ///     Moves offsets of layers by specified vector.
         /// </summary>
         /// </summary>
-        private void MoveOffsets(IEnumerable<Layer> layers, Coordinates moveVector)
+        private void MoveOffsets(IList<Layer> layers, IList<Int32Rect> bounds, Coordinates moveVector)
         {
         {
-            foreach (Layer layer in layers)
+            for (int i = 0; i < layers.Count; i++)
             {
             {
+                Layer layer = layers[i];
+                Int32Rect bound = bounds[i];
                 Thickness offset = layer.Offset;
                 Thickness offset = layer.Offset;
                 layer.Offset = new Thickness(offset.Left + moveVector.X, offset.Top + moveVector.Y, 0, 0);
                 layer.Offset = new Thickness(offset.Left + moveVector.X, offset.Top + moveVector.Y, 0, 0);
+                if (!bound.IsEmpty && layer.Bounds != bound)
+                {
+                    layer.DynamicResizeAbsolute(bound);
+                }
+                else
+                {
+                    layer.ClipCanvas();
+                }
             }
             }
         }
         }
 
 
         private void MoveOffsetsProcess(object[] arguments)
         private void MoveOffsetsProcess(object[] arguments)
         {
         {
-            if (arguments.Length > 0 && arguments[0] is IEnumerable<Layer> layers && arguments[1] is Coordinates vector)
+            if (arguments.Length > 0 && arguments[0] is List<Guid> guids && arguments[1] is List<Int32Rect> bounds && arguments[2] is Coordinates vector)
             {
             {
-                MoveOffsets(layers, vector);
+                List<Layer> layers = new List<Layer>(guids.Count);
+                foreach (Guid guid in guids)
+                {
+                    layers.Add(Layers.First(x => x.GuidValue == guid));
+                }
+
+                MoveOffsets(layers, bounds, vector);
             }
             }
             else
             else
             {
             {
@@ -662,7 +682,7 @@ namespace PixiEditor.Models.DataHolders
 
 
             LayerStructure.ReassignParent(group, referenceLayerGroup);
             LayerStructure.ReassignParent(group, referenceLayerGroup);
 
 
-            LayerStructure.PostMoveReassignBounds(new GroupData(group?.Parent?.GroupGuid), new GroupData(group?.GroupGuid));
+            LayerStructure.Assign(new GroupData(group?.Parent?.GroupGuid), new GroupData(group?.GroupGuid));
         }
         }
 
 
         private int CalculateNewIndex(int layerIndex, bool above, int oldIndex)
         private int CalculateNewIndex(int layerIndex, bool above, int oldIndex)
@@ -695,13 +715,13 @@ namespace PixiEditor.Models.DataHolders
 
 
             var startGroup = LayerStructure.GetGroupByLayer(layer);
             var startGroup = LayerStructure.GetGroupByLayer(layer);
 
 
-            LayerStructure.PreMoveReassignBounds(new GroupData(startGroup?.GroupGuid), layer);
+            LayerStructure.Unassign(new GroupData(startGroup?.GroupGuid), layer);
 
 
             Layers.Move(oldIndex, newIndex);
             Layers.Move(oldIndex, newIndex);
 
 
             var newFolder = LayerStructure.GetGroupByLayer(referenceLayer);
             var newFolder = LayerStructure.GetGroupByLayer(referenceLayer);
 
 
-            LayerStructure.PostMoveReassignBounds(new GroupData(newFolder?.GroupGuid), layer);
+            LayerStructure.Assign(new GroupData(newFolder?.GroupGuid), layer);
 
 
             if (Layers.IndexOf(ActiveLayer) == oldIndex)
             if (Layers.IndexOf(ActiveLayer) == oldIndex)
             {
             {
@@ -712,7 +732,7 @@ namespace PixiEditor.Models.DataHolders
             Renderer.ForceRerender();
             Renderer.ForceRerender();
         }
         }
 
 
-        private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
+        public void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
         {
         {
             for (int i = 0; i < layers.Length; i++)
             for (int i = 0; i < layers.Length; i++)
             {
             {
@@ -726,7 +746,7 @@ namespace PixiEditor.Models.DataHolders
             }
             }
         }
         }
 
 
-        private void RemoveLayerProcess(object[] parameters)
+        public void RemoveLayerProcess(object[] parameters)
         {
         {
             if (parameters is { Length: > 0 } && parameters[0] is Guid layerGuid)
             if (parameters is { Length: > 0 } && parameters[0] is Guid layerGuid)
             {
             {
@@ -740,8 +760,9 @@ namespace PixiEditor.Models.DataHolders
 
 
                 if (layerGroup?.Parent != null && LayerStructure.GroupContainsOnlyLayer(layer.GuidValue, layerGroup))
                 if (layerGroup?.Parent != null && LayerStructure.GroupContainsOnlyLayer(layer.GuidValue, layerGroup))
                 {
                 {
-                    LayerStructure.PreMoveReassignBounds(new GroupData(layerGroup.Parent.GroupGuid), new GroupData(layerGroup.GroupGuid));
+                    LayerStructure.Unassign(new GroupData(layerGroup.Parent.GroupGuid), new GroupData(layerGroup.GroupGuid));
                 }
                 }
+
                 LayerStructure.AssignParent(Layers[index].GuidValue, null);
                 LayerStructure.AssignParent(Layers[index].GuidValue, null);
                 RemoveGroupsIfEmpty(layer, layerGroup);
                 RemoveGroupsIfEmpty(layer, layerGroup);
 
 
@@ -837,7 +858,7 @@ namespace PixiEditor.Models.DataHolders
             return sucess;
             return sucess;
         }
         }
 
 
-        private void RemoveLayersProcess(object[] parameters)
+        public void RemoveLayersProcess(object[] parameters)
         {
         {
             if (parameters != null && parameters.Length > 0 && parameters[0] is IEnumerable<Guid> layerGuids)
             if (parameters != null && parameters.Length > 0 && parameters[0] is IEnumerable<Guid> layerGuids)
             {
             {

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

@@ -23,7 +23,7 @@ namespace PixiEditor.Models.DataHolders
         ///     Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
         ///     Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
         ///     vertical.
         ///     vertical.
         /// </param>
         /// </param>
-        public void ResizeCanvas(int width, int height, AnchorPoint anchor)
+        public void ResizeCanvas(int width, int height, AnchorPoint anchor, bool addToUndo = true)
         {
         {
             int oldWidth = Width;
             int oldWidth = Width;
             int oldHeight = Height;
             int oldHeight = Height;
@@ -31,20 +31,31 @@ namespace PixiEditor.Models.DataHolders
             int offsetX = GetOffsetXForAnchor(Width, width, anchor);
             int offsetX = GetOffsetXForAnchor(Width, width, anchor);
             int offsetY = GetOffsetYForAnchor(Height, height, 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))
             Thickness[] newOffsets = Layers.Select(x => new Thickness(offsetX + x.OffsetX, offsetY + x.OffsetY, 0, 0))
                 .ToArray();
                 .ToArray();
 
 
             object[] processArgs = { newOffsets, width, height };
             object[] processArgs = { newOffsets, width, height };
-            object[] reverseProcessArgs = { oldOffsets, Width, Height };
+            object[] reverseProcessArgs = { Width, Height };
+
+            if (addToUndo) 
+            { 
+                StorageBasedChange change = new(this, Layers);
+                ResizeCanvas(newOffsets, width, height);
+
+                UndoManager.AddUndoChange(change.ToChange(
+                    RestoreDocumentLayersProcess,
+                    reverseProcessArgs,
+                    ResizeCanvasProcess,
+                    processArgs,
+                    "Resize canvas"));
+            }
+            else
+            {
+                ResizeCanvas(newOffsets, width, height);
+            }
+
+            if (oldWidth == Width && Height == oldHeight) return;
 
 
-            ResizeCanvas(newOffsets, width, height);
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
-                reverseProcessArgs,
-                ResizeCanvasProcess,
-                processArgs,
-                "Resize canvas"));
             DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
             DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
         }
         }
 
 
@@ -137,7 +148,7 @@ namespace PixiEditor.Models.DataHolders
                         int diff = documentCenter.X - newOffsetX;
                         int diff = documentCenter.X - newOffsetX;
                         newOffsetX = layer.OffsetX + (diff * 2);
                         newOffsetX = layer.OffsetX + (diff * 2);
                     }
                     }
-                    else if(flip == FlipType.Vertical)
+                    else if (flip == FlipType.Vertical)
                     {
                     {
                         newOffsetY += layerCenter.Y;
                         newOffsetY += layerCenter.Y;
                         int diff = documentCenter.Y - newOffsetY;
                         int diff = documentCenter.Y - newOffsetY;
@@ -205,8 +216,13 @@ namespace PixiEditor.Models.DataHolders
 
 
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         {
         {
+            int oldWidth = Width;
+            int oldHeight = Height;
             Width = (int)args[0];
             Width = (int)args[0];
             Height = (int)args[1];
             Height = (int)args[1];
+            DocumentSizeChanged?.Invoke(
+                this,
+                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, Width, Height));
             Layers.Clear();
             Layers.Clear();
             Layers.AddRange(layers);
             Layers.AddRange(layers);
         }
         }
@@ -219,11 +235,32 @@ namespace PixiEditor.Models.DataHolders
         /// <param name="newHeight">New canvas height.</param>
         /// <param name="newHeight">New canvas height.</param>
         private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
         private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
         {
         {
+            Int32Rect newCanvasRect = new(0, 0, newWidth, newHeight);
             for (int i = 0; i < Layers.Count; i++)
             for (int i = 0; i < Layers.Count; i++)
             {
             {
-                Layers[i].Offset = offset[i];
+                Layer layer = Layers[i];
                 Layers[i].MaxWidth = newWidth;
                 Layers[i].MaxWidth = newWidth;
                 Layers[i].MaxHeight = newHeight;
                 Layers[i].MaxHeight = newHeight;
+                if (layer.IsReset)
+                    continue;
+
+                Thickness newOffset = offset[i];
+                Int32Rect newRect = new((int)newOffset.Left, (int)newOffset.Top, layer.Width, layer.Height);
+                Int32Rect newLayerRect = newRect.Intersect(newCanvasRect);
+                if (!newLayerRect.HasArea)
+                {
+                    layer.Reset();
+                    continue;
+                }
+                Surface newBitmap = new(newLayerRect.Width, newLayerRect.Height);
+                var oldBitmap = layer.LayerBitmap;
+                using var snapshot = oldBitmap.SkiaSurface.Snapshot();
+                newBitmap.SkiaSurface.Canvas.DrawImage(snapshot, newRect.X - newLayerRect.X, newRect.Y - newLayerRect.Y, Surface.ReplacingPaint);
+
+                layer.LayerBitmap = newBitmap;
+                oldBitmap.Dispose();
+
+                Layers[i].Offset = new Thickness(newLayerRect.X, newLayerRect.Y, 0, 0);
             }
             }
 
 
             Width = newWidth;
             Width = newWidth;

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

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

+ 81 - 25
PixiEditor/Models/DataHolders/Document/Document.cs

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

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

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

+ 598 - 577
PixiEditor/Models/DataHolders/RangeObservableCollection.cs

@@ -9,658 +9,679 @@ using System.Linq;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
-  // Licensed to the .NET Foundation under one or more agreements.
-  // The .NET Foundation licenses this file to you under the MIT license.
-  // See the LICENSE file in the project root for more information.
-  /// <summary>
-  /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
-  /// implementing INotifyCollectionChanged to notify listeners
-  /// when items get added, removed or the whole list is refreshed.
-  /// </summary>
-  public class RangeObservableCollection<T> : ObservableCollection<T>
-  {
-    //------------------------------------------------------
-    //
-    //  Private Fields
-    //
-    //------------------------------------------------------
-
-    #region Private Fields    
-    [NonSerialized]
-    private DeferredEventsCollection? _deferredEvents;
-    #endregion Private Fields
-
-
-    //------------------------------------------------------
-    //
-    //  Constructors
-    //
-    //------------------------------------------------------
-
-    #region Constructors
-    /// <summary>
-    /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
-    /// </summary>
-    public RangeObservableCollection() { }
+    // Licensed to the .NET Foundation under one or more agreements.
+    // The .NET Foundation licenses this file to you under the MIT license.
+    // See the LICENSE file in the project root for more information.
 
 
     /// <summary>
     /// <summary>
-    /// Initializes a new instance of the ObservableCollection class that contains
-    /// elements copied from the specified collection and has sufficient capacity
-    /// to accommodate the number of elements copied.
+    /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
+    /// implementing INotifyCollectionChanged to notify listeners
+    /// when items get added, removed or the whole list is refreshed.
     /// </summary>
     /// </summary>
-    /// <param name="collection">The collection whose elements are copied to the new list.</param>
-    /// <remarks>
-    /// The elements are copied onto the ObservableCollection in the
-    /// same order they are read by the enumerator of the collection.
-    /// </remarks>
-    /// <exception cref="ArgumentNullException"> collection is a null reference </exception>
-    public RangeObservableCollection(IEnumerable<T> collection) : base(collection) { }
+    public class RangeObservableCollection<T> : ObservableCollection<T>
+    {
+        //------------------------------------------------------
+        //
+        //  Private Fields
+        //
+        //------------------------------------------------------
+
+        #region Private Fields    
+        [NonSerialized]
+        private DeferredEventsCollection _deferredEvents;
+        #endregion Private Fields
+
+
+        //------------------------------------------------------
+        //
+        //  Constructors
+        //
+        //------------------------------------------------------
+
+        #region Constructors
+
+        /// <summary>
+        /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
+        /// </summary>
+        public RangeObservableCollection() { }
+
+        /// <summary>
+        /// Initializes a new instance of the ObservableCollection class that contains
+        /// elements copied from the specified collection and has sufficient capacity
+        /// to accommodate the number of elements copied.
+        /// </summary>
+        /// <param name="collection">The collection whose elements are copied to the new list.</param>
+        /// <remarks>
+        /// The elements are copied onto the ObservableCollection in the
+        /// same order they are read by the enumerator of the collection.
+        /// </remarks>
+        /// <exception cref="ArgumentNullException"> collection is a null reference </exception>
+        public RangeObservableCollection(IEnumerable<T> collection) : base(collection) { }
+
+        /// <summary>
+        /// Initializes a new instance of the ObservableCollection class
+        /// that contains elements copied from the specified list
+        /// </summary>
+        /// <param name="list">The list whose elements are copied to the new list.</param>
+        /// <remarks>
+        /// The elements are copied onto the ObservableCollection in the
+        /// same order they are read by the enumerator of the list.
+        /// </remarks>
+        /// <exception cref="ArgumentNullException"> list is a null reference </exception>
+        public RangeObservableCollection(List<T> list) : base(list) { }
+
+        #endregion Constructors
+
+        //------------------------------------------------------
+        //
+        //  Public Properties
+        //
+        //------------------------------------------------------
+
+        #region Public Properties
+#pragma warning disable SA1306 // Field names should begin with lower-case letter
+        EqualityComparer<T> _Comparer;
+#pragma warning restore SA1306 // Field names should begin with lower-case letter
+        public EqualityComparer<T> Comparer
+        {
+            get => _Comparer ??= EqualityComparer<T>.Default;
+            private set => _Comparer = value;
+        }
 
 
-    /// <summary>
-    /// Initializes a new instance of the ObservableCollection class
-    /// that contains elements copied from the specified list
-    /// </summary>
-    /// <param name="list">The list whose elements are copied to the new list.</param>
-    /// <remarks>
-    /// The elements are copied onto the ObservableCollection in the
-    /// same order they are read by the enumerator of the list.
-    /// </remarks>
-    /// <exception cref="ArgumentNullException"> list is a null reference </exception>
-    public RangeObservableCollection(List<T> list) : base(list) { }
+        /// <summary>
+        /// Gets or sets a value indicating whether this collection acts as a <see cref="HashSet{T}"/>,
+        /// disallowing duplicate items, based on <see cref="Comparer"/>.
+        /// This might indeed consume background performance, but in the other hand,
+        /// it will pay off in UI performance as less required UI updates are required.
+        /// </summary>
+        public bool AllowDuplicates { get; set; } = true;
+
+        #endregion Public Properties
+
+        //------------------------------------------------------
+        //
+        //  Public Methods
+        //
+        //------------------------------------------------------
+
+        #region Public Methods
+
+        /// <summary>
+        /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
+        /// </summary>
+        /// <param name="collection">
+        /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
+        /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+        /// </param>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void AddRange(IEnumerable<T> collection)
+        {
+            InsertRange(Count, collection);
+        }
 
 
-    #endregion Constructors
+        /// <summary>
+        /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
+        /// </summary>
+        /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
+        /// <param name="collection">The collection whose elements should be inserted into the List<T>.
+        /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
+        public void InsertRange(int index, IEnumerable<T> collection)
+        {
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (index > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+
+            if (!AllowDuplicates)
+            {
+                collection =
+                  collection
+                  .Distinct(Comparer)
+                  .Where(item => !Items.Contains(item, Comparer))
+                  .ToList();
+            }
+
+            if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                    return;
+            }
+            else if (!collection.Any())
+            {
+                return;
+            }
 
 
-    //------------------------------------------------------
-    //
-    //  Public Properties
-    //
-    //------------------------------------------------------
+            CheckReentrancy();
 
 
-    #region Public Properties
-    EqualityComparer<T>? _Comparer;
-    public EqualityComparer<T> Comparer
-    {
-      get => _Comparer ??= EqualityComparer<T>.Default;
-      private set => _Comparer = value;
-    }
+            //expand the following couple of lines when adding more constructors.
+            var target = (List<T>)Items;
+            target.InsertRange(index, collection);
 
 
-    /// <summary>
-    /// Gets or sets a value indicating whether this collection acts as a <see cref="HashSet{T}"/>,
-    /// disallowing duplicate items, based on <see cref="Comparer"/>.
-    /// This might indeed consume background performance, but in the other hand,
-    /// it will pay off in UI performance as less required UI updates are required.
-    /// </summary>
-    public bool AllowDuplicates { get; set; } = true;
+            OnEssentialPropertiesChanged();
 
 
-    #endregion Public Properties
+            if (!(collection is IList list))
+                list = new List<T>(collection);
 
 
-    //------------------------------------------------------
-    //
-    //  Public Methods
-    //
-    //------------------------------------------------------
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
+        }
 
 
-    #region Public Methods
 
 
-    /// <summary>
-    /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
-    /// </summary>
-    /// <param name="collection">
-    /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
-    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
-    /// </param>
-    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
-    public void AddRange(IEnumerable<T> collection)
-    {
-      InsertRange(Count, collection);
-    }
+        /// <summary> 
+        /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
+        /// </summary>
+        /// <param name="collection">The items to remove.</param>        
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void RemoveRange(IEnumerable<T> collection)
+        {
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
 
 
-    /// <summary>
-    /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
-    /// </summary>
-    /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
-    /// <param name="collection">The collection whose elements should be inserted into the List<T>.
-    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
-    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
-    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
-    public void InsertRange(int index, IEnumerable<T> collection)
-    {
-      if (collection == null)
-        throw new ArgumentNullException(nameof(collection));
-      if (index < 0)
-        throw new ArgumentOutOfRangeException(nameof(index));
-      if (index > Count)
-        throw new ArgumentOutOfRangeException(nameof(index));
-
-      if (!AllowDuplicates)
-        collection =
-          collection
-          .Distinct(Comparer)
-          .Where(item => !Items.Contains(item, Comparer))
-          .ToList();
-
-      if (collection is ICollection<T> countable)
-      {
-        if (countable.Count == 0)
-          return;
-      }
-      else if (!collection.Any())
-        return;
-
-      CheckReentrancy();
-
-      //expand the following couple of lines when adding more constructors.
-      var target = (List<T>)Items;
-      target.InsertRange(index, collection);
-
-      OnEssentialPropertiesChanged();
-
-      if (!(collection is IList list))
-        list = new List<T>(collection);
-
-      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
-    }
+            if (Count == 0)
+            {
+                return;
+            }
+            else if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                {
+                    return;
+                }
+                else if (countable.Count == 1)
+                {
+                    using (IEnumerator<T> enumerator = countable.GetEnumerator())
+                    {
+                        enumerator.MoveNext();
+                        Remove(enumerator.Current);
+                        return;
+                    }
+                }
+            }
+            else if (!collection.Any())
+            {
+                return;
+            }
 
 
+            CheckReentrancy();
 
 
-    /// <summary> 
-    /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
-    /// </summary>
-    /// <param name="collection">The items to remove.</param>        
-    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
-    public void RemoveRange(IEnumerable<T> collection)
-    {
-      if (collection == null)
-        throw new ArgumentNullException(nameof(collection));
-
-      if (Count == 0)
-        return;
-      else if (collection is ICollection<T> countable)
-      {
-        if (countable.Count == 0)
-          return;
-        else if (countable.Count == 1)
-          using (IEnumerator<T> enumerator = countable.GetEnumerator())
-          {
-            enumerator.MoveNext();
-            Remove(enumerator.Current);
-            return;
-          }
-      }
-      else if (!collection.Any())
-        return;
-
-      CheckReentrancy();
-
-      var clusters = new Dictionary<int, List<T>>();
-      var lastIndex = -1;
-      List<T>? lastCluster = null;
-      foreach (T item in collection)
-      {
-        var index = IndexOf(item);
-        if (index < 0)
-          continue;
-
-        Items.RemoveAt(index);
-
-        if (lastIndex == index && lastCluster != null)
-          lastCluster.Add(item);
-        else
-          clusters[lastIndex = index] = lastCluster = new List<T> { item };
-      }
-
-      OnEssentialPropertiesChanged();
-
-      if (Count == 0)
-        OnCollectionReset();
-      else
-        foreach (KeyValuePair<int, List<T>> cluster in clusters)
-          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
+            var clusters = new Dictionary<int, List<T>>();
+            var lastIndex = -1;
+            List<T> lastCluster = null;
+            foreach (T item in collection)
+            {
+                var index = IndexOf(item);
+                if (index < 0)
+                    continue;
 
 
-    }
+                Items.RemoveAt(index);
 
 
-    /// <summary>
-    /// Iterates over the collection and removes all items that satisfy the specified match.
-    /// </summary>
-    /// <remarks>The complexity is O(n).</remarks>
-    /// <param name="match"></param>
-    /// <returns>Returns the number of elements that where </returns>
-    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
-    public int RemoveAll(Predicate<T> match)
-    {
-      return RemoveAll(0, Count, match);
-    }
+                if (lastIndex == index && lastCluster != null)
+                    lastCluster.Add(item);
+                else
+                    clusters[lastIndex = index] = lastCluster = new List<T> { item };
+            }
 
 
-    /// <summary>
-    /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
-    /// </summary>
-    /// <remarks>The complexity is O(n).</remarks>
-    /// <param name="index">The index of where to start performing the search.</param>
-    /// <param name="count">The number of items to iterate on.</param>
-    /// <param name="match"></param>
-    /// <returns>Returns the number of elements that where </returns>
-    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
-    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
-    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
-    public int RemoveAll(int index, int count, Predicate<T> match)
-    {
-      if (index < 0)
-        throw new ArgumentOutOfRangeException(nameof(index));
-      if (count < 0)
-        throw new ArgumentOutOfRangeException(nameof(count));
-      if (index + count > Count)
-        throw new ArgumentOutOfRangeException(nameof(index));
-      if (match == null)
-        throw new ArgumentNullException(nameof(match));
-
-      if (Count == 0)
-        return 0;
-
-      List<T>? cluster = null;
-      var clusterIndex = -1;
-      var removedCount = 0;
-
-      using (BlockReentrancy())
-      using (DeferEvents())
-      {
-        for (var i = 0; i < count; i++, index++)
-        {
-          T item = Items[index];
-          if (match(item))
-          {
-            Items.RemoveAt(index);
-            removedCount++;
+            OnEssentialPropertiesChanged();
 
 
-            if (clusterIndex == index)
+            if (Count == 0)
             {
             {
-              Debug.Assert(cluster != null);
-              cluster!.Add(item);
+                OnCollectionReset();
             }
             }
             else
             else
             {
             {
-              cluster = new List<T> { item };
-              clusterIndex = index;
+                foreach (KeyValuePair<int, List<T>> cluster in clusters)
+                    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
             }
             }
 
 
-            index--;
-          }
-          else if (clusterIndex > -1)
-          {
-            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
-            clusterIndex = -1;
-            cluster = null;
-          }
         }
         }
 
 
-        if (clusterIndex > -1)
-          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
-      }
+        /// <summary>
+        /// Iterates over the collection and removes all items that satisfy the specified match.
+        /// </summary>
+        /// <remarks>The complexity is O(n).</remarks>
+        /// <returns>Returns the number of elements that where </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+        public int RemoveAll(Predicate<T> match)
+        {
+            return RemoveAll(0, Count, match);
+        }
 
 
-      if (removedCount > 0)
-        OnEssentialPropertiesChanged();
+        /// <summary>
+        /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
+        /// </summary>
+        /// <remarks>The complexity is O(n).</remarks>
+        /// <param name="index">The index of where to start performing the search.</param>
+        /// <param name="count">The number of items to iterate on.</param>
+        /// <returns>Returns the number of elements that where </returns>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+        public int RemoveAll(int index, int count, Predicate<T> match)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (match == null)
+                throw new ArgumentNullException(nameof(match));
+
+            if (Count == 0)
+                return 0;
+
+            List<T> cluster = null;
+            var clusterIndex = -1;
+            var removedCount = 0;
+
+            using (BlockReentrancy())
+            using (DeferEvents())
+            {
+                for (var i = 0; i < count; i++, index++)
+                {
+                    T item = Items[index];
+                    if (match(item))
+                    {
+                        Items.RemoveAt(index);
+                        removedCount++;
+
+                        if (clusterIndex == index)
+                        {
+                            Debug.Assert(cluster != null);
+                            cluster!.Add(item);
+                        }
+                        else
+                        {
+                            cluster = new List<T> { item };
+                            clusterIndex = index;
+                        }
+
+                        index--;
+                    }
+                    else if (clusterIndex > -1)
+                    {
+                        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+                        clusterIndex = -1;
+                        cluster = null;
+                    }
+                }
+
+                if (clusterIndex > -1)
+                    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+            }
 
 
-      return removedCount;
-    }
+            if (removedCount > 0)
+                OnEssentialPropertiesChanged();
 
 
-    /// <summary>
-    /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
-    /// </summary>
-    /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
-    /// <param name="count">The number of elements to remove.</param>
-    /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
-    public void RemoveRange(int index, int count)
-    {
-      if (index < 0)
-        throw new ArgumentOutOfRangeException(nameof(index));
-      if (count < 0)
-        throw new ArgumentOutOfRangeException(nameof(count));
-      if (index + count > Count)
-        throw new ArgumentOutOfRangeException(nameof(index));
+            return removedCount;
+        }
 
 
-      if (count == 0)
-        return;
+        /// <summary>
+        /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
+        /// </summary>
+        /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
+        /// <param name="count">The number of elements to remove.</param>
+        /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
+        public void RemoveRange(int index, int count)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
 
 
-      if (count == 1)
-      {
-        RemoveItem(index);
-        return;
-      }
+            if (count == 0)
+                return;
 
 
-      //Items will always be List<T>, see constructors
-      var items = (List<T>)Items;
-      List<T> removedItems = items.GetRange(index, count);
+            if (count == 1)
+            {
+                RemoveItem(index);
+                return;
+            }
 
 
-      CheckReentrancy();
+            //Items will always be List<T>, see constructors
+            var items = (List<T>)Items;
+            List<T> removedItems = items.GetRange(index, count);
 
 
-      items.RemoveRange(index, count);
+            CheckReentrancy();
 
 
-      OnEssentialPropertiesChanged();
+            items.RemoveRange(index, count);
 
 
-      if (Count == 0)
-        OnCollectionReset();
-      else
-        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
-    }
+            OnEssentialPropertiesChanged();
 
 
-    /// <summary> 
-    /// Clears the current collection and replaces it with the specified collection,
-    /// using <see cref="Comparer"/>.
-    /// </summary>             
-    /// <param name="collection">The items to fill the collection with, after clearing it.</param>
-    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
-    public void ReplaceRange(IEnumerable<T> collection)
-    {
-      ReplaceRange(0, Count, collection);
-    }
+            if (Count == 0)
+                OnCollectionReset();
+            else
+                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
+        }
 
 
-    /// <summary>
-    /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
-    /// </summary>
-    /// <param name="index">The index of where to start the replacement.</param>
-    /// <param name="count">The number of items to be replaced.</param>
-    /// <param name="collection">The collection to insert in that location.</param>
-    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
-    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
-    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
-    /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
-    public void ReplaceRange(int index, int count, IEnumerable<T> collection)
-    {
-      if (index < 0)
-        throw new ArgumentOutOfRangeException(nameof(index));
-      if (count < 0)
-        throw new ArgumentOutOfRangeException(nameof(count));
-      if (index + count > Count)
-        throw new ArgumentOutOfRangeException(nameof(index));
-
-      if (collection == null)
-        throw new ArgumentNullException(nameof(collection));
-
-      if (!AllowDuplicates)
-        collection =
-          collection
-          .Distinct(Comparer)
-          .ToList();
-
-      if (collection is ICollection<T> countable)
-      {
-        if (countable.Count == 0)
+        /// <summary> 
+        /// Clears the current collection and replaces it with the specified collection,
+        /// using <see cref="Comparer"/>.
+        /// </summary>             
+        /// <param name="collection">The items to fill the collection with, after clearing it.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void ReplaceRange(IEnumerable<T> collection)
         {
         {
-          RemoveRange(index, count);
-          return;
+            ReplaceRange(0, Count, collection);
         }
         }
-      }
-      else if (!collection.Any())
-      {
-        RemoveRange(index, count);
-        return;
-      }
-
-      if (index + count == 0)
-      {
-        InsertRange(0, collection);
-        return;
-      }
-
-      if (!(collection is IList<T> list))
-        list = new List<T>(collection);
-
-      using (BlockReentrancy())
-      using (DeferEvents())
-      {
-        var rangeCount = index + count;
-        var addedCount = list.Count;
-
-        var changesMade = false;
-        List<T>?
-          newCluster = null,
-          oldCluster = null;
-
-
-        int i = index;
-        for (; i < rangeCount && i - index < addedCount; i++)
+
+        /// <summary>
+        /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
+        /// </summary>
+        /// <param name="index">The index of where to start the replacement.</param>
+        /// <param name="count">The number of items to be replaced.</param>
+        /// <param name="collection">The collection to insert in that location.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
+        public void ReplaceRange(int index, int count, IEnumerable<T> collection)
         {
         {
-          //parallel position
-          T old = this[i], @new = list[i - index];
-          if (Comparer.Equals(old, @new))
-          {
-            OnRangeReplaced(i, newCluster!, oldCluster!);
-            continue;
-          }
-          else
-          {
-            Items[i] = @new;
-
-            if (newCluster == null)
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
+
+            if (!AllowDuplicates)
             {
             {
-              Debug.Assert(oldCluster == null);
-              newCluster = new List<T> { @new };
-              oldCluster = new List<T> { old };
+                collection =
+                  collection
+                  .Distinct(Comparer)
+                  .ToList();
             }
             }
-            else
+
+            if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                {
+                    RemoveRange(index, count);
+                    return;
+                }
+            }
+            else if (!collection.Any())
             {
             {
-              newCluster.Add(@new);
-              oldCluster!.Add(old);
+                RemoveRange(index, count);
+                return;
             }
             }
 
 
-            changesMade = true;
-          }
-        }
+            if (index + count == 0)
+            {
+                InsertRange(0, collection);
+                return;
+            }
 
 
-        OnRangeReplaced(i, newCluster!, oldCluster!);
+            if (!(collection is IList<T> list))
+                list = new List<T>(collection);
 
 
-        //exceeding position
-        if (count != addedCount)
-        {
-          var items = (List<T>)Items;
-          if (count > addedCount)
-          {
-            var removedCount = rangeCount - addedCount;
-            T[] removed = new T[removedCount];
-            items.CopyTo(i, removed, 0, removed.Length);
-            items.RemoveRange(i, removedCount);
-            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed, i));
-          }
-          else
-          {
-            var k = i - index;
-            T[] added = new T[addedCount - k];
-            for (int j = k; j < addedCount; j++)
+            using (BlockReentrancy())
+            using (DeferEvents())
             {
             {
-              T @new = list[j];
-              added[j - k] = @new;
+                var rangeCount = index + count;
+                var addedCount = list.Count;
+
+                var changesMade = false;
+                List<T>
+                  newCluster = null,
+                  oldCluster = null;
+
+
+                int i = index;
+                for (; i < rangeCount && i - index < addedCount; i++)
+                {
+                    //parallel position
+                    T old = this[i], @new = list[i - index];
+                    if (Comparer.Equals(old, @new))
+                    {
+                        OnRangeReplaced(i, newCluster!, oldCluster!);
+                        continue;
+                    }
+                    else
+                    {
+                        Items[i] = @new;
+
+                        if (newCluster == null)
+                        {
+                            Debug.Assert(oldCluster == null);
+                            newCluster = new List<T> { @new };
+                            oldCluster = new List<T> { old };
+                        }
+                        else
+                        {
+                            newCluster.Add(@new);
+                            oldCluster!.Add(old);
+                        }
+
+                        changesMade = true;
+                    }
+                }
+
+                OnRangeReplaced(i, newCluster!, oldCluster!);
+
+                //exceeding position
+                if (count != addedCount)
+                {
+                    var items = (List<T>)Items;
+                    if (count > addedCount)
+                    {
+                        var removedCount = rangeCount - addedCount;
+                        T[] removed = new T[removedCount];
+                        items.CopyTo(i, removed, 0, removed.Length);
+                        items.RemoveRange(i, removedCount);
+                        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed, i));
+                    }
+                    else
+                    {
+                        var k = i - index;
+                        T[] added = new T[addedCount - k];
+                        for (int j = k; j < addedCount; j++)
+                        {
+                            T @new = list[j];
+                            added[j - k] = @new;
+                        }
+                        items.InsertRange(i, added);
+                        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
+                    }
+
+                    OnEssentialPropertiesChanged();
+                }
+                else if (changesMade)
+                {
+                    OnIndexerPropertyChanged();
+                }
             }
             }
-            items.InsertRange(i, added);
-            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
-          }
-
-          OnEssentialPropertiesChanged();
-        }
-        else if (changesMade)
-        {
-          OnIndexerPropertyChanged();
         }
         }
-      }
-    }
 
 
-    #endregion Public Methods
+        #endregion Public Methods
 
 
 
 
-    //------------------------------------------------------
-    //
-    //  Protected Methods
-    //
-    //------------------------------------------------------
+        //------------------------------------------------------
+        //
+        //  Protected Methods
+        //
+        //------------------------------------------------------
 
 
-    #region Protected Methods
+        #region Protected Methods
 
 
-    /// <summary>
-    /// Called by base class Collection&lt;T&gt; when the list is being cleared;
-    /// raises a CollectionChanged event to any listeners.
-    /// </summary>
-    protected override void ClearItems()
-    {
-      if (Count == 0)
-        return;
+        /// <summary>
+        /// Called by base class Collection&lt;T&gt; when the list is being cleared;
+        /// raises a CollectionChanged event to any listeners.
+        /// </summary>
+        protected override void ClearItems()
+        {
+            if (Count == 0)
+                return;
 
 
-      CheckReentrancy();
-      base.ClearItems();
-      OnEssentialPropertiesChanged();
-      OnCollectionReset();
-    }
+            CheckReentrancy();
+            base.ClearItems();
+            OnEssentialPropertiesChanged();
+            OnCollectionReset();
+        }
 
 
-    /// <inheritdoc/>
-    protected override void InsertItem(int index, T item)
-    {
-      if (!AllowDuplicates && Items.Contains(item))
-        return;
+        /// <inheritdoc/>
+        protected override void InsertItem(int index, T item)
+        {
+            if (!AllowDuplicates && Items.Contains(item))
+                return;
 
 
-      base.InsertItem(index, item);
-    }
+            base.InsertItem(index, item);
+        }
 
 
-    /// <inheritdoc/>
-    protected override void SetItem(int index, T item)
-    {
-      if (AllowDuplicates)
-      {
-        if (Comparer.Equals(this[index], item))
-          return;
-      }
-      else
-        if (Items.Contains(item, Comparer))
-        return;
-
-      CheckReentrancy();
-      T oldItem = this[index];
-      base.SetItem(index, item);
-
-      OnIndexerPropertyChanged();
-      OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem!, item!, index);
-    }
+        /// <inheritdoc/>
+        protected override void SetItem(int index, T item)
+        {
+            if (AllowDuplicates)
+            {
+                if (Comparer.Equals(this[index], item))
+                    return;
+            }
+            else if (Items.Contains(item, Comparer))
+            {
+                return;
+            }
 
 
-    /// <summary>
-    /// Raise CollectionChanged event to any listeners.
-    /// Properties/methods modifying this ObservableCollection will raise
-    /// a collection changed event through this virtual method.
-    /// </summary>
-    /// <remarks>
-    /// When overriding this method, either call its base implementation
-    /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
-    /// </remarks>
-    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
-    {
-      if (_deferredEvents != null)
-      {
-        _deferredEvents.Add(e);
-        return;
-      }
-      base.OnCollectionChanged(e);
-    }
+            CheckReentrancy();
+            T oldItem = this[index];
+            base.SetItem(index, item);
+
+            OnIndexerPropertyChanged();
+            OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem!, item!, index);
+        }
 
 
-    protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
+        /// <summary>
+        /// Raise CollectionChanged event to any listeners.
+        /// Properties/methods modifying this ObservableCollection will raise
+        /// a collection changed event through this virtual method.
+        /// </summary>
+        /// <remarks>
+        /// When overriding this method, either call its base implementation
+        /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
+        /// </remarks>
+        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (_deferredEvents != null)
+            {
+                _deferredEvents.Add(e);
+                return;
+            }
+            base.OnCollectionChanged(e);
+        }
 
 
-    #endregion Protected Methods
+        protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
 
 
+        #endregion Protected Methods
 
 
-    //------------------------------------------------------
-    //
-    //  Private Methods
-    //
-    //------------------------------------------------------
 
 
-    #region Private Methods
+        //------------------------------------------------------
+        //
+        //  Private Methods
+        //
+        //------------------------------------------------------
 
 
-    /// <summary>
-    /// Helper to raise Count property and the Indexer property.
-    /// </summary>
-    void OnEssentialPropertiesChanged()
-    {
-      OnPropertyChanged(EventArgsCache.CountPropertyChanged);
-      OnIndexerPropertyChanged();
-    }
+        #region Private Methods
 
 
-    /// <summary>
-    /// /// Helper to raise a PropertyChanged event for the Indexer property
-    /// /// </summary>
-    void OnIndexerPropertyChanged() =>
-     OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+        /// <summary>
+        /// Helper to raise Count property and the Indexer property.
+        /// </summary>
+        void OnEssentialPropertiesChanged()
+        {
+            OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+            OnIndexerPropertyChanged();
+        }
 
 
-    /// <summary>
-    /// Helper to raise CollectionChanged event to any listeners
-    /// </summary>
-    void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
-     OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+        /// <summary>
+        /// /// Helper to raise a PropertyChanged event for the Indexer property
+        /// /// </summary>
+        void OnIndexerPropertyChanged() =>
+         OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+
+        /// <summary>
+        /// Helper to raise CollectionChanged event to any listeners
+        /// </summary>
+        void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
+         OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+
+        /// <summary>
+        /// Helper to raise CollectionChanged event with action == Reset to any listeners
+        /// </summary>
+        void OnCollectionReset() =>
+         OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+
+        /// <summary>
+        /// Helper to raise event for clustered action and clear cluster.
+        /// </summary>
+        /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
+        //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
+        //move when supported language version updated.
+        void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
+        {
+            if (oldCluster == null || oldCluster.Count == 0)
+            {
+                Debug.Assert(newCluster == null || newCluster.Count == 0);
+                return;
+            }
 
 
-    /// <summary>
-    /// Helper to raise CollectionChanged event with action == Reset to any listeners
-    /// </summary>
-    void OnCollectionReset() =>
-     OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+            OnCollectionChanged(
+              new NotifyCollectionChangedEventArgs(
+                NotifyCollectionChangedAction.Replace,
+                new List<T>(newCluster),
+                new List<T>(oldCluster),
+                followingItemIndex - oldCluster.Count));
 
 
-    /// <summary>
-    /// Helper to raise event for clustered action and clear cluster.
-    /// </summary>
-    /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
-    /// <param name="newCluster"></param>
-    /// <param name="oldCluster"></param>
-    //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
-    //move when supported language version updated.
-    void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
-    {
-      if (oldCluster == null || oldCluster.Count == 0)
-      {
-        Debug.Assert(newCluster == null || newCluster.Count == 0);
-        return;
-      }
-
-      OnCollectionChanged(
-        new NotifyCollectionChangedEventArgs(
-          NotifyCollectionChangedAction.Replace,
-          new List<T>(newCluster),
-          new List<T>(oldCluster),
-          followingItemIndex - oldCluster.Count));
-
-      oldCluster.Clear();
-      newCluster.Clear();
-    }
+            oldCluster.Clear();
+            newCluster.Clear();
+        }
 
 
-    #endregion Private Methods
+        #endregion Private Methods
 
 
-    //------------------------------------------------------
-    //
-    //  Private Types
-    //
-    //------------------------------------------------------
+        //------------------------------------------------------
+        //
+        //  Private Types
+        //
+        //------------------------------------------------------
 
 
-    #region Private Types
-    sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
-    {
-      readonly RangeObservableCollection<T> _collection;
-      public DeferredEventsCollection(RangeObservableCollection<T> collection)
-      {
-        Debug.Assert(collection != null);
-        Debug.Assert(collection._deferredEvents == null);
-        _collection = collection;
-        _collection._deferredEvents = this;
-      }
-
-      public void Dispose()
-      {
-        _collection._deferredEvents = null;
-        foreach (var args in this)
-          _collection.OnCollectionChanged(args);
-      }
-    }
+        #region Private Types
+        sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+        {
+            readonly RangeObservableCollection<T> _collection;
+            public DeferredEventsCollection(RangeObservableCollection<T> collection)
+            {
+                Debug.Assert(collection != null);
+                Debug.Assert(collection._deferredEvents == null);
+                _collection = collection;
+                _collection._deferredEvents = this;
+            }
 
 
-    #endregion Private Types
+            public void Dispose()
+            {
+                _collection._deferredEvents = null;
+                foreach (var args in this)
+                    _collection.OnCollectionChanged(args);
+            }
+        }
 
 
-  }
+        #endregion Private Types
+
+    }
 
 
-  /// <remarks>
-  /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
-  /// </remarks>
-  internal static class EventArgsCache
-  {
-    internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
-    internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
-    internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
-  }
-}
+    /// <remarks>
+    /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
+    /// </remarks>
+#pragma warning disable SA1402 // File may only contain a single type
+    internal static class EventArgsCache
+#pragma warning restore SA1402 // File may only contain a single type
+    {
+        internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
+        internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
+        internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+    }
+}

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

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

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

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

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

@@ -64,8 +64,9 @@ namespace PixiEditor.Models.DataHolders
             if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
             if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
                 throw new ArgumentException("Surface dimensions must be non-zero");
                 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;
             Width = original.PixelWidth;
             Height = original.PixelHeight;
             Height = original.PixelHeight;
@@ -117,8 +118,8 @@ namespace PixiEditor.Models.DataHolders
         public unsafe SKColor GetSRGBPixel(int x, int y)
         public unsafe SKColor GetSRGBPixel(int x, int y)
         {
         {
             Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
             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)
         public void SetSRGBPixel(int x, int y, SKColor color)

+ 100 - 89
PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs

@@ -9,96 +9,107 @@ using System.Windows.Data;
 
 
 namespace PixiEditor.Models.DataHolders
 namespace PixiEditor.Models.DataHolders
 {
 {
-public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
-{
+    public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
+    {
         public bool SuppressNotify { get; set; } = false;
         public bool SuppressNotify { get; set; } = false;
-  DeferredEventsCollection _deferredEvents;
-
-  public WpfObservableRangeCollection()
-  {
-  }
-
-  public WpfObservableRangeCollection(IEnumerable<T> collection) : base(collection)
-  {
-  }
-
-  public WpfObservableRangeCollection(List<T> list) : base(list)
-  {
-  }
-
-
-  /// <summary>
-  /// Raise CollectionChanged event to any listeners.
-  /// Properties/methods modifying this ObservableCollection will raise
-  /// a collection changed event through this virtual method.
-  /// </summary>
-  /// <remarks>
-  /// When overriding this method, either call its base implementation
-  /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
-  /// </remarks>
-  protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
-  {
+        DeferredEventsCollection _deferredEvents;
+
+        public WpfObservableRangeCollection()
+        {
+        }
+
+        public WpfObservableRangeCollection(IEnumerable<T> collection) : base(collection)
+        {
+        }
+
+        public WpfObservableRangeCollection(List<T> list) : base(list)
+        {
+        }
+
+
+        /// <summary>
+        /// Raise CollectionChanged event to any listeners.
+        /// Properties/methods modifying this ObservableCollection will raise
+        /// a collection changed event through this virtual method.
+        /// </summary>
+        /// <remarks>
+        /// When overriding this method, either call its base implementation
+        /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
+        /// </remarks>
+        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
             if (SuppressNotify) return;
             if (SuppressNotify) return;
-    var _deferredEvents = (ICollection<NotifyCollectionChangedEventArgs>) typeof(RangeObservableCollection<T>)
-      .GetField("_deferredEvents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
-    if (_deferredEvents != null)
-    {
-      _deferredEvents.Add(e);
-      return;
-    }
-
-    foreach (var handler in GetHandlers())
-      if (IsRange(e) && handler.Target is CollectionView cv)
-        cv.Refresh();
-      else
-        handler(this, e);
-  }
-
-  protected override IDisposable DeferEvents() => new DeferredEventsCollection(this);
-
-  bool IsRange(NotifyCollectionChangedEventArgs e) => e.NewItems?.Count > 1 || e.OldItems?.Count > 1;
-
-  IEnumerable<NotifyCollectionChangedEventHandler> GetHandlers()
-  {
-    var info = typeof(ObservableCollection<T>).GetField(nameof(CollectionChanged),
-      BindingFlags.Instance | BindingFlags.NonPublic);
-    var @event = (MulticastDelegate) info.GetValue(this);
-    return @event?.GetInvocationList()
-             .Cast<NotifyCollectionChangedEventHandler>()
-             .Distinct()
-           ?? Enumerable.Empty<NotifyCollectionChangedEventHandler>();
-  }
-
-  class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
-  {
-    private readonly WpfObservableRangeCollection<T> _collection;
-
-    public DeferredEventsCollection(WpfObservableRangeCollection<T> collection)
-    {
-      Debug.Assert(collection != null);
-      Debug.Assert(collection._deferredEvents == null);
-      _collection = collection;
-      _collection._deferredEvents = this;
-    }
-
-    public void Dispose()
-    {
-      _collection._deferredEvents = null;
-
-      var handlers = _collection
-        .GetHandlers()
-        .ToLookup(h => h.Target is CollectionView);
-
-      foreach (var handler in handlers[false])
-      foreach (var e in this)
-        handler(_collection, e);
-
-      foreach (var cv in handlers[true]
-                 .Select(h => h.Target)
-                 .Cast<CollectionView>()
-                 .Distinct())
-        cv.Refresh();
+#pragma warning disable SA1312 // Variable names should begin with lower-case letter
+            var _deferredEvents = (ICollection<NotifyCollectionChangedEventArgs>)typeof(RangeObservableCollection<T>)
+#pragma warning restore SA1312 // Variable names should begin with lower-case letter
+              .GetField("_deferredEvents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
+            if (_deferredEvents != null)
+            {
+                _deferredEvents.Add(e);
+                return;
+            }
+
+            foreach (var handler in GetHandlers())
+            {
+                if (IsRange(e) && handler.Target is CollectionView cv)
+                    cv.Refresh();
+                else
+                    handler(this, e);
+            }
+        }
+
+        protected override IDisposable DeferEvents() => new DeferredEventsCollection(this);
+
+        bool IsRange(NotifyCollectionChangedEventArgs e) => e.NewItems?.Count > 1 || e.OldItems?.Count > 1;
+
+        IEnumerable<NotifyCollectionChangedEventHandler> GetHandlers()
+        {
+            var info = typeof(ObservableCollection<T>).GetField(
+                nameof(CollectionChanged),
+                BindingFlags.Instance | BindingFlags.NonPublic);
+            var @event = (MulticastDelegate)info.GetValue(this);
+            return @event?.GetInvocationList()
+                     .Cast<NotifyCollectionChangedEventHandler>()
+                     .Distinct()
+                   ?? Enumerable.Empty<NotifyCollectionChangedEventHandler>();
+        }
+
+        class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+        {
+            private readonly WpfObservableRangeCollection<T> _collection;
+
+            public DeferredEventsCollection(WpfObservableRangeCollection<T> collection)
+            {
+                Debug.Assert(collection != null);
+                Debug.Assert(collection._deferredEvents == null);
+                _collection = collection;
+                _collection._deferredEvents = this;
+            }
+
+            public void Dispose()
+            {
+                _collection._deferredEvents = null;
+
+                var handlers = _collection
+                  .GetHandlers()
+                  .ToLookup(h => h.Target is CollectionView);
+
+                foreach (var handler in handlers[false])
+                {
+                    foreach (var e in this)
+                    {
+                        handler(_collection, e);
+                    }
+                }
+
+                foreach (var cv in handlers[true]
+                           .Select(h => h.Target)
+                           .Cast<CollectionView>()
+                           .Distinct())
+                {
+                    cv.Refresh();
+                }
+            }
+        }
     }
     }
-  }
 }
 }
-}

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

@@ -1,27 +1,10 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.Views;
 using PixiEditor.Views;
-using System;
 
 
 namespace PixiEditor.Models.Dialogs
 namespace PixiEditor.Models.Dialogs
 {
 {
     public static class ConfirmationDialog
     public static class ConfirmationDialog
-    {
-        [Obsolete(message: "Use Show(message, title) instead.")]
-        public static ConfirmationType Show(string message)
-        {
-            ConfirmationPopup popup = new ConfirmationPopup
-            {
-                Body = message,
-                Topmost = true
-            };
-            if (popup.ShowDialog().GetValueOrDefault())
-            {
-                return popup.Result ? ConfirmationType.Yes : ConfirmationType.No;
-            }
-
-            return ConfirmationType.Canceled;
-        }
-
+    {
         public static ConfirmationType Show(string message, string title)
         public static ConfirmationType Show(string message, string title)
         {
         {
             ConfirmationPopup popup = new ConfirmationPopup
             ConfirmationPopup popup = new ConfirmationPopup
@@ -38,4 +21,4 @@ namespace PixiEditor.Models.Dialogs
             return ConfirmationType.Canceled;
             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 PixiEditor.Views;
+using System.Drawing.Imaging;
+using System.Windows;
 
 
 namespace PixiEditor.Models.Dialogs
 namespace PixiEditor.Models.Dialogs
 {
 {
     public class ExportFileDialog : CustomDialog
     public class ExportFileDialog : CustomDialog
     {
     {
+        FileType _chosenFormat;
+
         private int fileHeight;
         private int fileHeight;
 
 
         private string filePath;
         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()
         public override bool ShowDialog()
         {
         {
-            SaveFilePopup popup = new SaveFilePopup
+            ExportFilePopup popup = new ExportFilePopup
             {
             {
                 SaveWidth = FileWidth,
                 SaveWidth = FileWidth,
                 SaveHeight = FileHeight
                 SaveHeight = FileHeight
@@ -69,9 +86,10 @@ namespace PixiEditor.Models.Dialogs
                 FileWidth = popup.SaveWidth;
                 FileWidth = popup.SaveWidth;
                 FileHeight = popup.SaveHeight;
                 FileHeight = popup.SaveHeight;
                 FilePath = popup.SavePath;
                 FilePath = popup.SavePath;
+                ChosenFormat = popup.SaveFormat;
             }
             }
 
 
             return (bool)popup.DialogResult;
             return (bool)popup.DialogResult;
         }
         }
     }
     }
-}
+}

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

@@ -5,11 +5,9 @@ namespace PixiEditor.Models.Dialogs
 {
 {
     public class NewFileDialog : CustomDialog
     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
         public int Width
         {
         {

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

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

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

@@ -27,7 +27,7 @@ namespace PixiEditor.Models.Dialogs
                 if (width != value)
                 if (width != value)
                 {
                 {
                     width = value;
                     width = value;
-                    RaisePropertyChanged("Width");
+                    RaisePropertyChanged(nameof(Width));
                 }
                 }
             }
             }
         }
         }
@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Dialogs
                 if (height != value)
                 if (height != value)
                 {
                 {
                     height = value;
                     height = value;
-                    RaisePropertyChanged("Height");
+                    RaisePropertyChanged(nameof(Height));
                 }
                 }
             }
             }
         }
         }
@@ -50,41 +50,39 @@ namespace PixiEditor.Models.Dialogs
             return OpenResizeCanvas ? ShowResizeCanvasDialog() : ShowResizeDocumentCanvas();
             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();
             popup.ShowDialog();
             if (popup.DialogResult == true)
             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;
             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>();
         }
         }
     }
     }
-}
+}

+ 0 - 8
PixiEditor/Models/Enums/CapType.cs

@@ -1,8 +0,0 @@
-namespace PixiEditor.Models.Enums
-{
-    public enum CapType
-    {
-        Square,
-        Round
-    }
-}

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

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

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

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

+ 49 - 21
PixiEditor/Models/IO/Exporter.cs

@@ -1,11 +1,17 @@
 using Microsoft.Win32;
 using Microsoft.Win32;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using SkiaSharp;
 using SkiaSharp;
 using System;
 using System;
+using System.Collections.Generic;
+using System.Drawing.Imaging;
 using System.IO;
 using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
@@ -23,8 +29,8 @@ namespace PixiEditor.Models.IO
         {
         {
             SaveFileDialog dialog = new SaveFileDialog
             SaveFileDialog dialog = new SaveFileDialog
             {
             {
-                Filter = "PixiEditor Files | *.pixi",
-                DefaultExt = "pixi"
+                Filter = SupportedFilesHelper.BuildSaveFilter(true),
+                FilterIndex = 0
             };
             };
             if ((bool)dialog.ShowDialog())
             if ((bool)dialog.ShowDialog())
             {
             {
@@ -44,10 +50,39 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         public static string SaveAsEditableFile(Document document, string path)
         {
         {
-            Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            if (Path.GetExtension(path) != Constants.NativeExtension)
+            {
+                var chosenFormat = ParseImageFormat(Path.GetExtension(path));
+                var bitmap = document.Renderer.FinalBitmap;
+                SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+            }
+            else if(Directory.Exists(Path.GetDirectoryName(path)))
+            {
+                Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            }
+            else
+            {
+                SaveAsEditableFileWithDialog(document, out path);
+            }
+
             return path;
             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>
         /// <summary>
         ///     Creates ExportFileDialog to get width, height and path of file.
         ///     Creates ExportFileDialog to get width, height and path of file.
         /// </summary>
         /// </summary>
@@ -55,22 +90,15 @@ namespace PixiEditor.Models.IO
         /// <param name="fileDimensions">Size of file.</param>
         /// <param name="fileDimensions">Size of file.</param>
         public static void Export(WriteableBitmap bitmap, Size fileDimensions)
         public static void Export(WriteableBitmap bitmap, Size fileDimensions)
         {
         {
-            ExportFileDialog info = new ExportFileDialog(fileDimensions);
-
-            // If OK on dialog has been clicked
-            if (info.ShowDialog())
-            {
-                // If sizes are incorrect
-                if (info.FileWidth < bitmap.Width || info.FileHeight < bitmap.Height)
-                {
-                    MessageBox.Show("Incorrect height or width value", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
-                    return;
-                }
+          ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
 
-                SaveAsPng(info.FilePath, info.FileWidth, info.FileHeight, bitmap);
-            }
+          // If OK on dialog has been clicked
+          if (info.ShowDialog())
+          {
+            if(encodersFactory.ContainsKey(info.ChosenFormat))
+              SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
+          }
         }
         }
-
         public static void SaveAsGZippedBytes(string path, Surface surface)
         public static void SaveAsGZippedBytes(string path, Surface surface)
         {
         {
             SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
             SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
@@ -101,25 +129,25 @@ namespace PixiEditor.Models.IO
         /// <summary>
         /// <summary>
         ///     Saves image to PNG file.
         ///     Saves image to PNG file.
         /// </summary>
         /// </summary>
+        /// <param name="encoder">encoder to do the job.</param>
         /// <param name="savePath">Save file path.</param>
         /// <param name="savePath">Save file path.</param>
         /// <param name="exportWidth">File width.</param>
         /// <param name="exportWidth">File width.</param>
         /// <param name="exportHeight">File height.</param>
         /// <param name="exportHeight">File height.</param>
         /// <param name="bitmap">Bitmap to save.</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
             try
             {
             {
                 bitmap = bitmap.Resize(exportWidth, exportHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
                 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.Frames.Add(BitmapFrame.Create(bitmap));
                     encoder.Save(stream);
                     encoder.Save(stream);
                 }
                 }
             }
             }
             catch (Exception err)
             catch (Exception err)
             {
             {
-                MessageBox.Show(err.ToString(), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+                NoticeDialog.Show(err.ToString(), "Error");
             }
             }
         }
         }
     }
     }

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

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

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

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

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

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

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

@@ -7,6 +7,7 @@ using SkiaSharp;
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
+using System.Linq;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
 
 
@@ -87,8 +88,7 @@ namespace PixiEditor.Models.IO
 
 
         public static bool IsSupportedFile(string path)
         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)
         public static Surface LoadFromGZippedBytes(string path)

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

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

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

@@ -1,7 +1,6 @@
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers.Utils;
 using PixiEditor.Models.Layers.Utils;
-using PixiEditor.Models.Position;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Skia;
 using PixiEditor.Parser.Skia;
 using SkiaSharp;
 using SkiaSharp;
@@ -129,32 +128,6 @@ namespace PixiEditor.Models.ImageManipulation
                 maxPreviewHeight);
                 maxPreviewHeight);
         }
         }
 
 
-        public static Dictionary<Guid, SKColor[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
-        {
-            Dictionary<Guid, SKColor[]> result = new();
-
-            foreach (Layer layer in layers)
-            {
-                SKColor[] pixels = new SKColor[selection.Length];
-
-                for (int j = 0; j < pixels.Length; j++)
-                {
-                    Coordinates position = layer.GetRelativePosition(selection[j]);
-                    if (position.X < 0 || position.X > layer.Width - 1 || position.Y < 0 ||
-                        position.Y > layer.Height - 1)
-                    {
-                        continue;
-                    }
-
-                    var cl = layer.GetPixel(position.X, position.Y);
-                    pixels[j] = cl;
-                }
-                result[layer.GuidValue] = pixels;
-            }
-
-            return result;
-        }
-
         public static SKColor BlendColors(SKColor bottomColor, SKColor topColor)
         public static SKColor BlendColors(SKColor bottomColor, SKColor topColor)
         {
         {
             if ((topColor.Alpha < 255 && topColor.Alpha > 0))
             if ((topColor.Alpha < 255 && topColor.Alpha > 0))
@@ -185,10 +158,7 @@ namespace PixiEditor.Models.ImageManipulation
                 throw new ArgumentException("There were not the same amount of bitmaps and offsets", nameof(layerBitmaps));
                 throw new ArgumentException("There were not the same amount of bitmaps and offsets", nameof(layerBitmaps));
             }
             }
 
 
-            using Surface previewSurface = new Surface(maxPreviewWidth, maxPreviewHeight);
-            return previewSurface.ToWriteableBitmap();
-            /*
-            WriteableBitmap previewBitmap = BitmapFactory.New(width, height);
+            using Surface previewSurface = new Surface(width, height);
 
 
             var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
             var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
             var offsetsXEnumerator = offsetsX.GetEnumerator();
             var offsetsXEnumerator = offsetsX.GetEnumerator();
@@ -199,19 +169,18 @@ namespace PixiEditor.Models.ImageManipulation
                 offsetsXEnumerator.MoveNext();
                 offsetsXEnumerator.MoveNext();
                 offsetsYEnumerator.MoveNext();
                 offsetsYEnumerator.MoveNext();
 
 
-                var bitmap = layerBitmapsEnumerator.Current;
+                var bitmap = layerBitmapsEnumerator.Current.SkiaSurface.Snapshot();
                 var offsetX = offsetsXEnumerator.Current;
                 var offsetX = offsetsXEnumerator.Current;
                 var offsetY = offsetsYEnumerator.Current;
                 var offsetY = offsetsYEnumerator.Current;
 
 
-                previewBitmap.Blit(
-                    new Rect(offsetX, offsetY, bitmap.Width, bitmap.Height),
+                previewSurface.SkiaSurface.Canvas.DrawImage(
                     bitmap,
                     bitmap,
-                    new Rect(0, 0, bitmap.Width, bitmap.Height));
+                    offsetX, offsetY, Surface.BlendingPaint);
             }
             }
 
 
             int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
             int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
             int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
             int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
-            return previewBitmap.Redesize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);*/
+            return previewSurface.ResizeNearestNeighbor(newWidth, newHeight).ToWriteableBitmap();
         }
         }
     }
     }
 }
 }

+ 0 - 99
PixiEditor/Models/ImageManipulation/Morphology.cs

@@ -1,99 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using PixiEditor.Models.Position;
-
-namespace PixiEditor.Models.ImageManipulation
-{
-    public class Morphology
-    {
-        public static IEnumerable<Coordinates> ApplyDilation(Coordinates[] points, int kernelSize, int[,] mask)
-        {
-            int kernelDim = kernelSize;
-
-            // This is the offset of center pixel from border of the kernel
-            int kernelOffset = (kernelDim - 1) / 2;
-            int margin = kernelDim;
-
-            byte[,] byteImg = GetByteArrayForPoints(points, margin);
-            byte[,] outputArray = byteImg.Clone() as byte[,];
-            Coordinates offset = new Coordinates(points.Min(x => x.X) - margin, points.Min(x => x.Y) - margin);
-
-            int width = byteImg.GetLength(0);
-            int height = byteImg.GetLength(1);
-            for (int y = kernelOffset; y < height - kernelOffset; y++)
-            {
-                for (int x = kernelOffset; x < width - kernelOffset; x++)
-                {
-                    byte value = 0;
-
-                    // Apply dilation
-                    for (int ykernel = -kernelOffset; ykernel <= kernelOffset; ykernel++)
-                    {
-                        for (int xkernel = -kernelOffset; xkernel <= kernelOffset; xkernel++)
-                        {
-                            if (mask[xkernel + kernelOffset, ykernel + kernelOffset] == 1)
-                            {
-                                value = Math.Max(value, byteImg[x + xkernel, y + ykernel]);
-                            }
-                            else
-                            {
-                                continue;
-                            }
-                        }
-                    }
-
-                    // Write processed data into the second array
-                    outputArray[x, y] = value;
-                }
-            }
-
-            return ToCoordinates(outputArray, offset).Distinct();
-        }
-
-        private static IEnumerable<Coordinates> ToCoordinates(byte[,] byteArray, Coordinates offset)
-        {
-            List<Coordinates> output = new List<Coordinates>();
-            int width = byteArray.GetLength(0);
-
-            for (int y = 0; y < byteArray.GetLength(1); y++)
-            {
-                for (int x = 0; x < width; x++)
-                {
-                    if (byteArray[x, y] == 1)
-                    {
-                        output.Add(new Coordinates(x + offset.X, y + offset.Y));
-                    }
-                }
-            }
-
-            return output;
-        }
-
-        private static byte[,] GetByteArrayForPoints(Coordinates[] points, int margin)
-        {
-            Tuple<int, int> dimensions = GetDimensionsForPoints(points);
-            int minX = points.Min(x => x.X);
-            int minY = points.Min(x => x.Y);
-            byte[,] array = new byte[dimensions.Item1 + (margin * 2), dimensions.Item2 + (margin * 2)];
-
-            for (int y = 0; y < dimensions.Item2 + margin; y++)
-            {
-                for (int x = 0; x < dimensions.Item1 + margin; x++)
-                {
-                    Coordinates cords = new Coordinates(x + minX, y + minY);
-                    array[x + margin, y + margin] = points.Contains(cords) ? (byte)1 : (byte)0;
-                }
-            }
-
-            return array;
-        }
-
-        private static Tuple<int, int> GetDimensionsForPoints(Coordinates[] points)
-        {
-            int width = points.Max(x => x.X) - points.Min(x => x.X);
-            int height = points.Max(x => x.Y) - points.Min(x => x.Y);
-            return new Tuple<int, int>(width + 1, height + 1);
-        }
-    }
-}

+ 0 - 31
PixiEditor/Models/ImageManipulation/Transform.cs

@@ -1,31 +0,0 @@
-using PixiEditor.Models.Position;
-
-namespace PixiEditor.Models.ImageManipulation
-{
-    public static class Transform
-    {
-        /// <summary>
-        ///     Returns translation between two coordinates.
-        /// </summary>
-        /// <param name="from">Starting coordinate.</param>
-        /// <param name="to">New coordinate.</param>
-        /// <returns>Translation as coordinate.</returns>
-        public static Coordinates GetTranslation(Coordinates from, Coordinates to)
-        {
-            int translationX = to.X - from.X;
-            int translationY = to.Y - from.Y;
-            return new Coordinates(translationX, translationY);
-        }
-
-        public static Coordinates[] Translate(Coordinates[] points, Coordinates vector)
-        {
-            Coordinates[] translatedPoints = new Coordinates[points.Length];
-            for (int i = 0; i < translatedPoints.Length; i++)
-            {
-                translatedPoints[i] = new Coordinates(points[i].X + vector.X, points[i].Y + vector.Y);
-            }
-
-            return translatedPoints;
-        }
-    }
-}

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

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

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

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

+ 12 - 12
PixiEditor/Models/Layers/LayerStructure.cs

@@ -191,7 +191,7 @@ namespace PixiEditor.Models.Layers
                 return;
                 return;
             }
             }
 
 
-            PreMoveReassignBounds(parentGroup, group);
+            Unassign(parentGroup, group);
 
 
             List<Guid> layersInOrder = GetLayersInOrder(new GroupData(groupTopIndex, groupBottomIndex));
             List<Guid> layersInOrder = GetLayersInOrder(new GroupData(groupTopIndex, groupBottomIndex));
 
 
@@ -250,9 +250,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="group">Group which data should be reassigned.</param>
         /// <param name="group">Group which data should be reassigned.</param>
-        public void PreMoveReassignBounds(GroupData parentGroup, GroupData group)
+        public void Unassign(GroupData parentGroup, GroupData group)
         {
         {
-            PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), GetGroupByGuid(group.GroupGuid));
+            Unassign(GetGroupByGuid(parentGroup.GroupGuid), GetGroupByGuid(group.GroupGuid));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -260,7 +260,7 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="layer">Layer which data should be reassigned.</param>
         /// <param name="layer">Layer which data should be reassigned.</param>
-        public void PreMoveReassignBounds(GroupData parentGroup, Guid layer)
+        public void Unassign(GroupData parentGroup, Guid layer)
         {
         {
             PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layer);
             PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layer);
         }
         }
@@ -270,9 +270,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="layerGuid">Group which data should be reassigned.</param>
         /// <param name="layerGuid">Group which data should be reassigned.</param>
-        public void PostMoveReassignBounds(GroupData parentGroup, Guid layerGuid)
+        public void Assign(GroupData parentGroup, Guid layerGuid)
         {
         {
-            PostMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layerGuid);
+            Assign(GetGroupByGuid(parentGroup.GroupGuid), layerGuid);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -280,9 +280,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="parentGroup">Parent group to reassign data in.</param>
         /// <param name="group">Group which data should be reassigned.</param>
         /// <param name="group">Group which data should be reassigned.</param>
-        public void PostMoveReassignBounds(GroupData parentGroup, GroupData group)
+        public void Assign(GroupData parentGroup, GroupData group)
         {
         {
-            PostMoveReassignBounds(GetGroupByGuid(parentGroup?.GroupGuid), GetGroupByGuid(group.GroupGuid));
+            Assign(GetGroupByGuid(parentGroup?.GroupGuid), GetGroupByGuid(group.GroupGuid));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -450,7 +450,7 @@ namespace PixiEditor.Models.Layers
             }
             }
         }
         }
 
 
-        private void PreMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+        private void Unassign(GuidStructureItem parentGroup, GuidStructureItem group)
         {
         {
             if (parentGroup != null)
             if (parentGroup != null)
             {
             {
@@ -481,7 +481,7 @@ namespace PixiEditor.Models.Layers
             }
             }
         }
         }
 
 
-        private void PostMoveReassignBounds(GuidStructureItem parentGroup, Guid layerGuid)
+        private void Assign(GuidStructureItem parentGroup, Guid layerGuid)
         {
         {
             if (parentGroup != null)
             if (parentGroup != null)
             {
             {
@@ -529,7 +529,7 @@ namespace PixiEditor.Models.Layers
             }
             }
         }
         }
 
 
-        private void PostMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+        private void Assign(GuidStructureItem parentGroup, GuidStructureItem group)
         {
         {
             if (parentGroup != null)
             if (parentGroup != null)
             {
             {
@@ -572,7 +572,7 @@ namespace PixiEditor.Models.Layers
                 PreMoveReassignBounds(currentParent, layer);
                 PreMoveReassignBounds(currentParent, layer);
             }
             }
 
 
-            PostMoveReassignBounds(parent, layer);
+            Assign(parent, layer);
 
 
             LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layer));
             LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layer));
         }
         }

+ 0 - 19
PixiEditor/Models/Position/MousePositionConverter.cs

@@ -1,19 +0,0 @@
-using System.Drawing;
-using System.Runtime.InteropServices;
-
-namespace PixiEditor.Models.Position
-{
-    public static class MousePositionConverter
-    {
-        public static Coordinates CurrentCoordinates { get; set; }
-
-        public static Point GetCursorPosition()
-        {
-            GetCursorPos(out Point point);
-            return point;
-        }
-
-        [DllImport("user32.dll")]
-        private static extern bool GetCursorPos(out Point point);
-    }
-}

+ 38 - 21
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.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
 using PixiEditor.Models.Undo;
 using SkiaSharp;
 using SkiaSharp;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 
 namespace PixiEditor.Models.Tools
 namespace PixiEditor.Models.Tools
 {
 {
@@ -17,6 +19,9 @@ namespace PixiEditor.Models.Tools
 
 
         public bool UseDocumentRectForUndo { get; set; } = false;
         public bool UseDocumentRectForUndo { get; set; } = false;
 
 
+        private SKRectI _rectReportedByTool;
+        private bool _customRectReported = false;
+
         private StorageBasedChange _change;
         private StorageBasedChange _change;
 
 
         public abstract void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color);
         public abstract void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color);
@@ -49,29 +54,41 @@ namespace PixiEditor.Models.Tools
             _change = null;
             _change = null;
         }
         }
 
 
+        protected void ReportCustomSessionRect(SKRectI rect)
+        {
+            _rectReportedByTool = rect;
+            _customRectReported = true;
+        }
+
         private void InitializeStorageBasedChange(SKRectI toolSessionRect)
         private void InitializeStorageBasedChange(SKRectI toolSessionRect)
         {
         {
             Document doc = ViewModels.ViewModelMain.Current.BitmapManager.ActiveDocument;
             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);
-            //}
-
-            //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);
-            //}
-
-            _change = new StorageBasedChange(doc, new[] { doc.ActiveLayer });
+            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 (UseDocumentRectForUndo)
+            {
+                finalRect = SKRectI.Create(0, 0, doc.Width, doc.Height);
+            }
+
+            if (_customRectReported)
+            {
+                _customRectReported = false;
+                finalRect = _rectReportedByTool;
+                _rectReportedByTool = SKRectI.Empty;
+            }
+
+            _change = new StorageBasedChange(doc, new[] { new LayerChunk(doc.ActiveLayer, finalRect) });
         }
         }
     }
     }
 }
 }

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

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

+ 5 - 3
PixiEditor/Models/Tools/Tools/CircleTool.cs

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

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

@@ -28,7 +28,7 @@ namespace PixiEditor.Models.Tools.Tools
 
 
         public override bool RequiresPreciseMouseData => true;
         public override 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)
         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();
             Toolbar = new BasicToolbar();
             pen = new PenTool(bitmapManager);
             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)
         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;
             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)
         public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
         {
         {

+ 5 - 3
PixiEditor/Models/Tools/Tools/LineTool.cs

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

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

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

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

@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Tools.Tools
             BitmapManager = bitmapManager;
             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;
         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();
                 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;
             Layer selLayer = selection.SelectionLayer;
             moveStartRect = anySelection ?
             moveStartRect = anySelection ?

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

@@ -1,5 +1,4 @@
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
-using PixiEditor.ViewModels.SubViewModels.Main;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Windows.Input;
 using System.Windows.Input;
 
 
@@ -7,18 +6,14 @@ namespace PixiEditor.Models.Tools.Tools
 {
 {
     public class MoveViewportTool : ReadonlyTool
     public class MoveViewportTool : ReadonlyTool
     {
     {
-        private ToolsViewModel ToolsViewModel { get; }
-
-        public MoveViewportTool(ToolsViewModel toolsViewModel)
+        public MoveViewportTool()
         {
         {
             Cursor = Cursors.SizeAll;
             Cursor = Cursors.SizeAll;
             ActionDisplay = "Click and move to pan viewport.";
             ActionDisplay = "Click and move to pan viewport.";
-
-            ToolsViewModel = toolsViewModel;
         }
         }
 
 
         public override bool HideHighlight => true;
         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)
         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;
         public bool AutomaticallyResizeCanvas { get; set; } = true;
 
 

+ 6 - 3
PixiEditor/Models/Tools/Tools/RectangleTool.cs

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

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

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

+ 134 - 425
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.IO;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers;
+using SkiaSharp;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text;
 using System.Text;
+using System.Windows;
 
 
 namespace PixiEditor.Models.Undo
 namespace PixiEditor.Models.Undo
 {
 {
@@ -14,45 +18,83 @@ namespace PixiEditor.Models.Undo
     /// </summary>
     /// </summary>
     public class StorageBasedChange : IDisposable
     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 string UndoChangeLocation { get; set; }
 
 
         public UndoLayer[] StoredLayers { get; set; }
         public UndoLayer[] StoredLayers { get; set; }
 
 
-        private List<Guid> layersToStore;
+        private List<Guid> layersToStore = new List<Guid>();
         public Document Document { get; }
         public Document Document { get; }
 
 
         public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
         public StorageBasedChange(Document doc, IEnumerable<Layer> layers, bool saveOnStartup = true)
         {
         {
             Document = doc;
             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)
         public StorageBasedChange(Document doc, IEnumerable<Layer> layers, string undoChangeLocation, bool saveOnStartup = true)
         {
         {
             Document = doc;
             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)
             if (saveOnStartup)
             {
             {
                 SaveLayersOnDevice();
                 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()
         public void SaveLayersOnDevice()
@@ -64,7 +106,21 @@ namespace PixiEditor.Models.Undo
                 UndoLayer storedLayer = StoredLayers[i];
                 UndoLayer storedLayer = StoredLayers[i];
                 if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName)))
                 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++;
                 i++;
@@ -73,6 +129,19 @@ namespace PixiEditor.Models.Undo
             layersToStore = new List<Guid>();
             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>
         /// <summary>
         /// Loads saved layers from disk.
         /// Loads saved layers from disk.
         /// </summary>
         /// </summary>
@@ -84,18 +153,17 @@ namespace PixiEditor.Models.Undo
             {
             {
                 UndoLayer storedLayer = StoredLayers[i];
                 UndoLayer storedLayer = StoredLayers[i];
                 var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName);
                 var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName);
-                layers[i] = new Layer(storedLayer.Name, bitmap)
+                layers[i] = new Layer(storedLayer.Name, bitmap, storedLayer.MaxWidth, storedLayer.MaxHeight)
                 {
                 {
-                    Offset = new System.Windows.Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
+                    Width = storedLayer.Width,
+                    Height = storedLayer.Height,
+                    Offset = new Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0),
                     Opacity = storedLayer.Opacity,
                     Opacity = storedLayer.Opacity,
-                    MaxWidth = storedLayer.MaxWidth,
-                    MaxHeight = storedLayer.MaxHeight,
                     IsVisible = storedLayer.IsVisible,
                     IsVisible = storedLayer.IsVisible,
                     IsActive = storedLayer.IsActive,
                     IsActive = storedLayer.IsActive,
-                    Width = storedLayer.Width,
-                    Height = storedLayer.Height,
                     LayerHighlightColor = storedLayer.LayerHighlightColor
                     LayerHighlightColor = storedLayer.LayerHighlightColor
                 };
                 };
+
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
 
 
                 File.Delete(StoredLayers[i].StoredPngLayerName);
                 File.Delete(StoredLayers[i].StoredPngLayerName);
@@ -139,14 +207,19 @@ namespace PixiEditor.Models.Undo
         /// <param name="undoRedoProcess">Process that is invoked on redo and undo.</param>
         /// <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="processArgs">Custom parameters for undo and redo process.</param>
         /// <param name="description">Undo change description.</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 = "")
         public Change ToChange(Action<Layer[], UndoLayer[], object[]> undoRedoProcess, object[] processArgs, string description = "")
         {
         {
             Action<object[]> finalProcess = processParameters =>
             Action<object[]> finalProcess = processParameters =>
             {
             {
-
                 Layer[] layers = LoadLayersFromDevice();
                 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();
                 SaveLayersOnDevice();
 
 
@@ -243,7 +316,7 @@ namespace PixiEditor.Models.Undo
         /// <summary>
         /// <summary>
         /// Generates UndoLayer[] StoredLayers data.
         /// Generates UndoLayer[] StoredLayers data.
         /// </summary>
         /// </summary>
-        private void GenerateUndoLayers()
+        private void GenerateUndoLayers(LayerChunk[] chunks)
         {
         {
             StoredLayers = new UndoLayer[layersToStore.Count];
             StoredLayers = new UndoLayer[layersToStore.Count];
             int i = 0;
             int i = 0;
@@ -255,16 +328,15 @@ namespace PixiEditor.Models.Undo
                     throw new ArgumentException("Provided document doesn't contain selected layer");
                     throw new ArgumentException("Provided document doesn't contain selected layer");
                 }
                 }
 
 
-                layer.ClipCanvas();
-
                 int index = Document.Layers.IndexOf(layer);
                 int index = Document.Layers.IndexOf(layer);
-                string pngName = layer.Name + Guid.NewGuid().ToString();
+                string fileName = layer.Name + Guid.NewGuid();
                 StoredLayers[i] = new UndoLayer(
                 StoredLayers[i] = new UndoLayer(
                     Path.Join(
                     Path.Join(
                         UndoChangeLocation,
                         UndoChangeLocation,
-                        Convert.ToBase64String(Encoding.UTF8.GetBytes(pngName)) + ".png"),
+                        Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName)) + ".undoimg"),
                     layer,
                     layer,
-                    index);
+                    index,
+                    chunks[i].AbsoluteChunkRect);
                 i++;
                 i++;
             }
             }
         }
         }
@@ -273,408 +345,45 @@ namespace PixiEditor.Models.Undo
         {
         {
             if (args.Length > 0 && args[0] is Document document)
             if (args.Length > 0 && args[0] is Document document)
             {
             {
-                var ls = document.LayerStructure.CloneGroups();
-
                 for (int i = 0; i < layers.Length; i++)
                 for (int i = 0; i < layers.Length; i++)
                 {
                 {
                     Layer layer = layers[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 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;
             StoredPngLayerName = storedPngLayerName;
             LayerIndex = layerIndex;
             LayerIndex = layerIndex;
@@ -54,7 +54,7 @@ namespace PixiEditor.Models.Undo
             IsActive = layer.IsActive;
             IsActive = layer.IsActive;
             LayerGuid = layer.GuidValue;
             LayerGuid = layer.GuidValue;
             LayerHighlightColor = layer.LayerHighlightColor;
             LayerHighlightColor = layer.LayerHighlightColor;
-            //SerializedRect = serializedRect;
+            SerializedRect = serializedRect;
         }
         }
     }
     }
 }
 }

+ 6 - 4
PixiEditor/PixiEditor.csproj

@@ -182,21 +182,23 @@
 		</None>
 		</None>
 	</ItemGroup>
 	</ItemGroup>
 	<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="DiscordRichPresence" Version="1.0.175" />
 		<PackageReference Include="Expression.Blend.Sdk">
 		<PackageReference Include="Expression.Blend.Sdk">
 			<Version>1.0.2</Version>
 			<Version>1.0.2</Version>
 			<NoWarn>NU1701</NoWarn>
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
 		</PackageReference>
+		<PackageReference Include="Hardware.Info" Version="1.1.1.1" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
 			<NoWarn>NU1701</NoWarn>
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
 		</PackageReference>
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
-		<PackageReference Include="PixiEditor.ColorPicker" Version="3.1.0" />
-		<PackageReference Include="PixiEditor.Parser" Version="2.0.0" />
-		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.0.0" />
+		<PackageReference Include="PixiEditor.ColorPicker" Version="3.2.0" />
+		<PackageReference Include="PixiEditor.Parser" Version="2.0.0.1" />
+		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.0.0.1" />
 		<PackageReference Include="SkiaSharp" Version="2.80.3" />
 		<PackageReference Include="SkiaSharp" Version="2.80.3" />
 		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
 		<PackageReference Include="WriteableBitmapEx">
 		<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
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.1.7.0")]
-[assembly: AssemblyFileVersion("0.1.7.0")]
+[assembly: AssemblyVersion("0.1.8.0")]
+[assembly: AssemblyFileVersion("0.1.8.0")]

+ 3 - 2
PixiEditor/Styles/AvalonDock/Themes/Generic.xaml

@@ -162,8 +162,9 @@
 	<Style x:Key="PixiEditorDockThemeButtonStyle" TargetType="Button">
 	<Style x:Key="PixiEditorDockThemeButtonStyle" TargetType="Button">
 		<Setter Property="Background" Value="Transparent" />
 		<Setter Property="Background" Value="Transparent" />
 		<Setter Property="BorderThickness" Value="1" />
 		<Setter Property="BorderThickness" Value="1" />
-		<Setter Property="BorderBrush" Value="Transparent" />
-		<Setter Property="Padding" Value="0" />
+        <Setter Property="BorderBrush" Value="Transparent" />
+        <Setter Property="Focusable" Value="False" />
+        <Setter Property="Padding" Value="0" />
 		<Setter Property="Template">
 		<Setter Property="Template">
 			<Setter.Value>
 			<Setter.Value>
 				<ControlTemplate TargetType="Button">
 				<ControlTemplate TargetType="Button">

+ 4 - 4
PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -11,9 +11,9 @@
                 <ControlTemplate TargetType="CheckBox">
                 <ControlTemplate TargetType="CheckBox">
                     <BulletDecorator Background="Transparent">
                     <BulletDecorator Background="Transparent">
                         <BulletDecorator.Bullet>
                         <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">
                                     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>
                             </Border>
                         </BulletDecorator.Bullet>
                         </BulletDecorator.Bullet>
                         <ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True"/>
                         <ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True"/>
@@ -21,7 +21,7 @@
                     <ControlTemplate.Triggers>
                     <ControlTemplate.Triggers>
                         <Trigger Property="IsMouseOver" Value="False">
                         <Trigger Property="IsMouseOver" Value="False">
                             <Setter TargetName="Border" Property="Background" Value="#252525" />
                             <Setter TargetName="Border" Property="Background" Value="#252525" />
-                            <Setter TargetName="Border" Property="BorderBrush" Value="#3F3F46"/>
+                            <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource UIElementBorder}"/>
                         </Trigger>
                         </Trigger>
                         <Trigger Property="IsChecked" Value="false">
                         <Trigger Property="IsChecked" Value="false">
                             <Setter TargetName="CheckMark" Property="Visibility" Value="Collapsed"/>
                             <Setter TargetName="CheckMark" Property="Visibility" Value="Collapsed"/>
@@ -43,4 +43,4 @@
             </Setter.Value>
             </Setter.Value>
         </Setter>
         </Setter>
     </Style>
     </Style>
-</ResourceDictionary>
+</ResourceDictionary>

+ 1 - 0
PixiEditor/Styles/ImageCheckBoxStyle.xaml

@@ -2,6 +2,7 @@
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:local="clr-namespace:PixiEditor.Styles">
                     xmlns:local="clr-namespace:PixiEditor.Styles">
     <Style TargetType="{x:Type CheckBox}" x:Key="ImageCheckBox">
     <Style TargetType="{x:Type CheckBox}" x:Key="ImageCheckBox">
+        <Setter Property="Focusable" Value="False"/>
         <Setter Property="Template">
         <Setter Property="Template">
             <Setter.Value>
             <Setter.Value>
                 <ControlTemplate TargetType="{x:Type CheckBox}">
                 <ControlTemplate TargetType="{x:Type CheckBox}">

+ 15 - 2
PixiEditor/Styles/LabelStyles.xaml

@@ -5,7 +5,20 @@
     <Style TargetType="Label" x:Key="BaseLabel">
     <Style TargetType="Label" x:Key="BaseLabel">
         <Setter Property="Foreground" Value="White"/>
         <Setter Property="Foreground" Value="White"/>
     </Style>
     </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}">
     <Style x:Key="Header1" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
         <Setter Property="FontSize" Value="36"/>
         <Setter Property="FontSize" Value="36"/>
         <Setter Property="Margin" Value="20"/>
         <Setter Property="Margin" Value="20"/>
@@ -19,4 +32,4 @@
     <Style x:Key="Paragraph" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
     <Style x:Key="Paragraph" TargetType="Label" BasedOn="{StaticResource BaseLabel}">
         <Setter Property="Margin" Value="0 10 0 10"/>
         <Setter Property="Margin" Value="0 10 0 10"/>
     </Style>
     </Style>
-</ResourceDictionary>
+</ResourceDictionary>

+ 1 - 0
PixiEditor/Styles/ListSwitchButtonStyle.xaml

@@ -7,6 +7,7 @@
         <Setter Property="BorderThickness" Value="1"/>
         <Setter Property="BorderThickness" Value="1"/>
         <Setter Property="FontSize" Value="12"/>
         <Setter Property="FontSize" Value="12"/>
         <Setter Property="Cursor" Value="Hand"/>
         <Setter Property="Cursor" Value="Hand"/>
+        <Setter Property="Focusable" Value="False"/>
         <Setter Property="Padding" Value="2, 0"/>
         <Setter Property="Padding" Value="2, 0"/>
         <Setter Property="Foreground" Value="White"/>
         <Setter Property="Foreground" Value="White"/>
         <Setter Property="Template">
         <Setter Property="Template">

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

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