Browse Source

Merge pull request #511 from PixiEditor/master

1.0.1.0 release
Krzysztof Krysiński 2 năm trước cách đây
mục cha
commit
31bbbc5864
83 tập tin đã thay đổi với 774 bổ sung397 xóa
  1. 3 5
      README.md
  2. 15 4
      src/ChunkyImageLib/ChunkyImage.cs
  3. 2 2
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  4. 2 2
      src/ChunkyImageLib/DataHolders/ShapeData.cs
  5. 5 5
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  6. 3 3
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  7. 4 4
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  8. 3 3
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  9. 5 5
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  10. 3 3
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  11. 1 1
      src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs
  12. 4 4
      src/ChunkyImageLib/Operations/ImageOperation.cs
  13. 4 4
      src/ChunkyImageLib/Operations/PathOperation.cs
  14. 31 4
      src/ChunkyImageLib/Operations/PixelOperation.cs
  15. 4 3
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  16. 4 4
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  17. 1 3
      src/ChunkyImageLib/Surface.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/Size_ChangeInfo.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryAxisPosition_ChangeInfo.cs
  20. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  21. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  22. 8 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  23. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  24. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  25. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  26. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs
  27. 10 13
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs
  28. 11 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs
  29. 10 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs
  30. 14 0
      src/PixiEditor.DrawingApi.Core/Numerics/VecI.cs
  31. 6 1
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  32. 8 2
      src/PixiEditor/App.xaml.cs
  33. 8 2
      src/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  34. 34 0
      src/PixiEditor/Helpers/CrashHelper.cs
  35. 10 2
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  36. 6 1
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  37. 5 0
      src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs
  38. 21 0
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  39. 19 25
      src/PixiEditor/Helpers/VersionHelpers.cs
  40. 19 3
      src/PixiEditor/Models/Commands/CommandController.cs
  41. 1 1
      src/PixiEditor/Models/DataHolders/CrashReport.cs
  42. 9 0
      src/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  43. 3 1
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  44. 37 23
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  45. 30 0
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  46. 7 10
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  47. 15 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  48. 3 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SymmetryExecutor.cs
  49. 16 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs
  50. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs
  51. 6 0
      src/PixiEditor/Models/Enums/ExecutorType.cs
  52. 27 30
      src/PixiEditor/Models/IO/Exporter.cs
  53. 1 1
      src/PixiEditor/PixiEditor.csproj
  54. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  55. 1 19
      src/PixiEditor/ViewModels/CrashReportViewModel.cs
  56. 10 2
      src/PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  57. 7 0
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs
  58. 56 10
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  59. 25 8
      src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  60. 11 3
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  61. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  62. 11 2
      src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  63. 4 4
      src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  64. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  65. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolViewModel.cs
  66. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/BrightnessToolViewModel.cs
  67. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  68. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EllipseToolViewModel.cs
  69. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/FloodFillToolViewModel.cs
  70. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LassoToolViewModel.cs
  71. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LineToolViewModel.cs
  72. 33 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/MoveToolViewModel.cs
  73. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/RectangleToolViewModel.cs
  74. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/SelectToolViewModel.cs
  75. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ZoomToolViewModel.cs
  76. 75 74
      src/PixiEditor/Views/MainWindow.xaml
  77. 9 0
      src/PixiEditor/Views/MainWindow.xaml.cs
  78. 1 1
      src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryAxisDragInfo.cs
  79. 18 17
      src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  80. 11 9
      windows-x64-release-dev.yml
  81. 9 8
      windows-x64-release.yml
  82. 10 9
      windows-x86-release-dev.yml
  83. 9 8
      windows-x86-release.yml

+ 3 - 5
README.md

@@ -38,9 +38,9 @@ PixiEditor started in 2018 and it's been actively developed since. We continuous
 
 
 <a href='//www.microsoft.com/store/apps/9NDDRHS8PBRN?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='Microsoft Store badge' width="184"/></a>
 <a href='//www.microsoft.com/store/apps/9NDDRHS8PBRN?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='Microsoft Store badge' width="184"/></a>
 
 
-Wishlist on Steam now!
+Get it on Steam now!
 
 
-[![wishlist_steam](https://user-images.githubusercontent.com/25402427/214952291-d81a4d79-bb75-44f2-bd24-10d7c3404997.png)](https://store.steampowered.com/app/2218560/PixiEditor__Pixel_Art_Editor?utm_source=GitHub)
+[![Get PixiEditor on Steam](https://user-images.githubusercontent.com/121322/228988640-32fe5bd3-9dd0-4f3b-a8f2-f744bd9b50b5.png)](https://store.steampowered.com/app/2218560/PixiEditor__Pixel_Art_Editor?utm_source=GitHub)
 
 
 **Or**
 **Or**
 
 
@@ -83,9 +83,7 @@ Struggling with something? You can find support in a few places:
 
 
 ### Software Requirements
 ### Software Requirements
 
 
-* .NET 7
-
-* latest Visual Studio 2022 (in order to code generators to work)
+* .NET 7.0.104 (7.0.202 is broken! Make sure to use 7.0.104)
 
 
 ### Instructions
 ### Instructions
 
 

+ 15 - 4
src/ChunkyImageLib/ChunkyImage.cs

@@ -82,8 +82,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     private BlendMode blendMode = BlendMode.Src;
     private BlendMode blendMode = BlendMode.Src;
     private bool lockTransparency = false;
     private bool lockTransparency = false;
     private VectorPath? clippingPath;
     private VectorPath? clippingPath;
-    private int? horizontalSymmetryAxis = null;
-    private int? verticalSymmetryAxis = null;
+    private double? horizontalSymmetryAxis = null;
+    private double? verticalSymmetryAxis = null;
 
 
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
@@ -429,7 +429,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
     }
 
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void SetHorizontalAxisOfSymmetry(int position)
+    public void SetHorizontalAxisOfSymmetry(double position)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
@@ -441,7 +441,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
     }
 
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void SetVerticalAxisOfSymmetry(int position)
+    public void SetVerticalAxisOfSymmetry(double position)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
@@ -606,6 +606,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
         }
     }
     }
 
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PixelOperation operation = new(pos, pixelProcessor, blendMode);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {
     {

+ 2 - 2
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -127,7 +127,7 @@ public struct ShapeCorners
         return crossTop == crossRight && crossTop == crossLeft && crossTop == crossBottom;
         return crossTop == crossRight && crossTop == crossLeft && crossTop == crossBottom;
     }
     }
 
 
-    public ShapeCorners AsMirroredAcrossHorAxis(int horAxisY) => new ShapeCorners
+    public ShapeCorners AsMirroredAcrossHorAxis(double horAxisY) => new ShapeCorners
     {
     {
         BottomLeft = BottomLeft.ReflectY(horAxisY),
         BottomLeft = BottomLeft.ReflectY(horAxisY),
         BottomRight = BottomRight.ReflectY(horAxisY),
         BottomRight = BottomRight.ReflectY(horAxisY),
@@ -135,7 +135,7 @@ public struct ShapeCorners
         TopRight = TopRight.ReflectY(horAxisY)
         TopRight = TopRight.ReflectY(horAxisY)
     };
     };
 
 
-    public ShapeCorners AsMirroredAcrossVerAxis(int verAxisX) => new ShapeCorners
+    public ShapeCorners AsMirroredAcrossVerAxis(double verAxisX) => new ShapeCorners
     {
     {
         BottomLeft = BottomLeft.ReflectX(verAxisX),
         BottomLeft = BottomLeft.ReflectX(verAxisX),
         BottomRight = BottomRight.ReflectX(verAxisX),
         BottomRight = BottomRight.ReflectX(verAxisX),

+ 2 - 2
src/ChunkyImageLib/DataHolders/ShapeData.cs

@@ -26,9 +26,9 @@ public record struct ShapeData
     public double Angle { get; }
     public double Angle { get; }
     public int StrokeWidth { get; }
     public int StrokeWidth { get; }
 
 
-    public ShapeData AsMirroredAcrossHorAxis(int horAxisY)
+    public ShapeData AsMirroredAcrossHorAxis(double horAxisY)
         => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
         => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
-    public ShapeData AsMirroredAcrossVerAxis(int verAxisX)
+    public ShapeData AsMirroredAcrossVerAxis(double verAxisX)
         => new ShapeData(Center.ReflectX(verAxisX), new(-Size.X, Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
         => new ShapeData(Center.ReflectX(verAxisX), new(-Size.X, Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
 
 
 }
 }

+ 5 - 5
src/ChunkyImageLib/Operations/BresenhamLineOperation.cs

@@ -44,19 +44,19 @@ internal class BresenhamLineOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         RectI newFrom = new RectI(from, new VecI(1));
         RectI newFrom = new RectI(from, new VecI(1));
         RectI newTo = new RectI(to, new VecI(1));
         RectI newTo = new RectI(to, new VecI(1));
         if (verAxisX is not null)
         if (verAxisX is not null)
         {
         {
-            newFrom = newFrom.ReflectX((int)verAxisX);
-            newTo = newTo.ReflectX((int)verAxisX);
+            newFrom = (RectI)newFrom.ReflectX((double)verAxisX).Round();
+            newTo = (RectI)newTo.ReflectX((double)verAxisX).Round();
         }
         }
         if (horAxisY is not null)
         if (horAxisY is not null)
         {
         {
-            newFrom = newFrom.ReflectY((int)horAxisY);
-            newTo = newTo.ReflectY((int)horAxisY);
+            newFrom = (RectI)newFrom.ReflectY((double)horAxisY).Round();
+            newTo = (RectI)newTo.ReflectY((double)horAxisY).Round();
         }
         }
         return new BresenhamLineOperation(newFrom.Pos, newTo.Pos, color, blendMode);
         return new BresenhamLineOperation(newFrom.Pos, newTo.Pos, color, blendMode);
     }
     }

+ 3 - 3
src/ChunkyImageLib/Operations/ChunkyImageOperation.cs

@@ -108,13 +108,13 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
         return topLeft;
         return topLeft;
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         var newPos = targetPos;
         var newPos = targetPos;
         if (verAxisX is not null)
         if (verAxisX is not null)
-            newPos = newPos.ReflectX((int)verAxisX);
+            newPos = (VecI)newPos.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            newPos = newPos.ReflectY((int)horAxisY);
+            newPos = (VecI)newPos.ReflectY((double)horAxisY).Round();
         return new ChunkyImageOperation(imageToDraw, newPos, mirrorHorizontal ^ (verAxisX is not null), mirrorVertical ^ (horAxisY is not null));
         return new ChunkyImageOperation(imageToDraw, newPos, mirrorHorizontal ^ (verAxisX is not null), mirrorVertical ^ (horAxisY is not null));
     }
     }
 
 

+ 4 - 4
src/ChunkyImageLib/Operations/ClearPathOperation.cs

@@ -38,17 +38,17 @@ internal class ClearPathOperation : IMirroredDrawOperation
         path.Dispose();
         path.Dispose();
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
-        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, verAxisX ?? 0, horAxisY ?? 0);
+        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, (float?)verAxisX ?? 0, (float?)horAxisY ?? 0);
         using var copy = new VectorPath(path);
         using var copy = new VectorPath(path);
         copy.Transform(matrix);
         copy.Transform(matrix);
 
 
         var newRect = pathTightBounds;
         var newRect = pathTightBounds;
         if (verAxisX is not null)
         if (verAxisX is not null)
-            newRect = newRect.ReflectX((int)verAxisX);
+            newRect = (RectI)newRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            newRect = newRect.ReflectY((int)horAxisY);
+            newRect = (RectI)newRect.ReflectY((double)horAxisY).Round();
         return new ClearPathOperation(copy, newRect);
         return new ClearPathOperation(copy, newRect);
     }
     }
 }
 }

+ 3 - 3
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -31,13 +31,13 @@ internal class ClearRegionOperation : IMirroredDrawOperation
     }
     }
     public void Dispose() { }
     public void Dispose() { }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         var newRect = rect;
         var newRect = rect;
         if (verAxisX is not null)
         if (verAxisX is not null)
-            newRect = newRect.ReflectX((int)verAxisX);
+            newRect = (RectI)newRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            newRect = newRect.ReflectY((int)horAxisY);
+            newRect = (RectI)newRect.ReflectY((double)horAxisY).Round();
         return new ClearRegionOperation(newRect);
         return new ClearRegionOperation(newRect);
     }
     }
 }
 }

+ 5 - 5
src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs

@@ -44,19 +44,19 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         VecI newFrom = from;
         VecI newFrom = from;
         VecI newTo = to;
         VecI newTo = to;
         if (verAxisX is not null)
         if (verAxisX is not null)
         {
         {
-            newFrom = newFrom.ReflectX((int)verAxisX);
-            newTo = newTo.ReflectX((int)verAxisX);
+            newFrom = (VecI)newFrom.ReflectX((double)verAxisX).Round();
+            newTo = (VecI)newTo.ReflectX((double)verAxisX).Round();
         }
         }
         if (horAxisY is not null)
         if (horAxisY is not null)
         {
         {
-            newFrom = newFrom.ReflectY((int)horAxisY);
-            newTo = newTo.ReflectY((int)horAxisY);
+            newFrom = (VecI)newFrom.ReflectY((double)horAxisY).Round();
+            newTo = (VecI)newTo.ReflectY((double)horAxisY).Round();
         }
         }
         return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, paint.Color, paint.BlendMode);
         return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, paint.Color, paint.BlendMode);
     }
     }

+ 3 - 3
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -105,13 +105,13 @@ internal class EllipseOperation : IMirroredDrawOperation
         return new AffectedArea(chunks, location);
         return new AffectedArea(chunks, location);
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         RectI newLocation = location;
         RectI newLocation = location;
         if (verAxisX is not null)
         if (verAxisX is not null)
-            newLocation = newLocation.ReflectX((int)verAxisX);
+            newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            newLocation = newLocation.ReflectY((int)horAxisY);
+            newLocation = (RectI)newLocation.ReflectY((double)horAxisY).Round();
         return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, paint);
         return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, paint);
     }
     }
 
 

+ 1 - 1
src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs

@@ -2,5 +2,5 @@
 
 
 internal interface IMirroredDrawOperation : IDrawOperation
 internal interface IMirroredDrawOperation : IDrawOperation
 {
 {
-    IDrawOperation AsMirrored(int? verAxisX, int? horAxisY);
+    IDrawOperation AsMirrored(double? verAxisX, double? horAxisY);
 }
 }

+ 4 - 4
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -106,22 +106,22 @@ internal class ImageOperation : IMirroredDrawOperation
         customPaint?.Dispose();
         customPaint?.Dispose();
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         if (verAxisX is not null && horAxisY is not null)
         if (verAxisX is not null && horAxisY is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((double)verAxisX).AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
         }
         }
         if (verAxisX is not null)
         if (verAxisX is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((double)verAxisX), toPaint, customPaint, imageWasCopied);
         }
         }
         if (horAxisY is not null)
         if (horAxisY is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
         }
         }
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
     }
     }

+ 4 - 4
src/ChunkyImageLib/Operations/PathOperation.cs

@@ -40,17 +40,17 @@ internal class PathOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
-        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, verAxisX ?? 0, horAxisY ?? 0);
+        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, (float?)verAxisX ?? 0, (float?)horAxisY ?? 0);
         using var copy = new VectorPath(path);
         using var copy = new VectorPath(path);
         copy.Transform(matrix);
         copy.Transform(matrix);
 
 
         RectI newBounds = bounds;
         RectI newBounds = bounds;
         if (verAxisX is not null)
         if (verAxisX is not null)
