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

+ 7 - 7
CONTRIBUTING.md

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

+ 5 - 2
PixiEditor/App.xaml

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

+ 36 - 1
PixiEditor/App.xaml.cs

@@ -1,7 +1,12 @@
-using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
+using PixiEditor.Views.Dialogs;
+using System;
+using System.Diagnostics;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Windows;
 
 namespace PixiEditor
@@ -11,6 +16,23 @@ namespace PixiEditor
     /// </summary>
     public partial class App : Application
     {
+        protected override void OnStartup(StartupEventArgs e)
+        {
+            string arguments = string.Join(' ', e.Args);
+
+            if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+            {
+                CrashReport report = CrashReport.Parse(groups[2].Value);
+                MainWindow = new CrashReportDialog(report);
+            }
+            else
+            {
+                MainWindow = new MainWindow();
+            }
+
+            MainWindow.Show();
+        }
+
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
         {
             base.OnSessionEnding(e);
@@ -21,5 +43,18 @@ namespace PixiEditor
                 e.Cancel = confirmation != ConfirmationType.Yes;
             }
         }
+
+        private bool ParseArgument(string pattern, string args, out Group[] groups)
+        {
+            Match match = Regex.Match(args, pattern, RegexOptions.IgnoreCase);
+            groups = null;
+
+            if (match.Success)
+            {
+                groups = match.Groups.Values.ToArray();
+            }
+
+            return match.Success;
+        }
     }
 }

+ 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 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 System.Windows;
+using System.Windows.Input;
 using System.Windows.Interactivity;
 
 namespace PixiEditor.Helpers.Behaviours
@@ -8,8 +9,14 @@ namespace PixiEditor.Helpers.Behaviours
     {
         protected override void OnAttached()
         {
-            AssociatedObject.MouseDown += AssociatedObject_MouseDown;
             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()
@@ -20,7 +27,7 @@ namespace PixiEditor.Helpers.Behaviours
         private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
         {
             AssociatedObject.Focus();
-            ShortcutController.BlockShortcutExecution = false;
+            ShortcutController.UnblockShortcutExecutionAll();
         }
     }
 }

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

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

+ 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.Input;
 using System.Windows.Interactivity;
@@ -8,27 +7,42 @@ namespace PixiEditor.Helpers.Behaviours
 {
     internal class TextBoxFocusBehavior : Behavior<TextBox>
     {
-        // Using a DependencyProperty as the backing store for FillSize.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty SelectOnFocusProperty =
+        public static readonly DependencyProperty SelectOnMouseClickProperty =
             DependencyProperty.Register(
-                nameof(SelectOnFocus),
+                nameof(SelectOnMouseClick),
                 typeof(bool),
                 typeof(TextBoxFocusBehavior),
-                new PropertyMetadata(true));
+                new PropertyMetadata(false));
 
-        public static readonly DependencyProperty NextControlProperty =
-            DependencyProperty.Register(nameof(NextControl), typeof(FrameworkElement), typeof(TextBoxFocusBehavior));
+        public static readonly DependencyProperty ConfirmOnEnterProperty =
+            DependencyProperty.Register(
+                nameof(ConfirmOnEnter),
+                typeof(bool),
+                typeof(TextBoxFocusBehavior),
+                new PropertyMetadata(false));
+
+        public 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()
@@ -36,6 +50,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnAttached();
             AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp += AssociatedObject_KeyUp;
         }
@@ -45,6 +60,7 @@ namespace PixiEditor.Helpers.Behaviours
             base.OnDetaching();
             AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
+            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
             AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
             AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
         }
@@ -52,39 +68,22 @@ namespace PixiEditor.Helpers.Behaviours
         // Converts number to proper format if enter is clicked and moves focus to next object
         private void AssociatedObject_KeyUp(object sender, KeyEventArgs e)
         {
-            if (e.Key != Key.Enter)
-            {
+            if (e.Key != Key.Enter || !ConfirmOnEnter)
                 return;
-            }
 
             RemoveFocus();
         }
 
         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(
             object sender,
             KeyboardFocusChangedEventArgs e)
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick || e.KeyboardDevice.IsKeyDown(Key.Tab))
                 AssociatedObject.SelectAll();
         }
 
@@ -92,12 +91,22 @@ namespace PixiEditor.Helpers.Behaviours
             object sender,
             MouseEventArgs e)
         {
-            if (SelectOnFocus)
+            if (SelectOnMouseClick)
                 AssociatedObject.SelectAll();
         }
 
