diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md
index 0c7285591..016e81f12 100644
--- a/.github/ISSUE_TEMPLATE/report_a_bug.md
+++ b/.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.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 64d5cd07d..a138ec406 100644
--- a/CONTRIBUTING.md
+++ b/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)
diff --git a/PixiEditor/App.xaml b/PixiEditor/App.xaml
index b9e8fc3d1..53c0c3188 100644
--- a/PixiEditor/App.xaml
+++ b/PixiEditor/App.xaml
@@ -1,7 +1,8 @@
+ >
+
@@ -17,6 +18,7 @@
+
@@ -25,7 +27,8 @@
+
-
\ No newline at end of file
+
diff --git a/PixiEditor/App.xaml.cs b/PixiEditor/App.xaml.cs
index 9535619a0..c33d900be 100644
--- a/PixiEditor/App.xaml.cs
+++ b/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
///
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 @@ protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
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;
+ }
}
}
diff --git a/PixiEditor/Exceptions/ArrayLengthMismatchException.cs b/PixiEditor/Exceptions/ArrayLengthMismatchException.cs
deleted file mode 100644
index 97c5a2033..000000000
--- a/PixiEditor/Exceptions/ArrayLengthMismatchException.cs
+++ /dev/null
@@ -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)
- {
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Exceptions/CorruptedFileException.cs b/PixiEditor/Exceptions/CorruptedFileException.cs
index 682b29a44..2f968f7ce 100644
--- a/PixiEditor/Exceptions/CorruptedFileException.cs
+++ b/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.")
{
}
diff --git a/PixiEditor/Helpers/Behaviours/AllowableCharactersTextBoxBehavior.cs b/PixiEditor/Helpers/Behaviours/AllowableCharactersTextBoxBehavior.cs
deleted file mode 100644
index 464edf3d2..000000000
--- a/PixiEditor/Helpers/Behaviours/AllowableCharactersTextBoxBehavior.cs
+++ /dev/null
@@ -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
- {
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs b/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
index 73069c7bf..2572c48da 100644
--- a/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
+++ b/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 @@ public class ClearFocusOnClickBehavior : Behavior
{
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 @@ protected override void OnDetaching()
private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
AssociatedObject.Focus();
- ShortcutController.BlockShortcutExecution = false;
+ ShortcutController.UnblockShortcutExecutionAll();
}
}
}
diff --git a/PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs b/PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs
index 2dacaa2a1..508288055 100644
--- a/PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs
+++ b/PixiEditor/Helpers/Behaviours/GlobalShortcutFocusBehavior.cs
@@ -27,12 +27,12 @@ protected override void OnDetaching()
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");
}
}
}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Behaviours/HintTextBehavior.cs b/PixiEditor/Helpers/Behaviours/HintTextBehavior.cs
deleted file mode 100644
index b7700f1d6..000000000
--- a/PixiEditor/Helpers/Behaviours/HintTextBehavior.cs
+++ /dev/null
@@ -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
- {
- // 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;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs b/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
index c41179dbd..728e4d59a 100644
--- a/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
+++ b/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
{
- // 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 @@ protected override void OnAttached()
base.OnAttached();
AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
+ AssociatedObject.LostFocus += AssociatedObject_LostFocus;
AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
AssociatedObject.KeyUp += AssociatedObject_KeyUp;
}
@@ -45,6 +60,7 @@ protected override void OnDetaching()
base.OnDetaching();
AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
+ AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
}
@@ -52,39 +68,22 @@ protected override void OnDetaching()
// 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 @@ private void RemoveFocus()
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();
diff --git a/PixiEditor/Helpers/Converters/BoolToBrushConverter.cs b/PixiEditor/Helpers/Converters/BoolToBrushConverter.cs
deleted file mode 100644
index 2d3770cc0..000000000
--- a/PixiEditor/Helpers/Converters/BoolToBrushConverter.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace PixiEditor.Helpers.Converters
-{
- public class BoolToBrushConverter
- : SingleInstanceConverter
- {
- public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- BrushTuple tuple = (BrushTuple)parameter;
- return (bool)value ? tuple.FirstBrush : tuple.SecondBrush;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Converters/BrushTuple.cs b/PixiEditor/Helpers/Converters/BrushTuple.cs
deleted file mode 100644
index 8f9a3d20a..000000000
--- a/PixiEditor/Helpers/Converters/BrushTuple.cs
+++ /dev/null
@@ -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;
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Converters/EnumBooleanConverter.cs b/PixiEditor/Helpers/Converters/EnumBooleanConverter.cs
new file mode 100644
index 000000000..0bb29c40d
--- /dev/null
+++ b/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
+ {
+ #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
+ }
+}
diff --git a/PixiEditor/Helpers/Converters/EnumToStringConverter.cs b/PixiEditor/Helpers/Converters/EnumToStringConverter.cs
new file mode 100644
index 000000000..382c934a9
--- /dev/null
+++ b/PixiEditor/Helpers/Converters/EnumToStringConverter.cs
@@ -0,0 +1,29 @@
+using PixiEditor.Models.Enums;
+using System;
+
+namespace PixiEditor.Helpers.Converters
+{
+ internal class EnumToStringConverter : SingleInstanceConverter
+ {
+ 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;
+ }
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs b/PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
index fa5c3fac0..ad93bbf00 100644
--- a/PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
+++ b/PixiEditor/Helpers/Converters/EqualityBoolToVisibilityConverter.cs
@@ -9,7 +9,9 @@ public class EqualityBoolToVisibilityConverter :
{
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;
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs b/PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
index 2e7bdf48c..b82e8cb81 100644
--- a/PixiEditor/Helpers/Converters/FileExtensionToColorConverter.cs
+++ b/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
{
- 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 extensionsToBrushes;
+ public static readonly SolidColorBrush UnknownBrush = ColorBrush(100, 100, 100);
+ static FileExtensionToColorConverter()
+ {
+ extensionsToBrushes = new Dictionary();
+ 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)
diff --git a/PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs b/PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs
deleted file mode 100644
index a7cf7f7f8..000000000
--- a/PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs
+++ /dev/null
@@ -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
- {
- 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;
- }
- }
-}
diff --git a/PixiEditor/Helpers/Converters/KeyToStringConverter.cs b/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
index c1b09da43..95b119707 100644
--- a/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
+++ b/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
@@ -11,7 +11,11 @@ public override object Convert(object value, Type targetType, object parameter,
{
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 @@ public override object Convert(object value, Type targetType, object parameter,
}
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs b/PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs
deleted file mode 100644
index e97d326be..000000000
--- a/PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs
+++ /dev/null
@@ -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
- {
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs b/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
index 044a0bce3..524788b3b 100644
--- a/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
+++ b/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
@@ -31,4 +31,4 @@ public override object ConvertBack(object value, Type targetType, object paramet
return int.Parse(match.Groups[0].ValueSpan);
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Helpers/Converters/WidthToBitmapScalingModeConverter.cs b/PixiEditor/Helpers/Converters/WidthToBitmapScalingModeConverter.cs
new file mode 100644
index 000000000..bb372d9f1
--- /dev/null
+++ b/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
+ {
+ 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();
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/Converters/ZoomLevelToBitmapScalingModeConverter.cs b/PixiEditor/Helpers/Converters/ZoomLevelToBitmapScalingModeConverter.cs
new file mode 100644
index 000000000..99456f905
--- /dev/null
+++ b/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
+ {
+ 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();
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/CrashHelper.cs b/PixiEditor/Helpers/CrashHelper.cs
index a314ae968..ab8e72744 100644
--- a/PixiEditor/Helpers/CrashHelper.cs
+++ b/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 @@ public static void SaveCrashInfo(Exception e)
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());
}
}
}
\ No newline at end of file
diff --git a/PixiEditor/Helpers/Extensions/Int32RectEx.cs b/PixiEditor/Helpers/Extensions/Int32RectHelper.cs
similarity index 86%
rename from PixiEditor/Helpers/Extensions/Int32RectEx.cs
rename to PixiEditor/Helpers/Extensions/Int32RectHelper.cs
index d9313bd1f..96a9d0581 100644
--- a/PixiEditor/Helpers/Extensions/Int32RectEx.cs
+++ b/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 @@ public static Int32Rect Expand(this Int32Rect rect, Int32Rect other)
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);
+ }
}
}
diff --git a/PixiEditor/Helpers/Extensions/ParserHelpers.cs b/PixiEditor/Helpers/Extensions/ParserHelpers.cs
index e2fc0fa34..ed8fba02f 100644
--- a/PixiEditor/Helpers/Extensions/ParserHelpers.cs
+++ b/PixiEditor/Helpers/Extensions/ParserHelpers.cs
@@ -35,19 +35,19 @@ public static WpfObservableRangeCollection ToLayers(this SerializableDocu
WpfObservableRangeCollection 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),
};
}
diff --git a/PixiEditor/Helpers/Extensions/PixiParserHelper.cs b/PixiEditor/Helpers/Extensions/PixiParserHelper.cs
deleted file mode 100644
index c391d1c8a..000000000
--- a/PixiEditor/Helpers/Extensions/PixiParserHelper.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/PixiEditor/Helpers/Extensions/SKRectIHelper.cs b/PixiEditor/Helpers/Extensions/SKRectIHelper.cs
new file mode 100644
index 000000000..267853f1b
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/ProcessHelpers.cs b/PixiEditor/Helpers/ProcessHelpers.cs
new file mode 100644
index 000000000..57cff81fd
--- /dev/null
+++ b/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));
+ }
+}
diff --git a/PixiEditor/Helpers/SizeCalculator.cs b/PixiEditor/Helpers/SizeCalculator.cs
new file mode 100644
index 000000000..1556a9bfe
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/SupportedFilesHelper.cs b/PixiEditor/Helpers/SupportedFilesHelper.cs
new file mode 100644
index 000000000..7432577df
--- /dev/null
+++ b/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 fileTypeDialogsData;
+ static List allFileTypeDialogsData;
+ public static string[] AllSupportedExtensions { get; private set; }
+ public static string[] PrimaryExtensions { get; private set; }
+
+ static SupportedFilesHelper()
+ {
+ fileTypeDialogsData = new Dictionary();
+ allFileTypeDialogsData = new List();
+
+ var allFormats = Enum.GetValues(typeof(FileType)).Cast().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 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;
+ }
+ }
+}
diff --git a/PixiEditor/Helpers/Validators/SizeValidationRule.cs b/PixiEditor/Helpers/Validators/SizeValidationRule.cs
deleted file mode 100644
index 0deb36e61..000000000
--- a/PixiEditor/Helpers/Validators/SizeValidationRule.cs
+++ /dev/null
@@ -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
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Constants.cs b/PixiEditor/Models/Constants.cs
new file mode 100644
index 000000000..a812ea176
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs b/PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs
deleted file mode 100644
index 2c37ca6a5..000000000
--- a/PixiEditor/Models/Controllers/BitmapChangedEventArgs.cs
+++ /dev/null
@@ -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; }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Controllers/BitmapManager.cs b/PixiEditor/Models/Controllers/BitmapManager.cs
index 58870ca43..ecc6177e2 100644
--- a/PixiEditor/Models/Controllers/BitmapManager.cs
+++ b/PixiEditor/Models/Controllers/BitmapManager.cs
@@ -34,6 +34,7 @@ public Document ActiveDocument
activeDocument?.UpdatePreviewImage();
Document oldDoc = activeDocument;
activeDocument = value;
+ activeDocument?.UpdatePreviewImage();
RaisePropertyChanged(nameof(ActiveDocument));
ActiveWindow = value;
DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
diff --git a/PixiEditor/Models/Controllers/BitmapOperationsUtility.cs b/PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
index d4eef82da..4688e891e 100644
--- a/PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
+++ b/PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
@@ -13,7 +13,7 @@ namespace PixiEditor.Models.Controllers
{
public class BitmapOperationsUtility
{
- public event EventHandler BitmapChanged;
+ public event EventHandler BitmapChanged;
public BitmapManager Manager { get; set; }
diff --git a/PixiEditor/Models/Controllers/ClipboardController.cs b/PixiEditor/Models/Controllers/ClipboardController.cs
index f1f716a6d..1ca873399 100644
--- a/PixiEditor/Models/Controllers/ClipboardController.cs
+++ b/PixiEditor/Models/Controllers/ClipboardController.cs
@@ -15,6 +15,7 @@
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 @@ public static void CopyToClipboard(Layer[] layers, Layer selLayer, LayerStructur
///
/// Pastes image from clipboard into new layer.
///
- public static void PasteFromClipboard()
+ public static void PasteFromClipboard(Document document)
{
- IEnumerable 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);
+ }
+ }
}
///
/// Gets image from clipboard, supported PNG, Dib and Bitmap.
///
- /// WriteableBitmap.
- private static IEnumerable GetLayersFromClipboard()
+ private static IEnumerable GetLayersFromClipboard(Document document)
{
DataObject data = ClipboardHelper.TryGetDataObject();
if (data == null)
@@ -173,7 +205,7 @@ private static IEnumerable GetLayersFromClipboard()
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 @@ private static IEnumerable GetLayersFromClipboard()
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 @@ public static bool IsImageInClipboard()
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 @@ private static bool TryFromSingleImage(DataObject data, out Surface result)
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 @@ private static bool TryFromSingleImage(DataObject data, out Surface result)
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 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 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);
}
}
}
diff --git a/PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs b/PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
index f990a2e23..4405c3981 100644
--- a/PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
+++ b/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 @@ public ShortcutController(params ShortcutGroup[] shortcutGroups)
ShortcutGroups = new ObservableCollection(shortcutGroups);
}
- public static bool BlockShortcutExecution { get; set; }
+ public static bool ShortcutExecutionBlocked => _shortcutExecutionBlockers.Count > 0;
+
+ private static List _shortcutExecutionBlockers = new List();
public ObservableCollection ShortcutGroups { get; init; }
public Shortcut LastShortcut { get; private set; }
+ public Dictionary TransientShortcuts { get; set; } = new Dictionary();
+
+ 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()
+ {
+ 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()
+ {
+ 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)
diff --git a/PixiEditor/Models/Controllers/SurfaceRenderer.cs b/PixiEditor/Models/Controllers/SurfaceRenderer.cs
index efc9d5657..a85c81e3d 100644
--- a/PixiEditor/Models/Controllers/SurfaceRenderer.cs
+++ b/PixiEditor/Models/Controllers/SurfaceRenderer.cs
@@ -12,6 +12,7 @@ class SurfaceRenderer : IDisposable
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 @@ public void Dispose()
{
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();
}
diff --git a/PixiEditor/Models/Controllers/UndoManager.cs b/PixiEditor/Models/Controllers/UndoManager.cs
index b5f86d29d..42d70ba51 100644
--- a/PixiEditor/Models/Controllers/UndoManager.cs
+++ b/PixiEditor/Models/Controllers/UndoManager.cs
@@ -56,7 +56,7 @@ public void AddUndoChange(Change change, bool invokedInsideSetter = false)
{
foreach (var redo in RedoStack)
{
- //redo.Dispose();
+ redo.Dispose();
}
RedoStack.Clear();
}
diff --git a/PixiEditor/Models/DataHolders/BitmapPixelChanges.cs b/PixiEditor/Models/DataHolders/BitmapPixelChanges.cs
index fb971a47e..62ed93d82 100644
--- a/PixiEditor/Models/DataHolders/BitmapPixelChanges.cs
+++ b/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 @@ public static BitmapPixelChanges CombineOverride(BitmapPixelChanges changes1, Bi
return CombineOverride(new[] { changes1, changes2 });
}
- ///
- /// Builds BitmapPixelChanges using 2 same-length enumerables of coordinates and colors.
- ///
- public static BitmapPixelChanges FromArrays(IEnumerable coordinates, IEnumerable color)
- {
- Coordinates[] coordinateArray = coordinates.ToArray();
- SKColor[] colorArray = color.ToArray();
- if (coordinateArray.Length != colorArray.Length)
- {
- throw new ArrayLengthMismatchException();
- }
-
- Dictionary dict = new Dictionary();
- 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));
diff --git a/PixiEditor/Models/DataHolders/CrashReport.cs b/PixiEditor/Models/DataHolders/CrashReport.cs
new file mode 100644
index 000000000..a1fcdddb3
--- /dev/null
+++ b/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 RecoverDocuments()
+ {
+ List 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; }
+ }
+ }
+}
diff --git a/PixiEditor/Models/DataHolders/Document/Document.Constructors.cs b/PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
index 7864bbeff..4707a2888 100644
--- a/PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
+++ b/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 @@ private Document()
LayerStructure.LayerStructureChanged += LayerStructure_LayerStructureChanged;
DocumentSizeChanged += (sender, args) =>
{
+ ActiveSelection = new Selection(Array.Empty(), new PixelSize(args.NewWidth, args.NewHeight));
Renderer.Resize(args.NewWidth, args.NewHeight);
GeneratePreviewLayer();
};
diff --git a/PixiEditor/Models/DataHolders/Document/Document.Layers.cs b/PixiEditor/Models/DataHolders/Document/Document.Layers.cs
index 602383fab..c242c5c8d 100644
--- a/PixiEditor/Models/DataHolders/Document/Document.Layers.cs
+++ b/PixiEditor/Models/DataHolders/Document/Document.Layers.cs
@@ -12,6 +12,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
+using Windows.Graphics;
namespace PixiEditor.Models.DataHolders
{
@@ -221,9 +222,7 @@ public void AddNewLayer(string name, int width, int height, bool setAsActive = t
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 @@ public Layer MergeLayers(Layer[] layersToMerge, bool nameOfLast, int index)
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 @@ public Layer MergeLayers(Layer[] layersToMerge, bool nameOfLast, int index)
{
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 @@ public Layer MergeLayers(Layer[] layersToMerge, bool nameIsLastLayers)
throw new ArgumentException("Not enough layers were provided to merge. Minimum amount is 2");
}
- IEnumerable undoArgs = layersToMerge;
+ Layer[] undoArgs = layersToMerge;
var oldLayerStructure = LayerStructure.CloneGroups();
@@ -491,6 +492,8 @@ public Layer MergeLayers(Layer[] layersToMerge, bool nameIsLastLayers)
UndoManager.SquashUndoChanges(2, "Undo merge layers", false);
+ LayersChanged?.Invoke(this, new LayersChangedEventArgs(layer.GuidValue, LayerAction.Add));
+
return layer;
}
@@ -534,13 +537,13 @@ private void ReverseMoveLayerInStructureProcess(object[] props)
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 @@ private void MergeLayersProcess(object[] args)
Layer layer = MergeLayers(layers, nameOfSecond, indexes[0]);
layer.ChangeGuid(mergedLayerGuid);
+ SetMainActiveLayer(Layers.IndexOf(layer));
}
}
@@ -602,7 +606,7 @@ private void InsertLayersAtIndexesProcess(Layer[] layers, UndoLayer[] data, obje
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 @@ private void InsertLayersAtIndexesProcess(Layer[] layers, UndoLayer[] data, obje
///
/// Moves offsets of layers by specified vector.
///
- private void MoveOffsets(IEnumerable layers, Coordinates moveVector)
+ private void MoveOffsets(IList layers, IList 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 layers && arguments[1] is Coordinates vector)
+ if (arguments.Length > 0 && arguments[0] is List guids && arguments[1] is List bounds && arguments[2] is Coordinates vector)
{
- MoveOffsets(layers, vector);
+ List layers = new List(guids.Count);
+ foreach (Guid guid in guids)
+ {
+ layers.Add(Layers.First(x => x.GuidValue == guid));
+ }
+
+ MoveOffsets(layers, bounds, vector);
}
else
{
@@ -662,7 +682,7 @@ private void MoveGroupInStructureProcess(object[] parameter)
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 @@ private void MoveLayerInStructureProcess(object[] parameter)
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 @@ private void MoveLayerInStructureProcess(object[] parameter)
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 @@ private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
}
}
- private void RemoveLayerProcess(object[] parameters)
+ public void RemoveLayerProcess(object[] parameters)
{
if (parameters is { Length: > 0 } && parameters[0] is Guid layerGuid)
{
@@ -740,8 +760,9 @@ private void RemoveLayerProcess(object[] parameters)
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 @@ private bool SetHighest(string number, ref int? highest, int? defaultValue = 0)
return sucess;
}
- private void RemoveLayersProcess(object[] parameters)
+ public void RemoveLayersProcess(object[] parameters)
{
if (parameters != null && parameters.Length > 0 && parameters[0] is IEnumerable layerGuids)
{
diff --git a/PixiEditor/Models/DataHolders/Document/Document.Operations.cs b/PixiEditor/Models/DataHolders/Document/Document.Operations.cs
index 3f4b5e82e..e47f7b76e 100644
--- a/PixiEditor/Models/DataHolders/Document/Document.Operations.cs
+++ b/PixiEditor/Models/DataHolders/Document/Document.Operations.cs
@@ -23,7 +23,7 @@ public partial class Document
/// Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
/// vertical.
///
- 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 @@ public void ResizeCanvas(int width, int height, AnchorPoint anchor)
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 @@ private void FlipDocumentProcess(object[] processArgs)
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 @@ private void RotateDocumentProcess(object[] parameters)
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 @@ private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, obje
/// New canvas height.
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;
diff --git a/PixiEditor/Models/DataHolders/Document/Document.Preview.cs b/PixiEditor/Models/DataHolders/Document/Document.Preview.cs
index e096b8691..bebc20bf9 100644
--- a/PixiEditor/Models/DataHolders/Document/Document.Preview.cs
+++ b/PixiEditor/Models/DataHolders/Document/Document.Preview.cs
@@ -43,11 +43,7 @@ public void UpdatePreviewImage()
public void GeneratePreviewLayer()
{
- PreviewLayer = new Layer("_previewLayer")
- {
- MaxWidth = Width,
- MaxHeight = Height
- };
+ PreviewLayer = new Layer("_previewLayer", Width, Height);
}
}
}
diff --git a/PixiEditor/Models/DataHolders/Document/Document.cs b/PixiEditor/Models/DataHolders/Document/Document.cs
index 3c89a5054..f0792a620 100644
--- a/PixiEditor/Models/DataHolders/Document/Document.cs
+++ b/PixiEditor/Models/DataHolders/Document/Document.cs
@@ -61,7 +61,7 @@ private set
}
}
- private Selection selection = new Selection(Array.Empty());
+ private Selection selection;
public Selection ActiveSelection
{
@@ -114,16 +114,19 @@ public void RaisePropertyChange(string name)
///
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 @@ public void ClipCanvas()
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 @@ public void ClipCanvas()
///
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 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 emptyBounds = Enumerable.Repeat(Int32Rect.Empty, layersToCenter.Count).ToList();
+
+ MoveOffsets(layersToCenter, emptyBounds, moveVector);
+
+ List 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 @@ public void Dispose()
private void SetAsActiveOnClick(object obj)
{
- if (XamlAccesibleViewModel.BitmapManager.ActiveDocument != this)
+ if (XamlAccesibleViewModel?.BitmapManager?.ActiveDocument != this)
{
XamlAccesibleViewModel.BitmapManager.ActiveDocument = this;
}
@@ -241,7 +248,47 @@ private int GetOffsetYForAnchor(int srcHeight, int destHeight, AnchorPoint ancho
return 0;
}
- private DoubleCoords GetEdgePoints(IEnumerable layers)
+ private DoubleCoords? GetEdgePoints(IEnumerable 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 layers)
{
if (Layers.Count == 0)
{
@@ -253,9 +300,15 @@ private DoubleCoords GetEdgePoints(IEnumerable layers)
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 @@ private DoubleCoords GetEdgePoints(IEnumerable layers)
}
}
+ if (allLayersSkipped)
+ return null;
+
return new DoubleCoords(
new Coordinates(smallestX, smallestY),
new Coordinates(biggestX, biggestY));
diff --git a/PixiEditor/Models/DataHolders/PixelSize.cs b/PixiEditor/Models/DataHolders/PixelSize.cs
new file mode 100644
index 000000000..9683ecc2d
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/PixiEditor/Models/DataHolders/RangeObservableCollection.cs b/PixiEditor/Models/DataHolders/RangeObservableCollection.cs
index 3e627e832..131fb9eb7 100644
--- a/PixiEditor/Models/DataHolders/RangeObservableCollection.cs
+++ b/PixiEditor/Models/DataHolders/RangeObservableCollection.cs
@@ -9,658 +9,679 @@
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.
- ///
- /// Implementation of a dynamic data collection based on generic Collection<T>,
- /// implementing INotifyCollectionChanged to notify listeners
- /// when items get added, removed or the whole list is refreshed.
- ///
- public class RangeObservableCollection : ObservableCollection
- {
- //------------------------------------------------------
- //
- // Private Fields
- //
- //------------------------------------------------------
-
- #region Private Fields
- [NonSerialized]
- private DeferredEventsCollection? _deferredEvents;
- #endregion Private Fields
-
-
- //------------------------------------------------------
- //
- // Constructors
- //
- //------------------------------------------------------
-
- #region Constructors
- ///
- /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
- ///
- 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.
///
- /// 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<T>,
+ /// implementing INotifyCollectionChanged to notify listeners
+ /// when items get added, removed or the whole list is refreshed.
///
- /// The collection whose elements are copied to the new list.
- ///
- /// The elements are copied onto the ObservableCollection in the
- /// same order they are read by the enumerator of the collection.
- ///
- /// collection is a null reference
- public RangeObservableCollection(IEnumerable collection) : base(collection) { }
+ public class RangeObservableCollection : ObservableCollection
+ {
+ //------------------------------------------------------
+ //
+ // Private Fields
+ //
+ //------------------------------------------------------
+
+ #region Private Fields
+ [NonSerialized]
+ private DeferredEventsCollection _deferredEvents;
+ #endregion Private Fields
+
+
+ //------------------------------------------------------
+ //
+ // Constructors
+ //
+ //------------------------------------------------------
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
+ ///
+ public RangeObservableCollection() { }
+
+ ///
+ /// 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.
+ ///
+ /// The collection whose elements are copied to the new list.
+ ///
+ /// The elements are copied onto the ObservableCollection in the
+ /// same order they are read by the enumerator of the collection.
+ ///
+ /// collection is a null reference
+ public RangeObservableCollection(IEnumerable collection) : base(collection) { }
+
+ ///
+ /// Initializes a new instance of the ObservableCollection class
+ /// that contains elements copied from the specified list
+ ///
+ /// The list whose elements are copied to the new list.
+ ///
+ /// The elements are copied onto the ObservableCollection in the
+ /// same order they are read by the enumerator of the list.
+ ///
+ /// list is a null reference
+ public RangeObservableCollection(List list) : base(list) { }
+
+ #endregion Constructors
+
+ //------------------------------------------------------
+ //
+ // Public Properties
+ //
+ //------------------------------------------------------
+
+ #region Public Properties
+#pragma warning disable SA1306 // Field names should begin with lower-case letter
+ EqualityComparer _Comparer;
+#pragma warning restore SA1306 // Field names should begin with lower-case letter
+ public EqualityComparer Comparer
+ {
+ get => _Comparer ??= EqualityComparer.Default;
+ private set => _Comparer = value;
+ }
- ///
- /// Initializes a new instance of the ObservableCollection class
- /// that contains elements copied from the specified list
- ///
- /// The list whose elements are copied to the new list.
- ///
- /// The elements are copied onto the ObservableCollection in the
- /// same order they are read by the enumerator of the list.
- ///
- /// list is a null reference
- public RangeObservableCollection(List list) : base(list) { }
+ ///
+ /// Gets or sets a value indicating whether this collection acts as a ,
+ /// disallowing duplicate items, based on .
+ /// 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.
+ ///
+ public bool AllowDuplicates { get; set; } = true;
+
+ #endregion Public Properties
+
+ //------------------------------------------------------
+ //
+ // Public Methods
+ //
+ //------------------------------------------------------
+
+ #region Public Methods
+
+ ///
+ /// Adds the elements of the specified collection to the end of the .
+ ///
+ ///
+ /// The collection whose elements should be added to the end of the .
+ /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+ ///
+ /// is null.
+ public void AddRange(IEnumerable collection)
+ {
+ InsertRange(Count, collection);
+ }
- #endregion Constructors
+ ///
+ /// Inserts the elements of a collection into the at the specified index.
+ ///
+ /// The zero-based index at which the new elements should be inserted.
+ /// The collection whose elements should be inserted into the List.
+ /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+ /// is null.
+ /// is not in the collection range.
+ public void InsertRange(int index, IEnumerable 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 countable)
+ {
+ if (countable.Count == 0)
+ return;
+ }
+ else if (!collection.Any())
+ {
+ return;
+ }
- //------------------------------------------------------
- //
- // Public Properties
- //
- //------------------------------------------------------
+ CheckReentrancy();
- #region Public Properties
- EqualityComparer? _Comparer;
- public EqualityComparer Comparer
- {
- get => _Comparer ??= EqualityComparer.Default;
- private set => _Comparer = value;
- }
+ //expand the following couple of lines when adding more constructors.
+ var target = (List)Items;
+ target.InsertRange(index, collection);
- ///
- /// Gets or sets a value indicating whether this collection acts as a ,
- /// disallowing duplicate items, based on .
- /// 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.
- ///
- public bool AllowDuplicates { get; set; } = true;
+ OnEssentialPropertiesChanged();
- #endregion Public Properties
+ if (!(collection is IList list))
+ list = new List(collection);
- //------------------------------------------------------
- //
- // Public Methods
- //
- //------------------------------------------------------
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
+ }
- #region Public Methods
- ///
- /// Adds the elements of the specified collection to the end of the .
- ///
- ///
- /// The collection whose elements should be added to the end of the .
- /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
- ///
- /// is null.
- public void AddRange(IEnumerable collection)
- {
- InsertRange(Count, collection);
- }
+ ///
+ /// Removes the first occurence of each item in the specified collection from the .
+ ///
+ /// The items to remove.
+ /// is null.
+ public void RemoveRange(IEnumerable collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
- ///
- /// Inserts the elements of a collection into the at the specified index.
- ///
- /// The zero-based index at which the new elements should be inserted.
- /// The collection whose elements should be inserted into the List.
- /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
- /// is null.
- /// is not in the collection range.
- public void InsertRange(int index, IEnumerable 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 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)Items;
- target.InsertRange(index, collection);
-
- OnEssentialPropertiesChanged();
-
- if (!(collection is IList list))
- list = new List(collection);
-
- OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
- }
+ if (Count == 0)
+ {
+ return;
+ }
+ else if (collection is ICollection countable)
+ {
+ if (countable.Count == 0)
+ {
+ return;
+ }
+ else if (countable.Count == 1)
+ {
+ using (IEnumerator enumerator = countable.GetEnumerator())
+ {
+ enumerator.MoveNext();
+ Remove(enumerator.Current);
+ return;
+ }
+ }
+ }
+ else if (!collection.Any())
+ {
+ return;
+ }
+ CheckReentrancy();
- ///
- /// Removes the first occurence of each item in the specified collection from the .
- ///
- /// The items to remove.
- /// is null.
- public void RemoveRange(IEnumerable collection)
- {
- if (collection == null)
- throw new ArgumentNullException(nameof(collection));
-
- if (Count == 0)
- return;
- else if (collection is ICollection countable)
- {
- if (countable.Count == 0)
- return;
- else if (countable.Count == 1)
- using (IEnumerator enumerator = countable.GetEnumerator())
- {
- enumerator.MoveNext();
- Remove(enumerator.Current);
- return;
- }
- }
- else if (!collection.Any())
- return;
-
- CheckReentrancy();
-
- var clusters = new Dictionary>();
- var lastIndex = -1;
- List? 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 { item };
- }
-
- OnEssentialPropertiesChanged();
-
- if (Count == 0)
- OnCollectionReset();
- else
- foreach (KeyValuePair> cluster in clusters)
- OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
+ var clusters = new Dictionary>();
+ var lastIndex = -1;
+ List lastCluster = null;
+ foreach (T item in collection)
+ {
+ var index = IndexOf(item);
+ if (index < 0)
+ continue;
- }
+ Items.RemoveAt(index);
- ///
- /// Iterates over the collection and removes all items that satisfy the specified match.
- ///
- /// The complexity is O(n).
- ///
- /// Returns the number of elements that where
- /// is null.
- public int RemoveAll(Predicate match)
- {
- return RemoveAll(0, Count, match);
- }
+ if (lastIndex == index && lastCluster != null)
+ lastCluster.Add(item);
+ else
+ clusters[lastIndex = index] = lastCluster = new List { item };
+ }
- ///
- /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
- ///
- /// The complexity is O(n).
- /// The index of where to start performing the search.
- /// The number of items to iterate on.
- ///
- /// Returns the number of elements that where
- /// is out of range.
- /// is out of range.
- /// is null.
- public int RemoveAll(int index, int count, Predicate 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? 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 { item };
- clusterIndex = index;
+ foreach (KeyValuePair> 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));
- }
+ ///
+ /// Iterates over the collection and removes all items that satisfy the specified match.
+ ///
+ /// The complexity is O(n).
+ /// Returns the number of elements that where
+ /// is null.
+ public int RemoveAll(Predicate match)
+ {
+ return RemoveAll(0, Count, match);
+ }
- if (removedCount > 0)
- OnEssentialPropertiesChanged();
+ ///
+ /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
+ ///
+ /// The complexity is O(n).
+ /// The index of where to start performing the search.
+ /// The number of items to iterate on.
+ /// Returns the number of elements that where
+ /// is out of range.
+ /// is out of range.
+ /// is null.
+ public int RemoveAll(int index, int count, Predicate 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 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 { 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();
- ///
- /// Removes a range of elements from the >.
- ///
- /// The zero-based starting index of the range of elements to remove.
- /// The number of elements to remove.
- /// The specified range is exceeding the collection.
- 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;
+ ///
+ /// Removes a range of elements from the >.
+ ///
+ /// The zero-based starting index of the range of elements to remove.
+ /// The number of elements to remove.
+ /// The specified range is exceeding the collection.
+ 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, see constructors
- var items = (List)Items;
- List removedItems = items.GetRange(index, count);
+ if (count == 1)
+ {
+ RemoveItem(index);
+ return;
+ }
- CheckReentrancy();
+ //Items will always be List, see constructors
+ var items = (List)Items;
+ List 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();
- ///
- /// Clears the current collection and replaces it with the specified collection,
- /// using .
- ///
- /// The items to fill the collection with, after clearing it.
- /// is null.
- public void ReplaceRange(IEnumerable collection)
- {
- ReplaceRange(0, Count, collection);
- }
+ if (Count == 0)
+ OnCollectionReset();
+ else
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
+ }
- ///
- /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
- ///
- /// The index of where to start the replacement.
- /// The number of items to be replaced.
- /// The collection to insert in that location.
- /// is out of range.
- /// is out of range.
- /// is null.
- /// is null.
- public void ReplaceRange(int index, int count, IEnumerable 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 countable)
- {
- if (countable.Count == 0)
+ ///
+ /// Clears the current collection and replaces it with the specified collection,
+ /// using .
+ ///
+ /// The items to fill the collection with, after clearing it.
+ /// is null.
+ public void ReplaceRange(IEnumerable 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 list))
- list = new List(collection);
-
- using (BlockReentrancy())
- using (DeferEvents())
- {
- var rangeCount = index + count;
- var addedCount = list.Count;
-
- var changesMade = false;
- List?
- newCluster = null,
- oldCluster = null;
-
-
- int i = index;
- for (; i < rangeCount && i - index < addedCount; i++)
+
+ ///
+ /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
+ ///
+ /// The index of where to start the replacement.
+ /// The number of items to be replaced.
+ /// The collection to insert in that location.
+ /// is out of range.
+ /// is out of range.
+ /// is null.
+ /// is null.
+ public void ReplaceRange(int index, int count, IEnumerable 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 { @new };
- oldCluster = new List { old };
+ collection =
+ collection
+ .Distinct(Comparer)
+ .ToList();
}
- else
+
+ if (collection is ICollection 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 list))
+ list = new List(collection);
- //exceeding position
- if (count != addedCount)
- {
- var items = (List)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
+ 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 { @new };
+ oldCluster = new List { old };
+ }
+ else
+ {
+ newCluster.Add(@new);
+ oldCluster!.Add(old);
+ }
+
+ changesMade = true;
+ }
+ }
+
+ OnRangeReplaced(i, newCluster!, oldCluster!);
+
+ //exceeding position
+ if (count != addedCount)
+ {
+ var items = (List)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
- ///
- /// Called by base class Collection<T> when the list is being cleared;
- /// raises a CollectionChanged event to any listeners.
- ///
- protected override void ClearItems()
- {
- if (Count == 0)
- return;
+ ///
+ /// Called by base class Collection<T> when the list is being cleared;
+ /// raises a CollectionChanged event to any listeners.
+ ///
+ protected override void ClearItems()
+ {
+ if (Count == 0)
+ return;
- CheckReentrancy();
- base.ClearItems();
- OnEssentialPropertiesChanged();
- OnCollectionReset();
- }
+ CheckReentrancy();
+ base.ClearItems();
+ OnEssentialPropertiesChanged();
+ OnCollectionReset();
+ }
- ///
- protected override void InsertItem(int index, T item)
- {
- if (!AllowDuplicates && Items.Contains(item))
- return;
+ ///
+ protected override void InsertItem(int index, T item)
+ {
+ if (!AllowDuplicates && Items.Contains(item))
+ return;
- base.InsertItem(index, item);
- }
+ base.InsertItem(index, item);
+ }
- ///
- 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);
- }
+ ///
+ protected override void SetItem(int index, T item)
+ {
+ if (AllowDuplicates)
+ {
+ if (Comparer.Equals(this[index], item))
+ return;
+ }
+ else if (Items.Contains(item, Comparer))
+ {
+ return;
+ }
- ///
- /// Raise CollectionChanged event to any listeners.
- /// Properties/methods modifying this ObservableCollection will raise
- /// a collection changed event through this virtual method.
- ///
- ///
- /// When overriding this method, either call its base implementation
- /// or call to guard against reentrant collection changes.
- ///
- 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);
+ ///
+ /// Raise CollectionChanged event to any listeners.
+ /// Properties/methods modifying this ObservableCollection will raise
+ /// a collection changed event through this virtual method.
+ ///
+ ///
+ /// When overriding this method, either call its base implementation
+ /// or call to guard against reentrant collection changes.
+ ///
+ 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
+ //
+ //------------------------------------------------------
- ///
- /// Helper to raise Count property and the Indexer property.
- ///
- void OnEssentialPropertiesChanged()
- {
- OnPropertyChanged(EventArgsCache.CountPropertyChanged);
- OnIndexerPropertyChanged();
- }
+ #region Private Methods
- ///
- /// /// Helper to raise a PropertyChanged event for the Indexer property
- /// ///
- void OnIndexerPropertyChanged() =>
- OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+ ///
+ /// Helper to raise Count property and the Indexer property.
+ ///
+ void OnEssentialPropertiesChanged()
+ {
+ OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+ OnIndexerPropertyChanged();
+ }
- ///
- /// Helper to raise CollectionChanged event to any listeners
- ///
- void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
- OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+ ///
+ /// /// Helper to raise a PropertyChanged event for the Indexer property
+ /// ///
+ void OnIndexerPropertyChanged() =>
+ OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+
+ ///
+ /// Helper to raise CollectionChanged event to any listeners
+ ///
+ void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+
+ ///
+ /// Helper to raise CollectionChanged event with action == Reset to any listeners
+ ///
+ void OnCollectionReset() =>
+ OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+
+ ///
+ /// Helper to raise event for clustered action and clear cluster.
+ ///
+ /// The index of the item following the replacement block.
+ //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable collection, IEqualityComparer comparer),
+ //move when supported language version updated.
+ void OnRangeReplaced(int followingItemIndex, ICollection newCluster, ICollection oldCluster)
+ {
+ if (oldCluster == null || oldCluster.Count == 0)
+ {
+ Debug.Assert(newCluster == null || newCluster.Count == 0);
+ return;
+ }
- ///
- /// Helper to raise CollectionChanged event with action == Reset to any listeners
- ///
- void OnCollectionReset() =>
- OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+ OnCollectionChanged(
+ new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Replace,
+ new List(newCluster),
+ new List(oldCluster),
+ followingItemIndex - oldCluster.Count));
- ///
- /// Helper to raise event for clustered action and clear cluster.
- ///
- /// The index of the item following the replacement block.
- ///
- ///
- //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable collection, IEqualityComparer comparer),
- //move when supported language version updated.
- void OnRangeReplaced(int followingItemIndex, ICollection newCluster, ICollection oldCluster)
- {
- if (oldCluster == null || oldCluster.Count == 0)
- {
- Debug.Assert(newCluster == null || newCluster.Count == 0);
- return;
- }
-
- OnCollectionChanged(
- new NotifyCollectionChangedEventArgs(
- NotifyCollectionChangedAction.Replace,
- new List(newCluster),
- new List(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, IDisposable
- {
- readonly RangeObservableCollection _collection;
- public DeferredEventsCollection(RangeObservableCollection 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, IDisposable
+ {
+ readonly RangeObservableCollection _collection;
+ public DeferredEventsCollection(RangeObservableCollection 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
+
+ }
- ///
- /// To be kept outside , since otherwise, a new instance will be created for each generic type used.
- ///
- 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);
- }
-}
\ No newline at end of file
+ ///
+ /// To be kept outside , since otherwise, a new instance will be created for each generic type used.
+ ///
+#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);
+ }
+}
diff --git a/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs b/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
index 8cdc35178..eb3ce2cb1 100644
--- a/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
+++ b/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
@@ -3,6 +3,7 @@
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 @@ public string FileExtension
{
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 @@ private WriteableBitmap LoadPreviewBitmap()
.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 @@ private WriteableBitmap LoadPreviewBitmap()
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;
+ }
}
}
diff --git a/PixiEditor/Models/DataHolders/Selection.cs b/PixiEditor/Models/DataHolders/Selection.cs
index 78e8e3253..cc11e44fe 100644
--- a/PixiEditor/Models/DataHolders/Selection.cs
+++ b/PixiEditor/Models/DataHolders/Selection.cs
@@ -17,10 +17,10 @@ public class Selection : NotifyableObject
private readonly SKColor selectionBlue;
private Layer selectionLayer;
- public Selection(Coordinates[] selectedPoints)
+ public Selection(Coordinates[] selectedPoints, PixelSize maxSize)
{
SelectedPoints = new ObservableCollection(selectedPoints);
- SelectionLayer = new Layer("_selectionLayer");
+ SelectionLayer = new Layer("_selectionLayer", maxSize.Width, maxSize.Height);
selectionBlue = new SKColor(142, 202, 255, 255);
}
diff --git a/PixiEditor/Models/DataHolders/Surface.cs b/PixiEditor/Models/DataHolders/Surface.cs
index 9d45ecfea..ead2c87e1 100644
--- a/PixiEditor/Models/DataHolders/Surface.cs
+++ b/PixiEditor/Models/DataHolders/Surface.cs
@@ -64,8 +64,9 @@ public Surface(BitmapSource original)
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 @@ public Surface Crop(int x, int y, int width, int height)
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)
diff --git a/PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs b/PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs
index 068240808..79b10f587 100644
--- a/PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs
+++ b/PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs
@@ -9,96 +9,107 @@
namespace PixiEditor.Models.DataHolders
{
-public class WpfObservableRangeCollection : RangeObservableCollection
-{
+ public class WpfObservableRangeCollection : RangeObservableCollection
+ {
public bool SuppressNotify { get; set; } = false;
- DeferredEventsCollection _deferredEvents;
-
- public WpfObservableRangeCollection()
- {
- }
-
- public WpfObservableRangeCollection(IEnumerable collection) : base(collection)
- {
- }
-
- public WpfObservableRangeCollection(List list) : base(list)
- {
- }
-
-
- ///
- /// Raise CollectionChanged event to any listeners.
- /// Properties/methods modifying this ObservableCollection will raise
- /// a collection changed event through this virtual method.
- ///
- ///
- /// When overriding this method, either call its base implementation
- /// or call to guard against reentrant collection changes.
- ///
- protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
- {
+ DeferredEventsCollection _deferredEvents;
+
+ public WpfObservableRangeCollection()
+ {
+ }
+
+ public WpfObservableRangeCollection(IEnumerable collection) : base(collection)
+ {
+ }
+
+ public WpfObservableRangeCollection(List list) : base(list)
+ {
+ }
+
+
+ ///
+ /// Raise CollectionChanged event to any listeners.
+ /// Properties/methods modifying this ObservableCollection will raise
+ /// a collection changed event through this virtual method.
+ ///
+ ///
+ /// When overriding this method, either call its base implementation
+ /// or call to guard against reentrant collection changes.
+ ///
+ protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ {
if (SuppressNotify) return;
- var _deferredEvents = (ICollection) typeof(RangeObservableCollection)
- .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 GetHandlers()
- {
- var info = typeof(ObservableCollection).GetField(nameof(CollectionChanged),
- BindingFlags.Instance | BindingFlags.NonPublic);
- var @event = (MulticastDelegate) info.GetValue(this);
- return @event?.GetInvocationList()
- .Cast()
- .Distinct()
- ?? Enumerable.Empty();
- }
-
- class DeferredEventsCollection : List, IDisposable
- {
- private readonly WpfObservableRangeCollection _collection;
-
- public DeferredEventsCollection(WpfObservableRangeCollection 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()
- .Distinct())
- cv.Refresh();
+#pragma warning disable SA1312 // Variable names should begin with lower-case letter
+ var _deferredEvents = (ICollection)typeof(RangeObservableCollection)
+#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 GetHandlers()
+ {
+ var info = typeof(ObservableCollection).GetField(
+ nameof(CollectionChanged),
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ var @event = (MulticastDelegate)info.GetValue(this);
+ return @event?.GetInvocationList()
+ .Cast()
+ .Distinct()
+ ?? Enumerable.Empty();
+ }
+
+ class DeferredEventsCollection : List, IDisposable
+ {
+ private readonly WpfObservableRangeCollection _collection;
+
+ public DeferredEventsCollection(WpfObservableRangeCollection 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()
+ .Distinct())
+ {
+ cv.Refresh();
+ }
+ }
+ }
}
- }
}
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Dialogs/ConfirmationDialog.cs b/PixiEditor/Models/Dialogs/ConfirmationDialog.cs
index 4d4a87a7f..6d30671c3 100644
--- a/PixiEditor/Models/Dialogs/ConfirmationDialog.cs
+++ b/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 @@ public static ConfirmationType Show(string message, string title)
return ConfirmationType.Canceled;
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Models/Dialogs/ExportFileDialog.cs b/PixiEditor/Models/Dialogs/ExportFileDialog.cs
index 4010c1de2..37c834ccc 100644
--- a/PixiEditor/Models/Dialogs/ExportFileDialog.cs
+++ b/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 @@ public string FilePath
}
}
+ 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 @@ public override bool ShowDialog()
FileWidth = popup.SaveWidth;
FileHeight = popup.SaveHeight;
FilePath = popup.SavePath;
+ ChosenFormat = popup.SaveFormat;
}
return (bool)popup.DialogResult;
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Models/Dialogs/NewFileDialog.cs b/PixiEditor/Models/Dialogs/NewFileDialog.cs
index 315e9c638..568f2ef8c 100644
--- a/PixiEditor/Models/Dialogs/NewFileDialog.cs
+++ b/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
{
diff --git a/PixiEditor/Models/Dialogs/NoticeDialog.cs b/PixiEditor/Models/Dialogs/NoticeDialog.cs
index fad3b81d6..fb8fd5a5e 100644
--- a/PixiEditor/Models/Dialogs/NoticeDialog.cs
+++ b/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();
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs b/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
index c6f10df4d..c9f8b8d66 100644
--- a/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
+++ b/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
@@ -27,7 +27,7 @@ public int Width
if (width != value)
{
width = value;
- RaisePropertyChanged("Width");
+ RaisePropertyChanged(nameof(Width));
}
}
}
@@ -40,7 +40,7 @@ public int Height
if (height != value)
{
height = value;
- RaisePropertyChanged("Height");
+ RaisePropertyChanged(nameof(Height));
}
}
}
@@ -50,41 +50,39 @@ public override bool ShowDialog()
return OpenResizeCanvas ? ShowResizeCanvasDialog() : ShowResizeDocumentCanvas();
}
- private bool ShowResizeDocumentCanvas()
- {
- ResizeDocumentPopup popup = new ResizeDocumentPopup
+ bool ShowDialog()
+ 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();
+ }
- return (bool)popup.DialogResult;
+ private bool ShowResizeCanvasDialog()
+ {
+ return ShowDialog();
}
}
-}
\ No newline at end of file
+}
diff --git a/PixiEditor/Models/Enums/CapType.cs b/PixiEditor/Models/Enums/CapType.cs
deleted file mode 100644
index c62f9ec68..000000000
--- a/PixiEditor/Models/Enums/CapType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace PixiEditor.Models.Enums
-{
- public enum CapType
- {
- Square,
- Round
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Enums/FileType.cs b/PixiEditor/Models/Enums/FileType.cs
index 50af1aee5..9bfde5cd7 100644
--- a/PixiEditor/Models/Enums/FileType.cs
+++ b/PixiEditor/Models/Enums/FileType.cs
@@ -2,6 +2,6 @@
{
public enum FileType
{
- Png = 0
+ Unset, Pixi, Png, Jpeg, Bmp, Gif
}
}
\ No newline at end of file
diff --git a/PixiEditor/Models/Enums/SizeUnit.cs b/PixiEditor/Models/Enums/SizeUnit.cs
new file mode 100644
index 000000000..9f8232667
--- /dev/null
+++ b/PixiEditor/Models/Enums/SizeUnit.cs
@@ -0,0 +1,4 @@
+namespace PixiEditor.Models.Enums
+{
+ public enum SizeUnit { Pixel, Percentage }
+}
diff --git a/PixiEditor/Models/IO/Exporter.cs b/PixiEditor/Models/IO/Exporter.cs
index 488e48721..51b442e75 100644
--- a/PixiEditor/Models/IO/Exporter.cs
+++ b/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 @@ public static bool SaveAsEditableFileWithDialog(Document document, out string pa
{
SaveFileDialog dialog = new SaveFileDialog
{
- Filter = "PixiEditor Files | *.pixi",
- DefaultExt = "pixi"
+ Filter = SupportedFilesHelper.BuildSaveFilter(true),
+ FilterIndex = 0
};
if ((bool)dialog.ShowDialog())
{
@@ -44,10 +50,39 @@ public static bool SaveAsEditableFileWithDialog(Document document, out string pa
/// Path.
public static string SaveAsEditableFile(Document document, string path)
{
- Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+ if (Path.GetExtension(path) != Constants.NativeExtension)
+ {
+ var chosenFormat = ParseImageFormat(Path.GetExtension(path));
+ var bitmap = document.Renderer.FinalBitmap;
+ SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+ }
+ else if(Directory.Exists(Path.GetDirectoryName(path)))
+ {
+ Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+ }
+ else
+ {
+ SaveAsEditableFileWithDialog(document, out path);
+ }
+
return path;
}
+ public static FileType ParseImageFormat(string extension)
+ {
+ return SupportedFilesHelper.ParseImageFormat(extension);
+ }
+
+ static Dictionary> encodersFactory = new Dictionary>();
+
+ static Exporter()
+ {
+ encodersFactory[FileType.Png] = () => new PngBitmapEncoder();
+ encodersFactory[FileType.Jpeg] = () => new JpegBitmapEncoder();
+ encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder();
+ encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
+ }
+
///
/// Creates ExportFileDialog to get width, height and path of file.
///
@@ -55,22 +90,15 @@ public static string SaveAsEditableFile(Document document, string path)
/// Size of file.
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 @@ public static void SaveAsGZippedBytes(string path, Surface surface, SKRectI rect
///
/// Saves image to PNG file.
///
+ /// encoder to do the job.
/// Save file path.
/// File width.
/// File height.
/// Bitmap to save.
- 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");
}
}
}
diff --git a/PixiEditor/Models/IO/FileTypeDialogData.cs b/PixiEditor/Models/IO/FileTypeDialogData.cs
new file mode 100644
index 000000000..4c3928034
--- /dev/null
+++ b/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; }
+
+ ///
+ /// Gets or sets file type extensions e.g. {jpg,jpeg}
+ ///
+ public List Extensions { get; set; }
+
+ ///
+ /// Gets file type's main extensions e.g. jpeg
+ ///
+ public string PrimaryExtension { get => Extensions.FirstOrDefault(); }
+
+ ///
+ /// Gets or sets name displayed before extension e.g. JPEG Files
+ ///
+ public string DisplayName { get; set; }
+
+ public FileTypeDialogData(FileType fileType)
+ {
+ FileType = fileType;
+ Extensions = new List();
+ 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;
+ }
+ }
+}
diff --git a/PixiEditor/Models/IO/FileTypeDialogDataSet.cs b/PixiEditor/Models/IO/FileTypeDialogDataSet.cs
new file mode 100644
index 000000000..23289d8b4
--- /dev/null
+++ b/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 fileTypes;
+ string displayName;
+
+ public FileTypeDialogDataSet(SetKind kind, IEnumerable 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 fileTypes, FileType? fileTypeToSkip = null)
+ {
+ Init(displayName, fileTypes, fileTypeToSkip);
+ }
+
+ private void Init(string displayName, IEnumerable 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));
+ }
+ }
+}
diff --git a/PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs b/PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
deleted file mode 100644
index a1b9fd701..000000000
--- a/PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/IO/Importer.cs b/PixiEditor/Models/IO/Importer.cs
index 376ff4ab6..843ce3b36 100644
--- a/PixiEditor/Models/IO/Importer.cs
+++ b/PixiEditor/Models/IO/Importer.cs
@@ -7,6 +7,7 @@
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 @@ public static Document ImportDocument(string path)
public static bool IsSupportedFile(string path)
{
- path = path.ToLower();
- return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
+ return SupportedFilesHelper.IsSupportedFile(path);
}
public static Surface LoadFromGZippedBytes(string path)
diff --git a/PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs b/PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs
deleted file mode 100644
index b86783ad5..000000000
--- a/PixiEditor/Models/IO/PixiFileMaxSizeChecker.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/ImageManipulation/BitmapUtils.cs b/PixiEditor/Models/ImageManipulation/BitmapUtils.cs
index 0fc627c63..14ef6b5d3 100644
--- a/PixiEditor/Models/ImageManipulation/BitmapUtils.cs
+++ b/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 @@ public static WriteableBitmap GeneratePreviewBitmap(IEnumerable GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
- {
- Dictionary 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 @@ public static SKColor BlendColors(SKColor bottomColor, SKColor topColor)
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 @@ public static SKColor BlendColors(SKColor bottomColor, SKColor topColor)
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();
}
}
}
diff --git a/PixiEditor/Models/ImageManipulation/Morphology.cs b/PixiEditor/Models/ImageManipulation/Morphology.cs
deleted file mode 100644
index b089ae11d..000000000
--- a/PixiEditor/Models/ImageManipulation/Morphology.cs
+++ /dev/null
@@ -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 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 ToCoordinates(byte[,] byteArray, Coordinates offset)
- {
- List output = new List();
- 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 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 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(width + 1, height + 1);
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/ImageManipulation/Transform.cs b/PixiEditor/Models/ImageManipulation/Transform.cs
deleted file mode 100644
index b2d5181cf..000000000
--- a/PixiEditor/Models/ImageManipulation/Transform.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using PixiEditor.Models.Position;
-
-namespace PixiEditor.Models.ImageManipulation
-{
- public static class Transform
- {
- ///
- /// Returns translation between two coordinates.
- ///
- /// Starting coordinate.
- /// New coordinate.
- /// Translation as coordinate.
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Layers/Layer.cs b/PixiEditor/Models/Layers/Layer.cs
index bd673f66a..a83a15a05 100644
--- a/PixiEditor/Models/Layers/Layer.cs
+++ b/PixiEditor/Models/Layers/Layer.cs
@@ -31,32 +31,38 @@ public class Layer : BasicLayer
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 @@ public Thickness Offset
public bool IsReset { get; private set; }
+ public Int32Rect TightBounds => GetContentDimensions();
+ public Int32Rect Bounds => new Int32Rect(OffsetX, OffsetY, Width, Height);
+
public event EventHandler LayerBitmapChanged;
public void InvokeLayerBitmapChange()
@@ -243,12 +252,10 @@ public IEnumerable GetLayers()
///
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 @@ public Int32Rect GetContentDimensions()
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);
diff --git a/PixiEditor/Models/Layers/LayerHelper.cs b/PixiEditor/Models/Layers/LayerHelper.cs
index 642199d12..df6bf73da 100644
--- a/PixiEditor/Models/Layers/LayerHelper.cs
+++ b/PixiEditor/Models/Layers/LayerHelper.cs
@@ -57,7 +57,7 @@ public static void GetCloser(this Layer layer1, Layer layer2, out Layer xCloser,
}
}
- 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 @@ public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string new
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 @@ public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string new
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));
}
}
}
diff --git a/PixiEditor/Models/Layers/LayerStructure.cs b/PixiEditor/Models/Layers/LayerStructure.cs
index 75fef3296..f8722110b 100644
--- a/PixiEditor/Models/Layers/LayerStructure.cs
+++ b/PixiEditor/Models/Layers/LayerStructure.cs
@@ -191,7 +191,7 @@ public void MoveGroup(Guid groupGuid, int newIndex)
return;
}
- PreMoveReassignBounds(parentGroup, group);
+ Unassign(parentGroup, group);
List layersInOrder = GetLayersInOrder(new GroupData(groupTopIndex, groupBottomIndex));
@@ -250,9 +250,9 @@ public bool IsChildOf(Guid layerGuid, GuidStructureItem parent)
///
/// Parent group to reassign data in.
/// Group which data should be reassigned.
- 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));
}
///
@@ -260,7 +260,7 @@ public void PreMoveReassignBounds(GroupData parentGroup, GroupData group)
///
/// Parent group to reassign data in.
/// Layer which data should be reassigned.
- public void PreMoveReassignBounds(GroupData parentGroup, Guid layer)
+ public void Unassign(GroupData parentGroup, Guid layer)
{
PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layer);
}
@@ -270,9 +270,9 @@ public void PreMoveReassignBounds(GroupData parentGroup, Guid layer)
///
/// Parent group to reassign data in.
/// Group which data should be reassigned.
- public void PostMoveReassignBounds(GroupData parentGroup, Guid layerGuid)
+ public void Assign(GroupData parentGroup, Guid layerGuid)
{
- PostMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layerGuid);
+ Assign(GetGroupByGuid(parentGroup.GroupGuid), layerGuid);
}
///
@@ -280,9 +280,9 @@ public void PostMoveReassignBounds(GroupData parentGroup, Guid layerGuid)
///
/// Parent group to reassign data in.
/// Group which data should be reassigned.
- 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));
}
///
@@ -450,7 +450,7 @@ private void PreMoveReassignBounds(GuidStructureItem parentGroup, Guid layer)
}
}
- private void PreMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+ private void Unassign(GuidStructureItem parentGroup, GuidStructureItem group)
{
if (parentGroup != null)
{
@@ -481,7 +481,7 @@ private void PreMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureI
}
}
- private void PostMoveReassignBounds(GuidStructureItem parentGroup, Guid layerGuid)
+ private void Assign(GuidStructureItem parentGroup, Guid layerGuid)
{
if (parentGroup != null)
{
@@ -529,7 +529,7 @@ private void PostMoveReassignBounds(GuidStructureItem parentGroup, Guid layerGui
}
}
- private void PostMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+ private void Assign(GuidStructureItem parentGroup, GuidStructureItem group)
{
if (parentGroup != null)
{
@@ -572,7 +572,7 @@ private void AssignParent(Guid layer, GuidStructureItem parent)
PreMoveReassignBounds(currentParent, layer);
}
- PostMoveReassignBounds(parent, layer);
+ Assign(parent, layer);
LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layer));
}
diff --git a/PixiEditor/Models/Position/MousePositionConverter.cs b/PixiEditor/Models/Position/MousePositionConverter.cs
deleted file mode 100644
index 332df27f2..000000000
--- a/PixiEditor/Models/Position/MousePositionConverter.cs
+++ /dev/null
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/PixiEditor/Models/Tools/BitmapOperationTool.cs b/PixiEditor/Models/Tools/BitmapOperationTool.cs
index 3246d1ec7..56a96113c 100644
--- a/PixiEditor/Models/Tools/BitmapOperationTool.cs
+++ b/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 @@ public abstract class BitmapOperationTool : Tool
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 allLayers, IReadOnlyList recordedMouseMovement, SKColor color);
@@ -49,29 +54,41 @@ public override void AfterUse(SKRectI sessionRect)
_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("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("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) });
}
}
}
diff --git a/PixiEditor/Models/Tools/Tool.cs b/PixiEditor/Models/Tools/Tool.cs
index 3d69f444a..a0122d7db 100644
--- a/PixiEditor/Models/Tools/Tool.cs
+++ b/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();
diff --git a/PixiEditor/Models/Tools/Tools/BrightnessTool.cs b/PixiEditor/Models/Tools/Tools/BrightnessTool.cs
index 6b42afee2..3a5d290c2 100644
--- a/PixiEditor/Models/Tools/Tools/BrightnessTool.cs
+++ b/PixiEditor/Models/Tools/Tools/BrightnessTool.cs
@@ -28,7 +28,7 @@ public BrightnessTool()
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;
diff --git a/PixiEditor/Models/Tools/Tools/CircleTool.cs b/PixiEditor/Models/Tools/Tools/CircleTool.cs
index c3c538303..4293e3f64 100644
--- a/PixiEditor/Models/Tools/Tools/CircleTool.cs
+++ b/PixiEditor/Models/Tools/Tools/CircleTool.cs
@@ -20,7 +20,7 @@ public CircleTool()
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 @@ public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable outlineCoordinates)
diff --git a/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs b/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
index 7e8a54869..e056948ee 100644
--- a/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
+++ b/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
@@ -28,7 +28,7 @@ public ColorPickerTool(DocumentProvider documentProvider, BitmapManager bitmapMa
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 recordedMouseMovement)
{
diff --git a/PixiEditor/Models/Tools/Tools/EraserTool.cs b/PixiEditor/Models/Tools/Tools/EraserTool.cs
index 63e77ead9..8e7f61f62 100644
--- a/PixiEditor/Models/Tools/Tools/EraserTool.cs
+++ b/PixiEditor/Models/Tools/Tools/EraserTool.cs
@@ -18,7 +18,7 @@ public EraserTool(BitmapManager bitmapManager)
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 allLayers, IReadOnlyList recordedMouseMovement, SKColor color)
{
diff --git a/PixiEditor/Models/Tools/Tools/FloodFillTool.cs b/PixiEditor/Models/Tools/Tools/FloodFillTool.cs
index 35b000704..7ba1b4d71 100644
--- a/PixiEditor/Models/Tools/Tools/FloodFillTool.cs
+++ b/PixiEditor/Models/Tools/Tools/FloodFillTool.cs
@@ -20,7 +20,7 @@ public FloodFillTool(BitmapManager bitmapManager)
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 allLayers, IReadOnlyList recordedMouseMovement, SKColor color)
{
diff --git a/PixiEditor/Models/Tools/Tools/LineTool.cs b/PixiEditor/Models/Tools/Tools/LineTool.cs
index 97614cccd..3e69708d9 100644
--- a/PixiEditor/Models/Tools/Tools/LineTool.cs
+++ b/PixiEditor/Models/Tools/Tools/LineTool.cs
@@ -25,7 +25,7 @@ public LineTool()
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 @@ public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable oldSelection;
private List newSelection = new List();
- 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 @@ private void ValidateCache(Document document)
cachedDocument ??= new Layer("_CombinedLayers", BitmapUtils.CombineLayers(
new Int32Rect(0, 0, document.Width, document.Height),
document.Layers,
- document.LayerStructure));
+ document.LayerStructure), document.Width, document.Height);
}
}
}
diff --git a/PixiEditor/Models/Tools/Tools/MoveTool.cs b/PixiEditor/Models/Tools/Tools/MoveTool.cs
index dd14222ff..cb85cbef7 100644
--- a/PixiEditor/Models/Tools/Tools/MoveTool.cs
+++ b/PixiEditor/Models/Tools/Tools/MoveTool.cs
@@ -40,7 +40,7 @@ public MoveTool(BitmapManager bitmapManager)
BitmapManager = bitmapManager;
}
- public override string Tooltip => "Moves selected pixels (V). Hold Ctrl to move all layers.";
+ public override string Tooltip => $"Moves selected pixels ({ShortcutKey}). Hold Ctrl to move all layers.";
public override bool HideHighlight => true;
@@ -69,7 +69,7 @@ public override void BeforeUse()
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 ?
diff --git a/PixiEditor/Models/Tools/Tools/MoveViewportTool.cs b/PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
index 23e6a26c9..a39d8fe32 100644
--- a/PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
+++ b/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 pixels)
{
diff --git a/PixiEditor/Models/Tools/Tools/PenTool.cs b/PixiEditor/Models/Tools/Tools/PenTool.cs
index d6891a6fe..9477309ae 100644
--- a/PixiEditor/Models/Tools/Tools/PenTool.cs
+++ b/PixiEditor/Models/Tools/Tools/PenTool.cs
@@ -49,7 +49,7 @@ public PenTool(BitmapManager bitmapManager)
};
}
- public override string Tooltip => "Standard brush. (B)";
+ public override string Tooltip => $"Standard brush. ({ShortcutKey})";
public bool AutomaticallyResizeCanvas { get; set; } = true;
diff --git a/PixiEditor/Models/Tools/Tools/RectangleTool.cs b/PixiEditor/Models/Tools/Tools/RectangleTool.cs
index b95632bb0..9f4b5202d 100644
--- a/PixiEditor/Models/Tools/Tools/RectangleTool.cs
+++ b/PixiEditor/Models/Tools/Tools/RectangleTool.cs
@@ -17,7 +17,7 @@ public RectangleTool()
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 @@ public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable("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, int thickness)
+ private Int32Rect CreateRectangle(Layer layer, SKColor color, SKColor? fillColor, IReadOnlyList coordinates, int thickness)
{
var (start, end) = Session.IsShiftDown ? CoordinatesHelper.GetSquareCoordiantes(coordinates) : (coordinates[0], coordinates[^1]);
@@ -75,7 +76,9 @@ private void CreateRectangle(Layer layer, SKColor color, SKColor? fillColor, IRe
paint.Color = color;
layer.LayerBitmap.SkiaSurface.Canvas.DrawRect(x, y, w, h, paint);
}
+
layer.InvokeLayerBitmapChange(dirtyRect);
+ return dirtyRect;
}
}
}
diff --git a/PixiEditor/Models/Tools/Tools/SelectTool.cs b/PixiEditor/Models/Tools/Tools/SelectTool.cs
index 490be3ee2..e3c26e609 100644
--- a/PixiEditor/Models/Tools/Tools/SelectTool.cs
+++ b/PixiEditor/Models/Tools/Tools/SelectTool.cs
@@ -36,7 +36,7 @@ public SelectTool(BitmapManager bitmapManager)
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()
{
diff --git a/PixiEditor/Models/Tools/Tools/ZoomTool.cs b/PixiEditor/Models/Tools/Tools/ZoomTool.cs
index 8638d23b8..c10ba8591 100644
--- a/PixiEditor/Models/Tools/Tools/ZoomTool.cs
+++ b/PixiEditor/Models/Tools/Tools/ZoomTool.cs
@@ -18,7 +18,7 @@ public ZoomTool(BitmapManager bitmapManager)
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)
{
diff --git a/PixiEditor/Models/Undo/StorageBasedChange.cs b/PixiEditor/Models/Undo/StorageBasedChange.cs
index c5aba795b..75cef6235 100644
--- a/PixiEditor/Models/Undo/StorageBasedChange.cs
+++ b/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
///
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 layersToStore;
+ private List layersToStore = new List();
public Document Document { get; }
public StorageBasedChange(Document doc, IEnumerable 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 layers, bool useDocumentSize, bool saveOnStartup)
+ {
+ Document = doc;
+ Initialize(layers, DefaultUndoChangeLocation, saveOnStartup, useDocumentSize);
}
public StorageBasedChange(Document doc, IEnumerable 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 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 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 @@ public void SaveLayersOnDevice()
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 @@ public void SaveLayersOnDevice()
layersToStore = new List();
}
+ [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);
+ }
+ }
+
///
/// Loads saved layers from disk.
///
@@ -84,18 +153,17 @@ public Layer[] LoadLayersFromDevice()
{
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 @@ public Change ToChange(Action undoProcess, objec
/// Process that is invoked on redo and undo.
/// Custom parameters for undo and redo process.
/// Undo change description.
- /// UndoManager ready Change instance.
+ /// UndoManager ready 'Change' instance.
public Change ToChange(Action undoRedoProcess, object[] processArgs, string description = "")
{
Action