-            newBounds = newBounds.ReflectX((int)verAxisX);
+            newBounds = (RectI)newBounds.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            newBounds = newBounds.ReflectY((int)horAxisY);
+            newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);
     }
     }
 
 

+ 31 - 4
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -6,6 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;
 
 
+public delegate Color PixelProcessor(Color input);
 internal class PixelOperation : IMirroredDrawOperation
 internal class PixelOperation : IMirroredDrawOperation
 {
 {
     public bool IgnoreEmptyChunks => false;
     public bool IgnoreEmptyChunks => false;
@@ -14,6 +15,8 @@ internal class PixelOperation : IMirroredDrawOperation
     private readonly BlendMode blendMode;
     private readonly BlendMode blendMode;
     private readonly Paint paint;
     private readonly Paint paint;
 
 
+    private readonly PixelProcessor? _colorProcessor = null;
+
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     {
     {
         this.pixel = pixel;
         this.pixel = pixel;
@@ -22,10 +25,18 @@ internal class PixelOperation : IMirroredDrawOperation
         paint = new Paint() { BlendMode = blendMode };
         paint = new Paint() { BlendMode = blendMode };
     }
     }
 
 
+    public PixelOperation(VecI pixel, PixelProcessor colorProcessor, BlendMode blendMode)
+    {
+        this.pixel = pixel;
+        this._colorProcessor = colorProcessor;
+        this.blendMode = blendMode;
+        paint = new Paint() { BlendMode = blendMode };
+    }
+
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     {
     {
         // a hacky way to make the lines look slightly better on non full res chunks
         // a hacky way to make the lines look slightly better on non full res chunks
-        paint.Color = new Color(color.R, color.G, color.B, (byte)(color.A * chunk.Resolution.Multiplier()));
+        paint.Color = GetColor(chunk, chunkPos);
 
 
         DrawingSurface surf = chunk.Surface.DrawingSurface;
         DrawingSurface surf = chunk.Surface.DrawingSurface;
         surf.Canvas.Save();
         surf.Canvas.Save();
@@ -35,18 +46,34 @@ internal class PixelOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
         surf.Canvas.Restore();
     }
     }
 
 
+    private Color GetColor(Chunk chunk, VecI chunkPos)
+    {
+        Color pixelColor = color;
+        if (_colorProcessor != null)
+        {
+            pixelColor = _colorProcessor(chunk.Surface.GetSRGBPixel(pixel - chunkPos * ChunkyImage.FullChunkSize));
+        }
+
+        return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));
+    }
+
     public AffectedArea FindAffectedArea(VecI imageSize)
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
     {
         return new AffectedArea(new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) }, new RectI(pixel, VecI.One));
         return new AffectedArea(new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) }, new RectI(pixel, VecI.One));
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         RectI pixelRect = new RectI(pixel, new VecI(1, 1));
         RectI pixelRect = new RectI(pixel, new VecI(1, 1));
         if (verAxisX is not null)
         if (verAxisX is not null)
-            pixelRect = pixelRect.ReflectX((int)verAxisX);
+            pixelRect = (RectI)pixelRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
-            pixelRect = pixelRect.ReflectY((int)horAxisY);
+            pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY);
+        if (_colorProcessor != null)
+        {
+            return new PixelOperation(pixelRect.Pos, _colorProcessor, blendMode);
+        }
+
         return new PixelOperation(pixelRect.Pos, color, blendMode);
         return new PixelOperation(pixelRect.Pos, color, blendMode);
     }
     }
 
 

+ 4 - 3
src/ChunkyImageLib/Operations/PixelsOperation.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -52,11 +53,11 @@ internal class PixelsOperation : IMirroredDrawOperation
         return new AffectedArea(affectedChunks, affectedArea);
         return new AffectedArea(affectedChunks, affectedArea);
     }
     }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         var arr = pixels.Select(pixel => new VecI(
         var arr = pixels.Select(pixel => new VecI(
-            verAxisX is not null ? 2 * (int)verAxisX - (int)pixel.X - 1 : (int)pixel.X,
-            horAxisY is not null ? 2 * (int)horAxisY - (int)pixel.Y - 1 : (int)pixel.Y
+            verAxisX is not null ? (int)Math.Round(2 * (double)verAxisX - (int)pixel.X - 1) : (int)pixel.X,
+            horAxisY is not null ? (int)Math.Round(2 * (double)horAxisY - (int)pixel.Y - 1) : (int)pixel.Y
         ));
         ));
         return new PixelsOperation(arr, color, blendMode);
         return new PixelsOperation(arr, color, blendMode);
     }
     }

+ 4 - 4
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -72,14 +72,14 @@ internal class RectangleOperation : IMirroredDrawOperation
 
 
     public void Dispose() { }
     public void Dispose() { }
 
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
     {
         if (verAxisX is not null && horAxisY is not null)
         if (verAxisX is not null && horAxisY is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((int)horAxisY).AsMirroredAcrossVerAxis((int)verAxisX));
+            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY).AsMirroredAcrossVerAxis((double)verAxisX));
         else if (verAxisX is not null)
         else if (verAxisX is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossVerAxis((int)verAxisX));
+            return new RectangleOperation(Data.AsMirroredAcrossVerAxis((double)verAxisX));
         else if (horAxisY is not null)
         else if (horAxisY is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((int)horAxisY));
+            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY));
         return new RectangleOperation(Data);
         return new RectangleOperation(Data);
     }
     }
 }
 }

+ 1 - 3
src/ChunkyImageLib/Surface.cs

@@ -22,9 +22,7 @@ public class Surface : IDisposable
     public Surface(VecI size)
     public Surface(VecI size)
     {
     {
         if (size.X < 1 || size.Y < 1)
         if (size.X < 1 || size.Y < 1)
-            throw new ArgumentException("Width and height must be >1");
-        if (size.X > 10000 || size.Y > 10000)
-            throw new ArgumentException("Width and height must be <=10000");
+            throw new ArgumentException("Width and height must be >=1");
 
 
         Size = size;
         Size = size;
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/Size_ChangeInfo.cs

@@ -2,4 +2,4 @@
 
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
 
 
-public record class Size_ChangeInfo(VecI Size, int VerticalSymmetryAxisX, int HorizontalSymmetryAxisY) : IChangeInfo;
+public record class Size_ChangeInfo(VecI Size, double VerticalSymmetryAxisX, double HorizontalSymmetryAxisY) : IChangeInfo;

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryAxisPosition_ChangeInfo.cs

@@ -1,4 +1,4 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
-public record class SymmetryAxisPosition_ChangeInfo(SymmetryAxisDirection Direction, int NewPosition) : IChangeInfo;
+public record class SymmetryAxisPosition_ChangeInfo(SymmetryAxisDirection Direction, double NewPosition) : IChangeInfo;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -26,8 +26,8 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     public VecI Size { get; set; } = DefaultSize;
     public VecI Size { get; set; } = DefaultSize;
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
-    public int HorizontalSymmetryAxisY { get; set; }
-    public int VerticalSymmetryAxisX { get; set; }
+    public double HorizontalSymmetryAxisY { get; set; }
+    public double VerticalSymmetryAxisX { get; set; }
 
 
     public void Dispose()
     public void Dispose()
     {
     {

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -33,12 +33,12 @@ public interface IReadOnlyDocument
     /// <summary>
     /// <summary>
     /// The position of the horizontal symmetry axis (Mirrors top and bottom)
     /// The position of the horizontal symmetry axis (Mirrors top and bottom)
     /// </summary>
     /// </summary>
-    int HorizontalSymmetryAxisY { get; }
+    double HorizontalSymmetryAxisY { get; }
 
 
     /// <summary>
     /// <summary>
     /// The position of the vertical symmetry axis (Mirrors left and right)
     /// The position of the vertical symmetry axis (Mirrors left and right)
     /// </summary>
     /// </summary>
-    int VerticalSymmetryAxisX { get; }
+    double VerticalSymmetryAxisX { get; }
 
 
     /// <summary>
     /// <summary>
     /// Performs the specified action on each readonly member of the document
     /// Performs the specified action on each readonly member of the document

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -88,9 +88,14 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             
             
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             {
             {
-                Color pixel = tempSurface.GetSRGBPixel(pos);
-                Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
-                layerImage.EnqueueDrawPixel(pos + offset, newColor, BlendMode.Src);
+                layerImage.EnqueueDrawPixel(
+                    pos + offset,
+                    (pixel) =>
+                    {
+                        Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
+                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
+                    },
+                    BlendMode.Src);
             }
             }
         }
         }
     }
     }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -6,8 +6,8 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal abstract class ResizeBasedChangeBase : Change
 internal abstract class ResizeBasedChangeBase : Change
 {
 {
     protected VecI _originalSize;
     protected VecI _originalSize;
-    protected int _originalHorAxisY;
-    protected int _originalVerAxisX;
+    protected double _originalHorAxisY;
+    protected double _originalVerAxisX;
     protected Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     protected Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     protected Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
     protected Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
     
     

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -12,8 +12,8 @@ internal class ResizeImage_Change : Change
     private readonly VecI newSize;
     private readonly VecI newSize;
     private readonly ResamplingMethod method;
     private readonly ResamplingMethod method;
     private VecI originalSize;
     private VecI originalSize;
-    private int originalHorAxisY;
-    private int originalVerAxisX;
+    private double originalHorAxisY;
+    private double originalVerAxisX;
     
     
     private Dictionary<Guid, CommittedChunkStorage> savedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> savedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> savedMaskChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> savedMaskChunks = new();

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -14,8 +14,8 @@ internal sealed class RotateImage_Change : Change
     private List<Guid> membersToRotate;
     private List<Guid> membersToRotate;
     
     
     private VecI originalSize;
     private VecI originalSize;
-    private int originalHorAxisY;
-    private int originalVerAxisX;
+    private double originalHorAxisY;
+    private double originalVerAxisX;
     private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
 
 
@@ -161,12 +161,12 @@ internal sealed class RotateImage_Change : Change
 
 
         VecI newSize = new VecI(newWidth, newHeight);
         VecI newSize = new VecI(newWidth, newHeight);
 
 
-        float normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
-        float normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
+        double normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
+        double normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
 
 
         target.Size = newSize;
         target.Size = newSize;
-        target.VerticalSymmetryAxisX = (int)(newSize.X * normalizedSymmX);
-        target.HorizontalSymmetryAxisY = (int)(newSize.Y * normalizedSymmY);
+        target.VerticalSymmetryAxisX = Math.Round(newSize.X * normalizedSymmX * 2) / 2;
+        target.HorizontalSymmetryAxisY = Math.Round(newSize.Y * normalizedSymmY * 2) / 2;
 
 
         target.ForEveryMember((member) =>
         target.ForEveryMember((member) =>
         {
         {

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs

@@ -5,18 +5,18 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
 internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
 {
 {
     private readonly SymmetryAxisDirection direction;
     private readonly SymmetryAxisDirection direction;
-    private int newPos;
-    private int originalPos;
+    private double newPos;
+    private double originalPos;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
-    public SymmetryAxisPosition_UpdateableChange(SymmetryAxisDirection direction, int pos)
+    public SymmetryAxisPosition_UpdateableChange(SymmetryAxisDirection direction, double pos)
     {
     {
         this.direction = direction;
         this.direction = direction;
         newPos = pos;
         newPos = pos;
     }
     }
 
 
     [UpdateChangeMethod]
     [UpdateChangeMethod]
-    public void Update(int pos)
+    public void Update(double pos)
     {
     {
         newPos = pos;
         newPos = pos;
     }
     }
@@ -32,7 +32,7 @@ internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
         return true;
         return true;
     }
     }
 
 
-    private void SetPosition(Document target, int position)
+    private void SetPosition(Document target, double position)
     {
     {
         if (direction == SymmetryAxisDirection.Horizontal)
         if (direction == SymmetryAxisDirection.Horizontal)
             target.HorizontalSymmetryAxisY = position;
             target.HorizontalSymmetryAxisY = position;

+ 10 - 13
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs

@@ -10,19 +10,15 @@ internal class MagicWand_Change : Change
     private VectorPath? originalPath;
     private VectorPath? originalPath;
     private VectorPath path = new() { FillType = PathFillType.EvenOdd };
     private VectorPath path = new() { FillType = PathFillType.EvenOdd };
     private VecI point;
     private VecI point;
-    private readonly Guid memberGuid;
-    private readonly bool referenceAll;
-    private readonly bool drawOnMask;
+    private readonly List<Guid> memberGuids;
     private readonly SelectionMode mode;
     private readonly SelectionMode mode;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
-    public MagicWand_Change(Guid memberGuid, VecI point, SelectionMode mode, bool referenceAll, bool drawOnMask)
+    public MagicWand_Change(List<Guid> memberGuids, VecI point, SelectionMode mode)
     {
     {
         path.MoveTo(point);
         path.MoveTo(point);
         this.mode = mode;
         this.mode = mode;
-        this.memberGuid = memberGuid;
-        this.referenceAll = referenceAll;
-        this.drawOnMask = drawOnMask;
+        this.memberGuids = memberGuids;
         this.point = point;
         this.point = point;
     }
     }
 
 
@@ -34,13 +30,14 @@ internal class MagicWand_Change : Change
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
     {
-        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-
         HashSet<Guid> membersToReference = new();
         HashSet<Guid> membersToReference = new();
-        if (referenceAll)
-            target.ForEveryReadonlyMember(member => membersToReference.Add(member.GuidValue));
-        else
-            membersToReference.Add(memberGuid);
+
+        target.ForEveryReadonlyMember(member =>
+        {
+            if (memberGuids.Contains(member.GuidValue))
+                membersToReference.Add(member.GuidValue);
+        });
+
         path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, target);
         path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, target);
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;

+ 11 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs

@@ -260,6 +260,17 @@ public struct RectD : IEquatable<RectD>
         }
         }
     }
     }
 
 
+    public readonly RectD Round()
+    {
+        return new RectD()
+        {
+            Left = Math.Round(left),
+            Right = Math.Round(right),
+            Top = Math.Round(top),
+            Bottom = Math.Round(bottom)
+        };
+    }
+
     public readonly RectD RoundOutwards()
     public readonly RectD RoundOutwards()
     {
     {
         return new RectD()
         return new RectD()

+ 10 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs

@@ -149,6 +149,16 @@ public struct RectI : IEquatable<RectI>
         };
         };
     }
     }
 
 
+    public readonly RectD ReflectX(double verLineX)
+    {
+        return RectD.FromTwoPoints(Pos.ReflectX(verLineX), (Pos + Size).ReflectX(verLineX));
+    }
+
+    public readonly RectD ReflectY(double horLineY)
+    {
+        return RectD.FromTwoPoints(Pos.ReflectY(horLineY), (Pos + Size).ReflectY(horLineY));
+    }
+
     public readonly RectI ReflectX(int verLineX)
     public readonly RectI ReflectX(int verLineX)
     {
     {
         return RectI.FromTwoPoints(Pos.ReflectX(verLineX), (Pos + Size).ReflectX(verLineX));
         return RectI.FromTwoPoints(Pos.ReflectX(verLineX), (Pos + Size).ReflectX(verLineX));

+ 14 - 0
src/PixiEditor.DrawingApi.Core/Numerics/VecI.cs

@@ -53,6 +53,20 @@ public struct VecI : IEquatable<VecI>
     {
     {
         return new(X, 2 * lineY - Y);
         return new(X, 2 * lineY - Y);
     }
     }
+    /// <summary>
+    /// Reflects the vector across a vertical line with the specified x position
+    /// </summary>
+    public VecD ReflectX(double lineX)
+    {
+        return new(2 * lineX - X, Y);
+    }
+    /// <summary>
+    /// Reflects the vector across a horizontal line with the specified y position
+    /// </summary>
+    public VecD ReflectY(double lineY)
+    {
+        return new(X, 2 * lineY - Y);
+    }
     public static VecI operator +(VecI a, VecI b)
     public static VecI operator +(VecI a, VecI b)
     {
     {
         return new VecI(a.X + b.X, a.Y + b.Y);
         return new VecI(a.X + b.X, a.Y + b.Y);

+ 6 - 1
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -81,12 +81,15 @@ public class UpdateChecker
 
 
     public bool CheckUpdateAvailable(ReleaseInfo latestRelease)
     public bool CheckUpdateAvailable(ReleaseInfo latestRelease)
     {
     {
+        if (latestRelease == null || string.IsNullOrEmpty(latestRelease.TagName)) return false;
+        if (CurrentVersionTag == null) return false;
+        
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
     }
     }
 
 
     public bool IsUpdateCompatible(string[] incompatibleVersions)
     public bool IsUpdateCompatible(string[] incompatibleVersions)
     {
     {
-        return !incompatibleVersions.Select(x => x.Trim()).Contains(CurrentVersionTag[..7].Trim());
+        return !incompatibleVersions.Select(x => x.Trim()).Contains(ExtractVersionString(CurrentVersionTag));
     }
     }
 
 
     public async Task<bool> IsUpdateCompatible()
     public async Task<bool> IsUpdateCompatible()
@@ -130,6 +133,8 @@ public class UpdateChecker
 
 
     private static string ExtractVersionString(string versionString)
     private static string ExtractVersionString(string versionString)
     {
     {
+        if (string.IsNullOrEmpty(versionString)) return string.Empty;
+        
         for (int i = 0; i < versionString.Length; i++)
         for (int i = 0; i < versionString.Length; i++)
         {
         {
             if (!char.IsDigit(versionString[i]) && versionString[i] != '.')
             if (!char.IsDigit(versionString[i]) && versionString[i] != '.')

+ 8 - 2
src/PixiEditor/App.xaml.cs

@@ -68,8 +68,14 @@ internal partial class App : Application
                                 if (mainWindow != null)
                                 if (mainWindow != null)
                                 {
                                 {
                                     mainWindow.BringToForeground();
                                     mainWindow.BringToForeground();
-                                    StartupArgs.Args = File.ReadAllText(passedArgsFile).Split(' ').ToList();
-                                    File.Delete(passedArgsFile);
+                                    List<string> args = new List<string>();
+                                    if (File.Exists(passedArgsFile))
+                                    {
+                                        args = File.ReadAllText(passedArgsFile).Split(' ').ToList();
+                                        File.Delete(passedArgsFile);
+                                    }
+                                    
+                                    StartupArgs.Args = args;
                                     StartupArgs.Args.Add("--openedInExisting");
                                     StartupArgs.Args.Add("--openedInExisting");
                                     mainWindow.DataContext.OnStartupCommand.Execute(null);
                                     mainWindow.DataContext.OnStartupCommand.Execute(null);
                                 }
                                 }

+ 8 - 2
src/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
+using System.Text;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Windows.Data;
 using System.Windows.Data;
 
 
@@ -28,6 +29,11 @@ internal class ToolSizeToIntConverter
             return null;
             return null;
         }
         }
 
 
-        return int.Parse(match.Groups[0].ValueSpan);
+        if (int.TryParse(match.Groups[0].ValueSpan.ToString().Normalize(NormalizationForm.FormKC), out int result))
+        {
+            return result;
+        }
+
+        return null;
     }
     }
-}
+}

+ 34 - 0
src/PixiEditor/Helpers/CrashHelper.cs

@@ -1,4 +1,7 @@
 using System.Globalization;
 using System.Globalization;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
 using System.Text;
 using System.Text;
 using ByteSizeLib;
 using ByteSizeLib;
 using Hardware.Info;
 using Hardware.Info;
@@ -97,4 +100,35 @@ internal class CrashHelper
             }
             }
         }
         }
     }
     }
