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 finalProcess = processParameters => { - Layer[] layers = LoadLayersFromDevice(); - GenerateUndoLayers(); + LayerChunk[] chunks = new LayerChunk[layers.Length]; + for (int i = 0; i < layers.Length; i++) + { + chunks[i] = new LayerChunk(layers[i], StoredLayers[i].SerializedRect); + } + + GenerateUndoLayers(chunks); SaveLayersOnDevice(); @@ -243,7 +316,7 @@ public Change ToChange(Action undoProcess, object[] undoProcessParamet /// /// Generates UndoLayer[] StoredLayers data. /// - private void GenerateUndoLayers() + private void GenerateUndoLayers(LayerChunk[] chunks) { StoredLayers = new UndoLayer[layersToStore.Count]; int i = 0; @@ -255,16 +328,15 @@ private void GenerateUndoLayers() throw new ArgumentException("Provided document doesn't contain selected layer"); } - layer.ClipCanvas(); - int index = Document.Layers.IndexOf(layer); - string pngName = layer.Name + Guid.NewGuid().ToString(); + string fileName = layer.Name + Guid.NewGuid(); StoredLayers[i] = new UndoLayer( Path.Join( UndoChangeLocation, - Convert.ToBase64String(Encoding.UTF8.GetBytes(pngName)) + ".png"), + Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName)) + ".undoimg"), layer, - index); + index, + chunks[i].AbsoluteChunkRect); i++; } } @@ -273,408 +345,45 @@ public static void BasicUndoProcess(Layer[] layers, UndoLayer[] data, object[] a { if (args.Length > 0 && args[0] is Document document) { - var ls = document.LayerStructure.CloneGroups(); - for (int i = 0; i < layers.Length; i++) { Layer layer = layers[i]; + UndoLayer layerData = data[i]; + var foundLayer = document.Layers.FirstOrDefault(x => x.GuidValue == layerData.LayerGuid); - document.RemoveLayer(data[i].LayerIndex, false); - document.Layers.Insert(data[i].LayerIndex, layer); + if (foundLayer != null) + { + ApplyChunkToLayer(foundLayer, layerData.SerializedRect, layer.LayerBitmap); + } + else + { + document.RemoveLayer(layerData.LayerIndex, false); + document.Layers.Insert(layerData.LayerIndex, layer); + } - if (data[i].IsActive) + if (layerData.IsActive) { - document.SetMainActiveLayer(data[i].LayerIndex); + document.SetMainActiveLayer(layerData.LayerIndex); } } + } + } - document.BuildLayerStructureProcess(new object[] { ls }); + private static void ApplyChunkToLayer(Layer layer, SKRectI rect, Surface chunk) + { + layer.DynamicResizeAbsolute(rect.ToInt32Rect()); + using var snapshot = chunk.SkiaSurface.Snapshot(); + layer.LayerBitmap.SkiaSurface.Canvas.DrawImage(snapshot, new SKPoint(rect.Left - layer.OffsetX, rect.Top - layer.OffsetY), Surface.ReplacingPaint); + layer.InvokeLayerBitmapChange(rect.ToInt32Rect()); + } + + public void Dispose() + { + for (int i = 0; i < StoredLayers.Length; i++) + { + if (File.Exists(StoredLayers[i].StoredPngLayerName)) + File.Delete(StoredLayers[i].StoredPngLayerName); } } } } - -//using PixiEditor.Models.DataHolders; -//using PixiEditor.Models.IO; -//using PixiEditor.Models.Layers; -//using SkiaSharp; -//using System; -//using System.Collections.Generic; -//using System.IO; -//using System.Linq; -//using System.Text; -//using System.Windows; - -//namespace PixiEditor.Models.Undo -//{ -// /// -// /// A class that allows to save layers on disk and load them on Undo/Redo. -// /// -// public class StorageBasedChange : IDisposable -// { -// public static string DefaultUndoChangeLocation { get; } = Path.Join(Path.GetTempPath(), "PixiEditor", Guid.NewGuid().ToString(), "UndoStack"); - -// public string UndoChangeLocation { get; set; } - -// public UndoLayer[] StoredLayers { get; set; } - -// private List layersToStore = new List(); -// public Document Document { get; } - -// public StorageBasedChange(Document doc, IEnumerable layers, bool saveOnStartup = true) -// { -// Document = doc; -// Initialize(layers, DefaultUndoChangeLocation, saveOnStartup); -// } - -// public StorageBasedChange(Document doc, IEnumerable layers, string undoChangeLocation, bool saveOnStartup = true) -// { -// Document = doc; -// 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(); -// } -// } - -// private void Initialize(IEnumerable layers, string undoChangeLocation, bool saveOnStartup) -// { -// var layersArray = layers as Layer[] ?? layers.ToArray(); -// LayerChunk[] layerChunks = new LayerChunk[layersArray.Length]; -// for (var i = 0; i < layersArray.Length; i++) -// { -// var layer = layersArray[i]; -// layerChunks[i] = new LayerChunk(layer, SKRectI.Create(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height)); -// layersToStore.Add(layer.GuidValue); -// } - -// UndoChangeLocation = undoChangeLocation; -// GenerateUndoLayers(layerChunks); -// if (saveOnStartup) -// { -// SaveLayersOnDevice(); -// } -// } - -// public void SaveLayersOnDevice() -// { -// int i = 0; -// foreach (var layerGuid in layersToStore) -// { -// Layer layer = Document.Layers.First(x => x.GuidValue == layerGuid); -// UndoLayer storedLayer = StoredLayers[i]; -// if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName))) -// { -// // Calculate absolute rect to relative rect -// SKRectI finalRect = SKRectI.Create( -// storedLayer.SerializedRect.Left - layer.OffsetX, -// storedLayer.SerializedRect.Top - layer.OffsetY, -// storedLayer.SerializedRect.Width, -// storedLayer.SerializedRect.Height); - -// using var image = layer.LayerBitmap.SkiaSurface.Snapshot(); -// Surface targetSizeSurface = new Surface(finalRect.Width, finalRect.Height); - -// targetSizeSurface.SkiaSurface.Canvas.DrawImage(image, finalRect, SKRect.Create(0, 0, finalRect.Width, finalRect.Height), Surface.ReplacingPaint); - -// Exporter.SaveAsGZippedBytes(storedLayer.StoredPngLayerName, targetSizeSurface); -// } - -// i++; -// } - -// layersToStore = new List(); -// } - -// /// -// /// Loads saved layers from disk. -// /// -// /// Array of saved layers. -// public Layer[] LoadLayersFromDevice() -// { -// Layer[] layers = new Layer[StoredLayers.Length]; -// for (int i = 0; i < StoredLayers.Length; i++) -// { -// UndoLayer storedLayer = StoredLayers[i]; -// var bitmap = Importer.LoadFromGZippedBytes(storedLayer.StoredPngLayerName); -// layers[i] = new Layer(storedLayer.Name, bitmap) -// { -// Width = storedLayer.Width, -// Height = storedLayer.Height, -// Offset = new Thickness(storedLayer.OffsetX, storedLayer.OffsetY, 0, 0), -// Opacity = storedLayer.Opacity, -// MaxWidth = storedLayer.MaxWidth, -// MaxHeight = storedLayer.MaxHeight, -// IsVisible = storedLayer.IsVisible, -// IsActive = storedLayer.IsActive, -// LayerHighlightColor = storedLayer.LayerHighlightColor -// }; - -// layers[i].ChangeGuid(storedLayer.LayerGuid); - -// File.Delete(StoredLayers[i].StoredPngLayerName); -// } - -// layersToStore = layers.Select(x => x.GuidValue).ToList(); -// return layers; -// } - -// /// -// /// Creates UndoManager ready Change instance, where undo process loads layers from device, and redo saves them. -// /// -// /// Method that is invoked on undo, with loaded layers parameter and UndoLayer array data. -// /// Custom parameters for undo process. -// /// Method that is invoked on redo with custom object array parameters. -// /// Parameters for redo process. -// /// Undo change description. -// /// UndoManager ready Change instance. -// public Change ToChange(Action undoProcess, object[] processArgs, Action redoProcess, object[] redoProcessParameters, string description = "") -// { -// Action finalUndoProcess = processParameters => -// { -// Layer[] layers = LoadLayersFromDevice(); -// undoProcess(layers, StoredLayers, processParameters); -// }; - -// Action finalRedoProcess = parameters => -// { -// SaveLayersOnDevice(); -// redoProcess(parameters); -// }; - -// var change = new Change(finalUndoProcess, processArgs, finalRedoProcess, redoProcessParameters, description); -// change.DisposeProcess = (_, _) => Dispose(); -// return change; -// } - -// /// -// /// Creates UndoManager ready Change instance, where undo and redo is the same, before process images are loaded from disk and current ones are saved. -// /// -// /// Process that is invoked on redo and undo. -// /// Custom parameters for undo and redo process. -// /// Undo change description. -// /// UndoManager ready 'Change' instance. -// public Change ToChange(Action undoRedoProcess, object[] processArgs, string description = "") -// { -// Action finalProcess = processParameters => -// { -// Layer[] layers = LoadLayersFromDevice(); -// LayerChunk[] chunks = new LayerChunk[layers.Length]; -// for (int i = 0; i < layers.Length; i++) -// { -// chunks[i] = new LayerChunk(layers[i], StoredLayers[i].SerializedRect); -// } - -// GenerateUndoLayers(chunks); - -// SaveLayersOnDevice(); - -// undoRedoProcess(layers, StoredLayers, processParameters); -// }; - -// var change = new Change(finalProcess, processArgs, finalProcess, processArgs, description); -// change.DisposeProcess = (_, _) => Dispose(); -// return change; -// } - -// /// -// /// Creates UndoManager ready Change instance, where undo process loads layers from device, and redo saves them. -// /// -// /// Method that is invoked on undo, with loaded layers parameter and UndoLayer array data. -// /// Method that is invoked on redo with custom object array parameters. -// /// Parameters for redo process. -// /// Undo change description. -// /// UndoManager ready Change instance. -// public Change ToChange(Action undoProcess, Action redoProcess, object[] redoProcessParameters, string description = "") -// { -// Action finalUndoProcess = _ => -// { -// Layer[] layers = LoadLayersFromDevice(); -// undoProcess(layers, StoredLayers); -// }; - -// Action finalRedoProcess = parameters => -// { -// SaveLayersOnDevice(); -// redoProcess(parameters); -// }; - -// var change = new Change(finalUndoProcess, null, finalRedoProcess, redoProcessParameters, description); -// change.DisposeProcess = (_, _) => Dispose(); -// return change; -// } - -// /// -// /// Creates UndoManager ready Change instance, where undo process saves layers on device, and redo loads them. -// /// -// /// Method that is invoked on undo, with loaded layers parameter and UndoLayer array data. -// /// Parameters for undo process. -// /// Method that is invoked on redo with custom object array parameters. -// /// Undo change description. -// /// UndoManager ready Change instance. -// public Change ToChange(Action undoProcess, object[] undoProcessParameters, Action redoProcess, string description = "") -// { -// Action finalUndoProcess = parameters => -// { -// SaveLayersOnDevice(); -// undoProcess(parameters); -// }; - -// Action finalRedoProcess = parameters => -// { -// Layer[] layers = LoadLayersFromDevice(); -// redoProcess(layers, StoredLayers); -// }; - -// var change = new Change(finalUndoProcess, undoProcessParameters, finalRedoProcess, null, description); -// change.DisposeProcess = (_, _) => Dispose(); -// return change; -// } - -// /// -// /// Creates UndoManager ready Change instance, where undo process saves layers on device, and redo loads them. -// /// -// /// Method that is invoked on undo, with loaded layers parameter and UndoLayer array data. -// /// Parameters for undo process. -// /// Method that is invoked on redo with custom object array parameters. -// /// Parameters for redo process. -// /// Undo change description. -// /// UndoManager ready Change instance. -// public Change ToChange(Action undoProcess, object[] undoProcessParameters, Action redoProcess, object[] redoProcessArgs, string description = "") -// { -// Action finalUndoProcess = parameters => -// { -// SaveLayersOnDevice(); -// undoProcess(parameters); -// }; - -// Action finalRedoProcess = parameters => -// { -// Layer[] layers = LoadLayersFromDevice(); -// redoProcess(layers, StoredLayers, parameters); -// }; - -// var change = new Change(finalUndoProcess, undoProcessParameters, finalRedoProcess, redoProcessArgs, description); -// change.DisposeProcess = (_, _) => Dispose(); -// return change; -// } - -// /// -// /// Generates UndoLayer[] StoredLayers data. -// /// -// private void GenerateUndoLayers(LayerChunk[] chunks) -// { -// StoredLayers = new UndoLayer[layersToStore.Count]; -// int i = 0; -// foreach (var layerGuid in layersToStore) -// { -// Layer layer = Document.Layers.First(x => x.GuidValue == layerGuid); -// if (!Document.Layers.Contains(layer)) -// { -// throw new ArgumentException("Provided document doesn't contain selected layer"); -// } - -// int index = Document.Layers.IndexOf(layer); -// string fileName = layer.Name + Guid.NewGuid(); -// StoredLayers[i] = new UndoLayer( -// Path.Join( -// UndoChangeLocation, -// Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName)) + ".undoimg"), -// layer, -// index, -// chunks[i].AbsoluteChunkRect); -// i++; -// } -// } - -// public static void BasicUndoProcess(Layer[] layers, UndoLayer[] data, object[] args) -// { -// if (args.Length > 0 && args[0] is Document document) -// { -// for (int i = 0; i < layers.Length; i++) -// { -// Layer layer = layers[i]; -// UndoLayer layerData = data[i]; -// var foundLayer = document.Layers.FirstOrDefault(x => x.GuidValue == layerData.LayerGuid); - -// if (foundLayer != null) -// { -// ApplyChunkToLayer(foundLayer, layerData, layer.LayerBitmap); -// } -// else -// { -// document.RemoveLayer(layerData.LayerIndex, false); -// document.Layers.Insert(layerData.LayerIndex, layer); -// } - -// if (layerData.IsActive) -// { -// document.SetMainActiveLayer(layerData.LayerIndex); -// } -// } -// } -// } - -// private static void ApplyChunkToLayer(Layer layer, UndoLayer layerData, Surface chunk) -// { -// bool widthBigger = layer.Width < chunk.Width; -// bool heightBigger = layer.Height < chunk.Height; -// int targetWidth = widthBigger ? chunk.Width : layer.Width; -// int targetHeight = heightBigger ? chunk.Height : layer.Height; - -// int offsetDiffX = layerData.OffsetX - layer.OffsetX; -// int offsetDiffY = layerData.OffsetY - layer.OffsetY; - -// int targetOffsetX = layerData.OffsetX == 0 && widthBigger ? layerData.SerializedRect.Left : layerData.OffsetX; -// int targetOffsetY = layerData.OffsetY == 0 && heightBigger ? layerData.SerializedRect.Top : layerData.OffsetY; - -// Surface targetSizeSurface = new Surface(targetWidth, targetHeight); -// using var foundLayerSnapshot = layer.LayerBitmap.SkiaSurface.Snapshot(); -// targetSizeSurface.SkiaSurface.Canvas.DrawImage( -// foundLayerSnapshot, -// SKRect.Create(offsetDiffX, offsetDiffY, layer.Width, layer.Height), -// SKRect.Create(0, 0, targetWidth, targetHeight), -// Surface.ReplacingPaint); - -// layer.Offset = new Thickness(targetOffsetX, targetOffsetY, 0, 0); - -// SKRect finalRect = SKRect.Create( -// layerData.SerializedRect.Left - layer.OffsetX, -// layerData.SerializedRect.Top - layer.OffsetY, -// layerData.SerializedRect.Width, -// layerData.SerializedRect.Height); - -// using var snapshot = chunk.SkiaSurface.Snapshot(); - -// targetSizeSurface.SkiaSurface.Canvas.DrawImage( -// snapshot, -// finalRect, -// Surface.ReplacingPaint); - -// layer.LayerBitmap = targetSizeSurface; -// } - -// public void Dispose() -// { -// var layers = LoadLayersFromDevice(); -// foreach (var layer in layers) -// layer.LayerBitmap.Dispose(); -// } -// } -//} diff --git a/PixiEditor/Models/Undo/UndoLayer.cs b/PixiEditor/Models/Undo/UndoLayer.cs index a4102051b..b7538137c 100644 --- a/PixiEditor/Models/Undo/UndoLayer.cs +++ b/PixiEditor/Models/Undo/UndoLayer.cs @@ -36,9 +36,9 @@ public record UndoLayer public float Opacity { get; set; } - //public SKRectI SerializedRect { get; set; } + public SKRectI SerializedRect { get; set; } - public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex/*, SKRectI serializedRect*/) + public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex, SKRectI serializedRect) { StoredPngLayerName = storedPngLayerName; LayerIndex = layerIndex; @@ -54,7 +54,7 @@ public UndoLayer(string storedPngLayerName, Layer layer, int layerIndex/*, SKRec IsActive = layer.IsActive; LayerGuid = layer.GuidValue; LayerHighlightColor = layer.LayerHighlightColor; - //SerializedRect = serializedRect; + SerializedRect = serializedRect; } } } \ No newline at end of file diff --git a/PixiEditor/PixiEditor.csproj b/PixiEditor/PixiEditor.csproj index f2e4c029e..187cb4057 100644 --- a/PixiEditor/PixiEditor.csproj +++ b/PixiEditor/PixiEditor.csproj @@ -182,21 +182,23 @@ - + + 1.0.2 NU1701 + NU1701 - - - + + + diff --git a/PixiEditor/Properties/AssemblyInfo.cs b/PixiEditor/Properties/AssemblyInfo.cs index 8a1c817d2..74c109e93 100644 --- a/PixiEditor/Properties/AssemblyInfo.cs +++ b/PixiEditor/Properties/AssemblyInfo.cs @@ -50,5 +50,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.7.0")] -[assembly: AssemblyFileVersion("0.1.7.0")] \ No newline at end of file +[assembly: AssemblyVersion("0.1.8.0")] +[assembly: AssemblyFileVersion("0.1.8.0")] \ No newline at end of file diff --git a/PixiEditor/Styles/AvalonDock/Themes/Generic.xaml b/PixiEditor/Styles/AvalonDock/Themes/Generic.xaml index 2f6d305d8..102c95451 100644 --- a/PixiEditor/Styles/AvalonDock/Themes/Generic.xaml +++ b/PixiEditor/Styles/AvalonDock/Themes/Generic.xaml @@ -162,8 +162,9 @@ - \ No newline at end of file + diff --git a/PixiEditor/Styles/ImageCheckBoxStyle.xaml b/PixiEditor/Styles/ImageCheckBoxStyle.xaml index aa6bbb99e..1321587af 100644 --- a/PixiEditor/Styles/ImageCheckBoxStyle.xaml +++ b/PixiEditor/Styles/ImageCheckBoxStyle.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PixiEditor.Styles"> - + + + + + - \ No newline at end of file + diff --git a/PixiEditor/Styles/ListSwitchButtonStyle.xaml b/PixiEditor/Styles/ListSwitchButtonStyle.xaml index 87684b5d9..a7101a00a 100644 --- a/PixiEditor/Styles/ListSwitchButtonStyle.xaml +++ b/PixiEditor/Styles/ListSwitchButtonStyle.xaml @@ -7,6 +7,7 @@ + diff --git a/PixiEditor/Styles/PixiListBoxItemStyle.xaml b/PixiEditor/Styles/PixiListBoxItemStyle.xaml new file mode 100644 index 000000000..8db897af9 --- /dev/null +++ b/PixiEditor/Styles/PixiListBoxItemStyle.xaml @@ -0,0 +1,26 @@ + + + diff --git a/PixiEditor/Styles/RadioButtonStyle.xaml b/PixiEditor/Styles/RadioButtonStyle.xaml new file mode 100644 index 000000000..9c49529c1 --- /dev/null +++ b/PixiEditor/Styles/RadioButtonStyle.xaml @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/PixiEditor/Styles/ThemeColors.xaml b/PixiEditor/Styles/ThemeColors.xaml index 952eb65c0..9c64ec2c5 100644 --- a/PixiEditor/Styles/ThemeColors.xaml +++ b/PixiEditor/Styles/ThemeColors.xaml @@ -7,4 +7,7 @@ - \ No newline at end of file + + + + diff --git a/PixiEditor/Styles/ThemeStyle.xaml b/PixiEditor/Styles/ThemeStyle.xaml index b292aab20..fc3432195 100644 --- a/PixiEditor/Styles/ThemeStyle.xaml +++ b/PixiEditor/Styles/ThemeStyle.xaml @@ -5,10 +5,23 @@ + + + + + + + + - \ No newline at end of file + diff --git a/PixiEditor/ViewModels/CrashReportViewModel.cs b/PixiEditor/ViewModels/CrashReportViewModel.cs new file mode 100644 index 000000000..1b84fc676 --- /dev/null +++ b/PixiEditor/ViewModels/CrashReportViewModel.cs @@ -0,0 +1,63 @@ +using GalaSoft.MvvmLight.CommandWpf; +using PixiEditor.Models.DataHolders; +using PixiEditor.Views.Dialogs; +using System.Diagnostics; +using System.Linq; +using System.Windows; + +namespace PixiEditor.ViewModels +{ + public class CrashReportViewModel : ViewModelBase + { + private bool hasRecoveredDocuments = true; + + public CrashReport CrashReport { get; } + + public string ReportText { get; } + + public int DocumentCount { get; } + + public RelayCommand OpenSendCrashReportCommand { get; } + + public RelayCommand RecoverDocumentsCommand { get; } + + public RelayCommand AttachDebuggerCommand { get; } + + public bool IsDebugBuild { get; set; } + + public CrashReportViewModel(CrashReport report) + { + SetIsDebug(); + + CrashReport = report; + ReportText = report.ReportText; + DocumentCount = report.GetDocumentCount(); + OpenSendCrashReportCommand = new(() => new SendCrashReportWindow(CrashReport).Show()); + RecoverDocumentsCommand = new(RecoverDocuments, () => hasRecoveredDocuments, false); + AttachDebuggerCommand = new(AttachDebugger); + } + + public void RecoverDocuments() + { + MainWindow window = MainWindow.CreateWithDocuments(CrashReport.RecoverDocuments()); + + Application.Current.MainWindow = window; + window.Show(); + hasRecoveredDocuments = false; + } + + [Conditional("DEBUG")] + private void SetIsDebug() + { + IsDebugBuild = true; + } + + private void AttachDebugger() + { + if (!Debugger.Launch()) + { + MessageBox.Show("Starting debugger failed", "Starting debugger failed", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} diff --git a/PixiEditor/ViewModels/ImportFilePopupViewModel.cs b/PixiEditor/ViewModels/ImportFilePopupViewModel.cs index 8c6d7cd1b..2ac4dd3dd 100644 --- a/PixiEditor/ViewModels/ImportFilePopupViewModel.cs +++ b/PixiEditor/ViewModels/ImportFilePopupViewModel.cs @@ -1,6 +1,6 @@ -using Microsoft.Win32; -using PixiEditor.Exceptions; +using PixiEditor.Exceptions; using PixiEditor.Helpers; +using PixiEditor.Models.IO; using System; using System.IO; using System.Windows; @@ -15,53 +15,19 @@ internal class ImportFilePopupViewModel : ViewModelBase private int importHeight = 16; private int importWidth = 16; - - private string pathButtonBorder = "#f08080"; - - private bool pathIsCorrect; - public ImportFilePopupViewModel() { CloseButtonCommand = new RelayCommand(CloseWindow); DragMoveCommand = new RelayCommand(MoveWindow); - ChoosePathCommand = new RelayCommand(ChoosePath); - OkCommand = new RelayCommand(OkButton, CanClickOk); + OkCommand = new RelayCommand(OkButton); } public RelayCommand CloseButtonCommand { get; set; } public RelayCommand DragMoveCommand { get; set; } - public RelayCommand ChoosePathCommand { get; set; } - public RelayCommand OkCommand { get; set; } - public string PathButtonBorder - { - get => pathButtonBorder; - set - { - if (pathButtonBorder != value) - { - pathButtonBorder = value; - RaisePropertyChanged("PathButtonBorder"); - } - } - } - - public bool PathIsCorrect - { - get => pathIsCorrect; - set - { - if (pathIsCorrect != value) - { - pathIsCorrect = value; - RaisePropertyChanged("PathIsCorrect"); - } - } - } - public string FilePath { get => filePath; @@ -71,7 +37,7 @@ public string FilePath { filePath = value; CheckForPath(value); - RaisePropertyChanged("FilePath"); + RaisePropertyChanged(nameof(FilePath)); } } } @@ -84,7 +50,7 @@ public int ImportWidth if (importWidth != value) { importWidth = value; - RaisePropertyChanged("ImportWidth"); + RaisePropertyChanged(nameof(ImportWidth)); } } } @@ -97,47 +63,19 @@ public int ImportHeight if (importHeight != value) { importHeight = value; - RaisePropertyChanged("ImportHeight"); - } - } - } - - /// - /// Command that handles Path choosing to save file. - /// - /// Binding parameter. - private void ChoosePath(object parameter) - { - OpenFileDialog path = new OpenFileDialog - { - Title = "Import path", - CheckPathExists = true, - Filter = "Image Files|*.png;*.jpeg;*.jpg" - }; - if (path.ShowDialog() == true) - { - if (string.IsNullOrEmpty(path.FileName) == false) - { - CheckForPath(path.FileName); - } - else - { - PathButtonBorder = "#f08080"; - PathIsCorrect = false; + RaisePropertyChanged(nameof(ImportHeight)); } } } private void CheckForPath(string path) { - if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpeg") || path.EndsWith(".jpg"))) + if (SupportedFilesHelper.IsSupportedFile(path)) { try { - PathButtonBorder = "#b8f080"; - PathIsCorrect = true; filePath = path; - BitmapImage bitmap = new BitmapImage(new Uri(path)); + var bitmap = new BitmapImage(new Uri(path)); ImportHeight = bitmap.PixelHeight; ImportWidth = bitmap.PixelWidth; } @@ -168,10 +106,5 @@ private void OkButton(object parameter) ((Window)parameter).DialogResult = true; CloseButton(parameter); } - - private bool CanClickOk(object property) - { - return PathIsCorrect; - } } -} \ No newline at end of file +} diff --git a/PixiEditor/ViewModels/MenuButtonViewModel.cs b/PixiEditor/ViewModels/MenuButtonViewModel.cs deleted file mode 100644 index abf7e7fbd..000000000 --- a/PixiEditor/ViewModels/MenuButtonViewModel.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Windows; -using PixiEditor.Helpers; - -namespace PixiEditor.ViewModels -{ - internal class MenuButtonViewModel : ViewModelBase - { - private Visibility listViewVisibility; - - public MenuButtonViewModel() - { - OpenListViewCommand = new RelayCommand(OpenListView); - CloseListViewCommand = new RelayCommand(CloseListView); - ListViewVisibility = Visibility.Hidden; - } - - public RelayCommand OpenListViewCommand { get; set; } - - public RelayCommand CloseListViewCommand { get; set; } - - public Visibility ListViewVisibility - { - get => listViewVisibility; - set - { - listViewVisibility = value; - RaisePropertyChanged("ListViewVisibility"); - } - } - - private void OpenListView(object parameter) - { - ListViewVisibility = ListViewVisibility == Visibility.Hidden ? Visibility.Visible : Visibility.Hidden; - } - - private void CloseListView(object parameter) - { - ListViewVisibility = Visibility.Hidden; - } - } -} \ No newline at end of file diff --git a/PixiEditor/ViewModels/SaveFilePopupViewModel.cs b/PixiEditor/ViewModels/SaveFilePopupViewModel.cs index fcff786b9..9371e8ff0 100644 --- a/PixiEditor/ViewModels/SaveFilePopupViewModel.cs +++ b/PixiEditor/ViewModels/SaveFilePopupViewModel.cs @@ -1,5 +1,8 @@ using Microsoft.Win32; using PixiEditor.Helpers; +using PixiEditor.Models.Enums; +using PixiEditor.Models.IO; +using System.IO; using System.Windows; namespace PixiEditor.ViewModels @@ -7,91 +10,66 @@ namespace PixiEditor.ViewModels internal class SaveFilePopupViewModel : ViewModelBase { private string _filePath; - - - private string _pathButtonBorder = "#f08080"; - - - private bool _pathIsCorrect; + private FileType _chosenFormat; public SaveFilePopupViewModel() { CloseButtonCommand = new RelayCommand(CloseWindow); DragMoveCommand = new RelayCommand(MoveWindow); - ChoosePathCommand = new RelayCommand(ChoosePath); - OkCommand = new RelayCommand(OkButton, CanClickOk); + OkCommand = new RelayCommand(OkButton); } public RelayCommand CloseButtonCommand { get; set; } public RelayCommand DragMoveCommand { get; set; } - public RelayCommand ChoosePathCommand { get; set; } public RelayCommand OkCommand { get; set; } - public string PathButtonBorder - { - get => _pathButtonBorder; - set - { - if (_pathButtonBorder != value) - { - _pathButtonBorder = value; - RaisePropertyChanged("PathButtonBorder"); - } - } - } - - public bool PathIsCorrect + public string FilePath { - get => _pathIsCorrect; + get => _filePath; set { - if (_pathIsCorrect != value) + if (_filePath != value) { - _pathIsCorrect = value; - RaisePropertyChanged("PathIsCorrect"); + _filePath = value; + RaisePropertyChanged(nameof(FilePath)); } } } - public string FilePath - { - get => _filePath; + public FileType ChosenFormat + { + get => _chosenFormat; set { - if (_filePath != value) + if (_chosenFormat != value) { - _filePath = value; - RaisePropertyChanged("FilePath"); + _chosenFormat = value; + RaisePropertyChanged(nameof(ChosenFormat)); } } } - + /// /// Command that handles Path choosing to save file /// - private void ChoosePath(object parameter) + private string ChoosePath() { SaveFileDialog path = new SaveFileDialog { Title = "Export path", CheckPathExists = true, - DefaultExt = "PNG Image (.png) | *.png", - Filter = "PNG Image (.png) | *.png" + Filter = SupportedFilesHelper.BuildSaveFilter(false), + FilterIndex = 0 }; if (path.ShowDialog() == true) { if (string.IsNullOrEmpty(path.FileName) == false) { - PathButtonBorder = "#b8f080"; - PathIsCorrect = true; - FilePath = path.FileName; - } - else - { - PathButtonBorder = "#f08080"; - PathIsCorrect = false; + ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName)); + return path.FileName; } } + return null; } private void CloseWindow(object parameter) @@ -107,13 +85,13 @@ private void MoveWindow(object parameter) private void OkButton(object parameter) { + string path = ChoosePath(); + if (path == null) + return; + FilePath = path; + ((Window)parameter).DialogResult = true; CloseButton(parameter); } - - private bool CanClickOk(object property) - { - return PathIsCorrect; - } } } diff --git a/PixiEditor/ViewModels/SettingsWindowViewModel.cs b/PixiEditor/ViewModels/SettingsWindowViewModel.cs index 8e14450b5..188dc5884 100644 --- a/PixiEditor/ViewModels/SettingsWindowViewModel.cs +++ b/PixiEditor/ViewModels/SettingsWindowViewModel.cs @@ -10,20 +10,6 @@ namespace PixiEditor.ViewModels { public class SettingsWindowViewModel : ViewModelBase { - public RelayCommand SelectCategoryCommand { get; set; } - - private string selectedCategory = "General"; - - public string SelectedCategory - { - get => selectedCategory; - set - { - selectedCategory = value; - RaisePropertyChanged(nameof(SelectedCategory)); - } - } - public bool ShowUpdateTab { get @@ -41,15 +27,6 @@ public bool ShowUpdateTab public SettingsWindowViewModel() { SettingsSubViewModel = new SettingsViewModel(this); - SelectCategoryCommand = new RelayCommand(SelectCategory); - } - - private void SelectCategory(object parameter) - { - if (parameter is not null && parameter is string value) - { - SelectedCategory = value; - } } } -} \ No newline at end of file +} diff --git a/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs index 3d12de78e..efdac089d 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs @@ -45,7 +45,8 @@ public void Cut(object parameter) public void Paste(object parameter) { - ClipboardController.PasteFromClipboard(); + if (Owner.BitmapManager.ActiveDocument == null) return; + ClipboardController.PasteFromClipboard(Owner.BitmapManager.ActiveDocument); } private bool CanPaste(object property) diff --git a/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs index 3f60d21b7..0810f8a1e 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs @@ -1,6 +1,5 @@ using PixiEditor.Helpers; using System; -using System.Diagnostics; using System.IO; using System.Reflection; @@ -12,30 +11,24 @@ public class DebugViewModel : SubViewModel public RelayCommand OpenInstallLocationCommand { get; set; } + public RelayCommand CrashCommand { get; set; } + public DebugViewModel(ViewModelMain owner) : base(owner) { OpenFolderCommand = new RelayCommand(OpenFolder); OpenInstallLocationCommand = new RelayCommand(OpenInstallLocation); + CrashCommand = new RelayCommand(_ => throw new InvalidOperationException("Debug Crash")); } public static void OpenFolder(object parameter) { - OpenShellExecute((string)parameter); + ProcessHelpers.ShellExecuteEV(parameter as string); } public static void OpenInstallLocation(object parameter) { - OpenShellExecute(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - } - - private static void OpenShellExecute(string path) - { - ProcessStartInfo startInfo = new (Environment.ExpandEnvironmentVariables(path)); - - startInfo.UseShellExecute = true; - - Process.Start(startInfo); + ProcessHelpers.ShellExecuteEV(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); } } } \ No newline at end of file diff --git a/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs index 937f9abdb..5ef1308be 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs @@ -1,8 +1,8 @@ -using System; -using DiscordRPC; +using DiscordRPC; using PixiEditor.Helpers.Extensions; using PixiEditor.Models.DataHolders; using PixiEditor.Models.UserPreferences; +using System; namespace PixiEditor.ViewModels.SubViewModels.Main { @@ -158,7 +158,7 @@ private static RichPresence NewDefaultRP() Assets = new Assets { LargeImageKey = "editorlogo", - LargeImageText = "You discovered PixiEditor's logo", + LargeImageText = "You've discovered PixiEditor's logo", SmallImageKey = "github", SmallImageText = "Download PixiEditor on GitHub (github.com/PixiEditor/PixiEditor)!" }, @@ -210,4 +210,4 @@ private void OnReady(object sender, DiscordRPC.Message.ReadyMessage args) Enabled = false; } } -} \ No newline at end of file +} diff --git a/PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs index 4556a3c03..b835c645b 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs @@ -8,7 +8,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main { public class DocumentViewModel : SubViewModel { - public const string ConfirmationDialogMessage = "Document was modified. Do you want to save changes?"; + public const string ConfirmationDialogTitle = "Unsaved changes"; + public const string ConfirmationDialogMessage = "The document has been modified. Do you want to save changes?"; public RelayCommand CenterContentCommand { get; set; } @@ -61,7 +62,7 @@ public void RequestCloseDocument(Document document) { if (!document.ChangesSaved) { - ConfirmationType result = ConfirmationDialog.Show(ConfirmationDialogMessage); + ConfirmationType result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle); if (result == ConfirmationType.Yes) { Owner.FileSubViewModel.SaveDocument(false); diff --git a/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs index 7e64734f2..e0985b9fd 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq; using PixiEditor.Exceptions; using PixiEditor.Helpers; +using PixiEditor.Models; using PixiEditor.Models.DataHolders; using PixiEditor.Models.Dialogs; using PixiEditor.Models.IO; @@ -10,6 +11,7 @@ using PixiEditor.Views.Dialogs; using System; using System.Collections.Generic; +using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Windows; @@ -80,7 +82,7 @@ public void OpenRecent(object parameter) if (!File.Exists(path)) { - NoticeDialog.Show("The file does no longer exist at that path"); + NoticeDialog.Show("The file does not exist", "Failed to open the file"); RecentlyOpened.Remove(path); return; } @@ -146,6 +148,7 @@ public void OpenFile(string path) Owner.BitmapManager.ActiveDocument.AddNewLayer( "Image", Importer.ImportImage(dialog.FilePath, dialog.FileWidth, dialog.FileHeight)); + Owner.BitmapManager.ActiveDocument.UpdatePreviewImage(); } } @@ -176,7 +179,7 @@ public void Open(string path) } catch (CorruptedFileException ex) { - NoticeDialog.Show(ex.Message, "Failed to open file."); + NoticeDialog.Show(ex.Message, "Failed to open the file"); } catch (OldFileFormatException) { @@ -186,12 +189,13 @@ public void Open(string path) private void Owner_OnStartupEvent(object sender, System.EventArgs e) { - var lastArg = Environment.GetCommandLineArgs().Last(); - if (Importer.IsSupportedFile(lastArg) && File.Exists(lastArg)) + var args = Environment.GetCommandLineArgs(); + var file = args.Last(); + if (Importer.IsSupportedFile(file) && File.Exists(file)) { - Open(lastArg); + Open(file); } - else + else if (Owner.BitmapManager.Documents.Count == 0 || !args.Contains("--crash")) { if (IPreferences.Current.GetPreference("ShowStartupWindow", true)) { @@ -199,16 +203,15 @@ private void Owner_OnStartupEvent(object sender, System.EventArgs e) } } } - + private void Open(object property) { + var filter = SupportedFilesHelper.BuildOpenFilter(); + OpenFileDialog dialog = new OpenFileDialog { - Filter = - "Any|*.pixi;*.png;*.jpg;*.jpeg;|" + - "PixiEditor Files | *.pixi|" + - "Image Files|*.png;*.jpg;*.jpeg;", - DefaultExt = "pixi" + Filter = filter, + FilterIndex = 0 }; if ((bool)dialog.ShowDialog()) @@ -244,8 +247,7 @@ private void SaveDocument(object parameter) { bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew"; if (paramIsAsNew || - string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath) || - !Owner.BitmapManager.ActiveDocument.DocumentFilePath.EndsWith(".pixi")) + string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) { Owner.BitmapManager.ActiveDocument.SaveWithDialog(); } diff --git a/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs index 991b5a15f..18da65c61 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs @@ -1,8 +1,10 @@ using PixiEditor.Helpers; using PixiEditor.Models.Controllers; using PixiEditor.Models.Controllers.Shortcuts; +using PixiEditor.Models.Tools; using PixiEditor.Models.Tools.Tools; using System; +using System.Windows; using System.Windows.Input; namespace PixiEditor.ViewModels.SubViewModels.Main @@ -17,14 +19,10 @@ public class IoViewModel : SubViewModel public RelayCommand MouseUpCommand { get; set; } - public RelayCommand KeyDownCommand { get; set; } - - public RelayCommand KeyUpCommand { get; set; } - private bool restoreToolOnKeyUp = false; private MouseInputFilter filter = new(); - + public IoViewModel(ViewModelMain owner) : base(owner) { @@ -33,17 +31,42 @@ public IoViewModel(ViewModelMain owner) MouseUpCommand = new RelayCommand(filter.MouseUp); PreviewMouseMiddleButtonCommand = new RelayCommand(OnPreviewMiddleMouseButton); GlobalMouseHook.OnMouseUp += filter.MouseUp; - KeyDownCommand = new RelayCommand(OnKeyDown); - KeyUpCommand = new RelayCommand(OnKeyUp); + + InputManager.Current.PreProcessInput += Current_PreProcessInput; filter.OnMouseDown += OnMouseDown; filter.OnMouseMove += OnMouseMove; filter.OnMouseUp += OnMouseUp; } - private void OnKeyDown(object parameter) + private void Current_PreProcessInput(object sender, PreProcessInputEventArgs e) + { + if (e != null && e.StagingItem != null && e.StagingItem.Input != null) + { + InputEventArgs inputEvent = e.StagingItem.Input; + + if (inputEvent is KeyboardEventArgs) + { + KeyboardEventArgs k = inputEvent as KeyboardEventArgs; + RoutedEvent r = k.RoutedEvent; + KeyEventArgs keyEvent = k as KeyEventArgs; + + if (keyEvent != null && keyEvent?.InputSource?.RootVisual != MainWindow.Current) return; + if (r == Keyboard.KeyDownEvent) + { + OnKeyDown(keyEvent); + } + + if (r == Keyboard.KeyUpEvent) + { + OnKeyUp(keyEvent); + } + } + } + } + + private void OnKeyDown(KeyEventArgs args) { - KeyEventArgs args = (KeyEventArgs)parameter; var key = args.Key; if (key == Key.System) key = args.SystemKey; @@ -54,6 +77,24 @@ private void OnKeyDown(object parameter) { Owner.BitmapManager.InputTarget.OnKeyDown(key); } + + HandleTransientKey(args, true); + } + + private void HandleTransientKey(KeyEventArgs args, bool state) + { + var controller = Owner.ShortcutController; + + Key finalKey = args.Key; + if (finalKey == Key.System) + { + finalKey = args.SystemKey; + } + + if (controller.TransientShortcuts.ContainsKey(finalKey)) + { + ChangeToolState(controller.TransientShortcuts[finalKey].GetType(), state); + } } private void ProcessShortcutDown(bool isRepeat, Key key) @@ -62,15 +103,14 @@ private void ProcessShortcutDown(bool isRepeat, Key key) Owner.ShortcutController.LastShortcut.Command == Owner.ToolsSubViewModel.SelectToolCommand) { restoreToolOnKeyUp = true; - ShortcutController.BlockShortcutExecution = true; + ShortcutController.BlockShortcutExection("ShortcutDown"); } Owner.ShortcutController.KeyPressed(key, Keyboard.Modifiers); } - private void OnKeyUp(object parameter) + private void OnKeyUp(KeyEventArgs args) { - KeyEventArgs args = (KeyEventArgs)parameter; var key = args.Key; if (key == Key.System) key = args.SystemKey; @@ -79,6 +119,8 @@ private void OnKeyUp(object parameter) if (Owner.BitmapManager.ActiveDocument != null) Owner.BitmapManager.InputTarget.OnKeyUp(key); + + HandleTransientKey(args, false); } private void ProcessShortcutUp(Key key) @@ -88,7 +130,7 @@ private void ProcessShortcutUp(Key key) { restoreToolOnKeyUp = false; Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool); - ShortcutController.BlockShortcutExecution = false; + ShortcutController.UnblockShortcutExecution("ShortcutDown"); } } @@ -107,7 +149,32 @@ private void OnMouseDown(object sender, MouseButton button) private void OnPreviewMiddleMouseButton(object sender) { - Owner.ToolsSubViewModel.SetActiveTool(); + ChangeToolState(true); + } + + private void ChangeToolState(bool setOn) + where T : Tool + { + ChangeToolState(typeof(T), setOn); + } + + private void ChangeToolState(Type type, bool setOn) + { + if (setOn) + { + var transientToolIsActive = Owner.ToolsSubViewModel.ActiveTool.GetType() == type; + if (!transientToolIsActive) + { + Owner.ToolsSubViewModel.SetActiveTool(type); + Owner.ToolsSubViewModel.ActiveToolIsTransient = true; + } + } + else if (Owner.ToolsSubViewModel.LastActionTool != null && Owner.ToolsSubViewModel.ActiveToolIsTransient) + { + Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool); + restoreToolOnKeyUp = false; + ShortcutController.UnblockShortcutExecution("ShortcutDown"); + } } private void OnMouseMove(object sender, EventArgs args) @@ -128,8 +195,7 @@ private void OnMouseUp(object sender, MouseButton button) } else if (button == MouseButton.Middle) { - if (Owner.ToolsSubViewModel.LastActionTool != null) - Owner.ToolsSubViewModel.SetActiveTool(Owner.ToolsSubViewModel.LastActionTool); + ChangeToolState(false); } } } diff --git a/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs index 1456dff27..8f875d499 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs @@ -1,7 +1,6 @@ using PixiEditor.Helpers; using PixiEditor.Models.Controllers; using PixiEditor.Models.Layers; -using PixiEditor.Models.Undo; using PixiEditor.Views.UserControls.Layers; using System; using System.Linq; @@ -48,7 +47,7 @@ public LayersViewModel(ViewModelMain owner) NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer); NewGroupCommand = new RelayCommand(NewGroup, CanCreateNewLayer); CreateGroupFromActiveLayersCommand = new RelayCommand(CreateGroupFromActiveLayers, CanCreateGroupFromSelected); - DeleteLayersCommand = new RelayCommand(DeleteLayer, CanDeleteLayer); + DeleteLayersCommand = new RelayCommand(DeleteActiveLayers, CanDeleteActiveLayers); DuplicateLayerCommand = new RelayCommand(DuplicateLayer, CanDuplicateLayer); MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack); MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront); @@ -57,7 +56,7 @@ public LayersViewModel(ViewModelMain owner) MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove); MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow); RenameGroupCommand = new RelayCommand(RenameGroup); - DeleteGroupCommand = new RelayCommand(DeleteGroup); + DeleteGroupCommand = new RelayCommand(DeleteGroup, CanDeleteGroup); DeleteSelectedCommand = new RelayCommand(DeleteSelected, CanDeleteSelected); Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged; } @@ -73,22 +72,37 @@ public void CreateGroupFromActiveLayers(object parameter) public bool CanDeleteSelected(object parameter) { - return ( - ( - parameter is not null and (Layer or LayerGroup)) || (Owner.BitmapManager?.ActiveDocument?.ActiveLayer != null) - ) - && Owner.BitmapManager.ActiveDocument != null; + bool paramIsLayerOrGroup = parameter is not null and (Layer or LayerGroup); + bool activeLayerExists = Owner.BitmapManager?.ActiveDocument?.ActiveLayer != null; + bool activeDocumentExists = Owner.BitmapManager.ActiveDocument != null; + bool allGood = (paramIsLayerOrGroup || activeLayerExists) && activeDocumentExists; + if (!allGood) + return false; + + if (parameter is Layer or LayerStructureItemContainer) + { + return CanDeleteActiveLayers(null); + } + else if (parameter is LayerGroup group) + { + return CanDeleteGroup(group.GuidValue); + } + else if (parameter is LayerGroupControl groupControl) + { + return CanDeleteGroup(groupControl.GroupGuid); + } + else if (Owner.BitmapManager.ActiveDocument.ActiveLayer != null) + { + return CanDeleteActiveLayers(null); + } + return false; } public void DeleteSelected(object parameter) { - if (parameter is Layer layer) - { - DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(layer)); - } - else if (parameter is LayerStructureItemContainer container) + if (parameter is Layer or LayerStructureItemContainer) { - DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(container.Layer)); + DeleteActiveLayers(null); } else if (parameter is LayerGroup group) { @@ -100,14 +114,35 @@ public void DeleteSelected(object parameter) } else if (Owner.BitmapManager.ActiveDocument.ActiveLayer != null) { - DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(Owner.BitmapManager.ActiveDocument.ActiveLayer)); + DeleteActiveLayers(null); } } + public bool CanDeleteGroup(object parameter) + { + if (parameter is not Guid guid) + return false; + + var document = Owner.BitmapManager.ActiveDocument; + if (document == null) + return false; + + var group = document.LayerStructure.GetGroupByGuid(guid); + if (group == null) + return false; + + return document.LayerStructure.GetGroupLayers(group).Count < document.Layers.Count; + } + public void DeleteGroup(object parameter) { if (parameter is Guid guid) { + foreach (var layer in Owner.BitmapManager.ActiveDocument?.Layers) + { + layer.IsActive = false; + } + var group = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupByGuid(guid); var layers = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupLayers(group); foreach (var layer in layers) @@ -182,7 +217,11 @@ public void NewLayer(object parameter) if (doc.Layers.Count > 1) { doc.MoveLayerInStructure(doc.Layers[^1].GuidValue, lastActiveLayerGuid, true); - Guid? parent = parameter is Layer or LayerStructureItemContainer ? activeLayerParent?.GroupGuid : activeLayerParent.Parent?.GroupGuid; + Guid? parent = null; + if (activeLayerParent != null) + { + parent = parameter is Layer or LayerStructureItemContainer ? activeLayerParent?.GroupGuid : activeLayerParent.Parent?.GroupGuid; + } doc.LayerStructure.AssignParent(doc.ActiveLayerGuid, parent); doc.AddLayerStructureToUndo(oldGroups); doc.UndoManager.SquashUndoChanges(3, "Add New Layer"); @@ -224,15 +263,18 @@ public void SetActiveLayer(object parameter) } } - public void DeleteLayer(object parameter) + public void DeleteActiveLayers(object unusedParameter) { var doc = Owner.BitmapManager.ActiveDocument; doc.RemoveActiveLayers(); } - public bool CanDeleteLayer(object property) + public bool CanDeleteActiveLayers(object unusedParam) { - return Owner.BitmapManager.ActiveDocument != null && Owner.BitmapManager.ActiveDocument.Layers.Count > 1; + if (Owner.BitmapManager.ActiveDocument == null) + return false; + int activeLayerCount = Owner.BitmapManager.ActiveDocument.Layers.Where(layer => layer.IsActive).Count(); + return Owner.BitmapManager.ActiveDocument.Layers.Count > activeLayerCount; } public void DuplicateLayer(object parameter) @@ -247,6 +289,9 @@ public bool CanDuplicateLayer(object property) public void RenameLayer(object parameter) { + if (Owner.BitmapManager.ActiveDocument == null) + return; + int? index = (int?)parameter; if (index == null) diff --git a/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs index 7b7a8da4d..d320e5cab 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs @@ -40,18 +40,12 @@ private void OpenSettingsWindow(object parameter) private void OpenHyperlink(object parameter) { - if (parameter == null) + if (parameter is not string s) { return; } - var url = (string)parameter; - var processInfo = new ProcessStartInfo() - { - FileName = url, - UseShellExecute = true - }; - Process.Start(processInfo); + ProcessHelpers.ShellExecute(s); } private void OpenShortcutWindow(object parameter) diff --git a/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs index 006e0666a..edce21bcd 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs @@ -5,6 +5,7 @@ using PixiEditor.Models.Tools; using PixiEditor.Models.Tools.Tools; using PixiEditor.Models.Tools.ToolSettings.Settings; +using PixiEditor.Models.UserPreferences; using System; using System.Collections.Generic; using System.Linq; @@ -23,6 +24,8 @@ public class ToolsViewModel : SubViewModel public Tool LastActionTool { get; private set; } + public bool ActiveToolIsTransient { get; set; } + public Cursor ToolCursor { get => toolCursor; @@ -54,7 +57,7 @@ public int ToolSize } } - public IEnumerable ToolSet { get; private set; } + public List ToolSet { get; private set; } public event EventHandler SelectedToolChanged; @@ -67,13 +70,21 @@ public ToolsViewModel(ViewModelMain owner) public void SetupTools(IServiceProvider services) { - ToolSet = services.GetServices(); + ToolSet = services.GetServices().ToList(); SetActiveTool(); Owner.BitmapManager.BitmapOperations.BitmapChanged += (_, _) => TriggerCacheOutdated(); Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged; } + public void SetupToolsTooltipShortcuts(IServiceProvider services) + { + foreach (var tool in ToolSet) + { + tool.ShortcutKey = Owner.ShortcutController.GetToolShortcutKey(tool.GetType()); + } + } + public void SetActiveTool() where T : Tool { @@ -82,9 +93,16 @@ public void SetActiveTool() public void SetActiveTool(Tool tool) { + if (ActiveTool == tool) return; + ActiveToolIsTransient = false; + bool shareToolbar = IPreferences.Current.GetPreference("EnableSharedToolbar"); if (ActiveTool != null) { activeTool.IsActive = false; + if (shareToolbar) + { + ActiveTool.Toolbar.SaveToolbarSettings(); + } } LastActionTool = ActiveTool; @@ -92,6 +110,11 @@ public void SetActiveTool(Tool tool) ActiveTool = tool; + if (shareToolbar) + { + ActiveTool.Toolbar.LoadSharedSettings(); + } + if (LastActionTool != ActiveTool) SelectedToolChanged?.Invoke(this, new SelectedToolEventArgs(LastActionTool, ActiveTool)); @@ -173,8 +196,9 @@ private void ChangeToolSize(object parameter) } } - private void SetActiveTool(Type toolType) + public void SetActiveTool(Type toolType) { + if (!typeof(Tool).IsAssignableFrom(toolType)) { throw new ArgumentException($"'{toolType}' does not inherit from {typeof(Tool)}"); } Tool foundTool = ToolSet.First(x => x.GetType() == toolType); SetActiveTool(foundTool); } diff --git a/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs index 6941058af..400edc362 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs @@ -34,7 +34,10 @@ public void Redo(object parameter) //sometimes CanRedo gets changed after UndoRedoCalled invoke, so check again (normally this is checked by the relaycommand) if (CanRedo(null)) + { Owner.BitmapManager.ActiveDocument.UndoManager.Redo(); + Owner.BitmapManager.ActiveDocument.ChangesSaved = false; + } } /// @@ -47,7 +50,10 @@ public void Undo(object parameter) //sometimes CanUndo gets changed after UndoRedoCalled invoke, so check again (normally this is checked by the relaycommand) if (CanUndo(null)) + { Owner.BitmapManager.ActiveDocument.UndoManager.Undo(); + Owner.BitmapManager.ActiveDocument.ChangesSaved = false; + } } /// diff --git a/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs index c5a89968e..32c33e45e 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs @@ -101,7 +101,7 @@ private static void AskToInstall() if (updateZipExists || updateExeExists) { ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true; - var result = ConfirmationDialog.Show("Update is ready to install. Do you want to install it now?"); + var result = ConfirmationDialog.Show("Update is ready to be installed. Do you want to install it now?", "New update"); if (result == Models.Enums.ConfirmationType.Yes) { if (updateZipExists && File.Exists(updaterPath)) @@ -127,11 +127,9 @@ private static void InstallHeadless(string updaterPath) } catch (Win32Exception) { - MessageBox.Show( + NoticeDialog.Show( "Couldn't update without administrator rights.", - "Insufficient permissions", - MessageBoxButton.OK, - MessageBoxImage.Error); + "Insufficient permissions"); } } @@ -185,7 +183,7 @@ private async void ConditionalUPDATE() } catch (System.Net.Http.HttpRequestException) { - NoticeDialog.Show("Could not check if there's an update available"); + NoticeDialog.Show("Could not check if there is an update available", "Update check failed"); } AskToInstall(); @@ -210,4 +208,4 @@ private UpdateChannel GetUpdateChannel(string channelName) return selectedChannel; } } -} \ No newline at end of file +} diff --git a/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs b/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs index 0df1b0e67..a62249c76 100644 --- a/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs @@ -1,5 +1,5 @@ using AvalonDock.Layout; -using PixiEditor.Helpers; +using GalaSoft.MvvmLight.CommandWpf; using System.Collections.Generic; using System.Linq; @@ -7,9 +7,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main { public class WindowViewModel : SubViewModel, ISettableOwner { - public MainWindow MainWindow { get; private set; } - - public RelayCommand ShowAvalonDockWindowCommand { get; set; } + public RelayCommand ShowAvalonDockWindowCommand { get; set; } public WindowViewModel() : this(null) @@ -19,9 +17,7 @@ public WindowViewModel() public WindowViewModel(ViewModelMain owner) : base(owner) { - ShowAvalonDockWindowCommand = new RelayCommand(ShowAvalonDockWindow); - - MainWindow = (MainWindow)System.Windows.Application.Current?.MainWindow; + ShowAvalonDockWindowCommand = new(ShowAvalonDockWindow); } public void SetOwner(ViewModelMain owner) @@ -29,11 +25,10 @@ public void SetOwner(ViewModelMain owner) Owner = owner; } - private void ShowAvalonDockWindow(object parameter) + private void ShowAvalonDockWindow(string id) { - string id = (string)parameter; - - var anchorables = new List(MainWindow.LayoutRoot.Manager.Layout + if (MainWindow.Current?.LayoutRoot?.Manager?.Layout == null) return; + var anchorables = new List(MainWindow.Current.LayoutRoot.Manager.Layout .Descendents() .OfType()); diff --git a/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs b/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs index d780adbbd..ea8ff12bd 100644 --- a/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs +++ b/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs @@ -1,4 +1,5 @@ -using PixiEditor.Models.Dialogs; +using PixiEditor.Models; +using PixiEditor.Models.Dialogs; namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings { @@ -12,7 +13,7 @@ public bool ShowStartupWindow set => RaiseAndUpdatePreference(ref showStartupWindow, value); } - private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", NewFileDialog.defaultSize); + private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize); public int DefaultNewFileWidth { @@ -25,7 +26,7 @@ public int DefaultNewFileWidth } } - private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", NewFileDialog.defaultSize); + private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize); public int DefaultNewFileHeight { diff --git a/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/ToolsSettings.cs b/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/ToolsSettings.cs new file mode 100644 index 000000000..8c589cedb --- /dev/null +++ b/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/ToolsSettings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings +{ + public class ToolsSettings : SettingsGroup + { + private bool enableSharedToolbar = GetPreference(nameof(EnableSharedToolbar), false); + + public bool EnableSharedToolbar + { + get => enableSharedToolbar; + set + { + enableSharedToolbar = value; + RaiseAndUpdatePreference(nameof(EnableSharedToolbar), value); + } + } + } +} diff --git a/PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs b/PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs index 6f1463791..39e3ae410 100644 --- a/PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs +++ b/PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsViewModel.cs @@ -6,6 +6,8 @@ public class SettingsViewModel : SubViewModel { public GeneralSettings General { get; set; } = new GeneralSettings(); + public ToolsSettings Tools { get; set; } = new ToolsSettings(); + public FileSettings File { get; set; } = new FileSettings(); public UpdateSettings Update { get; set; } = new UpdateSettings(); diff --git a/PixiEditor/ViewModels/ViewModelMain.cs b/PixiEditor/ViewModels/ViewModelMain.cs index e573970a5..563551c0e 100644 --- a/PixiEditor/ViewModels/ViewModelMain.cs +++ b/PixiEditor/ViewModels/ViewModelMain.cs @@ -163,45 +163,45 @@ public void Setup(IServiceProvider services) ShortcutController = new ShortcutController( new ShortcutGroup( "Tools", - CreateToolShortcut(Key.B, "Select Pen Tool"), - CreateToolShortcut(Key.E, "Select Eraser Tool"), - CreateToolShortcut(Key.O, "Select Color Picker Tool"), - CreateToolShortcut(Key.R, "Select Rectangle Tool"), - CreateToolShortcut(Key.C, "Select Circle Tool"), - CreateToolShortcut(Key.L, "Select Line Tool"), - CreateToolShortcut(Key.G, "Select Flood Fill Tool"), - CreateToolShortcut(Key.U, "Select Brightness Tool"), - CreateToolShortcut(Key.V, "Select Move Tool"), - CreateToolShortcut(Key.M, "Select Select Tool"), - CreateToolShortcut(Key.Z, "Select Zoom Tool"), - CreateToolShortcut(Key.Space, "Select Viewport Move Tool"), - CreateToolShortcut(Key.W, "Select Magic Wand Tool"), + CreateToolShortcut(Key.B, "Pen"), + CreateToolShortcut(Key.E, "Eraser"), + CreateToolShortcut(Key.O, "Color picker"), + CreateToolShortcut(Key.R, "Rectangle"), + CreateToolShortcut(Key.C, "Ellipse"), + CreateToolShortcut(Key.L, "Line"), + CreateToolShortcut(Key.G, "Flood fill"), + CreateToolShortcut(Key.U, "Brightness"), + CreateToolShortcut(Key.V, "Move selection"), + CreateToolShortcut(Key.M, "Select"), + CreateToolShortcut(Key.Z, "Zoom"), + CreateToolShortcut(Key.H, "Move viewport"), + CreateToolShortcut(Key.W, "Magic wand"), new Shortcut(Key.OemPlus, ViewportSubViewModel.ZoomCommand, "Zoom in", 1), new Shortcut(Key.OemMinus, ViewportSubViewModel.ZoomCommand, "Zoom out", -1), - new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Decrease Tool Size", -1), - new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Increase Tool Size", 1)), + new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Decrease tool size", -1), + new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Increase tool size", 1)), new ShortcutGroup( "Editor", - new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand, "Swap primary and secondary color"), + new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand, "Swap primary and secondary colors"), new Shortcut(Key.Y, UndoSubViewModel.RedoCommand, "Redo", modifier: ModifierKeys.Control), new Shortcut(Key.Z, UndoSubViewModel.UndoCommand, "Undo", modifier: ModifierKeys.Control), - new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, "Deselect all command", modifier: ModifierKeys.Control), - new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, "Select all command", modifier: ModifierKeys.Control), + new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, "Clear selection", modifier: ModifierKeys.Control), + new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, "Select all", modifier: ModifierKeys.Control), new Shortcut(Key.C, ClipboardSubViewModel.CopyCommand, "Copy", modifier: ModifierKeys.Control), new Shortcut(Key.V, ClipboardSubViewModel.PasteCommand, "Paste", modifier: ModifierKeys.Control), new Shortcut(Key.J, ClipboardSubViewModel.DuplicateCommand, "Duplicate", modifier: ModifierKeys.Control), new Shortcut(Key.X, ClipboardSubViewModel.CutCommand, "Cut", modifier: ModifierKeys.Control), - new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand, "Delete selected pixels"), - new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, "Resize document", modifier: ModifierKeys.Control | ModifierKeys.Shift), + new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand, "Clear selected area"), + new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, "Resize image", modifier: ModifierKeys.Control | ModifierKeys.Shift), new Shortcut(Key.C, DocumentSubViewModel.OpenResizePopupCommand, "Resize canvas", "canvas", ModifierKeys.Control | ModifierKeys.Shift), - new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand, "Maximize")), + new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand, "Maximize window")), new ShortcutGroup( "File", - new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, "Open a Document", modifier: ModifierKeys.Control), - new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, "Export as image", modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt), - new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Document", modifier: ModifierKeys.Control), - new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Document As New", "AsNew", ModifierKeys.Control | ModifierKeys.Shift), - new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, "Create new Document", modifier: ModifierKeys.Control)), + new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, "Open image", modifier: ModifierKeys.Control), + new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, "Export image", modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt), + new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save", modifier: ModifierKeys.Control), + new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save as new", "AsNew", ModifierKeys.Control | ModifierKeys.Shift), + new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, "Create new image", modifier: ModifierKeys.Control)), new ShortcutGroup( "Layers", new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, "Rename active layer", BitmapManager.ActiveDocument?.ActiveLayerGuid)), @@ -215,9 +215,14 @@ public void Setup(IServiceProvider services) ShortcutController.ShortcutGroups.Add( new ShortcutGroup( "Misc", - new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open the shortcut window", true))); + new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open shortcuts window", true))); + + ShortcutController.TransientShortcuts[Key.Space] = ToolsSubViewModel.ToolSet.First(x => x is MoveViewportTool); + ShortcutController.TransientShortcuts[Key.LeftAlt] = ToolsSubViewModel.ToolSet.First(x => x is ColorPickerTool); BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor; + + ToolsSubViewModel?.SetupToolsTooltipShortcuts(services); } /// @@ -321,7 +326,7 @@ private bool RemoveDocumentWithSaveConfirmation() if (!BitmapManager.ActiveDocument.ChangesSaved) { - result = ConfirmationDialog.Show(DocumentViewModel.ConfirmationDialogMessage); + result = ConfirmationDialog.Show(DocumentViewModel.ConfirmationDialogMessage, DocumentViewModel.ConfirmationDialogTitle); if (result == ConfirmationType.Yes) { FileSubViewModel.SaveDocument(false); @@ -360,12 +365,12 @@ private void BitmapManager_DocumentChanged(object sender, DocumentChangedEventAr private void ActiveDocument_DocumentSizeChanged(object sender, DocumentSizeChangedEventArgs e) { - BitmapManager.ActiveDocument.ActiveSelection = new Selection(Array.Empty()); + BitmapManager.ActiveDocument.ActiveSelection = new Selection(Array.Empty(), new PixelSize(e.NewWidth, e.NewHeight)); BitmapManager.ActiveDocument.ChangesSaved = false; BitmapManager.ActiveDocument.CenterViewportTrigger.Execute(this, new Size(BitmapManager.ActiveDocument.Width, BitmapManager.ActiveDocument.Height)); } - private void BitmapUtility_BitmapChanged(object sender, BitmapChangedEventArgs e) + private void BitmapUtility_BitmapChanged(object sender, EventArgs e) { BitmapManager.ActiveDocument.ChangesSaved = false; if (ToolsSubViewModel.ActiveTool is BitmapOperationTool) diff --git a/PixiEditor/Views/Dialogs/ConfirmationPopup.xaml b/PixiEditor/Views/Dialogs/ConfirmationPopup.xaml index 0fb6ace7e..352d25d64 100644 --- a/PixiEditor/Views/Dialogs/ConfirmationPopup.xaml +++ b/PixiEditor/Views/Dialogs/ConfirmationPopup.xaml @@ -3,9 +3,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" - mc:Ignorable="d" - Title="ConfirmationPopup" Name="popup" WindowStartupLocation="CenterScreen" Height="200" Width="500" + xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" + xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" + xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs" + mc:Ignorable="d" d:Title="Unsaved changes" + Name="popup" WindowStartupLocation="CenterScreen" + Height="180" Width="400" MinHeight="180" MinWidth="400" WindowStyle="None"> @@ -13,40 +17,40 @@ ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" /> - - - - - - + - - - - - + + + + + + + diff --git a/PixiEditor/Views/Dialogs/PopupTemplate.xaml.cs b/PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs similarity index 55% rename from PixiEditor/Views/Dialogs/PopupTemplate.xaml.cs rename to PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs index 7e08a54c3..8e8a52786 100644 --- a/PixiEditor/Views/Dialogs/PopupTemplate.xaml.cs +++ b/PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs @@ -1,15 +1,18 @@ -using System.Windows; +using PixiEditor.Models.DataHolders; +using PixiEditor.ViewModels; +using System.Windows; using System.Windows.Input; -namespace PixiEditor.Views +namespace PixiEditor.Views.Dialogs { /// - /// Interaction logic for PopupTemplate.xaml + /// Interaction logic for CrashReportDialog.xaml /// - public partial class PopupTemplate : Window + public partial class CrashReportDialog : Window { - public PopupTemplate() + public CrashReportDialog(CrashReport report) { + DataContext = new CrashReportViewModel(report); InitializeComponent(); } @@ -23,4 +26,4 @@ private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArg SystemCommands.CloseWindow(this); } } -} \ No newline at end of file +} diff --git a/PixiEditor/Views/Dialogs/DialogTitleBar.xaml b/PixiEditor/Views/Dialogs/DialogTitleBar.xaml new file mode 100644 index 000000000..d3f7bae50 --- /dev/null +++ b/PixiEditor/Views/Dialogs/DialogTitleBar.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + You can send your crash report using: + + + + + + The report contains the documents that were opened when the crash happened, feel free to review it before sending. + + + diff --git a/PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs b/PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs new file mode 100644 index 000000000..9423fb7b7 --- /dev/null +++ b/PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs @@ -0,0 +1,101 @@ +using PixiEditor.Helpers; +using PixiEditor.Models.DataHolders; +using System; +using System.IO; +using System.Text; +using System.Web; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace PixiEditor.Views.Dialogs +{ + /// + /// Interaction logic for SendCrashReportWindow.xaml + /// + public partial class SendCrashReportWindow : Window + { + const string DiscordInviteLink = "https://discord.gg/eh8gx6vNEp"; + + private readonly CrashReport report; + + public SendCrashReportWindow(CrashReport report) + { + this.report = report; + InitializeComponent(); + } + + private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) + { + e.CanExecute = true; + } + + private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e) + { + SystemCommands.CloseWindow(this); + } + + private void OpenInExplorer(object sender, RoutedEventArgs e) + { + string tempPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PixiEditor", + "crash_logs", + "to-copy"); + + DirectoryInfo info = Directory.CreateDirectory(tempPath); + + foreach (var file in info.EnumerateFiles()) + { + file.Delete(); + } + + File.Copy(report.FilePath, Path.Combine(tempPath, Path.GetFileName(report.FilePath)), true); + + ProcessHelpers.ShellExecute(tempPath); + } + + private void OpenHyperlink(object sender, RoutedEventArgs e) + { + var button = sender as Button; + var tag = button.Tag as string; + + string body = HttpUtility.UrlEncode($"** IMPORTANT: Drop the \"{Path.GetFileName(report.FilePath)}\" file in here **"); + + var result = tag switch + { + "github" => GetGitHubLink(), + "discord" => DiscordInviteLink, + "email" => GetMailtoLink(), + _ => throw new NotImplementedException() + }; + + OpenInExplorer(null, null); + ProcessHelpers.ShellExecute(result); + + string GetGitHubLink() + { + StringBuilder builder = new(); + + builder.Append("https://github.com/PixiEditor/PixiEditor/issues/new?title="); + builder.Append(HttpUtility.UrlEncode($"Crash Report")); + builder.Append("&body="); + builder.Append(body); + + return builder.ToString(); + } + + string GetMailtoLink() + { + StringBuilder builder = new(); + + builder.Append("mailto:pixieditorproject@gmail.com?subject="); + builder.Append(HttpUtility.UrlEncode($"Crash Report")); + builder.Append("&body="); + builder.Append(body); + + return builder.ToString(); + } + } + } +} diff --git a/PixiEditor/Views/Dialogs/SettingsWindow.xaml b/PixiEditor/Views/Dialogs/SettingsWindow.xaml index e6b541518..04c8b44df 100644 --- a/PixiEditor/Views/Dialogs/SettingsWindow.xaml +++ b/PixiEditor/Views/Dialogs/SettingsWindow.xaml @@ -3,17 +3,27 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:local="clr-namespace:PixiEditor.Views.Dialogs" xmlns:viewmodels="clr-namespace:PixiEditor.ViewModels" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:views="clr-namespace:PixiEditor.Views" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" + xmlns:local="clr-namespace:PixiEditor.Views.Dialogs" + xmlns:sys="clr-namespace:System;assembly=System.Runtime" + xmlns:viewmodels="clr-namespace:PixiEditor.ViewModels" + xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" + xmlns:views="clr-namespace:PixiEditor.Views" + xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" + xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" + xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" + xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs" mc:Ignorable="d" Title="Settings" Name="window" - Height="600" Width="800" - MinHeight="350" MinWidth="600" + Height="500" Width="640" + MinHeight="500" MinWidth="640" WindowStyle="None" DataContext="{DynamicResource SettingsWindowViewModel}" + WindowStartupLocation="CenterScreen" BorderBrush="Black" BorderThickness="1"> + @@ -24,88 +34,133 @@ Executed="CommandBinding_Executed_Close" /> - - - - - - - - - + - - - - - - - + + + + + + + + + + + + General + Discord + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + Show startup window + + Show image preview in taskbar + + + + + + + + + + + + + + + Enable shared toolbar + + + + Check updates on startup + + + + + + + + + + + + + + + + + + + + + Enabled + Show image name + Show image size + Show layer count + + + - + diff --git a/PixiEditor/Views/Dialogs/ShortcutPopup.xaml b/PixiEditor/Views/Dialogs/ShortcutPopup.xaml index d9cf003de..eee981590 100644 --- a/PixiEditor/Views/Dialogs/ShortcutPopup.xaml +++ b/PixiEditor/Views/Dialogs/ShortcutPopup.xaml @@ -8,11 +8,12 @@ xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:shortcuts="clr-namespace:PixiEditor.Models.Controllers.Shortcuts" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" mc:Ignorable="d" - Title="ShortcutPopup" Height="815" Width="620" WindowStyle="None" - MinHeight="400" MinWidth="350" Topmost="{Binding IsTopmost}"> + WindowStartupLocation="CenterScreen" + Title="ShortcutPopup" Height="770" Width="620" WindowStyle="None" + MinHeight="770" MinWidth="620" Topmost="{Binding IsTopmost}"> - + - + - - - - diff --git a/PixiEditor/Views/UserControls/Zoombox.xaml.cs b/PixiEditor/Views/UserControls/Zoombox.xaml.cs index f6937ac48..12d0ca4f2 100644 --- a/PixiEditor/Views/UserControls/Zoombox.xaml.cs +++ b/PixiEditor/Views/UserControls/Zoombox.xaml.cs @@ -105,7 +105,7 @@ public void Terminate() public static readonly DependencyProperty UseTouchGesturesProperty = DependencyProperty.Register(nameof(UseTouchGestures), typeof(bool), typeof(Zoombox)); - private const double zoomFactor = 1.1; + private const double zoomFactor = 1.09050773267; //2^(1/8) private const double maxZoom = 50; private double minZoom = -28; public object AdditionalContent diff --git a/PixiEditorTests/HelpersTests/ConvertersTests/FileExtensionToColorConverterTests.cs b/PixiEditorTests/HelpersTests/ConvertersTests/FileExtensionToColorConverterTests.cs new file mode 100644 index 000000000..7af088625 --- /dev/null +++ b/PixiEditorTests/HelpersTests/ConvertersTests/FileExtensionToColorConverterTests.cs @@ -0,0 +1,38 @@ +using PixiEditor.Helpers; +using PixiEditor.Helpers.Converters; +using System.Globalization; +using System.Linq; +using System.Windows.Media; +using Xunit; + +namespace PixiEditorTests.HelpersTests.ConvertersTests +{ + public class FileExtensionToColorConverterTests + { + private static SolidColorBrush GetTypedColor(string ext) + { + var converter = new FileExtensionToColorConverter(); + object value = converter.Convert(ext, typeof(int), null, CultureInfo.CurrentCulture); + Assert.IsType(value); + return value as SolidColorBrush; + } + + [Fact] + public void TestThatEachFormatHasColor() + { + SupportedFilesHelper.AllSupportedExtensions.ToList().ForEach(i => + { + var typed = GetTypedColor(i); + Assert.NotEqual(FileExtensionToColorConverter.UnknownBrush, typed); + }); + } + + [Fact] + public void TestThatUnsupportedFormatHasDefaultColor() + { + var converter = new FileExtensionToColorConverter(); + var typed = GetTypedColor(".abc"); + Assert.Equal(FileExtensionToColorConverter.UnknownBrush, typed); + } + } +} diff --git a/PixiEditorTests/HelpersTests/SizeCalculatorTest.cs b/PixiEditorTests/HelpersTests/SizeCalculatorTest.cs new file mode 100644 index 000000000..5150b9874 --- /dev/null +++ b/PixiEditorTests/HelpersTests/SizeCalculatorTest.cs @@ -0,0 +1,29 @@ +using PixiEditor.Helpers; +using Xunit; + +namespace PixiEditorTests.HelpersTests +{ + public class SizeCalculatorTest + { + [Theory] + [InlineData(50, 64, 64, 32, 32)] + [InlineData(100, 64, 64, 64, 64)] + [InlineData(200, 128, 128, 256, 256)] + public void TestCalculationOfAbsoluteFromPercentageWorks(int percent, int currentWidth, int currentHeight, int expectedWidth, int expectedHeight) + { + var newSize = SizeCalculator.CalcAbsoluteFromPercentage(percent, new System.Drawing.Size(currentWidth, currentHeight)); + Assert.Equal(expectedWidth, newSize.Width); + Assert.Equal(expectedHeight, newSize.Height); + } + + [Theory] + [InlineData(32, 64, 50)] + [InlineData(32, 32, 100)] + [InlineData(64, 32, 200)] + public void TestCalculationOfPercentageFromAbsoluteWorks(int currentSize, int initSize, int expectedPerc) + { + var perc = SizeCalculator.CalcPercentageFromAbsolute(initSize, currentSize); + Assert.Equal(perc, expectedPerc); + } + } +} diff --git a/PixiEditorTests/HelpersTests/SupportedFilesHelperTests.cs b/PixiEditorTests/HelpersTests/SupportedFilesHelperTests.cs new file mode 100644 index 000000000..adc025dbc --- /dev/null +++ b/PixiEditorTests/HelpersTests/SupportedFilesHelperTests.cs @@ -0,0 +1,45 @@ +using PixiEditor.Helpers; +using Xunit; + +namespace PixiEditorTests.HelpersTests +{ + public class SupportedFilesHelperTests + { + [Fact] + public void TestAllExtensionsAreSupported() + { + var all = SupportedFilesHelper.AllSupportedExtensions; + Assert.Contains(all, i => i == ".pixi"); + Assert.Contains(all, i => i == ".png"); + Assert.Contains(all, i => i == ".jpg"); + Assert.Contains(all, i => i == ".jpeg"); + Assert.Contains(all, i => i == ".bmp"); + Assert.Contains(all, i => i == ".gif"); + } + + [Fact] + public void TestBuildSaveFilter() + { + var filter = SupportedFilesHelper.BuildSaveFilter(true); + Assert.Equal("PixiEditor Files|*.pixi|Png Images|*.png|Jpeg Images|*.jpeg|Bmp Images|*.bmp|Gif Images|*.gif", filter); + } + + [Fact] + public void TestBuildOpenFilter() + { + var filter = SupportedFilesHelper.BuildOpenFilter(); + Assert.Equal("Any |*.pixi;*.png;*.jpeg;*.jpg;*.bmp;*.gif|PixiEditor Files |*.pixi|Image Files |*.png;*.jpeg;*.jpg;*.bmp;*.gif", filter); + } + + [Fact] + public void TestIsSupportedFile() + { + Assert.True(SupportedFilesHelper.IsSupportedFile("foo.png")); + Assert.True(SupportedFilesHelper.IsSupportedFile("foo.bmp")); + Assert.True(SupportedFilesHelper.IsSupportedFile("foo.jpg")); + Assert.True(SupportedFilesHelper.IsSupportedFile("foo.jpeg")); + + Assert.False(SupportedFilesHelper.IsSupportedFile("foo.abc")); + } + } +} diff --git a/PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs b/PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs index f658b8499..6caf661d4 100644 --- a/PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs +++ b/PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; -using System.Windows.Media; -using PixiEditor.Models.DataHolders; -using PixiEditor.Models.Layers; +using PixiEditor.Models.Layers; using PixiEditor.Models.Position; using PixiEditor.Models.Tools; using SkiaSharp; +using System; +using System.Collections.Generic; namespace PixiEditorTests.ModelsTests.ControllersTests { @@ -15,8 +14,9 @@ public class MockedSinglePixelPenTool : BitmapOperationTool public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable allLayers, IReadOnlyList recordedMouseMovement, SKColor color) { + if (recordedMouseMovement == null || activeLayer == null) + throw new ArgumentException("Parameter is null"); activeLayer.LayerBitmap.SkiaSurface.Canvas.DrawPoint(recordedMouseMovement[0].ToSKPoint(), color); - } } } diff --git a/PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs b/PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs index 7ea88ac4c..e76a7d31c 100644 --- a/PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs +++ b/PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs @@ -42,7 +42,7 @@ public void TestThatShortcutControllerIsBlocked() RelayCommand shortcutCommand = new RelayCommand(arg => { result = (int)arg; }); ShortcutController controller = GenerateStandardShortcutController(Key.A, ModifierKeys.None, shortcutCommand); - ShortcutController.BlockShortcutExecution = true; + ShortcutController.BlockShortcutExection("Test"); controller.KeyPressed(Key.A, ModifierKeys.None); Assert.Equal(-1, result); @@ -75,7 +75,7 @@ private static ShortcutController GenerateStandardShortcutController(Key shortcu { ShortcutController controller = new ShortcutController(); controller.ShortcutGroups.Add(new ShortcutGroup(string.Empty, new Shortcut(shortcutKey, shortcutCommand, 0, modifiers))); - ShortcutController.BlockShortcutExecution = false; + ShortcutController.UnblockShortcutExecutionAll(); return controller; } } diff --git a/PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs b/PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs index e1af99c11..b1f794007 100644 --- a/PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs +++ b/PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs @@ -1,5 +1,4 @@ using PixiEditor.Models.Controllers; -using PixiEditor.Models.DataHolders; using PixiEditor.Models.Undo; using Xunit; @@ -20,7 +19,7 @@ public UndoManagerTests() public void TestSetRoot() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); Assert.Equal(this, undoManager.MainRoot); } @@ -28,9 +27,10 @@ public void TestSetRoot() public void TestAddToUndoStack() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty)); + using var change = new Change("ExampleProperty", ExampleProperty, ExampleProperty); + undoManager.AddUndoChange(change); Assert.True(undoManager.UndoStack.Count == 1); Assert.True((int)undoManager.UndoStack.Peek().OldValue == ExampleProperty); } @@ -39,9 +39,10 @@ public void TestAddToUndoStack() public void TestThatUndoAddsToRedoStack() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty)); + using var change = new Change("ExampleProperty", ExampleProperty, ExampleProperty); + undoManager.AddUndoChange(change); undoManager.Undo(); Assert.True(undoManager.RedoStack.Count == 1); } @@ -50,9 +51,10 @@ public void TestThatUndoAddsToRedoStack() public void TestUndo() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, 55)); + using var change = new Change("ExampleProperty", ExampleProperty, 55); + undoManager.AddUndoChange(change); ExampleProperty = 55; undoManager.Undo(); Assert.True((int)undoManager.RedoStack.Peek().OldValue == ExampleProperty); @@ -62,9 +64,10 @@ public void TestUndo() public void TestThatRedoAddsToUndoStack() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty)); + using var change = new Change("ExampleProperty", ExampleProperty, ExampleProperty); + undoManager.AddUndoChange(change); undoManager.Undo(); undoManager.Redo(); Assert.True(undoManager.UndoStack.Count == 1); @@ -74,10 +77,11 @@ public void TestThatRedoAddsToUndoStack() public void TestRedo() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); ExampleProperty = 55; - undoManager.AddUndoChange(new Change("ExampleProperty", 1, ExampleProperty)); + using var change = new Change("ExampleProperty", 1, ExampleProperty); + undoManager.AddUndoChange(change); undoManager.Undo(); undoManager.Redo(); Assert.True((int)undoManager.UndoStack.Peek().NewValue == ExampleProperty); @@ -87,12 +91,13 @@ public void TestRedo() public void TestThatUndoManagerUndoAndRedoWithCustomRootCorrectly() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); TestPropertyClass testProp = new TestPropertyClass(); int newVal = 5; testProp.IntProperty = newVal; - undoManager.AddUndoChange(new Change("IntProperty", 0, newVal, root: testProp)); + using var change = new Change("IntProperty", 0, newVal, root: testProp); + undoManager.AddUndoChange(change); Assert.Equal(newVal, testProp.IntProperty); undoManager.Undo(); @@ -108,16 +113,15 @@ public void TestThatUndoManagerUndoAndRedoWithCustomRootCorrectly() public void TestThatMixedProcessOfUndoAndRedoWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); int newVal = 5; - - undoManager.AddUndoChange( - new Change( + using var change = new Change( "ExampleProperty", ReverseProcess, new object[] { ExampleProperty }, - newVal)); + newVal); + undoManager.AddUndoChange(change); ExampleProperty = newVal; @@ -136,14 +140,15 @@ public void TestThatMixedProcessOfUndoAndRedoWorks() public void TestThatProcessBasedUndoAndRedoWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); int newVal = 5; - undoManager.AddUndoChange(new Change( + using var change = new Change( ReverseProcess, new object[] { ExampleProperty }, ReverseProcess, - new object[] { newVal })); + new object[] { newVal }); + undoManager.AddUndoChange(change); ExampleProperty = newVal; @@ -162,11 +167,11 @@ public void TestThatProcessBasedUndoAndRedoWorks() public void TestThatNestedPropertyUndoWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); int newVal = 5; - - undoManager.AddUndoChange(new Change("TestPropClass.IntProperty", TestPropClass.IntProperty, newVal)); + using var change = new Change("TestPropClass.IntProperty", TestPropClass.IntProperty, newVal); + undoManager.AddUndoChange(change); TestPropClass.IntProperty = newVal; @@ -185,9 +190,10 @@ public void TestThatNestedPropertyUndoWorks() public void TestThatFindRootProcessWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null)); + using var change1 = new Change("IntProperty", 0, 5, FindRootProcess, null); + undoManager.AddUndoChange(change1); Change change = undoManager.UndoStack.Peek(); @@ -198,9 +204,10 @@ public void TestThatFindRootProcessWorks() public void TestThatUndoForFindRootProcessWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null)); + using var change = new Change("IntProperty", 0, 5, FindRootProcess, null); + undoManager.AddUndoChange(change); TestPropClass.IntProperty = 5; @@ -213,9 +220,10 @@ public void TestThatUndoForFindRootProcessWorks() public void TestThatUndoAndRedoForFindRootProcessWorks() { PrepareUndoManagerForTest(); - UndoManager undoManager = new UndoManager(this); + using UndoManager undoManager = new UndoManager(this); - undoManager.AddUndoChange(new Change("IntProperty", 0, 5, FindRootProcess, null)); + using var change = new Change("IntProperty", 0, 5, FindRootProcess, null); + undoManager.AddUndoChange(change); TestPropClass.IntProperty = 5; @@ -244,4 +252,4 @@ private void PrepareUndoManagerForTest() TestPropClass = new TestPropertyClass { IntProperty = 0 }; } } -} \ No newline at end of file +} diff --git a/PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs b/PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs index dd07e0248..9ad62e9c2 100644 --- a/PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs +++ b/PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs @@ -1,5 +1,4 @@ -using PixiEditor.Exceptions; -using PixiEditor.Models.DataHolders; +using PixiEditor.Models.DataHolders; using PixiEditor.Models.Position; using SkiaSharp; using Xunit; @@ -32,27 +31,5 @@ public void TestThatCombineCombineOverrideCombinesValues() Assert.Equal(SKColors.Red, output.ChangedPixels[new Coordinates(0, 0)]); Assert.Equal(SKColors.Lime, output.ChangedPixels[new Coordinates(1, 0)]); } - - [Fact] - public void TestThatFromArraysThrowsError() - { - Assert.Throws( - () => BitmapPixelChanges.FromArrays(new[] { new Coordinates(0, 0) }, new[] { SKColors.Red, SKColors.Lime })); - } - - [Fact] - public void TestThatFormArraysWorks() - { - Coordinates[] coordinatesArray = { new Coordinates(0, 0), new Coordinates(2, 3), new Coordinates(5, 5) }; - SKColor[] colorsArray = { SKColors.Red, SKColors.Lime, SKColors.Blue }; - BitmapPixelChanges result = BitmapPixelChanges.FromArrays(coordinatesArray, colorsArray); - for (int i = 0; i < coordinatesArray.Length; i++) - { - Coordinates cords = coordinatesArray[i]; - Assert.Equal(colorsArray[i], result.ChangedPixels[cords]); - } - - Assert.False(result.WasBuiltAsSingleColored); - } } } diff --git a/PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs b/PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs index e35e9ff57..cbf382740 100644 --- a/PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs +++ b/PixiEditorTests/ModelsTests/DataHoldersTests/DocumentLayersTests.cs @@ -1,6 +1,5 @@ -using System; -using PixiEditor.Models.DataHolders; -using PixiEditor.ViewModels.SubViewModels.Main; +using PixiEditor.Models.DataHolders; +using System; using Xunit; namespace PixiEditorTests.ModelsTests.DataHoldersTests @@ -11,7 +10,7 @@ public class DocumentLayersTests [Fact] public void TestThatToggleLayerDoesNotToggleLastLayer() { - Document doc = new (5, 5); + using Document doc = new(5, 5); doc.AddNewLayer("layer"); bool isActive = doc.Layers[^1].IsActive; doc.ToggleLayer(0); @@ -21,7 +20,7 @@ public void TestThatToggleLayerDoesNotToggleLastLayer() [Fact] public void TestThatToggleLayerTogglesLayer() { - Document doc = new (5, 5); + using Document doc = new(5, 5); doc.AddNewLayer("layer"); doc.AddNewLayer("layer 1"); doc.Layers[0].IsActive = true; @@ -35,7 +34,7 @@ public void TestThatToggleLayerTogglesLayer() [Fact] public void TestThatToggleLayerDoesNothingOnNonExistingIndex() { - Document document = new Document(5, 5); + using Document document = new Document(5, 5); document.AddNewLayer("test"); document.ToggleLayer(1); document.ToggleLayer(-1); @@ -48,7 +47,7 @@ public void TestThatToggleLayerDoesNothingOnNonExistingIndex() [InlineData(1, 1)] public void TestThatSelectLayersRangeSelectsRange(int startIndex, int endIndex) { - Document document = new Document(5, 5); + using Document document = new Document(5, 5); document.AddNewLayer("1"); document.AddNewLayer("2"); @@ -62,7 +61,7 @@ public void TestThatSelectLayersRangeSelectsRange(int startIndex, int endIndex) { Assert.Equal( i >= Math.Min(startIndex, endIndex) - && i <= Math.Max(startIndex, endIndex), + && i <= Math.Max(startIndex, endIndex), document.Layers[i].IsActive); } } @@ -73,7 +72,7 @@ public void TestThatSelectLayersRangeSelectsRange(int startIndex, int endIndex) [InlineData(2)] public void TestThatDeselectAllExceptDeselectsAllExceptLayer(int index) { - Document document = new Document(5, 5); + using Document document = new Document(5, 5); document.AddNewLayer("1"); document.AddNewLayer("2"); @@ -94,7 +93,7 @@ public void TestThatDeselectAllExceptDeselectsAllExceptLayer(int index) [Fact] public void TestThatUpdateLayersColorMakesOnlyOneLayerMainColorAndOtherSecondary() { - Document document = new Document(1, 1); + using Document document = new Document(1, 1); document.AddNewLayer("1"); document.AddNewLayer("2"); @@ -114,7 +113,7 @@ public void TestThatUpdateLayersColorMakesOnlyOneLayerMainColorAndOtherSecondary [Fact] public void TestThatUpdateLayersColorMakesLayerMainColorAndRestNonActiveReturnsTransparent() { - Document document = new Document(1, 1); + using Document document = new Document(1, 1); document.AddNewLayer("1"); document.AddNewLayer("2"); @@ -134,7 +133,7 @@ public void TestThatUpdateLayersColorMakesLayerMainColorAndRestNonActiveReturnsT [Fact] public void TestThatSetNextSelectedLayerAsActiveSelectsFirstAvailableLayer() { - Document document = new Document(1, 1); + using Document document = new Document(1, 1); document.AddNewLayer("1"); document.AddNewLayer("2"); @@ -151,4 +150,4 @@ public void TestThatSetNextSelectedLayerAsActiveSelectsFirstAvailableLayer() Assert.Equal(document.Layers[0].GuidValue, document.ActiveLayerGuid); } } -} \ No newline at end of file +} diff --git a/PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs b/PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs index b324e0130..36376f307 100644 --- a/PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs +++ b/PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs @@ -10,8 +10,8 @@ public class LayerStructureTests [Fact] public void TestThatAddNewGroupAddsNewGroup() { - Document doc = new Document(1, 1); - doc.Layers.Add(new("_testLayer")); + using Document doc = new Document(1, 1); + doc.Layers.Add(new("_testLayer", 1, 1)); var testLayer = doc.Layers[^1]; doc.LayerStructure.AddNewGroup("test", testLayer.GuidValue); @@ -23,8 +23,8 @@ public void TestThatAddNewGroupAddsNewGroup() [Fact] public void TestThatAddNewGroupAddsNewGroupAsASubgroup() { - Document doc = new Document(1, 1); - doc.Layers.Add(new("_testLayer")); + using Document doc = new Document(1, 1); + doc.Layers.Add(new("_testLayer", 1, 1)); var testLayer = doc.Layers[^1]; doc.LayerStructure.AddNewGroup("test", testLayer.GuidValue); doc.LayerStructure.AddNewGroup("test1", testLayer.GuidValue); @@ -40,9 +40,9 @@ public void TestThatAddNewGroupAddsNewGroupAsASubgroup() [Fact] public void TestThatMoveGroupMovesSwapsLayerPlacesWithOtherGroup() { - Document doc = new Document(1, 1); - doc.Layers.Add(new Layer("_testLayer")); - doc.Layers.Add(new Layer("_testLayer1")); + using Document doc = new Document(1, 1); + doc.Layers.Add(new Layer("_testLayer", 1, 1)); + doc.Layers.Add(new Layer("_testLayer1", 1, 1)); var testLayer = doc.Layers[0]; var testLayer1 = doc.Layers[^1]; doc.LayerStructure.AddNewGroup("test", testLayer.GuidValue); @@ -60,8 +60,9 @@ public void TestThatMoveGroupMovesSwapsLayerPlacesWithOtherGroup() [Fact] public void TestThatIsChildOfDetectsNestedGroupCorrectly() { - LayerStructure ls = new LayerStructure(new Document(1, 1)); - Layer testLayer = new Layer("tst"); + using var doc = new Document(1, 1); + LayerStructure ls = new LayerStructure(doc); + Layer testLayer = new Layer("tst", 1, 1); ls.Groups.Add(new GuidStructureItem("group 1", testLayer.GuidValue)); ls.Groups[0].Subgroups.Add(new GuidStructureItem("group 1 nested", testLayer.GuidValue)); @@ -72,8 +73,8 @@ public void TestThatIsChildOfDetectsNestedGroupCorrectly() [Fact] public void TestThatIsChildOfDetectsNestedLayersCorrectly() { - var doc = new Document(1, 1); - doc.Layers.Add(new Layer("tst")); + using var doc = new Document(1, 1); + doc.Layers.Add(new Layer("tst", 1, 1)); Guid testLayerGuid = doc.Layers[0].GuidValue; LayerStructure ls = new LayerStructure(doc); ls.AddNewGroup("Test group", testLayerGuid); @@ -86,8 +87,8 @@ public void TestThatIsChildOfDetectsNestedLayersCorrectly() [Fact] public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerCorrectly() { - var doc = new Document(1, 1); - doc.Layers.Add(new Layer("layer")); + using var doc = new Document(1, 1); + doc.Layers.Add(new Layer("layer", 1, 1)); var guid = doc.Layers[0].GuidValue; doc.LayerStructure.AddNewGroup("layer group", guid); Assert.True(LayerStructure.GroupContainsOnlyLayer(guid, doc.LayerStructure.Groups[0])); @@ -96,8 +97,8 @@ public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerCorrectly() [Fact] public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerThatIsNested() { - var doc = new Document(1, 1); - doc.Layers.Add(new Layer("layer")); + using var doc = new Document(1, 1); + doc.Layers.Add(new Layer("layer", 1, 1)); var guid = doc.Layers[0].GuidValue; doc.LayerStructure.AddNewGroup("layer group", guid); doc.LayerStructure.AddNewGroup("layer group nested", guid); @@ -108,9 +109,9 @@ public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerThatIsNested() [Fact] public void TestThatCloneReturnsSameLayerStructure() { - Document doc = new(1, 1); - doc.Layers.Add(new("Test")); - doc.Layers.Add(new("Test2")); + using Document doc = new(1, 1); + doc.Layers.Add(new("Test", 1, 1)); + doc.Layers.Add(new("Test2", 1, 1)); LayerStructure structure = new(doc); structure.AddNewGroup("Test group", doc.Layers[0].GuidValue); @@ -124,8 +125,8 @@ public void TestThatCloneReturnsSameLayerStructure() [Fact] public void TestThatGetGroupByGuidReturnsNullForNonExistingGroup() { - Document doc = new(1, 1); - doc.Layers.Add(new("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new("Test", 1, 1)); Assert.Null(doc.LayerStructure.GetGroupByGuid(null)); Assert.Null(doc.LayerStructure.GetGroupByGuid(Guid.NewGuid())); @@ -134,8 +135,8 @@ public void TestThatGetGroupByGuidReturnsNullForNonExistingGroup() [Fact] public void TestThatGetGroupByGuidReturnsGroupCorrectly() { - Document doc = new(1, 1); - doc.Layers.Add(new("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new("Test", 1, 1)); var group = doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); Assert.Equal(group.GroupGuid, doc.LayerStructure.GetGroupByGuid(group.GroupGuid).GroupGuid); @@ -144,12 +145,12 @@ public void TestThatGetGroupByGuidReturnsGroupCorrectly() [Fact] public void TestThatPreMoveReassignBoundsMakesNestedGroupEmptyAndRemovesItAndParent() { - Document doc = new(1, 1); - doc.Layers.Add(new("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new("Test", 1, 1)); doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); var group1 = doc.LayerStructure.AddNewGroup("Test group nested", doc.Layers[0].GuidValue); - doc.LayerStructure.PreMoveReassignBounds(new GroupData(group1.GroupGuid), doc.Layers[0].GuidValue); + doc.LayerStructure.Unassign(new GroupData(group1.GroupGuid), doc.Layers[0].GuidValue); Assert.Empty(doc.LayerStructure.Groups); } @@ -157,17 +158,17 @@ public void TestThatPreMoveReassignBoundsMakesNestedGroupEmptyAndRemovesItAndPar [Fact] public void TestThatPostMoveReassignBoundsAssignsNewLayerToGroup() { - Document doc = new(1, 1); - doc.Layers.Add(new("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new("Test", 1, 1)); doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); var group1 = doc.LayerStructure.AddNewGroup("Test group nested", doc.Layers[0].GuidValue); - doc.Layers.Add(new("Test 1")); + doc.Layers.Add(new("Test 1", 1, 1)); var firstLayer = doc.Layers[0]; var layer = doc.Layers[^1]; - doc.LayerStructure.PostMoveReassignBounds(new GroupData(group1.GroupGuid), layer.GuidValue); + doc.LayerStructure.Assign(new GroupData(group1.GroupGuid), layer.GuidValue); Assert.Single(doc.LayerStructure.Groups); Assert.Single(doc.LayerStructure.Groups[0].Subgroups); @@ -180,14 +181,14 @@ public void TestThatPostMoveReassignBoundsAssignsNewLayerToGroup() [Fact] public void TestThatAssignParentAssignsParent() { - Document doc = new(1, 1); - doc.Layers.Add(new Layer("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new Layer("Test", 1, 1)); var firstLayer = doc.Layers[0]; doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); - doc.Layers.Add(new Layer("Test 1")); + doc.Layers.Add(new Layer("Test 1", 1, 1)); var layer = doc.Layers[^1]; @@ -200,14 +201,14 @@ public void TestThatAssignParentAssignsParent() [Fact] public void TestThatAssignParentDeAssignsParentOnNull() { - Document doc = new(1, 1); - doc.Layers.Add(new Layer("Test")); + using Document doc = new(1, 1); + doc.Layers.Add(new Layer("Test", 1, 1)); var firstLayer = doc.Layers[0]; doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); - doc.Layers.Add(new Layer("Test 1")); + doc.Layers.Add(new Layer("Test 1", 1, 1)); var layer = doc.Layers[^1]; @@ -221,11 +222,11 @@ public void TestThatAssignParentDeAssignsParentOnNull() [Fact] public void TestThatGetGroupLayersReturnsAllLayersInGroup() { - Document doc = new(1, 1); - doc.Layers.Add(new Layer("Test")); - doc.Layers.Add(new Layer("Test 1")); - doc.Layers.Add(new Layer("Test 2")); - doc.Layers.Add(new Layer("Test 3")); + using Document doc = new(1, 1); + doc.Layers.Add(new Layer("Test", 1, 1)); + doc.Layers.Add(new Layer("Test 1", 1, 1)); + doc.Layers.Add(new Layer("Test 2", 1, 1)); + doc.Layers.Add(new Layer("Test 3", 1, 1)); doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].GuidValue); doc.LayerStructure.AssignParent(doc.Layers[1].GuidValue, doc.LayerStructure.Groups[0].GroupGuid); diff --git a/PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs b/PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs index eda92e5b1..03498d3ab 100644 --- a/PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs +++ b/PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs @@ -13,7 +13,7 @@ public class SelectionTests [Fact] public void TestThatSetSelectionNewSetsCorrectSelection() { - Selection selection = new Selection(Array.Empty()); + Selection selection = new Selection(Array.Empty(), new(10, 10)); Coordinates[] points = { new Coordinates(0, 0), new Coordinates(1, 1) }; selection.SetSelection(points, SelectionType.New); @@ -25,7 +25,7 @@ public void TestThatSetSelectionNewSetsCorrectSelection() [Fact] public void TestThatSetSelectionAddSetsCorrectSelection() { - Selection selection = new Selection(Array.Empty()); + Selection selection = new Selection(Array.Empty(), new PixelSize(10, 10)); Coordinates[] points = { new Coordinates(0, 0), new Coordinates(1, 1) }; Coordinates[] points2 = { new Coordinates(2, 4), new Coordinates(5, 7) }; @@ -38,7 +38,7 @@ public void TestThatSetSelectionAddSetsCorrectSelection() [Fact] public void TestThatSetSelectionSubtractSetsCorrectSelection() { - Selection selection = new Selection(Array.Empty()); + Selection selection = new Selection(Array.Empty(), new PixelSize(10, 10)); Coordinates[] points = { new Coordinates(0, 0), new Coordinates(1, 1) }; Coordinates[] points2 = { new Coordinates(1, 1) }; @@ -51,7 +51,7 @@ public void TestThatSetSelectionSubtractSetsCorrectSelection() [Fact] public void TestClearWorks() { - Selection selection = new Selection(new[] { new Coordinates(0, 0), new Coordinates(5, 7) }); + Selection selection = new Selection(new[] { new Coordinates(0, 0), new Coordinates(5, 7) }, new PixelSize(10, 10)); selection.Clear(); Assert.Empty(selection.SelectedPoints); @@ -62,7 +62,7 @@ public void TestClearWorks() [Fact] public void TestThatUndoWorks() { - Document document = new Document(10, 10); + using Document document = new Document(10, 10); IEnumerable oldSelection = new List(document.ActiveSelection.SelectedPoints); diff --git a/PixiEditorTests/ModelsTests/DataHoldersTests/SurfaceTests.cs b/PixiEditorTests/ModelsTests/DataHoldersTests/SurfaceTests.cs index 9e8b4970d..729bbe415 100644 --- a/PixiEditorTests/ModelsTests/DataHoldersTests/SurfaceTests.cs +++ b/PixiEditorTests/ModelsTests/DataHoldersTests/SurfaceTests.cs @@ -4,7 +4,9 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests { +#pragma warning disable CA1001 // Types that own disposable fields should be disposable public class SurfaceTests +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { SKColor redColor = new SKColor(254, 2, 3); SKColor greenColor = new SKColor(6, 224, 3); diff --git a/PixiEditorTests/ModelsTests/IO/ImporterTests.cs b/PixiEditorTests/ModelsTests/IO/ImporterTests.cs index 71eb51c76..6eb65a51a 100644 --- a/PixiEditorTests/ModelsTests/IO/ImporterTests.cs +++ b/PixiEditorTests/ModelsTests/IO/ImporterTests.cs @@ -26,6 +26,8 @@ public ImporterTests() [InlineData("dub.jpeg")] [InlineData("-.JPEG")] [InlineData("dub.jpg")] + [InlineData("dub.gif")] + [InlineData("dub.bmp")] public void TestThatIsSupportedFile(string file) { Assert.True(Importer.IsSupportedFile(file)); diff --git a/PixiEditorTests/ModelsTests/ImageManipulationTests/TransformTests.cs b/PixiEditorTests/ModelsTests/ImageManipulationTests/TransformTests.cs deleted file mode 100644 index ff2a1bea1..000000000 --- a/PixiEditorTests/ModelsTests/ImageManipulationTests/TransformTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PixiEditor.Models.ImageManipulation; -using PixiEditor.Models.Position; -using Xunit; - -namespace PixiEditorTests.ModelsTests.ImageManipulationTests -{ - public class TransformTests - { - [Theory] - [InlineData(0, 0, 1, 1, 1, 1)] - [InlineData(1, 1, 0, 0, -1, -1)] - [InlineData(5, 5, 4, 6, -1, 1)] - [InlineData(-15, -15, -16, -16, -1, -1)] - [InlineData(150, 150, 1150, 1150, 1000, 1000)] - public void TestGetTranslation(int x1, int y1, int x2, int y2, int expectedX, int expectedY) - { - Coordinates translation = Transform.GetTranslation(new Coordinates(x1, y1), new Coordinates(x2, y2)); - Assert.Equal(new Coordinates(expectedX, expectedY), translation); - } - - [Theory] - [InlineData(0, 0)] - [InlineData(1, 1)] - [InlineData(5, 2)] - [InlineData(50, 150)] - [InlineData(-5, -52)] - public void TestTranslate(int vectorX, int vectorY) - { - Coordinates[] points = { new Coordinates(0, 0), new Coordinates(5, 5), new Coordinates(15, 2) }; - Coordinates[] translatedCords = Transform.Translate(points, new Coordinates(vectorX, vectorY)); - - for (int i = 0; i < points.Length; i++) - { - Assert.Equal(points[i].X + vectorX, translatedCords[i].X); - Assert.Equal(points[i].Y + vectorY, translatedCords[i].Y); - } - } - } -} \ No newline at end of file diff --git a/PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs b/PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs index b9d44eb5a..4f1695278 100644 --- a/PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs +++ b/PixiEditorTests/ModelsTests/LayersTests/LayersTestHelper.cs @@ -7,6 +7,9 @@ public static class LayersTestHelper { public static void LayersAreEqual(Layer expected, Layer actual) { + Assert.NotNull(actual); + Assert.NotNull(expected); +#pragma warning disable CA1062 // Validate arguments of public methods Assert.Equal(expected.Name, actual.Name); Assert.Equal(expected.Offset, actual.Offset); Assert.Equal(expected.Width, actual.Width); @@ -17,6 +20,7 @@ public static void LayersAreEqual(Layer expected, Layer actual) Assert.Equal(expected.IsVisible, actual.IsVisible); Assert.Equal(expected.IsRenaming, actual.IsRenaming); Assert.Equal(expected.ConvertBitmapToBytes(), actual.ConvertBitmapToBytes()); +#pragma warning restore CA1062 // Validate arguments of public methods } } -} \ No newline at end of file +} diff --git a/PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs b/PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs index 976167873..5da43a14c 100644 --- a/PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs +++ b/PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs @@ -1,4 +1,5 @@ using PixiEditor.Models.Position; +using System.Globalization; using Xunit; namespace PixiEditorTests.ModelsTests.PositionTests @@ -10,7 +11,7 @@ public void TestThatToStringReturnsCorrectFormat() { Coordinates cords = new Coordinates(5, 5); - Assert.Equal("5, 5", cords.ToString()); + Assert.Equal("5, 5", cords.ToString(CultureInfo.InvariantCulture)); } [Fact] @@ -22,4 +23,4 @@ public void TestThatNotEqualOperatorWorks() Assert.True(cords != cords2); } } -} \ No newline at end of file +} diff --git a/PixiEditorTests/ModelsTests/PositionTests/RectTests.cs b/PixiEditorTests/ModelsTests/PositionTests/RectTests.cs new file mode 100644 index 000000000..fa7b6709a --- /dev/null +++ b/PixiEditorTests/ModelsTests/PositionTests/RectTests.cs @@ -0,0 +1,32 @@ +using PixiEditor.Helpers.Extensions; +using SkiaSharp; +using System.Windows; +using Xunit; + +namespace PixiEditorTests.ModelsTests.PositionTests +{ + public class RectTests + { + [Fact] + public void TestThatInt32RectToSKRectIWorks() + { + Int32Rect rect = new Int32Rect(5, 2, 8, 10); + SKRectI converted = rect.ToSKRectI(); + Assert.Equal(rect.X, converted.Left); + Assert.Equal(rect.Y, converted.Top); + Assert.Equal(rect.Width, converted.Width); + Assert.Equal(rect.Height, converted.Height); + } + + [Fact] + public void TestThatSKRectIToInt32RectWorks() + { + SKRectI rect = new SKRectI(5, 2, 8, 10); + Int32Rect converted = rect.ToInt32Rect(); + Assert.Equal(rect.Left, converted.X); + Assert.Equal(rect.Top, converted.Y); + Assert.Equal(rect.Width, converted.Width); + Assert.Equal(rect.Height, converted.Height); + } + } +} diff --git a/PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs b/PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs index 4ba0d6e49..2b6689fbe 100644 --- a/PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs +++ b/PixiEditorTests/ModelsTests/UndoTests/StorageBasedChangeTests.cs @@ -5,7 +5,6 @@ using PixiEditorTests.ModelsTests.LayersTests; using SkiaSharp; using System; -using System.Collections.ObjectModel; using System.IO; using Xunit; @@ -23,18 +22,18 @@ public StorageBasedChangeTests() } } - public Document GenerateTestDocument() + public static Document GenerateTestDocument() { - Document testDocument = new Document(10, 10); - Surface testBitmap = new Surface(10, 10); - Surface testBitmap2 = new Surface(5, 8); + using Document testDocument = new Document(10, 10); + using Surface testBitmap = new Surface(10, 10); + using Surface testBitmap2 = new Surface(5, 8); testBitmap.SetSRGBPixel(0, 0, SKColors.Black); testBitmap2.SetSRGBPixel(4, 4, SKColors.Blue); Random random = new Random(); testDocument.Layers = new WpfObservableRangeCollection() { - new Layer("Test layer" + random.Next(int.MinValue, int.MaxValue), testBitmap), - new Layer("Test layer 2" + random.Next(int.MinValue, int.MaxValue), testBitmap2) { Offset = new System.Windows.Thickness(2, 3, 0, 0) } + new Layer("Test layer" + random.Next(int.MinValue, int.MaxValue), testBitmap, testDocument.Width, testDocument.Height), + new Layer("Test layer 2" + random.Next(int.MinValue, int.MaxValue), testBitmap2, testDocument.Width, testDocument.Height) { Offset = new System.Windows.Thickness(2, 3, 0, 0) } }; return testDocument; } @@ -42,9 +41,9 @@ public Document GenerateTestDocument() [Fact] public void TestThatConstructorGeneratesUndoLayersProperly() { - Document testDocument = GenerateTestDocument(); + using Document testDocument = GenerateTestDocument(); - StorageBasedChange change = new StorageBasedChange(testDocument, testDocument.Layers, UndoStoreLocation); + using StorageBasedChange change = new StorageBasedChange(testDocument, testDocument.Layers, UndoStoreLocation); Assert.Equal(testDocument.Layers.Count, change.StoredLayers.Length); @@ -69,9 +68,9 @@ public void TestThatConstructorGeneratesUndoLayersProperly() [Fact] public void TestThatSaveLayersOnDeviceSavesLayers() { - Document document = GenerateTestDocument(); + using Document document = GenerateTestDocument(); - StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); + using StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); foreach (var layer in change.StoredLayers) { @@ -83,9 +82,9 @@ public void TestThatSaveLayersOnDeviceSavesLayers() [Fact] public void TestThatLoadLayersFromDeviceLoadsLayers() { - Document document = GenerateTestDocument(); + using Document document = GenerateTestDocument(); - StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); + using StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); Layer[] layers = change.LoadLayersFromDevice(); @@ -101,9 +100,9 @@ public void TestThatLoadLayersFromDeviceLoadsLayers() [Fact] public void TestThatUndoInvokesLoadFromDeviceAndExecutesProcess() { - Document document = GenerateTestDocument(); + using Document document = GenerateTestDocument(); - StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); + using StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); bool undoInvoked = false; Action testUndoProcess = (layers, data) => @@ -120,7 +119,7 @@ public void TestThatUndoInvokesLoadFromDeviceAndExecutesProcess() Action testRedoProcess = parameters => { }; Change undoChange = change.ToChange(testUndoProcess, testRedoProcess, null); - UndoManager manager = new UndoManager(this); + using UndoManager manager = new UndoManager(this); manager.AddUndoChange(undoChange); manager.Undo(); @@ -131,9 +130,9 @@ public void TestThatUndoInvokesLoadFromDeviceAndExecutesProcess() [Fact] public void TestThatRedoInvokesSaveToDeviceAndExecutesProcess() { - Document document = GenerateTestDocument(); + using Document document = GenerateTestDocument(); - StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); + using StorageBasedChange change = new StorageBasedChange(document, document.Layers, UndoStoreLocation); bool redoInvoked = false; Action testUndoProcess = (layers, data) => { }; @@ -152,7 +151,7 @@ public void TestThatRedoInvokesSaveToDeviceAndExecutesProcess() }; Change undoChange = change.ToChange(testUndoProcess, testRedoProcess, new object[] { 2 }); - UndoManager manager = new UndoManager(this); + using UndoManager manager = new UndoManager(this); manager.AddUndoChange(undoChange); manager.Undo(); diff --git a/README.md b/README.md index 970c59586..981aab1a6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -**PixiEditor** is a Pixel art editing software. Create beautiful sprites for your games, animations (coming soon!) and edit images. All packed in eye-friendly dark theme. +**PixiEditor** is a Pixel art editing software. Create beautiful sprites for your games, animations (coming soon!), and edit images. All packed in eye-friendly dark theme. [![Build Status](https://img.shields.io/azure-devops/build/flabbet/PixiEditor/6/master)](https://dev.azure.com/flabbet/PixiEditor/_build?definitionId=6) [![codecov](https://codecov.io/gh/PixiEditor/PixiEditor/branch/master/graph/badge.svg)](https://codecov.io/gh/PixiEditor/PixiEditor) @@ -17,19 +17,19 @@ ## About PixiEditor -Want to create beautiful pixel arts for your games? PixiEditor can help you! Our goal is to create fully open-source, fast and feature rich pixel art creator. +Want to create beautiful pixel art for your games? PixiEditor can help you! Our goal is to create a fully open-source, fast, and feature-rich pixel art creator. ### Familiar interface Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we wanted users to get familiar with the tool quickly and with ease. -![](https://github.com/flabbet/PixiEditor/blob/master/Screenshot.png) +![](https://user-images.githubusercontent.com/45312141/146670495-ae521a18-a89e-4e94-9317-6838b51407fa.png) ### Lightweight -Program weighs only 3.3 MB! Already have .NET 5 installed? Download installer and enjoy saved space. +The program weighs only 9 MB! Already have .NET 6 installed? Download installer and enjoy saved space. ### Active development @@ -74,7 +74,7 @@ Struggling with something? You can find support in a few places: * Ask on [Discord](https://discord.gg/qSRMYmq) * Open new [Issue](https://github.com/flabbet/PixiEditor/issues) -* Check out [FAQ](https://github.com/PixiEditor/PixiEditor/wiki/FAQ). +* Check out the [FAQ](https://github.com/PixiEditor/PixiEditor/wiki/FAQ). @@ -82,7 +82,7 @@ Struggling with something? You can find support in a few places: ### Software Requirements -* .NET 5 +* .NET 6 * Visual Studio diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c43dab715..b68c571ef 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,6 +11,11 @@ variables: buildConfiguration: 'Release' steps: +- task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '6.0.100' + - task: NuGetToolInstaller@1 - task: NuGetCommand@2 @@ -44,7 +49,9 @@ steps: workingDirectory: 'PixiEditorTests\' displayName: Collect code coverage -- task: CmdLine@2 - inputs: - script: codecov -f .\PixiEditorTests\PixiEditor_coverage.xml -t $(CODECOV_TOKEN) - displayName: Upload to Codecov.io + # Disiabled, because there is a problem with .NET 6 and OpenCover.Console.exe +#- task: CmdLine@2 +# continueOnError: true +# inputs: +# script: codecov -f .\PixiEditorTests\PixiEditor_coverage.xml -t $(CODECOV_TOKEN) +# displayName: Upload to Codecov.io