+        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
+        {
+            if (DeselectOnFocusLoss)
+                AssociatedObject.Select(0, 0);
+            RemoveFocus();
+        }
+
         private void AssociatedObjectPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
         {
+            if (!SelectOnMouseClick)
+                return;
+
             if (!AssociatedObject.IsKeyboardFocusWithin)
             {
                 AssociatedObject.Focus();

+ 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)
         {
+            if (value == null)
+                return false;
             return value.Equals(parameter) ? Visibility.Visible : Visibility.Collapsed;
         }
     }
-}
+}

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

@@ -1,38 +1,40 @@
-using System;
+using PixiEditor.Models;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Drawing.Imaging;
 using System.Globalization;
 using System.Windows.Media;
+using PixiEditor.Models.Enums;
 
 namespace PixiEditor.Helpers.Converters
 {
     public class FileExtensionToColorConverter :
         SingleInstanceConverter<FileExtensionToColorConverter>
     {
-        private static readonly SolidColorBrush PixiBrush = ColorBrush(226, 1, 45);
-
-        private static readonly SolidColorBrush PngBrush = ColorBrush(56, 108, 254);
-
-        private static readonly SolidColorBrush JpgBrush = ColorBrush(36, 179, 66);
-
-        private static readonly SolidColorBrush UnknownBrush = ColorBrush(100, 100, 100);
+        private static readonly Dictionary<string, SolidColorBrush> extensionsToBrushes;
+        public static readonly SolidColorBrush UnknownBrush = ColorBrush(100, 100, 100);
 
+        static FileExtensionToColorConverter()
+        {
+            extensionsToBrushes = new Dictionary<string, SolidColorBrush>();
+            AssignFormatToBrush(FileType.Unset, UnknownBrush);
+            AssignFormatToBrush(FileType.Pixi, ColorBrush(226, 1, 45));
+            AssignFormatToBrush(FileType.Png, ColorBrush(56, 108, 254));
+            AssignFormatToBrush(FileType.Jpeg, ColorBrush(36, 179, 66));
+            AssignFormatToBrush(FileType.Bmp, ColorBrush(255, 140, 0));
+            AssignFormatToBrush(FileType.Gif, ColorBrush(180, 0, 255));
+        }
+        static void AssignFormatToBrush(FileType format, SolidColorBrush brush)
+        {
+            SupportedFilesHelper.GetFileTypeDialogData(format).Extensions.ForEach(i => extensionsToBrushes[i] = brush);
+        }
+        
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
             string extension = (string)value;
 
-            if (extension == ".pixi")
-            {
-                return PixiBrush;
-            }
-            else if (extension == ".png")
-            {
-                return PngBrush;
-            }
-            else if (extension is ".jpg" or ".jpeg")
-            {
-                return JpgBrush;
-            }
-
-            return UnknownBrush;
+            return extensionsToBrushes.ContainsKey(extension) ? extensionsToBrushes[extension] : UnknownBrush;
         }
 
         private static SolidColorBrush ColorBrush(byte r, byte g, byte b)

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

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

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

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

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

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

+ 63 - 20
PixiEditor/Helpers/CrashHelper.cs

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

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

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

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

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

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

+ 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.Windows.Documents;
 using System.Windows.Input;
 
 namespace PixiEditor.Models.Controllers.Shortcuts
@@ -11,15 +16,57 @@ namespace PixiEditor.Models.Controllers.Shortcuts
             ShortcutGroups = new ObservableCollection<ShortcutGroup>(shortcutGroups);
         }
 
-        public static bool BlockShortcutExecution { get; set; }
+        public static bool ShortcutExecutionBlocked => _shortcutExecutionBlockers.Count > 0;
+
+        private static List<string> _shortcutExecutionBlockers = new List<string>();
 
         public ObservableCollection<ShortcutGroup> ShortcutGroups { get; init; }
 
         public Shortcut LastShortcut { get; private set; }
 