+
+    public static async Task SendExceptionInfoToWebhook(Exception e, [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
+    {
+        if (ViewModelMain.Current.DebugSubViewModel.IsDebugBuild)
+            return;
+        await SendReportTextToWebhook(CrashReport.Generate(e), $"{filePath}; Method {memberName}");
+    }
+
+    public static async Task SendReportTextToWebhook(CrashReport report, string catchLocation = null)
+    {
+        string reportText = report.ReportText;
+        if (catchLocation is not null)
+        {
+            reportText = $"The report was generated from an exception caught in {catchLocation}.\r\n{reportText}";
+        }
+
+        byte[] bytes = Encoding.UTF8.GetBytes(reportText);
+        string filename = Path.GetFileNameWithoutExtension(report.FilePath) + ".txt";
+
+        MultipartFormDataContent formData = new MultipartFormDataContent
+        {
+            { new ByteArrayContent(bytes, 0, bytes.Length), "crash-report", filename }
+        };
+        try
+        {
+            using HttpClient httpClient = new HttpClient();
+            string url = BuildConstants.CrashReportWebhookUrl;
+            await httpClient.PostAsync(url, formData);
+        }
+        catch { }
+    }
 }
 }

+ 10 - 2
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -5,6 +5,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Parser;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
 namespace PixiEditor.Helpers;
 namespace PixiEditor.Helpers;
@@ -351,9 +352,16 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
             return this;
             return this;
         }
         }
 
 
-        public ReferenceLayerBuilder WithRect(VecD offset, VecD size)
+        public ReferenceLayerBuilder WithShape(Corners rect)
         {
         {
-            Shape = new ShapeCorners(new RectD(offset, size));
+            Shape = new ShapeCorners
+            {
+                TopLeft = rect.TopLeft.ToVecD(), 
+                TopRight = rect.TopRight.ToVecD(), 
+                BottomLeft = rect.BottomLeft.ToVecD(), 
+                BottomRight = rect.BottomRight.ToVecD()
+            };
+            
             return this;
             return this;
         }
         }
     }
     }

+ 6 - 1
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -9,6 +9,11 @@ namespace PixiEditor.Helpers.Extensions;
 
 
 internal static class PixiParserDocumentEx
 internal static class PixiParserDocumentEx
 {
 {
+    public static VecD ToVecD(this Vector2 vec)
+    {
+        return new VecD(vec.X, vec.Y);
+    }
+    
     public static DocumentViewModel ToDocument(this Document document)
     public static DocumentViewModel ToDocument(this Document document)
     {
     {
         return DocumentViewModel.Build(b =>
         return DocumentViewModel.Build(b =>
@@ -18,7 +23,7 @@ internal static class PixiParserDocumentEx
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A))
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A))
                 .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
                 .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
                     .WithIsVisible(r.Enabled)
                     .WithIsVisible(r.Enabled)
-                    .WithRect(new VecD(r.OffsetX, r.OffsetY), new VecD(r.Width, r.Height))
+                    .WithShape(r.Corners)
                     .WithSurface(Surface.Load(r.ImageBytes)));
                     .WithSurface(Surface.Load(r.ImageBytes)));
 
 
             BuildChildren(b, document.RootFolder.Children);
             BuildChildren(b, document.RootFolder.Children);

+ 5 - 0
src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs

@@ -2,6 +2,7 @@
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Parser;
 using PixiEditor.Parser.Collections.Deprecated;
 using PixiEditor.Parser.Collections.Deprecated;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -10,6 +11,10 @@ namespace PixiEditor.Helpers.Extensions;
 
 
 internal static class SerializableDocumentEx
 internal static class SerializableDocumentEx
 {
 {
+    public static Vector2 ToVector2(this VecD serializableVector2)
+    {
+        return new Vector2 { X = serializableVector2.X, Y = serializableVector2.Y };
+    }
     public static Image ToImage(this SerializableLayer serializableLayer)
     public static Image ToImage(this SerializableLayer serializableLayer)
     {
     {
         if (serializableLayer.PngBytes == null)
         if (serializableLayer.PngBytes == null)

+ 21 - 0
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -36,6 +36,27 @@ internal class SupportedFilesHelper
         return allFileTypeDialogsData.Where(i => i.FileType == type).Single();
         return allFileTypeDialogsData.Where(i => i.FileType == type).Single();
     }
     }
 
 
+    public static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
+    {
+        if (requestedType == FileType.Unset)
+            throw new ArgumentException("A valid filetype is required", nameof(requestedType));
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
+        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
+            return pathWithOrWithoutExtension;
+        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
+    }
+
+    public static string AppendExtension(string path, FileTypeDialogData data)
+    {
+        string ext = data.Extensions.First();
+        string filename = Path.GetFileName(path);
+        if (filename.Length + ext.Length > 255)
+            filename = filename.Substring(0, 255 - ext.Length);
+        filename += ext;
+        return Path.Combine(Path.GetDirectoryName(path), filename);
+    }
+
     public static bool IsSupportedFile(string path)
     public static bool IsSupportedFile(string path)
     {
     {
         var ext = Path.GetExtension(path.ToLower());
         var ext = Path.GetExtension(path.ToLower());

+ 19 - 25
src/PixiEditor/Helpers/VersionHelpers.cs

@@ -10,37 +10,31 @@ internal static class VersionHelpers
 
 
     public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
     public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
 
 
-    public static string GetCurrentAssemblyVersionString()
+    public static string GetCurrentAssemblyVersionString(bool moreSpecific = false)
     {
     {
         StringBuilder builder = new(GetCurrentAssemblyVersion().ToString());
         StringBuilder builder = new(GetCurrentAssemblyVersion().ToString());
 
 
-        bool isDone = false;
-
-        AppendDevBuild(builder, ref isDone);
+#if DEVRELEASE
+        builder.Append(" Dev Build");
+        return builder.ToString();
+#elif MSIX_DEBUG
+        builder.Append(" MSIX Debug Build");
+        return builder.ToString();
+#elif DEBUG
+        builder.Append(" Debug Build");
+        return builder.ToString();
+#endif
 
 
-        if (isDone)
-        {
+        if (!moreSpecific)
             return builder.ToString();
             return builder.ToString();
-        }
-
-        AppendDebugBuild(builder, ref isDone);
 
 
+#if STEAM
+        builder.Append(" Steam Build");
+#elif MSIX
+        builder.Append(" MSIX Build");
+#elif RELEASE
+        builder.Append(" Release Build");
+#endif
         return builder.ToString();
         return builder.ToString();
     }
     }
-
-    [Conditional("DEVRELEASE")]
-    private static void AppendDevBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Dev Build");
-    }
-
-    [Conditional("DEBUG")]
-    private static void AppendDebugBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Debug Build");
-    }
 }
 }

+ 19 - 3
src/PixiEditor/Models/Commands/CommandController.cs

@@ -282,15 +282,31 @@ internal class CommandController
 
 
             var parameters = method?.GetParameters();
             var parameters = method?.GetParameters();
 
 
-            Action<object> action;
+            async void ActionOnException(Task faultedTask)
+            {
+                // since this method is "async void" and not "async Task", the runtime will propagate exceptions out if it
+                // (instead of putting them into the returned task and forgetting about them)
+                await faultedTask; // this instantly throws the exception from the already faulted task
+            }
 
 
+            Action<object> action;
             if (parameters is not { Length: 1 })
             if (parameters is not { Length: 1 })
             {
             {
-                action = x => method.Invoke(instance, null);
+                action = x =>
+                {
+                    object result = method.Invoke(instance, null);
+                    if (result is Task task)
+                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+                };
             }
             }
             else
             else
             {
             {
-                action = x => method.Invoke(instance, new[] { x });
+                action = x =>
+                {
+                    object result = method.Invoke(instance, new[] { x });
+                    if (result is Task task)
+                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+                };
             }
             }
 
 
             string name = attribute.InternalName;
             string name = attribute.InternalName;

+ 1 - 1
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -19,7 +19,7 @@ internal class CrashReport : IDisposable
         DateTime currentTime = DateTime.Now;
         DateTime currentTime = DateTime.Now;
 
 
         builder
         builder
-            .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString()} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
+            .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString(moreSpecific: true)} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
             .AppendLine("-----System Information----")
             .AppendLine("-----System Information----")
             .AppendLine("General:")
             .AppendLine("General:")
             .AppendLine($"  OS: {Environment.OSVersion.VersionString}")
             .AppendLine($"  OS: {Environment.OSVersion.VersionString}")

+ 9 - 0
src/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -41,6 +41,10 @@ internal class RecentlyOpenedDocument : NotifyableObject
     {
     {
         get
         get
         {
         {
+            if (!File.Exists(FilePath))
+            {
+                return "? (Not found)";
+            }
             if (Corrupt)
             if (Corrupt)
             {
             {
                 return "? (Corrupt)";
                 return "? (Corrupt)";
@@ -71,6 +75,11 @@ internal class RecentlyOpenedDocument : NotifyableObject
 
 
     private WriteableBitmap LoadPreviewBitmap()
     private WriteableBitmap LoadPreviewBitmap()
     {
     {
+        if (!File.Exists(FilePath))
+        {
+            return null;
+        }
+        
         if (FileExtension == ".pixi")
         if (FileExtension == ".pixi")
         {
         {
             SerializableDocument serializableDocument;
             SerializableDocument serializableDocument;

+ 3 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -7,6 +7,7 @@ using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.Models.Rendering.RenderInfos;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -75,6 +76,7 @@ internal class ActionAccumulator
             // update viewmodels based on changes
             // update viewmodels based on changes
             List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
             List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
             bool undoBoundaryPassed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
             bool undoBoundaryPassed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
+            bool viewportRefreshRequest = toExecute.Any(static action => action is RefreshViewport_PassthroughAction);
             foreach (IChangeInfo info in optimizedChanges)
             foreach (IChangeInfo info in optimizedChanges)
             {
             {
                 internals.Updater.ApplyChangeFromChangeInfo(info);
                 internals.Updater.ApplyChangeFromChangeInfo(info);
@@ -104,7 +106,7 @@ internal class ActionAccumulator
             // update the contents of the bitmaps
             // update the contents of the bitmaps
             var affectedAreas = new AffectedAreasGatherer(internals.Tracker, optimizedChanges);
             var affectedAreas = new AffectedAreasGatherer(internals.Tracker, optimizedChanges);
             List<IRenderInfo> renderResult = new();
             List<IRenderInfo> renderResult = new();
-            renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+            renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
 
 
             // lock bitmaps
             // lock bitmaps

+ 37 - 23
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -15,9 +15,6 @@ internal class ChangeExecutionController
     public ShapeCorners LastTransformState { get; private set; }
     public ShapeCorners LastTransformState { get; private set; }
     public VecI LastPixelPosition => lastPixelPos;
     public VecI LastPixelPosition => lastPixelPos;
     public VecD LastPrecisePosition => lastPrecisePos;
     public VecD LastPrecisePosition => lastPrecisePos;
-    public float LastOpacityValue = 1f;
-    public int LastHorizontalSymmetryAxisPosition { get; private set; }
-    public int LastVerticalSymmetryAxisPosition { get; private set; }
     public bool IsChangeActive => currentSession is not null;
     public bool IsChangeActive => currentSession is not null;
 
 
     private readonly DocumentViewModel document;
     private readonly DocumentViewModel document;
@@ -27,6 +24,8 @@ internal class ChangeExecutionController
     private VecD lastPrecisePos;
     private VecD lastPrecisePos;
 
 
     private UpdateableChangeExecutor? currentSession = null;
     private UpdateableChangeExecutor? currentSession = null;
+    
+    private UpdateableChangeExecutor? _queuedExecutor = null;
 
 
     public ChangeExecutionController(DocumentViewModel document, DocumentInternalParts internals)
     public ChangeExecutionController(DocumentViewModel document, DocumentInternalParts internals)
     {
     {
@@ -44,32 +43,51 @@ internal class ChangeExecutionController
     public bool TryStartExecutor<T>(bool force = false)
     public bool TryStartExecutor<T>(bool force = false)
         where T : UpdateableChangeExecutor, new()
         where T : UpdateableChangeExecutor, new()
     {
     {
-        if (currentSession is not null && !force)
+        if (CanStartExecutor(force))
             return false;
             return false;
         if (force)
         if (force)
             currentSession?.ForceStop();
             currentSession?.ForceStop();
+        
         T executor = new T();
         T executor = new T();
-        executor.Initialize(document, internals, this, EndExecutor);
-        if (executor.Start() == ExecutionState.Success)
-        {
-            currentSession = executor;
-            return true;
-        }
-        return false;
+        return TryStartExecutorInternal(executor);
     }
     }
 
 
     public bool TryStartExecutor(UpdateableChangeExecutor brandNewExecutor, bool force = false)
     public bool TryStartExecutor(UpdateableChangeExecutor brandNewExecutor, bool force = false)
     {
     {
-        if (currentSession is not null && !force)
+        if (CanStartExecutor(force))
             return false;
             return false;
         if (force)
         if (force)
             currentSession?.ForceStop();
             currentSession?.ForceStop();
-        brandNewExecutor.Initialize(document, internals, this, EndExecutor);
+        
+        return TryStartExecutorInternal(brandNewExecutor);
+    }
+
+    private bool CanStartExecutor(bool force)
+    {
+        return (currentSession is not null || _queuedExecutor is not null) && !force;
+    }
+
+    private bool TryStartExecutorInternal(UpdateableChangeExecutor executor)
+    {
+        executor.Initialize(document, internals, this, EndExecutor);
+
+        if (executor.StartMode == ExecutorStartMode.OnMouseLeftButtonDown)
+        {
+            _queuedExecutor = executor;
+            return true;
+        }
+
+        return StartExecutor(executor);
+    }
+    
+    private bool StartExecutor(UpdateableChangeExecutor brandNewExecutor)
+    {
         if (brandNewExecutor.Start() == ExecutionState.Success)
         if (brandNewExecutor.Start() == ExecutionState.Success)
         {
         {
             currentSession = brandNewExecutor;
             currentSession = brandNewExecutor;
             return true;
             return true;
         }
         }
+
         return false;
         return false;
     }
     }
 
 
@@ -78,6 +96,7 @@ internal class ChangeExecutionController
         if (executor != currentSession)
         if (executor != currentSession)
             throw new InvalidOperationException();
             throw new InvalidOperationException();
         currentSession = null;
         currentSession = null;
+        _queuedExecutor = null;
     }
     }
 
 
     public bool TryStopActiveExecutor()
     public bool TryStopActiveExecutor()
@@ -126,7 +145,6 @@ internal class ChangeExecutionController
     public void OpacitySliderDragStartedInlet() => currentSession?.OnOpacitySliderDragStarted();
     public void OpacitySliderDragStartedInlet() => currentSession?.OnOpacitySliderDragStarted();
     public void OpacitySliderDraggedInlet(float newValue)
     public void OpacitySliderDraggedInlet(float newValue)
     {
     {
-        LastOpacityValue = newValue;
         currentSession?.OnOpacitySliderDragged(newValue);
         currentSession?.OnOpacitySliderDragged(newValue);
     }
     }
     public void OpacitySliderDragEndedInlet() => currentSession?.OnOpacitySliderDragEnded();
     public void OpacitySliderDragEndedInlet() => currentSession?.OnOpacitySliderDragEnded();
@@ -134,15 +152,6 @@ internal class ChangeExecutionController
     public void SymmetryDragStartedInlet(SymmetryAxisDirection dir) => currentSession?.OnSymmetryDragStarted(dir);
     public void SymmetryDragStartedInlet(SymmetryAxisDirection dir) => currentSession?.OnSymmetryDragStarted(dir);
     public void SymmetryDraggedInlet(SymmetryAxisDragInfo info)
     public void SymmetryDraggedInlet(SymmetryAxisDragInfo info)
     {
     {
-        switch (info.Direction)
-        {
-            case SymmetryAxisDirection.Horizontal:
-                LastHorizontalSymmetryAxisPosition = info.NewPosition;
-                break;
-            case SymmetryAxisDirection.Vertical:
-                LastVerticalSymmetryAxisPosition = info.NewPosition;
-                break;
-        }
         currentSession?.OnSymmetryDragged(info);
         currentSession?.OnSymmetryDragged(info);
     }
     }
 
 
@@ -153,6 +162,11 @@ internal class ChangeExecutionController
         //update internal state
         //update internal state
         LeftMouseState = MouseButtonState.Pressed;
         LeftMouseState = MouseButtonState.Pressed;
 
 
+        if (_queuedExecutor != null && currentSession == null)
+        {
+            StartExecutor(_queuedExecutor);
+        }
+        
         //call session event
         //call session event
         currentSession?.OnLeftMouseButtonDown(canvasPos);
         currentSession?.OnLeftMouseButtonDown(canvasPos);
     }
     }

+ 30 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -68,6 +68,36 @@ internal class DocumentStructureModule
             list.Add(doc.StructureRoot);
             list.Add(doc.StructureRoot);
         return list;
         return list;
     }
     }
+    
+    /// <summary>
+    ///     Returns all layers in the document.
+    /// </summary>
+    /// <returns>List of LayerViewModels. Empty if no layers found.</returns>
+    public List<LayerViewModel> GetAllLayers()
+    {
+        List<LayerViewModel> layers = new List<LayerViewModel>();
+        foreach (StructureMemberViewModel? member in doc.StructureRoot.Children)
+        {
+            if (member is LayerViewModel layer)
+                layers.Add(layer);
+            else if (member is FolderViewModel folder)
+                layers.AddRange(GetAllLayers(folder, layers));
+        }
+        
+        return layers;
+    }
+    
+    private List<LayerViewModel> GetAllLayers(FolderViewModel folder, List<LayerViewModel> layers)
+    {
+        foreach (StructureMemberViewModel? member in folder.Children)
+        {
+            if (member is LayerViewModel layer)
+                layers.Add(layer);
+            else if (member is FolderViewModel innerFolder)
+                layers.AddRange(GetAllLayers(innerFolder, layers));
+        }
+        return layers;
+    }
 
 
     private bool FillPath(FolderViewModel folder, Guid guid, List<StructureMemberViewModel> toFill)
     private bool FillPath(FolderViewModel folder, Guid guid, List<StructureMemberViewModel> toFill)
     {
     {

+ 7 - 10
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -11,28 +11,25 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
 {
 {
     private bool considerAllLayers;
     private bool considerAllLayers;
     private bool drawOnMask;
     private bool drawOnMask;
-    private Guid memberGuid;
+    private List<Guid> memberGuids;
     private SelectionMode mode;
     private SelectionMode mode;
 
 
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         var magicWand = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MagicWandToolViewModel>();
         var magicWand = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MagicWandToolViewModel>();
-        var member = document!.SelectedStructureMember;
+        var members = document!.ExtractSelectedLayers(true);
 
 
-        if (magicWand is null || member is null)
-            return ExecutionState.Error;
-        drawOnMask = member is not LayerViewModel layer || layer.ShouldDrawOnMask;
-        if (drawOnMask && !member.HasMaskBindable)
-            return ExecutionState.Error;
-        if (!drawOnMask && member is not LayerViewModel)
+        if (magicWand is null || members.Count == 0)
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
         mode = magicWand.SelectMode;
         mode = magicWand.SelectMode;
-        memberGuid = member.GuidValue;
+        memberGuids = members;
         considerAllLayers = magicWand.DocumentScope == DocumentScope.AllLayers;
         considerAllLayers = magicWand.DocumentScope == DocumentScope.AllLayers;
+        if (considerAllLayers)
+            memberGuids = document!.StructureHelper.GetAllLayers().Select(x => x.GuidValue).ToList();
         var pos = controller!.LastPixelPosition;
         var pos = controller!.LastPixelPosition;
 
 
-        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuid, pos, mode, considerAllLayers, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode));
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }

+ 15 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs

@@ -13,17 +13,28 @@ internal class ShiftLayerExecutor : UpdateableChangeExecutor
     private VecI startPos;
     private VecI startPos;
     private MoveToolViewModel? tool;
     private MoveToolViewModel? tool;
 
 
+    public override ExecutorStartMode StartMode => ExecutorStartMode.OnMouseLeftButtonDown;
+
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         ViewModelMain? vm = ViewModelMain.Current;
         ViewModelMain? vm = ViewModelMain.Current;
         StructureMemberViewModel? member = document!.SelectedStructureMember;
         StructureMemberViewModel? member = document!.SelectedStructureMember;
-        if(member != null)
-            _affectedMemberGuids.Add(member.GuidValue);
-        _affectedMemberGuids.AddRange(document!.SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        
         tool = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MoveToolViewModel>();
         tool = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MoveToolViewModel>();
         if (vm is null || tool is null)
         if (vm is null || tool is null)
             return ExecutionState.Error;
             return ExecutionState.Error;
-        
+
+        if (tool.MoveAllLayers)
+        {
+            _affectedMemberGuids.AddRange(document.StructureHelper.GetAllLayers().Select(x => x.GuidValue));
+        }
+        else
+        {
+            if (member != null)
+                _affectedMemberGuids.Add(member.GuidValue);
+            _affectedMemberGuids.AddRange(document!.SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        }
+
         RemoveDrawOnMaskLayers(_affectedMemberGuids);
         RemoveDrawOnMaskLayers(_affectedMemberGuids);
         
         
         startPos = controller!.LastPixelPosition;
         startPos = controller!.LastPixelPosition;

+ 3 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SymmetryExecutor.cs

@@ -18,10 +18,10 @@ internal class SymmetryExecutor : UpdateableChangeExecutor
             !document.VerticalSymmetryAxisEnabledBindable && dir == SymmetryAxisDirection.Vertical)
             !document.VerticalSymmetryAxisEnabledBindable && dir == SymmetryAxisDirection.Vertical)
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
-        int lastPos = dir switch
+        double lastPos = dir switch
         {
         {
-            SymmetryAxisDirection.Horizontal => controller.LastHorizontalSymmetryAxisPosition,
-            SymmetryAxisDirection.Vertical => controller.LastVerticalSymmetryAxisPosition,
+            SymmetryAxisDirection.Horizontal => document.HorizontalSymmetryAxisYBindable,
+            SymmetryAxisDirection.Vertical => document.VerticalSymmetryAxisXBindable,
             _ => throw new NotImplementedException(),
             _ => throw new NotImplementedException(),
         };
         };
         internals.ActionAccumulator.AddActions(new SymmetryAxisPosition_Action(dir, lastPos));
         internals.ActionAccumulator.AddActions(new SymmetryAxisPosition_Action(dir, lastPos));

+ 16 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs

@@ -11,7 +11,6 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
 {
 {
     private Guid[]? membersToTransform;
     private Guid[]? membersToTransform;
     private MoveToolViewModel? tool;
     private MoveToolViewModel? tool;
-
     public override ExecutorType Type { get; }
     public override ExecutorType Type { get; }
 
 
     public TransformSelectedAreaExecutor(bool toolLinked)
     public TransformSelectedAreaExecutor(bool toolLinked)
@@ -25,10 +24,13 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
         if (tool is null || document!.SelectedStructureMember is null || document!.SelectionPathBindable.IsEmpty)
         if (tool is null || document!.SelectedStructureMember is null || document!.SelectionPathBindable.IsEmpty)
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
-        var members = document.SoftSelectedStructureMembers
+        tool.TransformingSelectedArea = true;
+        List<StructureMemberViewModel> members = new();
+        
+        members = document.SoftSelectedStructureMembers
             .Append(document.SelectedStructureMember)
             .Append(document.SelectedStructureMember)
-            .Where(static m => m is LayerViewModel);
-
+            .Where(static m => m is LayerViewModel).ToList();
+        
         if (!members.Any())
         if (!members.Any())
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
@@ -54,6 +56,11 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
 
 
     public override void OnTransformApplied()
     public override void OnTransformApplied()
     {
     {
+        if (tool is not null)
+        {
+            tool.TransformingSelectedArea = false;
+        }
+        
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformViewModel.HideTransform();
         document!.TransformViewModel.HideTransform();
@@ -67,6 +74,11 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
 
 
     public override void ForceStop()
     public override void ForceStop()
     {
     {
+        if (tool is not null)
+        {
+            tool.TransformingSelectedArea = false;
+        }
+        
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformViewModel.HideTransform();
         document!.TransformViewModel.HideTransform();

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -17,6 +17,7 @@ internal abstract class UpdateableChangeExecutor
 
 
     protected Action<UpdateableChangeExecutor>? onEnded;
     protected Action<UpdateableChangeExecutor>? onEnded;
     public virtual ExecutorType Type => ExecutorType.Regular;
     public virtual ExecutorType Type => ExecutorType.Regular;
+    public virtual ExecutorStartMode StartMode => ExecutorStartMode.RightAway;
 
 
     public void Initialize(DocumentViewModel document, DocumentInternalParts internals, ChangeExecutionController controller, Action<UpdateableChangeExecutor> onEnded)
     public void Initialize(DocumentViewModel document, DocumentInternalParts internals, ChangeExecutionController controller, Action<UpdateableChangeExecutor> onEnded)
     {
     {

+ 6 - 0
src/PixiEditor/Models/Enums/ExecutorType.cs

@@ -5,3 +5,9 @@ internal enum ExecutorType
     Regular,
     Regular,
     ToolLinked,
     ToolLinked,
 }
 }
+
+internal enum ExecutorStartMode
+{
+    RightAway,
+    OnMouseLeftButtonDown,
+}

+ 27 - 30
src/PixiEditor/Models/IO/Exporter.cs

@@ -1,6 +1,8 @@
 using System.IO;
 using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
+using System.Reflection.Metadata;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
+using System.Security;
 using System.Windows;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib;
@@ -22,8 +24,10 @@ internal enum DialogSaveResult
     Success = 0,
     Success = 0,
     InvalidPath = 1,
     InvalidPath = 1,
     ConcurrencyError = 2,
     ConcurrencyError = 2,
-    UnknownError = 3,
-    Cancelled = 4,
+    SecurityError = 3,
+    IoError = 4,
+    UnknownError = 5,
+    Cancelled = 6,
 }
 }
 
 
 internal enum SaveResult
 internal enum SaveResult
@@ -31,7 +35,9 @@ internal enum SaveResult
     Success = 0,
     Success = 0,
     InvalidPath = 1,
     InvalidPath = 1,
     ConcurrencyError = 2,
     ConcurrencyError = 2,
-    UnknownError = 3,
+    SecurityError = 3,
+    IoError = 4,
+    UnknownError = 5,
 }
 }
 
 
 internal class Exporter
 internal class Exporter
@@ -67,7 +73,7 @@ internal class Exporter
     /// </summary>
     /// </summary>
     public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, FileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
     public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, FileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
     {
     {
-        finalPath = FixFileExtension(pathFromDialog, fileTypeFromDialog);
+        finalPath = SupportedFilesHelper.FixFileExtension(pathFromDialog, fileTypeFromDialog);
         var saveResult = TrySave(document, finalPath, exportSize);
         var saveResult = TrySave(document, finalPath, exportSize);
         if (saveResult != SaveResult.Success)
         if (saveResult != SaveResult.Success)
             finalPath = "";
             finalPath = "";
@@ -75,17 +81,6 @@ internal class Exporter
         return saveResult;
         return saveResult;
     }
     }
 
 
-    private static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
-    {
-        if (requestedType == FileType.Unset)
-            throw new ArgumentException("A valid filetype is required", nameof(requestedType));
-
-        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
-        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
-            return pathWithOrWithoutExtension;
-        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
-    }
-
     /// <summary>
     /// <summary>
     /// Attempts to save the document into the given location, filetype is inferred from path
     /// Attempts to save the document into the given location, filetype is inferred from path
     /// </summary>
     /// </summary>
@@ -104,8 +99,12 @@ internal class Exporter
                 return SaveResult.ConcurrencyError;
                 return SaveResult.ConcurrencyError;
             var bitmap = maybeBitmap.AsT1;
             var bitmap = maybeBitmap.AsT1;
 
 
-            if (!TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize))
+            if (!encodersFactory.ContainsKey(typeFromPath))
+            {
                 return SaveResult.UnknownError;
                 return SaveResult.UnknownError;
+            }
+
+            return TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize);
         }
         }
         else
         else
         {
         {
@@ -115,16 +114,6 @@ internal class Exporter
         return SaveResult.Success;
         return SaveResult.Success;
     }
     }
 
 
-    private static string AppendExtension(string path, FileTypeDialogData data)
-    {
-        string ext = data.Extensions.First();
-        string filename = Path.GetFileName(path);
-        if (filename.Length + ext.Length > 255)
-            filename = filename.Substring(0, 255 - ext.Length);
-        filename += ext;
-        return Path.Combine(Path.GetDirectoryName(path), filename);
-    }
-
     static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
     static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
 
 
     static Exporter()
     static Exporter()
@@ -165,7 +154,7 @@ internal class Exporter
     /// <summary>
     /// <summary>
     /// Saves image to PNG file. Messes with the passed bitmap.
     /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
     /// </summary>
-    private static bool TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
+    private static SaveResult TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
     {
     {
         try
         try
         {
         {
@@ -179,10 +168,18 @@ internal class Exporter
             encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
             encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
             encoder.Save(stream);
             encoder.Save(stream);
         }
         }
-        catch (Exception err)
+        catch (SecurityException)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (IOException)
         {
         {
-            return false;
+            return SaveResult.IoError;
         }
         }
-        return true;
+        catch
+        {
+            return SaveResult.UnknownError;
+        }
+        return SaveResult.Success;
     }
     }
 }
 }

+ 1 - 1
src/PixiEditor/PixiEditor.csproj

@@ -236,7 +236,7 @@
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
 		<PackageReference Include="OneOf" Version="3.0.223" />
 		<PackageReference Include="OneOf" Version="3.0.223" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
-		<PackageReference Include="PixiEditor.Parser" Version="3.2.0" />
+		<PackageReference Include="PixiEditor.Parser" Version="3.3.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="3.0.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="3.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -50,5 +50,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.1")]
-[assembly: AssemblyFileVersion("1.0.0.1")]
+[assembly: AssemblyVersion("1.0.1.0")]
+[assembly: AssemblyFileVersion("1.0.1.0")]

+ 1 - 19
src/PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -40,25 +40,7 @@ internal class CrashReportViewModel : ViewModelBase
         AttachDebuggerCommand = new(AttachDebugger);
         AttachDebuggerCommand = new(AttachDebugger);
 
 
         if (!IsDebugBuild)
         if (!IsDebugBuild)
-            SendReportTextToWebhook(report);
-    }
-
-    private async void SendReportTextToWebhook(CrashReport report)
-    {
-        byte[] bytes = Encoding.UTF8.GetBytes(report.ReportText);
-        string filename = Path.GetFileNameWithoutExtension(report.FilePath) + ".txt";
-
-        MultipartFormDataContent formData = new MultipartFormDataContent
-        {
-            { new ByteArrayContent(bytes, 0, bytes.Length), "crash-report", filename }
-        };
-        try
-        {
-            using HttpClient httpClient = new HttpClient();
-            string url = BuildConstants.CrashReportWebhookUrl;
-            await httpClient.PostAsync(url, formData);
-        }
-        catch { }
+            _ = CrashHelper.SendReportTextToWebhook(report);
     }
     }
 
 
     public void RecoverDocuments(object args)
     public void RecoverDocuments(object args)

+ 10 - 2
src/PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -59,13 +59,21 @@ internal class SaveFilePopupViewModel : ViewModelBase
             Title = "Export path",
             Title = "Export path",
             CheckPathExists = true,
             CheckPathExists = true,
             Filter = SupportedFilesHelper.BuildSaveFilter(false),
             Filter = SupportedFilesHelper.BuildSaveFilter(false),
-            FilterIndex = 0
+            FilterIndex = 0,
+            AddExtension = true
         };
         };
         if (path.ShowDialog() == true)
         if (path.ShowDialog() == true)
         {
         {
             if (string.IsNullOrEmpty(path.FileName) == false)
             if (string.IsNullOrEmpty(path.FileName) == false)
             {
             {
-                ChosenFormat = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(path.SafeFileName));
+                ChosenFormat = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(false, path.FilterIndex);
+                if (ChosenFormat == FileType.Unset)
+                {
+                    return null;
+                }
+
+                path.FileName = SupportedFilesHelper.FixFileExtension(path.FileName, ChosenFormat);
+
                 return path.FileName;
                 return path.FileName;
             }
             }
         }
         }