+        public 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)
         {
-            if (!BlockShortcutExecution)
+            if (!ShortcutExecutionBlocked)
             {
                 Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
                 if (shortcuts.Length < 1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -64,8 +64,9 @@ namespace PixiEditor.Models.DataHolders
             if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
                 throw new ArgumentException("Surface dimensions must be non-zero");
 
-            byte[] pixels = new byte[original.PixelWidth * original.PixelHeight * 4];
-            original.CopyPixels(pixels, original.PixelWidth * 4, 0);
+            int stride = (original.PixelWidth * original.Format.BitsPerPixel + 7) / 8;
+            byte[] pixels = new byte[stride * original.PixelHeight];
+            original.CopyPixels(pixels, stride, 0);
 
             Width = original.PixelWidth;
             Height = original.PixelHeight;
@@ -117,8 +118,8 @@ namespace PixiEditor.Models.DataHolders
         public unsafe SKColor GetSRGBPixel(int x, int y)
         {
             Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
-            SKColor color = (SKColor)new SKColorF((float)ptr[0], (float)ptr[1], (float)ptr[2], (float)ptr[3]);
-            return SKPMColor.UnPreMultiply(new SKPMColor((uint)color));
+            float a = (float)ptr[3];
+            return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
         }
 
         public void SetSRGBPixel(int x, int y, SKColor color)

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

@@ -9,96 +9,107 @@ using System.Windows.Data;
 
 namespace PixiEditor.Models.DataHolders
 {
-public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
-{
+    public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
+    {
         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;
-    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.Views;
-using System;
 
 namespace PixiEditor.Models.Dialogs
 {
     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)
         {
             ConfirmationPopup popup = new ConfirmationPopup
@@ -38,4 +21,4 @@ namespace PixiEditor.Models.Dialogs
             return ConfirmationType.Canceled;
         }
     }
-}
+}

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

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

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

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

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

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

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

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

+ 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
     {
-        Png = 0
+        Unset, Pixi, Png, Jpeg, Bmp, Gif
     }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,7 +1,6 @@
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers.Utils;
-using PixiEditor.Models.Position;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Skia;
 using SkiaSharp;
@@ -129,32 +128,6 @@ namespace PixiEditor.Models.ImageManipulation
                 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)
         {
             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));
             }
 
-            using Surface previewSurface = new Surface(maxPreviewWidth, maxPreviewHeight);
-            return previewSurface.ToWriteableBitmap();
-            /*
-            WriteableBitmap previewBitmap = BitmapFactory.New(width, height);
+            using Surface previewSurface = new Surface(width, height);
 
             var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
             var offsetsXEnumerator = offsetsX.GetEnumerator();
@@ -199,19 +169,18 @@ namespace PixiEditor.Models.ImageManipulation
                 offsetsXEnumerator.MoveNext();
                 offsetsYEnumerator.MoveNext();
 
-                var bitmap = layerBitmapsEnumerator.Current;
+                var bitmap = layerBitmapsEnumerator.Current.SkiaSurface.Snapshot();
                 var offsetX = offsetsXEnumerator.Current;
                 var offsetY = offsetsYEnumerator.Current;
 
-                previewBitmap.Blit(
-                    new Rect(offsetX, offsetY, bitmap.Width, bitmap.Height),
+                previewSurface.SkiaSurface.Canvas.DrawImage(
                     bitmap,
-                    new Rect(0, 0, bitmap.Width, bitmap.Height));
+                    offsetX, offsetY, Surface.BlendingPaint);
             }
 
             int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
             int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
-            return previewBitmap.Redesize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);*/
+            return previewSurface.ResizeNearestNeighbor(newWidth, newHeight).ToWriteableBitmap();
         }
     }
 }

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

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

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

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

@@ -191,7 +191,7 @@ namespace PixiEditor.Models.Layers
                 return;
             }
 
-            PreMoveReassignBounds(parentGroup, group);
+            Unassign(parentGroup, group);
 
             List<Guid> layersInOrder = GetLayersInOrder(new GroupData(groupTopIndex, groupBottomIndex));
 
@@ -250,9 +250,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</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>
@@ -260,7 +260,7 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</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);
         }
@@ -270,9 +270,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</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>
@@ -280,9 +280,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="parentGroup">Parent group to reassign data in.</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>
@@ -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)
             {
@@ -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)
             {
@@ -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)
             {
@@ -572,7 +572,7 @@ namespace PixiEditor.Models.Layers
                 PreMoveReassignBounds(currentParent, layer);
             }
 
-            PostMoveReassignBounds(parent, layer);
+            Assign(parent, 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.Position;
 using PixiEditor.Models.Undo;
 using SkiaSharp;
 using System.Collections.Generic;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
 
 namespace PixiEditor.Models.Tools
 {
@@ -17,6 +19,9 @@ namespace PixiEditor.Models.Tools
 
         public bool UseDocumentRectForUndo { get; set; } = false;
 
+        private SKRectI _rectReportedByTool;
+        private bool _customRectReported = false;
+
         private StorageBasedChange _change;
 
         public abstract void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color);
@@ -49,29 +54,41 @@ namespace PixiEditor.Models.Tools
             _change = null;
         }
 
+        protected void ReportCustomSessionRect(SKRectI rect)
+        {
+            _rectReportedByTool = rect;
+            _customRectReported = true;
+        }
+
         private void InitializeStorageBasedChange(SKRectI toolSessionRect)
         {
             Document doc = ViewModels.ViewModelMain.Current.BitmapManager.ActiveDocument;
-            //var toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize");
-            //SKRectI finalRect = toolSessionRect;
-            //if (toolSize != null)
-            //{
-            //    int halfSize = (int)Math.Ceiling(toolSize.Value / 2f);
-            //    finalRect.Inflate(halfSize, halfSize);
-            //}
-
-            //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 Key ShortcutKey { get; set; }
         public virtual string ToolName => GetType().Name.Replace("Tool", string.Empty);
 
         public virtual string DisplayName => ToolName.AddSpacesBeforeUppercaseLetters();

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

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

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

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

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

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

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

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

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

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

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

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

@@ -25,7 +25,7 @@ namespace PixiEditor.Models.Tools.Tools
             Toolbar = new BasicToolbar();
         }
 