+ 7 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs

@@ -69,6 +69,13 @@ internal partial class DocumentViewModel
             Height = (float)layer.Shape.RectSize.Y,
             Height = (float)layer.Shape.RectSize.Y,
             OffsetX = (float)layer.Shape.TopLeft.X,
             OffsetX = (float)layer.Shape.TopLeft.X,
             OffsetY = (float)layer.Shape.TopLeft.Y,
             OffsetY = (float)layer.Shape.TopLeft.Y,
+            Corners = new Corners
+            {
+                TopLeft = layer.Shape.TopLeft.ToVector2(), 
+                TopRight = layer.Shape.TopRight.ToVector2(), 
+                BottomLeft = layer.Shape.BottomLeft.ToVector2(), 
+                BottomRight = layer.Shape.BottomRight.ToVector2()
+            },
             Opacity = 1,
             Opacity = 1,
             ImageBytes = stream.ToArray()
             ImageBytes = stream.ToArray()
         };
         };

+ 56 - 10
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -102,11 +102,11 @@ internal partial class DocumentViewModel : NotifyableObject
     public int Height => size.Y;
     public int Height => size.Y;
     public VecI SizeBindable => size;
     public VecI SizeBindable => size;
 
 
-    private int horizontalSymmetryAxisY;
-    public int HorizontalSymmetryAxisYBindable => horizontalSymmetryAxisY;
+    private double horizontalSymmetryAxisY;
+    public double HorizontalSymmetryAxisYBindable => horizontalSymmetryAxisY;
 
 
-    private int verticalSymmetryAxisX;
-    public int VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
+    private double verticalSymmetryAxisX;
+    public double VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
 
 
     private readonly HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
     private readonly HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
     public IReadOnlyCollection<StructureMemberViewModel> SoftSelectedStructureMembers => softSelectedStructureMembers;
     public IReadOnlyCollection<StructureMemberViewModel> SoftSelectedStructureMembers => softSelectedStructureMembers;
@@ -197,9 +197,9 @@ internal partial class DocumentViewModel : NotifyableObject
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(new SymmetryAxisDragInfo(SymmetryAxisDirection.Vertical, builderInstance.Width / 2));
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(new SymmetryAxisDragInfo(SymmetryAxisDirection.Vertical, builderInstance.Width / 2));
 
 
         acc.AddActions(
         acc.AddActions(
-            new SymmetryAxisPosition_Action(SymmetryAxisDirection.Horizontal, builderInstance.Height / 2),
+            new SymmetryAxisPosition_Action(SymmetryAxisDirection.Horizontal, (double)builderInstance.Height / 2),
             new EndSymmetryAxisPosition_Action(),
             new EndSymmetryAxisPosition_Action(),
-            new SymmetryAxisPosition_Action(SymmetryAxisDirection.Vertical, builderInstance.Width / 2),
+            new SymmetryAxisPosition_Action(SymmetryAxisDirection.Vertical, (double)builderInstance.Width / 2),
             new EndSymmetryAxisPosition_Action());
             new EndSymmetryAxisPosition_Action());
 
 
         if (builderInstance.ReferenceLayer is { } refLayer)
         if (builderInstance.ReferenceLayer is { } refLayer)
@@ -476,13 +476,13 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(HorizontalSymmetryAxisEnabledBindable));
         RaisePropertyChanged(nameof(HorizontalSymmetryAxisEnabledBindable));
     }
     }
 
 
-    public void InternalSetVerticalSymmetryAxisX(int verticalSymmetryAxisX)
+    public void InternalSetVerticalSymmetryAxisX(double verticalSymmetryAxisX)
     {
     {
         this.verticalSymmetryAxisX = verticalSymmetryAxisX;
         this.verticalSymmetryAxisX = verticalSymmetryAxisX;
         RaisePropertyChanged(nameof(VerticalSymmetryAxisXBindable));
         RaisePropertyChanged(nameof(VerticalSymmetryAxisXBindable));
     }
     }
 
 
-    public void InternalSetHorizontalSymmetryAxisY(int horizontalSymmetryAxisY)
+    public void InternalSetHorizontalSymmetryAxisY(double horizontalSymmetryAxisY)
     {
     {
         this.horizontalSymmetryAxisY = horizontalSymmetryAxisY;
         this.horizontalSymmetryAxisY = horizontalSymmetryAxisY;
         RaisePropertyChanged(nameof(HorizontalSymmetryAxisYBindable));
         RaisePropertyChanged(nameof(HorizontalSymmetryAxisYBindable));
@@ -520,8 +520,54 @@ internal partial class DocumentViewModel : NotifyableObject
     /// </summary>
     /// </summary>
     public List<Guid> GetSelectedMembers()
     public List<Guid> GetSelectedMembers()
     {
     {
-        List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
-        layerGuids.AddRange( SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        List<Guid> layerGuids = new List<Guid>();
+        if (SelectedStructureMember is not null)
+            layerGuids.Add(SelectedStructureMember.GuidValue);
+
+        layerGuids.AddRange(SoftSelectedStructureMembers.Select(x => x.GuidValue));
         return layerGuids;
         return layerGuids;
     }
     }
+
+    public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false)
+    {
+        var result = new List<Guid>();
+        List<Guid> selectedMembers = GetSelectedMembers();
+        foreach (var member in selectedMembers)
+        {
+            var foundMember = StructureHelper.Find(member);
+            if (foundMember != null)
+            {
+                if (foundMember is LayerViewModel layer && selectedMembers.Contains(foundMember.GuidValue) && !result.Contains(layer.GuidValue))
+                {
+                    result.Add(layer.GuidValue);
+                }
+                else if (foundMember is FolderViewModel folder && selectedMembers.Contains(foundMember.GuidValue))
+                {
+                    if (includeFoldersWithMask && folder.HasMaskBindable && !result.Contains(folder.GuidValue))
+                        result.Add(folder.GuidValue);
+                    ExtractSelectedLayers(folder, result, includeFoldersWithMask);
+                }
+            }
+        }
+        return result;
+    }
+
+    private void ExtractSelectedLayers(FolderViewModel folder, List<Guid> list,
+        bool includeFoldersWithMask)
+    {
+        foreach (var member in folder.Children)
+        {
+            if (member is LayerViewModel layer && !list.Contains(layer.GuidValue))
+            {
+                list.Add(layer.GuidValue);
+            }
+            else if (member is FolderViewModel childFolder)
+            {
+                if (includeFoldersWithMask && childFolder.HasMaskBindable && !list.Contains(childFolder.GuidValue))
+                    list.Add(childFolder.GuidValue);
+
+                ExtractSelectedLayers(childFolder, list, includeFoldersWithMask);
+            }
+        }
+    }
 }
 }

+ 25 - 8
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -36,22 +36,39 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         UpdateDebugMode(preferences.GetPreference<bool>("IsDebugModeEnabled"));
         UpdateDebugMode(preferences.GetPreference<bool>("IsDebugModeEnabled"));
     }
     }
 
 
-    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"%Temp%\PixiEditor", "Open Temp Directory", "Open Temp Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"%LocalAppData%\PixiEditor", "Open Local AppData Directory", "Open Local AppData Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"%AppData%\PixiEditor", "Open Roaming AppData Directory", "Open Roaming AppData Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"%LocalAppData%\PixiEditor\crash_logs", "Open Crash Reports Directory", "Open Crash Reports Directory", IconPath = "Folder.png")]
     public static void OpenFolder(string path)
     public static void OpenFolder(string path)
     {
     {
-        string expandedPath = Environment.ExpandEnvironmentVariables(path);
-        if (!Directory.Exists(expandedPath))
+        if (!Directory.Exists(path))
         {
         {
-            NoticeDialog.Show($"{expandedPath} does not exist.", "Location does not exist");
+            NoticeDialog.Show($"{path} does not exist.", "Location does not exist");
             return;
             return;
         }
         }
 
 
         ProcessHelpers.ShellExecuteEV(path);
         ProcessHelpers.ShellExecuteEV(path);
     }
     }
-    
+
+    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"PixiEditor", "Open Local AppData Directory", "Open Local AppData Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"PixiEditor\crash_logs", "Open Crash Reports Directory", "Open Crash Reports Directory", IconPath = "Folder.png")]
+    public static void OpenLocalAppDataFolder(string subDirectory)
+    {
+        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subDirectory);
+        OpenFolder(path);
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"PixiEditor", "Open Roaming AppData Directory", "Open Roaming AppData Directory", IconPath = "Folder.png")]
+    public static void OpenAppDataFolder(string subDirectory)
+    {
+        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), subDirectory);
+        OpenFolder(path);
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"PixiEditor", "Open Temp Directory", "Open Temp Directory", IconPath = "Folder.png")]
+    public static void OpenTempFolder(string subDirectory)
+    {
+        var path = Path.Combine(Path.GetTempPath(), subDirectory);
+        OpenFolder(path);
+    }
+
     [Command.Debug("PixiEditor.Debug.DumpAllCommands", "Dump All Commands", "Dump All Commands to a text file")]
     [Command.Debug("PixiEditor.Debug.DumpAllCommands", "Dump All Commands", "Dump All Commands to a text file")]
     public void DumpAllCommands()
     public void DumpAllCommands()
     {
     {

+ 11 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -323,13 +323,19 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         switch (result)
         switch (result)
         {
         {
             case DialogSaveResult.InvalidPath:
             case DialogSaveResult.InvalidPath:
-                NoticeDialog.Show("Error", "Couldn't save the file to the specified location");
+                NoticeDialog.Show(title: "Error", message: "Couldn't save the file to the specified location");
                 break;
                 break;
             case DialogSaveResult.ConcurrencyError:
             case DialogSaveResult.ConcurrencyError:
-                NoticeDialog.Show("Internal error", "An internal error occured while saving. Please try again.");
+                NoticeDialog.Show(title: "Internal error", message: "An internal error occured while saving. Please try again.");
+                break;
+            case DialogSaveResult.SecurityError:
+                NoticeDialog.Show(title: "Security error", message: "No rights to write to the specified location.");
+                break;
+            case DialogSaveResult.IoError:
+                NoticeDialog.Show(title: "IO error", message: "Error while writing to disk.");
                 break;
                 break;
             case DialogSaveResult.UnknownError:
             case DialogSaveResult.UnknownError:
-                NoticeDialog.Show("Error", "An error occured while saving.");
+                NoticeDialog.Show(title: "Error", message: "An error occured while saving.");
                 break;
                 break;
         }
         }
     }
     }
@@ -362,6 +368,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
 
         foreach (string path in paths)
         foreach (string path in paths)
         {
         {
+            if (!File.Exists(path))
+                continue;
             documents.Add(new RecentlyOpenedDocument(path));
             documents.Add(new RecentlyOpenedDocument(path));
         }
         }
 
 

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs

@@ -157,8 +157,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
             if (activeDocument == null)
             if (activeDocument == null)
                 return;
                 return;
 
 
-            activeDocument.EventInlet.OnCanvasLeftMouseButtonDown(args.PositionOnCanvas);
             Owner.ToolsSubViewModel.LeftMouseButtonDownInlet(args.PositionOnCanvas);
             Owner.ToolsSubViewModel.LeftMouseButtonDownInlet(args.PositionOnCanvas);
+            activeDocument.EventInlet.OnCanvasLeftMouseButtonDown(args.PositionOnCanvas);
         }
         }
     }
     }
 
 

+ 11 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -1,6 +1,7 @@
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 namespace PixiEditor.ViewModels.SubViewModels.Main;
@@ -19,8 +20,16 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "Repository", "Open Repository", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "Repository", "Open Repository", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenLicense", "https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE", "License", "Open License", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenLicense", "https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE", "License", "Open License", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "https://pixieditor.net/docs/Third-party-licenses", "Third Party Licenses", "Open Third Party Licenses", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "https://pixieditor.net/docs/Third-party-licenses", "Third Party Licenses", "Open Third Party Licenses", IconPath = "Globe.png")]
-    public static void OpenHyperlink(string url)
+    public static async Task OpenHyperlink(string url)
     {
     {
-        ProcessHelpers.ShellExecute(url);
+        try
+        {
+            ProcessHelpers.ShellExecute(url);
+        }
+        catch (Exception e)
+        {
+            NoticeDialog.Show(title: "Error", message: $"Couldn't open the address {url} in your default browser");
+            await CrashHelper.SendExceptionInfoToWebhook(e);
+        }
     }
     }
 }
 }

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -130,9 +130,9 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
             SelectedToolChanged?.Invoke(this, new SelectedToolEventArgs(LastActionTool, ActiveTool));
             SelectedToolChanged?.Invoke(this, new SelectedToolEventArgs(LastActionTool, ActiveTool));
 
 
         //update old tool
         //update old tool
-        LastActionTool?.UpdateActionDisplay(false, false, false);
+        LastActionTool?.ModifierKeyChanged(false, false, false);
         //update new tool
         //update new tool
-        ActiveTool.UpdateActionDisplay(ctrlIsDown, shiftIsDown, altIsDown);
+        ActiveTool.ModifierKeyChanged(ctrlIsDown, shiftIsDown, altIsDown);
         ActiveTool.OnSelected();
         ActiveTool.OnSelected();
 
 
         tool.IsActive = true;
         tool.IsActive = true;
@@ -225,11 +225,11 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
 
 
     public void ConvertedKeyDownInlet(FilteredKeyEventArgs args)
     public void ConvertedKeyDownInlet(FilteredKeyEventArgs args)
     {
     {
-        ActiveTool?.UpdateActionDisplay(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
+        ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
     }
     }
 
 
     public void ConvertedKeyUpInlet(FilteredKeyEventArgs args)
     public void ConvertedKeyUpInlet(FilteredKeyEventArgs args)
     {
     {
-        ActiveTool?.UpdateActionDisplay(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
+        ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
     }
     }
 }
 }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -100,7 +100,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                 string dir = AppDomain.CurrentDomain.BaseDirectory;
                 string dir = AppDomain.CurrentDomain.BaseDirectory;
                 
                 
                 UpdateDownloader.CreateTempDirectory();
                 UpdateDownloader.CreateTempDirectory();