-        public override string Tooltip => "Draws line on canvas (L). Hold Shift to draw even line.";
+        public override string Tooltip => $"Draws line on canvas ({ShortcutKey}). Hold Shift to draw even line.";
 
         public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
         {
@@ -45,10 +45,11 @@ namespace PixiEditor.Models.Tools.Tools
             if (Session.IsShiftDown)
                 (start, end) = CoordinatesHelper.GetSquareOrLineCoordinates(recordedMouseMovement);
 
-            DrawLine(previewLayer, start, end, color, thickness, SKBlendMode.Src);
+            var dirtyRect = DrawLine(previewLayer, start, end, color, thickness, SKBlendMode.Src);
+            ReportCustomSessionRect(SKRectI.Create(dirtyRect.X, dirtyRect.Y, dirtyRect.Width, dirtyRect.Height));
         }
 
-        public void DrawLine(
+        public Int32Rect DrawLine(
             Layer layer, Coordinates start, Coordinates end, SKColor color, int thickness, SKBlendMode blendMode,
             SKStrokeCap strokeCap = SKStrokeCap.Butt)
         {
@@ -90,6 +91,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
 
             layer.InvokeLayerBitmapChange(dirtyRect);
+            return dirtyRect;
         }
 
         private void DrawBresenhamLine(Layer layer, int x1, int y1, int x2, int y2, SKPaint paint)

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

@@ -24,7 +24,7 @@ namespace PixiEditor.Models.Tools.Tools
         private IEnumerable<Coordinates> oldSelection;
         private List<Coordinates> newSelection = new List<Coordinates>();
 
-        public override string Tooltip => "Magic Wand (W). Flood's the selection";
+        public override string Tooltip => $"Magic Wand ({ShortcutKey}). Flood's the selection";
 
         private Layer cachedDocument;
 
@@ -88,7 +88,7 @@ namespace PixiEditor.Models.Tools.Tools
             cachedDocument ??= new Layer("_CombinedLayers", BitmapUtils.CombineLayers(
                 new Int32Rect(0, 0, document.Width, document.Height),
                 document.Layers,
-                document.LayerStructure));
+                document.LayerStructure), document.Width, document.Height);
         }
     }
 }

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

@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Tools.Tools
             BitmapManager = bitmapManager;
         }
 
-        public override string Tooltip => "Moves selected pixels (V). Hold Ctrl to move all layers.";
+        public override string Tooltip => $"Moves selected pixels ({ShortcutKey}). Hold Ctrl to move all layers.";
 
         public override bool HideHighlight => true;
 
@@ -69,7 +69,7 @@ namespace PixiEditor.Models.Tools.Tools
                 affectedLayers = doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray();
             }
 
-            change = new StorageBasedChange(doc, affectedLayers, true);
+            change = new StorageBasedChange(doc, affectedLayers, true, true);
 
             Layer selLayer = selection.SelectionLayer;
             moveStartRect = anySelection ?

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 6 - 4
PixiEditor/PixiEditor.csproj

@@ -182,21 +182,23 @@
 		</None>
 	</ItemGroup>
 	<ItemGroup>
-		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.0" />
+		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.1" />
+		<PackageReference Include="ByteSize" Version="2.1.1" />
 		<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
 		<PackageReference Include="Expression.Blend.Sdk">
 			<Version>1.0.2</Version>
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
+		<PackageReference Include="Hardware.Info" Version="1.1.1.1" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
-		<PackageReference Include="PixiEditor.ColorPicker" Version="3.1.0" />
-		<PackageReference Include="PixiEditor.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="System.Drawing.Common" Version="6.0.0" />
 		<PackageReference Include="WriteableBitmapEx">

+ 2 - 2
PixiEditor/Properties/AssemblyInfo.cs

@@ -50,5 +50,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.1.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">
 		<Setter Property="Background" Value="Transparent" />
 		<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.Value>
 				<ControlTemplate TargetType="Button">

+ 4 - 4
PixiEditor/Styles/DarkCheckboxStyle.xaml

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

+ 1 - 0
PixiEditor/Styles/ImageCheckBoxStyle.xaml

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

+ 15 - 2
PixiEditor/Styles/LabelStyles.xaml

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

+ 1 - 0
PixiEditor/Styles/ListSwitchButtonStyle.xaml

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