-                if(UpdateChecker.LatestReleaseInfo == null) return;
+                if(UpdateChecker.LatestReleaseInfo == null || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
                 bool updateFileExists = File.Exists(
                 bool updateFileExists = File.Exists(
                     Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
                     Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
                 string exePath = Path.Join(UpdateDownloader.DownloadLocation,
                 string exePath = Path.Join(UpdateDownloader.DownloadLocation,

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolViewModel.cs

@@ -53,7 +53,7 @@ internal abstract class ToolViewModel : NotifyableObject
 
 
     public Toolbar Toolbar { get; set; } = new EmptyToolbar();
     public Toolbar Toolbar { get; set; } = new EmptyToolbar();
 
 
-    public virtual void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
+    public virtual void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
     public virtual void OnLeftMouseButtonDown(VecD pos) { }
     public virtual void OnLeftMouseButtonDown(VecD pos) { }
     public virtual void OnSelected() 
     public virtual void OnSelected() 
     {
     {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -35,7 +35,7 @@ internal class BrightnessToolViewModel : ToolViewModel
     
     
     public bool Darken { get; private set; } = false;
     public bool Darken { get; private set; } = false;
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (!ctrlIsDown)
         if (!ctrlIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -47,7 +47,7 @@ internal class ColorPickerToolViewModel : ToolViewModel
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseColorPickerTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseColorPickerTool();
     }
     }
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (ctrlIsDown)
         if (ctrlIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EllipseToolViewModel.cs

@@ -18,7 +18,7 @@ internal class EllipseToolViewModel : ShapeTool
     public override string Tooltip => $"Draws an ellipse on canvas ({Shortcut}). Hold Shift to draw a circle.";
     public override string Tooltip => $"Draws an ellipse on canvas ({Shortcut}). Hold Shift to draw a circle.";
     public bool DrawCircle { get; private set; }
     public bool DrawCircle { get; private set; }
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -22,7 +22,7 @@ internal class FloodFillToolViewModel : ToolViewModel
         ActionDisplay = defaultActionDisplay;
         ActionDisplay = defaultActionDisplay;
     }
     }
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (ctrlIsDown)
         if (ctrlIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LassoToolViewModel.cs

@@ -21,7 +21,7 @@ internal class LassoToolViewModel : ToolViewModel
     private SelectionMode modifierKeySelectionMode = SelectionMode.New;
     private SelectionMode modifierKeySelectionMode = SelectionMode.New;
     public SelectionMode ResultingSelectionMode => modifierKeySelectionMode != SelectionMode.New ? modifierKeySelectionMode : SelectMode;
     public SelectionMode ResultingSelectionMode => modifierKeySelectionMode != SelectionMode.New ? modifierKeySelectionMode : SelectMode;
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LineToolViewModel.cs

@@ -24,7 +24,7 @@ internal class LineToolViewModel : ShapeTool
 
 
     public bool Snap { get; private set; }
     public bool Snap { get; private set; }
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {

+ 33 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -11,6 +11,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 internal class MoveToolViewModel : ToolViewModel
 internal class MoveToolViewModel : ToolViewModel
 {
 {
     private string defaultActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";
     private string defaultActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";
+    private string transformingActionDisplay = "Click and hold mouse to move pixels in selected layers.";
+    private bool transformingSelectedArea = false;
+
+    public bool MoveAllLayers { get; set; }
 
 
     public MoveToolViewModel()
     public MoveToolViewModel()
     {
     {
@@ -27,11 +31,40 @@ internal class MoveToolViewModel : ToolViewModel
     public override BrushShape BrushShape => BrushShape.Hidden;
     public override BrushShape BrushShape => BrushShape.Hidden;
     public override bool HideHighlight => true;
     public override bool HideHighlight => true;
 
 
+    public bool TransformingSelectedArea
+    {
+        get => transformingSelectedArea;
+        set
+        {
+            transformingSelectedArea = value;
+            ActionDisplay = value ? transformingActionDisplay : defaultActionDisplay;
+        }
+    }
+
     public override void OnLeftMouseButtonDown(VecD pos)
     public override void OnLeftMouseButtonDown(VecD pos)
     {
     {
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseShiftLayerTool();
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseShiftLayerTool();
     }
     }
 
 
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    {
+        if (TransformingSelectedArea)
+        {
+            return;
+        }
+        
+        if (ctrlIsDown)
+        {
+            ActionDisplay = "Hold mouse to move all layers.";
+            MoveAllLayers = true;
+        }
+        else
+        {
+            ActionDisplay = defaultActionDisplay;
+            MoveAllLayers = false;
+        }
+    }
+
     public override void OnSelected()
     public override void OnSelected()
     {
     {
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/RectangleToolViewModel.cs

@@ -18,7 +18,7 @@ internal class RectangleToolViewModel : ShapeTool
 
 
     public bool Filled { get; set; } = false;
     public bool Filled { get; set; } = false;
     public bool DrawSquare { get; private set; } = false;
     public bool DrawSquare { get; private set; } = false;
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/SelectToolViewModel.cs

@@ -24,7 +24,7 @@ internal class SelectToolViewModel : ToolViewModel
     private SelectionMode modifierKeySelectionMode = SelectionMode.New;
     private SelectionMode modifierKeySelectionMode = SelectionMode.New;
     public SelectionMode ResultingSelectionMode => modifierKeySelectionMode != SelectionMode.New ? modifierKeySelectionMode : SelectMode;
     public SelectionMode ResultingSelectionMode => modifierKeySelectionMode != SelectionMode.New ? modifierKeySelectionMode : SelectMode;
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ZoomToolViewModel.cs

@@ -27,7 +27,7 @@ internal class ZoomToolViewModel : ToolViewModel
 
 
     public override string Tooltip => $"Zooms viewport ({Shortcut}). Click to zoom in, hold alt and click to zoom out.";
     public override string Tooltip => $"Zooms viewport ({Shortcut}). Click to zoom in, hold alt and click to zoom out.";
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
     {
         if (ctrlIsDown)
         if (ctrlIsDown)
         {
         {

+ 75 - 74
src/PixiEditor/Views/MainWindow.xaml

@@ -25,6 +25,7 @@
     xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
     xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
     xmlns:commandSearch="clr-namespace:PixiEditor.Views.UserControls.CommandSearch"
     xmlns:commandSearch="clr-namespace:PixiEditor.Views.UserControls.CommandSearch"
     xmlns:palettes="clr-namespace:PixiEditor.Views.UserControls.Palettes"
     xmlns:palettes="clr-namespace:PixiEditor.Views.UserControls.Palettes"
+    KeyDown="MainWindow_OnKeyDown"
     d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}"
     d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}"
     mc:Ignorable="d"
     mc:Ignorable="d"
     WindowStyle="None"
     WindowStyle="None"
@@ -146,16 +147,16 @@
                             TargetType="{x:Type MenuItem}"
                             TargetType="{x:Type MenuItem}"
                             BasedOn="{StaticResource menuItemStyle}" />
                             BasedOn="{StaticResource menuItemStyle}" />
                     </Menu.Resources>
                     </Menu.Resources>
-                    <MenuItem
-                        Header="_File">
+                    <MenuItem Focusable="False"
+                              Header="File">
                         <MenuItem
                         <MenuItem
-                            Header="_New"
+                            Header="New"
                             cmds:Menu.Command="PixiEditor.File.New" />
                             cmds:Menu.Command="PixiEditor.File.New" />
                         <MenuItem
                         <MenuItem
-                            Header="_Open"
+                            Header="Open"
                             cmds:Menu.Command="PixiEditor.File.Open" />
                             cmds:Menu.Command="PixiEditor.File.Open" />
                         <MenuItem
                         <MenuItem
-                            Header="_Recent"
+                            Header="Recent"
                             ItemsSource="{Binding FileSubViewModel.RecentlyOpened}"
                             ItemsSource="{Binding FileSubViewModel.RecentlyOpened}"
                             x:Name="recentItemMenu"
                             x:Name="recentItemMenu"
                             IsEnabled="{Binding FileSubViewModel.HasRecent}">
                             IsEnabled="{Binding FileSubViewModel.HasRecent}">
@@ -180,104 +181,104 @@
                             </MenuItem.ItemTemplate>
                             </MenuItem.ItemTemplate>
                         </MenuItem>
                         </MenuItem>
                         <MenuItem
                         <MenuItem
-                            Header="_Save (.pixi)"
+                            Header="Save (.pixi)"
                             cmds:Menu.Command="PixiEditor.File.Save" />
                             cmds:Menu.Command="PixiEditor.File.Save" />
                         <MenuItem
                         <MenuItem
-                            Header="_Save As... (.pixi)"
+                            Header="Save As... (.pixi)"
                             cmds:Menu.Command="PixiEditor.File.SaveAsNew" />
                             cmds:Menu.Command="PixiEditor.File.SaveAsNew" />
                         <MenuItem
                         <MenuItem
-                            Header="_Export (.png, .jpeg, etc.)"
+                            Header="Export (.png, .jpeg, etc.)"
                             cmds:Menu.Command="PixiEditor.File.Export" />
                             cmds:Menu.Command="PixiEditor.File.Export" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_Exit"
+                            Header="Exit"
                             Command="{x:Static SystemCommands.CloseWindowCommand}" />
                             Command="{x:Static SystemCommands.CloseWindowCommand}" />
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_Edit">
+                    <MenuItem Focusable="False"
+                              Header="Edit">
                         <MenuItem
                         <MenuItem
-                            Header="_Undo"
+                            Header="Undo"
                             cmds:Menu.Command="PixiEditor.Undo.Undo" />
                             cmds:Menu.Command="PixiEditor.Undo.Undo" />
                         <MenuItem
                         <MenuItem
-                            Header="_Redo"
+                            Header="Redo"
                             cmds:Menu.Command="PixiEditor.Undo.Redo" />
                             cmds:Menu.Command="PixiEditor.Undo.Redo" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_Cut"
+                            Header="Cut"
                             cmds:Menu.Command="PixiEditor.Clipboard.Cut" />
                             cmds:Menu.Command="PixiEditor.Clipboard.Cut" />
                         <MenuItem
                         <MenuItem
-                            Header="_Copy"
+                            Header="Copy"
                             cmds:Menu.Command="PixiEditor.Clipboard.Copy" />
                             cmds:Menu.Command="PixiEditor.Clipboard.Copy" />
                         <MenuItem
                         <MenuItem
-                            Header="_Paste"
+                            Header="Paste"
                             cmds:Menu.Command="PixiEditor.Clipboard.Paste" />
                             cmds:Menu.Command="PixiEditor.Clipboard.Paste" />
                         <MenuItem
                         <MenuItem
-                            Header="_Duplicate"
+                            Header="Duplicate"
                             cmds:Menu.Command="PixiEditor.Layer.DuplicateSelectedLayer" />
                             cmds:Menu.Command="PixiEditor.Layer.DuplicateSelectedLayer" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_Delete Selected"
+                            Header="Delete Selected"
                             cmds:Menu.Command="PixiEditor.Document.DeletePixels" />
                             cmds:Menu.Command="PixiEditor.Document.DeletePixels" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_Settings"
+                            Header="Settings"
                             cmds:Menu.Command="PixiEditor.Window.OpenSettingsWindow" />
                             cmds:Menu.Command="PixiEditor.Window.OpenSettingsWindow" />
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_Select">
+                    <MenuItem Focusable="False"
+                              Header="Select">
                         <MenuItem
                         <MenuItem
-                            Header="_Select All"
+                            Header="Select All"
                             cmds:Menu.Command="PixiEditor.Selection.SelectAll" />
                             cmds:Menu.Command="PixiEditor.Selection.SelectAll" />
                         <MenuItem
                         <MenuItem
-                            Header="_Deselect"
+                            Header="Deselect"
                             cmds:Menu.Command="PixiEditor.Selection.Clear" />
                             cmds:Menu.Command="PixiEditor.Selection.Clear" />
                         <MenuItem
                         <MenuItem
-                            Header="_Invert"
+                            Header="Invert"
                             cmds:Menu.Command="PixiEditor.Selection.InvertSelection" />
                             cmds:Menu.Command="PixiEditor.Selection.InvertSelection" />
                         <Separator/>
                         <Separator/>
-                        <MenuItem Header="Selection _to Mask">
+                        <MenuItem Header="Selection to Mask">
                             <MenuItem
                             <MenuItem
-                                Header="to _new mask"
+                                Header="to new mask"
                                 cmds:Menu.Command="PixiEditor.Selection.NewToMask" />
                                 cmds:Menu.Command="PixiEditor.Selection.NewToMask" />
                             <MenuItem
                             <MenuItem
-                                Header="_add to mask"
+                                Header="add to mask"
                                 cmds:Menu.Command="PixiEditor.Selection.AddToMask" />
                                 cmds:Menu.Command="PixiEditor.Selection.AddToMask" />
                             <MenuItem
                             <MenuItem
-                                Header="_subtract from mask"
+                                Header="subtract from mask"
                                 cmds:Menu.Command="PixiEditor.Selection.SubtractFromMask" />
                                 cmds:Menu.Command="PixiEditor.Selection.SubtractFromMask" />
                             <MenuItem
                             <MenuItem
-                                Header="_intersect with mask"
+                                Header="intersect with mask"
                                 cmds:Menu.Command="PixiEditor.Selection.IntersectSelectionMask" />
                                 cmds:Menu.Command="PixiEditor.Selection.IntersectSelectionMask" />
                         </MenuItem>
                         </MenuItem>
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_Image">
+                    <MenuItem Focusable="False"
+                              Header="Image">
                         <MenuItem
                         <MenuItem
-                            Header="Resize _Image..."
+                            Header="Resize Image..."
                             cmds:Menu.Command="PixiEditor.Document.ResizeDocument" />
                             cmds:Menu.Command="PixiEditor.Document.ResizeDocument" />
                         <MenuItem
                         <MenuItem
-                            Header="Resize _Canvas..."
+                            Header="Resize Canvas..."
                             cmds:Menu.Command="PixiEditor.Document.ResizeCanvas" />
                             cmds:Menu.Command="PixiEditor.Document.ResizeCanvas" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="Cli_p Canvas"
+                            Header="Clip Canvas"
                             cmds:Menu.Command="PixiEditor.Document.ClipCanvas" />
                             cmds:Menu.Command="PixiEditor.Document.ClipCanvas" />
                         <MenuItem
                         <MenuItem
-                            Header="Cente_r Content"
+                            Header="Center Content"
                             cmds:Menu.Command="PixiEditor.Document.CenterContent" />
                             cmds:Menu.Command="PixiEditor.Document.CenterContent" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
                             IsCheckable="True"
                             IsCheckable="True"
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.HorizontalSymmetryAxisEnabledBindable}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.HorizontalSymmetryAxisEnabledBindable}"
-                            Header="_Horizontal Line Symmetry"/>
+                            Header="Horizontal Line Symmetry"/>
                         <MenuItem
                         <MenuItem
                             IsCheckable="True"
                             IsCheckable="True"
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
-                            Header="_Vertical Line Symmetry"/>
+                            Header="Vertical Line Symmetry"/>
                         <Separator/>
                         <Separator/>
-                        <MenuItem Header="_Rotation">
+                        <MenuItem Header="Rotation">
                             <MenuItem Header="Rotate Image 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90Deg"/>
                             <MenuItem Header="Rotate Image 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90Deg"/>
                             <MenuItem Header="Rotate Image 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180Deg"/>
                             <MenuItem Header="Rotate Image 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180Deg"/>
                             <MenuItem Header="Rotate Image -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270Deg"/>
                             <MenuItem Header="Rotate Image -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270Deg"/>
@@ -287,84 +288,84 @@
                             <MenuItem Header="Rotate Selected Layers 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
                             <MenuItem Header="Rotate Selected Layers 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
                             <MenuItem Header="Rotate Selected Layers -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
                             <MenuItem Header="Rotate Selected Layers -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
                         </MenuItem>
                         </MenuItem>
-                        <MenuItem Header="_Flip">
-                            <MenuItem Header="Flip Image _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipImageHorizontal"/>
-                            <MenuItem Header="Flip Image _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipImageVertical"/>
-                            <MenuItem Header="Flip Selected Layers _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
-                            <MenuItem Header="Flip Selected Layers _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                        <MenuItem Header="Flip">
+                            <MenuItem Header="Flip Image Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipImageHorizontal"/>
+                            <MenuItem Header="Flip Image Vertically" cmds:Menu.Command="PixiEditor.Document.FlipImageVertical"/>
+                            <MenuItem Header="Flip Selected Layers Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                            <MenuItem Header="Flip Selected Layers Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
                         </MenuItem>
                         </MenuItem>
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_View">
+                    <MenuItem Focusable="False"
+                              Header="View">
                         <MenuItem
                         <MenuItem
                             Header="New window for current image"
                             Header="New window for current image"
                             cmds:Menu.Command="PixiEditor.Window.CreateNewViewport" />
                             cmds:Menu.Command="PixiEditor.Window.CreateNewViewport" />
                         <Separator/>
                         <Separator/>
                         <MenuItem
                         <MenuItem
-                            Header="Open _Startup Window"
+                            Header="Open Startup Window"
                             ToolTip="Hello there!"
                             ToolTip="Hello there!"
                             cmds:Menu.Command="PixiEditor.Window.OpenStartupWindow" />
                             cmds:Menu.Command="PixiEditor.Window.OpenStartupWindow" />
                         <MenuItem
                         <MenuItem
-                            Header="Open _Navigation Window"
+                            Header="Open Navigation Window"
                             cmds:Menu.Command="PixiEditor.Window.OpenNavigationWindow" />
                             cmds:Menu.Command="PixiEditor.Window.OpenNavigationWindow" />
                         <MenuItem
                         <MenuItem
-                            Header="Open Short_cuts Window"
+                            Header="Open Shortcuts Window"
                             cmds:Menu.Command="PixiEditor.Window.OpenShortcutWindow" />
                             cmds:Menu.Command="PixiEditor.Window.OpenShortcutWindow" />
                         <Separator/>
                         <Separator/>
                         <MenuItem
                         <MenuItem
-                            Header="Show _Grid Lines"
+                            Header="Show Grid Lines"
                             IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
                             IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
                             IsCheckable="True"
                             IsCheckable="True"
                             InputGestureText="{cmds:ShortcutBinding PixiEditor.View.ToggleGrid}" />
                             InputGestureText="{cmds:ShortcutBinding PixiEditor.View.ToggleGrid}" />
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_Help">
+                    <MenuItem Focusable="False"
+                              Header="Help">
                         <MenuItem
                         <MenuItem
-                            Header="_Documentation"
+                            Header="Documentation"
                             cmds:Menu.Command="PixiEditor.Links.OpenDocumentation" />
                             cmds:Menu.Command="PixiEditor.Links.OpenDocumentation" />
                         <MenuItem
                         <MenuItem
-                            Header="_Website"
+                            Header="Website"
                             cmds:Menu.Command="PixiEditor.Links.OpenWebsite" />
                             cmds:Menu.Command="PixiEditor.Links.OpenWebsite" />
                         <MenuItem
                         <MenuItem
-                            Header="_Repository"
+                            Header="Repository"
                             cmds:Menu.Command="PixiEditor.Links.OpenRepository" />
                             cmds:Menu.Command="PixiEditor.Links.OpenRepository" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_License"
+                            Header="License"
                             cmds:Menu.Command="PixiEditor.Links.OpenLicense" />
                             cmds:Menu.Command="PixiEditor.Links.OpenLicense" />
                         <MenuItem
                         <MenuItem
-                            Header="_Third Party Licenses"
+                            Header="Third Party Licenses"
                             cmds:Menu.Command="PixiEditor.Links.OpenOtherLicenses" />
                             cmds:Menu.Command="PixiEditor.Links.OpenOtherLicenses" />
                         <Separator/>
                         <Separator/>
                         <MenuItem
                         <MenuItem
-                            Header="_About"
+                            Header="About"
                             cmds:Menu.Command="PixiEditor.Window.OpenAboutWindow" />
                             cmds:Menu.Command="PixiEditor.Window.OpenAboutWindow" />
                     </MenuItem>
                     </MenuItem>
-                    <MenuItem
-                        Header="_Debug"
-                        Visibility="{Binding DebugSubViewModel.UseDebug, Converter={StaticResource BoolToVisibilityConverter}}">
+                    <MenuItem Focusable="False"
+                              Header="Debug"
+                              Visibility="{Binding DebugSubViewModel.UseDebug, Converter={StaticResource BoolToVisibilityConverter}}">
                         <MenuItem
                         <MenuItem
                             Header="Open Command Debug Window"
                             Header="Open Command Debug Window"
                             cmds:Menu.Command="PixiEditor.Debug.OpenCommandDebugWindow"/>
                             cmds:Menu.Command="PixiEditor.Debug.OpenCommandDebugWindow"/>
                         <Separator/>
                         <Separator/>
                         <MenuItem
                         <MenuItem
-                            Header="Open _Local App Data"
+                            Header="Open Local App Data"
                             cmds:Menu.Command="PixiEditor.Debug.OpenLocalAppDataDirectory" />
                             cmds:Menu.Command="PixiEditor.Debug.OpenLocalAppDataDirectory" />
                         <MenuItem
                         <MenuItem
-                            Header="Open _Roaming App Data"
+                            Header="Open Roaming App Data"
                             cmds:Menu.Command="PixiEditor.Debug.OpenRoamingAppDataDirectory" />
                             cmds:Menu.Command="PixiEditor.Debug.OpenRoamingAppDataDirectory" />
                         <MenuItem
                         <MenuItem
-                            Header="Open _Temp App Data"
+                            Header="Open Temp App Data"
                             cmds:Menu.Command="PixiEditor.Debug.OpenTempDirectory" />
                             cmds:Menu.Command="PixiEditor.Debug.OpenTempDirectory" />
                         <MenuItem
                         <MenuItem
-                            Header="Open _Install Location"
+                            Header="Open Install Location"
                             cmds:Menu.Command="PixiEditor.Debug.OpenInstallDirectory" />
                             cmds:Menu.Command="PixiEditor.Debug.OpenInstallDirectory" />
                         <MenuItem
                         <MenuItem
-                            Header="Open Crash _Reports Location"
+                            Header="Open Crash Reports Location"
                             cmds:Menu.Command="PixiEditor.Debug.OpenCrashReportsDirectory" />
                             cmds:Menu.Command="PixiEditor.Debug.OpenCrashReportsDirectory" />
                         <Separator />
                         <Separator />
                         <MenuItem
                         <MenuItem
-                            Header="_Crash"
+                            Header="Crash"
                             cmds:Menu.Command="PixiEditor.Debug.Crash" />
                             cmds:Menu.Command="PixiEditor.Debug.Crash" />
                         <MenuItem
                         <MenuItem
                             Header="Delete">
                             Header="Delete">
@@ -379,7 +380,7 @@
                                 cmds:Menu.Command="PixiEditor.Debug.DeleteEditorData" />
                                 cmds:Menu.Command="PixiEditor.Debug.DeleteEditorData" />
                             <Separator/>
                             <Separator/>
                             <MenuItem
                             <MenuItem
-                                Header="_Clear recent documents"
+                                Header="Clear recent documents"
                                 cmds:Menu.Command="PixiEditor.Debug.ClearRecentDocument"/>
                                 cmds:Menu.Command="PixiEditor.Debug.ClearRecentDocument"/>
                         </MenuItem>
                         </MenuItem>
                     </MenuItem>
                     </MenuItem>
@@ -590,24 +591,24 @@
                                                                     <Border Grid.Column="1" BorderThickness="0 0 1 0" BorderBrush="Black">
                                                                     <Border Grid.Column="1" BorderThickness="0 0 1 0" BorderBrush="Black">
                                                                         <StackPanel Orientation="Vertical" Grid.Column="0">
                                                                         <StackPanel Orientation="Vertical" Grid.Column="0">
                                                                             <MenuItem
                                                                             <MenuItem
-																		Header="_Select All"
+                                                                                Header="Select All"
 																		cmds:ContextMenu.Command="PixiEditor.Selection.SelectAll" />
 																		cmds:ContextMenu.Command="PixiEditor.Selection.SelectAll" />
                                                                             <MenuItem
                                                                             <MenuItem
-																		Header="_Deselect"
+																		Header="Deselect"
 																		cmds:ContextMenu.Command="PixiEditor.Selection.Clear" />
 																		cmds:ContextMenu.Command="PixiEditor.Selection.Clear" />
                                                                             <Separator />
                                                                             <Separator />
                                                                             <MenuItem
                                                                             <MenuItem
-																		Header="_Cut"
+																		Header="Cut"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Cut" />
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Cut" />
                                                                             <MenuItem
                                                                             <MenuItem
-																		Header="_Copy"
+																		Header="Copy"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Copy" />
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Copy" />
                                                                             <MenuItem
                                                                             <MenuItem
-																		Header="_Paste"
+																		Header="Paste"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
                                                                             <Separator />
                                                                             <Separator />
-                                                                            <MenuItem Header="Flip _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
-                                                                            <MenuItem Header="Flip _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                                                                            <MenuItem Header="Flip Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                                                                            <MenuItem Header="Flip Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
                                                                             <Separator />
                                                                             <Separator />
                                                                             <MenuItem Header="Rotate 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
                                                                             <MenuItem Header="Rotate 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
                                                                             <MenuItem Header="Rotate 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
                                                                             <MenuItem Header="Rotate 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>

+ 9 - 0
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Windows;
 using System.Windows;
+using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Interop;
 using System.Windows.Interop;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
@@ -235,4 +236,12 @@ internal partial class MainWindow : Window
     {
     {
         DataContext.ActionDisplays[nameof(MainWindow_Drop)] = null;
         DataContext.ActionDisplays[nameof(MainWindow_Drop)] = null;
     }
     }
+
+    private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
+    {
+        if (e.Key == Key.System) // Disables alt menu item navigation, I hope it won't break anything else.
+        {
+            e.Handled = true;
+        }
+    }
 }
 }

+ 1 - 1
src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryAxisDragInfo.cs

@@ -2,4 +2,4 @@
 
 
 namespace PixiEditor.Views.UserControls.SymmetryOverlay;
 namespace PixiEditor.Views.UserControls.SymmetryOverlay;
 #nullable enable
 #nullable enable
-internal record class SymmetryAxisDragInfo(SymmetryAxisDirection Direction, int NewPosition);
+internal record class SymmetryAxisDragInfo(SymmetryAxisDirection Direction, double NewPosition);

+ 18 - 17
src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -18,22 +18,22 @@ namespace PixiEditor.Views.UserControls.Overlays.SymmetryOverlay;
 internal class SymmetryOverlay : Control
 internal class SymmetryOverlay : Control
 {
 {
     public static readonly DependencyProperty HorizontalAxisYProperty =
     public static readonly DependencyProperty HorizontalAxisYProperty =
-        DependencyProperty.Register(nameof(HorizontalAxisY), typeof(int), typeof(SymmetryOverlay),
-            new(0, OnPositionUpdate));
+        DependencyProperty.Register(nameof(HorizontalAxisY), typeof(double), typeof(SymmetryOverlay),
+            new(0.0, OnPositionUpdate));
 
 
-    public int HorizontalAxisY
+    public double HorizontalAxisY
     {
     {
-        get => (int)GetValue(HorizontalAxisYProperty);
+        get => (double)GetValue(HorizontalAxisYProperty);
         set => SetValue(HorizontalAxisYProperty, value);
         set => SetValue(HorizontalAxisYProperty, value);
     }
     }
 
 
     public static readonly DependencyProperty VerticalAxisXProperty =
     public static readonly DependencyProperty VerticalAxisXProperty =
-        DependencyProperty.Register(nameof(VerticalAxisX), typeof(int), typeof(SymmetryOverlay),
-            new(0, OnPositionUpdate));
+        DependencyProperty.Register(nameof(VerticalAxisX), typeof(double), typeof(SymmetryOverlay),
+            new(0.0, OnPositionUpdate));
 
 
-    public int VerticalAxisX
+    public double VerticalAxisX
     {
     {
-        get => (int)GetValue(VerticalAxisXProperty);
+        get => (double)GetValue(VerticalAxisXProperty);
         set => SetValue(VerticalAxisXProperty, value);
         set => SetValue(VerticalAxisXProperty, value);
     }
     }
 
 
@@ -114,8 +114,8 @@ internal class SymmetryOverlay : Control
 
 
     private double PenThickness => 1.0 / ZoomboxScale;
     private double PenThickness => 1.0 / ZoomboxScale;
 
 
-    private int horizontalAxisY;
-    private int verticalAxisX;
+    private double horizontalAxisY;
+    private double verticalAxisX;
 
 
     private MouseUpdateController mouseUpdateController;
     private MouseUpdateController mouseUpdateController;
 
 
@@ -334,7 +334,7 @@ internal class SymmetryOverlay : Control
         UpdateHovered(null);
         UpdateHovered(null);
     }
     }
 
 
-    private void CallSymmetryDragCommand(SymmetryAxisDirection direction, int position)
+    private void CallSymmetryDragCommand(SymmetryAxisDirection direction, double position)
     {
     {
         SymmetryAxisDragInfo dragInfo = new(direction, position);
         SymmetryAxisDragInfo dragInfo = new(direction, position);
         if (DragCommand is not null && DragCommand.CanExecute(dragInfo))
         if (DragCommand is not null && DragCommand.CanExecute(dragInfo))
@@ -373,8 +373,6 @@ internal class SymmetryOverlay : Control
 
 
     protected void MouseMoved(object sender, MouseEventArgs e)
     protected void MouseMoved(object sender, MouseEventArgs e)
     {
     {
-        /*base.OnMouseMove(e);*/
-
         var pos = ToVecD(e.GetPosition(this));
         var pos = ToVecD(e.GetPosition(this));
         UpdateHovered(IsTouchingHandle(pos));
         UpdateHovered(IsTouchingHandle(pos));
 
 
@@ -382,22 +380,25 @@ internal class SymmetryOverlay : Control
             return;
             return;
         if (capturedDirection == SymmetryAxisDirection.Horizontal)
         if (capturedDirection == SymmetryAxisDirection.Horizontal)
         {
         {
-            horizontalAxisY = (int)Math.Round(Math.Clamp(pos.Y, 0, ActualHeight));
+            horizontalAxisY = Math.Round(Math.Clamp(pos.Y, 0, ActualHeight) * 2) / 2;
 
 
             if (Keyboard.IsKeyDown(Key.LeftShift))
             if (Keyboard.IsKeyDown(Key.LeftShift))
             {
             {
-                horizontalAxisY = (int)(Math.Round(horizontalAxisY / RenderSize.Height * 8) / 8 * RenderSize.Height);
+                double temp = Math.Round(horizontalAxisY / RenderSize.Height * 8) / 8 * RenderSize.Height;
+                horizontalAxisY = Math.Round(temp * 2) / 2;
             }
             }
 
 
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, horizontalAxisY);
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, horizontalAxisY);
         }
         }
         else if (capturedDirection == SymmetryAxisDirection.Vertical)
         else if (capturedDirection == SymmetryAxisDirection.Vertical)
         {
         {
-            verticalAxisX = (int)Math.Round(Math.Clamp(pos.X, 0, ActualWidth));
+            verticalAxisX = Math.Round(Math.Clamp(pos.X, 0, ActualWidth) * 2) / 2;
 
 
             if (Keyboard.IsKeyDown(Key.LeftShift))
             if (Keyboard.IsKeyDown(Key.LeftShift))
             {
             {
-                verticalAxisX = (int)(Math.Round(verticalAxisX / RenderSize.Width * 8) / 8 * RenderSize.Width);
+
+                double temp = Math.Round(verticalAxisX / RenderSize.Width * 8) / 8 * RenderSize.Width;
+                verticalAxisX = Math.Round(temp * 2) / 2;
             }
             }
 
 
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);

+ 11 - 9
windows-x64-release-dev.yml

@@ -16,26 +16,28 @@ variables:
 - name: solution 
 - name: solution 
   value: '**/*.sln'
   value: '**/*.sln'
 - name: buildPlatform 
 - name: buildPlatform 
-  value: 'x64'
+  value: 'win-x64'
 - name: buildConfiguration
 - name: buildConfiguration
-  value: 'Release'
+  value: 'DevRelease'
 
 
 steps:
 steps:
 - task: UseDotNet@2
 - task: UseDotNet@2
   inputs:
   inputs:
     packageType: 'sdk'
     packageType: 'sdk'
-    version: '7.0.103'
+    version: '7.0.104'
+    
 - task: NuGetToolInstaller@1
 - task: NuGetToolInstaller@1
 
 
 - task: NuGetCommand@2
 - task: NuGetCommand@2
   inputs:
   inputs:
     restoreSolution: '$(solution)'
     restoreSolution: '$(solution)'
 
 
-- task: VSBuild@1
+- task: DotNetCoreCLI@2
+  displayName: "Build PixiEditor Solution"
   inputs:
   inputs:
-    solution: '$(solution)'
-    platform: '$(buildPlatform)'
-    configuration: '$(buildConfiguration)'
+    command: 'build'
+    projects: 'src/PixiEditor'
+    arguments: '-r "$(buildPlatform)" -c $(buildConfiguration)'
 
 
 - task: DotNetCoreCLI@2
 - task: DotNetCoreCLI@2
   displayName: "Build release PixiEditor.UpdateInstaller"
   displayName: "Build release PixiEditor.UpdateInstaller"
@@ -43,7 +45,7 @@ steps:
     command: 'publish'
     command: 'publish'
     publishWebProjects: false
     publishWebProjects: false
     projects: '**/PixiEditor.UpdateInstaller.csproj'
     projects: '**/PixiEditor.UpdateInstaller.csproj'
-    arguments: '-o "UpdateInstaller" -r "win-x64" --self-contained=false -p:PublishSingleFile=true -c Release'
+    arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false
     zipAfterPublish: false
 
 
 - task: PowerShell@2
 - task: PowerShell@2
@@ -55,7 +57,7 @@ steps:
   displayName: Publish PixiEditor
   displayName: Publish PixiEditor
   inputs:
   inputs:
     filePath: 'src/PixiEditor.Builder/build.ps1'
     filePath: 'src/PixiEditor.Builder/build.ps1'
-    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "DevRelease" --runtime "win-x64" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x64-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
+    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x64-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
     workingDirectory: 'src/PixiEditor.Builder'
     workingDirectory: 'src/PixiEditor.Builder'
 
 
 - task: ArchiveFiles@2
 - task: ArchiveFiles@2

+ 9 - 8
windows-x64-release.yml

@@ -16,7 +16,7 @@ variables:
 - name: solution 
 - name: solution 
   value: '**/*.sln'
   value: '**/*.sln'
 - name: buildPlatform 
 - name: buildPlatform 
-  value: 'x64'
+  value: 'win-x64'
 - name: buildConfiguration
 - name: buildConfiguration
   value: 'Release'
   value: 'Release'
 
 
@@ -24,18 +24,19 @@ steps:
 - task: UseDotNet@2
 - task: UseDotNet@2
   inputs:
   inputs:
     packageType: 'sdk'
     packageType: 'sdk'
-    version: '7.0.103'
+    version: '7.0.104'
 - task: NuGetToolInstaller@1
 - task: NuGetToolInstaller@1
 
 
 - task: NuGetCommand@2
 - task: NuGetCommand@2
   inputs:
   inputs:
     restoreSolution: '$(solution)'
     restoreSolution: '$(solution)'
 
 
-- task: VSBuild@1
+- task: DotNetCoreCLI@2
+  displayName: "Build PixiEditor Solution"
   inputs:
   inputs:
-    solution: '$(solution)'
-    platform: '$(buildPlatform)'
-    configuration: '$(buildConfiguration)'
+    command: 'build'
+    projects: 'src/PixiEditor'
+    arguments: '-r "$(buildPlatform)" -c $(buildConfiguration)'
 
 
 - task: DotNetCoreCLI@2
 - task: DotNetCoreCLI@2
   displayName: "Build release PixiEditor.UpdateInstaller"
   displayName: "Build release PixiEditor.UpdateInstaller"
@@ -43,7 +44,7 @@ steps:
     command: 'publish'
     command: 'publish'
     publishWebProjects: false
     publishWebProjects: false
     projects: '**/PixiEditor.UpdateInstaller.csproj'
     projects: '**/PixiEditor.UpdateInstaller.csproj'
-    arguments: '-o "UpdateInstaller" -r "win-x64" --self-contained=false -p:PublishSingleFile=true -c Release'
+    arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false
     zipAfterPublish: false
 
 
 - task: PowerShell@2
 - task: PowerShell@2
@@ -55,7 +56,7 @@ steps:
   displayName: Publish PixiEditor
   displayName: Publish PixiEditor
   inputs:
   inputs:
     filePath: 'src/PixiEditor.Builder/build.ps1'
     filePath: 'src/PixiEditor.Builder/build.ps1'
-    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "Release" --runtime "win-x64" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x64-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
+    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x64-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
     workingDirectory: 'src/PixiEditor.Builder'
     workingDirectory: 'src/PixiEditor.Builder'
 
 
 - task: ArchiveFiles@2
 - task: ArchiveFiles@2

+ 10 - 9
windows-x86-release-dev.yml

@@ -15,16 +15,16 @@ variables:
 - name: solution 
 - name: solution 
   value: '**/*.sln'
   value: '**/*.sln'
 - name: buildPlatform 
 - name: buildPlatform 
-  value: 'x86'
+  value: 'win-x86'
 - name: buildConfiguration
 - name: buildConfiguration
-  value: 'Release'
+  value: 'DevRelease'
 
 
 steps:
 steps:
 
 
 - task: UseDotNet@2
 - task: UseDotNet@2
   inputs:
   inputs:
     packageType: 'sdk'
     packageType: 'sdk'
-    version: '7.0.103'
+    version: '7.0.104'
 
 
 - task: NuGetToolInstaller@1
 - task: NuGetToolInstaller@1
 
 
@@ -32,11 +32,12 @@ steps:
   inputs:
   inputs:
     restoreSolution: '$(solution)'
     restoreSolution: '$(solution)'
 
 
-- task: VSBuild@1
+- task: DotNetCoreCLI@2
+  displayName: "Build PixiEditor Solution"
   inputs:
   inputs:
-    solution: '$(solution)'
-    platform: '$(buildPlatform)'
-    configuration: '$(buildConfiguration)'
+    command: 'build'
+    projects: 'src/PixiEditor'
+    arguments: '-r "$(buildPlatform)" -c $(buildConfiguration)'
 
 
 - task: DotNetCoreCLI@2
 - task: DotNetCoreCLI@2
   displayName: "Build release PixiEditor.UpdateInstaller"
   displayName: "Build release PixiEditor.UpdateInstaller"
@@ -44,7 +45,7 @@ steps:
     command: 'publish'
     command: 'publish'
     publishWebProjects: false
     publishWebProjects: false
     projects: '**/PixiEditor.UpdateInstaller.csproj'
     projects: '**/PixiEditor.UpdateInstaller.csproj'
-    arguments: '-o "UpdateInstaller" -r "win-x86" --self-contained=false -p:PublishSingleFile=true -c Release'
+    arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false
     zipAfterPublish: false
 
 
 
 
@@ -57,7 +58,7 @@ steps:
   displayName: Publish PixiEditor
   displayName: Publish PixiEditor
   inputs:
   inputs:
     filePath: 'src/PixiEditor.Builder/build.ps1'
     filePath: 'src/PixiEditor.Builder/build.ps1'
-    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "DevRelease" --runtime "win-x86" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x86-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
+    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x86-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
     workingDirectory: 'src/PixiEditor.Builder'
     workingDirectory: 'src/PixiEditor.Builder'
 
 
 - task: ArchiveFiles@2
 - task: ArchiveFiles@2

+ 9 - 8
windows-x86-release.yml

@@ -15,7 +15,7 @@ variables:
 - name: solution 
 - name: solution 
   value: '**/*.sln'
   value: '**/*.sln'
 - name: buildPlatform 
 - name: buildPlatform 
-  value: 'x86'
+  value: 'win-x86'
 - name: buildConfiguration
 - name: buildConfiguration
   value: 'Release'
   value: 'Release'
 
 
@@ -24,7 +24,7 @@ steps:
 - task: UseDotNet@2
 - task: UseDotNet@2
   inputs:
   inputs:
     packageType: 'sdk'
     packageType: 'sdk'
-    version: '7.0.103'
+    version: '7.0.104'
 
 
 - task: NuGetToolInstaller@1
 - task: NuGetToolInstaller@1
 
 
@@ -32,11 +32,12 @@ steps:
   inputs:
   inputs:
     restoreSolution: '$(solution)'
     restoreSolution: '$(solution)'
 
 
-- task: VSBuild@1
+- task: DotNetCoreCLI@2
+  displayName: "Build PixiEditor Solution"
   inputs:
   inputs:
-    solution: '$(solution)'
-    platform: '$(buildPlatform)'
-    configuration: '$(buildConfiguration)'
+    command: 'build'
+    projects: 'src/PixiEditor'
+    arguments: '-r "$(buildPlatform)" -c $(buildConfiguration)'
 
 
 - task: DotNetCoreCLI@2
 - task: DotNetCoreCLI@2
   displayName: "Build release PixiEditor.UpdateInstaller"
   displayName: "Build release PixiEditor.UpdateInstaller"
@@ -44,7 +45,7 @@ steps:
     command: 'publish'
     command: 'publish'
     publishWebProjects: false
     publishWebProjects: false
     projects: '**/PixiEditor.UpdateInstaller.csproj'
     projects: '**/PixiEditor.UpdateInstaller.csproj'
-    arguments: '-o "UpdateInstaller" -r "win-x86" --self-contained=false -p:PublishSingleFile=true -c Release'
+    arguments: '-o "UpdateInstaller" -r "$(buildPlatform)" --self-contained=false -p:PublishSingleFile=true -c $(buildConfiguration)'
     zipAfterPublish: false
     zipAfterPublish: false
 
 
 
 
@@ -57,7 +58,7 @@ steps:
   displayName: Publish PixiEditor
   displayName: Publish PixiEditor
   inputs:
   inputs:
     filePath: 'src/PixiEditor.Builder/build.ps1'
     filePath: 'src/PixiEditor.Builder/build.ps1'
-    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "Release" --runtime "win-x86" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x86-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
+    arguments: '--project-path "$(System.DefaultWorkingDirectory)\src\PixiEditor" --build-configuration "$(buildConfiguration)" --runtime "$(buildPlatform)" -o "$(System.DefaultWorkingDirectory)\Builds\PixiEditor-x86-light\PixiEditor" --crash-report-webhook-url "$(crash-webhook-url)"'
     workingDirectory: 'src/PixiEditor.Builder'
     workingDirectory: 'src/PixiEditor.Builder'
 
 
 - task: ArchiveFiles@2
 - task: ArchiveFiles@2