Browse Source

Merge pull request #862 from PixiEditor/fixes/25.03.25

A bag of juicy fixes
Krzysztof Krysiński 4 months ago
parent
commit
a04f52dd91
33 changed files with 282 additions and 177 deletions
  1. 1 1
      src/Drawie
  2. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  3. 17 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  4. 0 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  5. 39 27
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  6. 11 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  7. 3 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  8. 6 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  9. 7 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  10. 11 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  11. 8 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  12. 7 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  14. 8 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs
  15. 10 20
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  16. 38 28
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  17. 5 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  18. 4 3
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs
  19. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  20. 3 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/PreferencesConstants.cs
  21. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  22. 0 8
      src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json
  23. 3 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  24. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  25. 2 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  26. 1 0
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  27. 4 1
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  28. 51 36
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  29. 4 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs
  30. 6 3
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  31. 7 0
      src/PixiEditor/ViewModels/UserPreferences/Settings/FileSettings.cs
  32. 1 1
      src/PixiEditor/Views/Main/ActionDisplayBar.axaml
  33. 18 0
      src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 2bae841cdf55369fc483bc8d007d1d950a838ec3
+Subproject commit 1f966ffb17a8f2140c4b0d95e5e318880a4b53e5

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs

@@ -16,5 +16,5 @@ public interface IReadOnlyShapeVectorData
     public RectD GeometryAABB { get; }
     public RectD TransformedAABB { get; }
     public ShapeCorners TransformationCorners { get; }
-    public VectorPath ToPath();
+    public VectorPath ToPath(bool transformed = false);
 }

+ 17 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -3,6 +3,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
@@ -67,8 +68,6 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
             return;
         }
 
-        RectD bounds = RectD.Create(VecI.Zero, sceneContext.DocumentSize);
-
         if (sceneContext.TargetPropertyOutput == Output)
         {
             if (Background.Value != null)
@@ -76,19 +75,24 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
 
-            RenderFolderContent(sceneContext, bounds, true);
+            RenderFolderContent(sceneContext, true);
         }
         else if (sceneContext.TargetPropertyOutput == FilterlessOutput ||
                  sceneContext.TargetPropertyOutput == RawOutput)
         {
-            RenderFolderContent(sceneContext, bounds, false);
+            RenderFolderContent(sceneContext, false);
         }
     }
 
-    private void RenderFolderContent(SceneObjectRenderContext sceneContext, RectD bounds, bool useFilters)
+    private void RenderFolderContent(SceneObjectRenderContext sceneContext, bool useFilters)
     {
-        VecI size = (VecI)bounds.Size;
+        VecI size = sceneContext.RenderSurface.DeviceClipBounds.Size + sceneContext.RenderSurface.DeviceClipBounds.Pos;
         var outputWorkingSurface = RequestTexture(0, size, sceneContext.ProcessingColorSpace, true);
+        outputWorkingSurface.DrawingSurface.Canvas.Save();
+        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(sceneContext.RenderSurface.Canvas.TotalMatrix);
+
+        int saved = sceneContext.RenderSurface.Canvas.Save();
+        sceneContext.RenderSurface.Canvas.SetMatrix(Matrix3X3.Identity);
 
         blendPaint.ImageFilter = null;
         blendPaint.ColorFilter = null;
@@ -100,6 +104,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
             Texture tempSurface = RequestTexture(1, outputWorkingSurface.Size, sceneContext.ProcessingColorSpace);
+            tempSurface.DrawingSurface.Canvas.Save();
+            tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
+
+            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             if (Background.Connection.Node is IClipSource clipSource && ClipToPreviousMember)
             {
                 DrawClipSource(tempSurface.DrawingSurface, clipSource, sceneContext);
@@ -112,6 +120,9 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         sceneContext.RenderSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
+
+        sceneContext.RenderSurface.Canvas.RestoreToCount(saved);
+        outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
 
     private void AdjustPaint(bool useFilters)

+ 0 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -47,11 +47,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
     }
 
-    protected override VecI GetTargetSize(RenderContext ctx)
-    {
-        return (GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage).LatestSize;
-    }
-
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
         bool useFilters = true)

+ 39 - 27
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -31,13 +31,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         blendPaint.Color = new Color(255, 255, 255, 255);
         blendPaint.BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver;
 
-        VecI targetSize = GetTargetSize(sceneContext);
-
-        RenderContent(targetSize, sceneContext, sceneContext.RenderSurface,
+        RenderContent(sceneContext, sceneContext.RenderSurface,
             sceneContext.TargetPropertyOutput != FilterlessOutput);
     }
 
-    private void RenderContent(VecI size, SceneObjectRenderContext context, DrawingSurface renderOnto, bool useFilters)
+    private void RenderContent(SceneObjectRenderContext context, DrawingSurface renderOnto, bool useFilters)
     {
         if (!HasOperations())
         {
@@ -50,9 +48,16 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             return;
         }
 
+        VecI size = renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos;
+        int saved = renderOnto.Canvas.Save();
+
         var outputWorkingSurface =
             TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
+        outputWorkingSurface.DrawingSurface.Canvas.Save();
+        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
+
+        renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
 
         DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters);
 
@@ -61,6 +66,12 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         if (Background.Value != null)
         {
             Texture tempSurface = TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 4);
+
+            tempSurface.DrawingSurface.Canvas.Save();
+            tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
+
+            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             {
@@ -72,6 +83,9 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution);
+
+        renderOnto.Canvas.RestoreToCount(saved);
+        outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
 
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
@@ -97,7 +111,6 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         target.Canvas.RestoreToCount(scaled);
     }
 
-    protected abstract VecI GetTargetSize(RenderContext ctx);
 
     protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
@@ -111,35 +124,34 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
     {
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
 
+        var targetSurface = workingSurface;
+        Texture? tex = null;
+        int saved = -1;
+        if (!ctx.ProcessingColorSpace.IsSrgb)
+        {
+            saved = workingSurface.Canvas.Save();
+
+            tex = Texture.ForProcessing(workingSurface,
+                ColorSpace.CreateSrgb());
+            targetSurface = tex.DrawingSurface;
+            workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        }
         if (useFilters && Filters.Value != null)
         {
             blendPaint.SetFilters(Filters.Value);
-
-            var targetSurface = workingSurface;
-            Texture? tex = null;
-            int saved = -1;
-            if (!ctx.ProcessingColorSpace.IsSrgb)
-            {
-                saved = workingSurface.Canvas.Save();
-
-                tex = Texture.ForProcessing(workingSurface, ColorSpace.CreateSrgb()); // filters are meant to be applied in sRGB
-                targetSurface = tex.DrawingSurface;
-                workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
-            }
-
             DrawWithFilters(ctx, targetSurface, blendPaint);
-
-            if(targetSurface != workingSurface)
-            {
-                workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
-                tex.Dispose();
-                workingSurface.Canvas.RestoreToCount(saved);
-            }
         }
         else
         {
             blendPaint.SetFilters(null);
-            DrawWithoutFilters(ctx, workingSurface, blendPaint);
+            DrawWithoutFilters(ctx, targetSurface, blendPaint);
+        }
+
+        if (targetSurface != workingSurface)
+        {
+            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            tex.Dispose();
+            workingSurface.Canvas.RestoreToCount(saved);
         }
     }
 
@@ -168,6 +180,6 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
     void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
     {
-        RenderContent(GetTargetSize(context), context, drawOnto, false);
+        RenderContent(context, drawOnto, false);
     }
 }

+ 11 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -48,6 +48,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
 
+    private int lastContentCacheHash = -1;
+
     protected virtual int GetContentCacheHash()
     {
         return 0;
@@ -91,6 +93,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
             changed |= lastFrameTime.Frame != context.FrameTime.Frame || Math.Abs(lastFrameTime.NormalizedTime - context.FrameTime.NormalizedTime) > float.Epsilon;
         }
 
+        int contentCacheHash = GetContentCacheHash();
+
+        changed |= contentCacheHash != lastContentCacheHash;
+
         return changed;
     }
 
@@ -102,6 +108,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
 
         lastFrameTime = context.FrameTime;
+
+        lastContentCacheHash = GetContentCacheHash();
     }
 
     public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
@@ -516,10 +524,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
     }
 
-    internal virtual OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target,
-        IReadOnlyDictionary<string, object> data)
+    internal virtual void DeserializeAdditionalData(IReadOnlyDocument target,
+        IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
-        return new None();
+
     }
 
     private void InvokeConnectionsChanged()

+ 3 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
@@ -85,14 +86,12 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         additionalData["AllowHighDpiRendering"] = AllowHighDpiRendering;
     }
 
-    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
-        base.DeserializeAdditionalData(target, data);
+        base.DeserializeAdditionalData(target, data, infos);
 
         if(data.TryGetValue("AllowHighDpiRendering", out var value))
             AllowHighDpiRendering = (bool)value;
-
-        return new None();
     }
 
     public override void Dispose()

+ 6 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -85,14 +85,17 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
 
     protected override void AdjustCopy(ShapeVectorData copy)
     {
-       
     }
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
-        // TODO: Apply transformation matrix
         VectorPath path = new VectorPath();
         path.AddOval(RectD.FromCenterAndSize(Center, Radius * 2));
+        if (transformed)
+        {
+            path.Transform(TransformationMatrix);
+        }
+
         return path;
     }
 }

+ 7 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -58,7 +58,7 @@ public class LineVectorData : ShapeVectorData, IReadOnlyLineData
     {
         Start = startPos;
         End = endPos;
-        
+
         Fill = false;
     }
 
@@ -108,13 +108,16 @@ public class LineVectorData : ShapeVectorData, IReadOnlyLineData
         return hash.ToHashCode();
     }
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
-        // TODO: Apply transformation matrix
-
         VectorPath path = new VectorPath();
         path.MoveTo((VecF)Start);
         path.LineTo((VecF)End);
+        if (transformed)
+        {
+            path.Transform(TransformationMatrix);
+        }
+
         return path;
     }
 }

+ 11 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -18,7 +18,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
-    
+
     public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
 
     public PathVectorData(VectorPath path)
@@ -42,7 +42,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
     private void Rasterize(Canvas canvas, bool applyTransform)
     {
-        if(Path == null)
+        if (Path == null)
         {
             return;
         }
@@ -72,7 +72,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             paint.SetPaintable(Stroke);
             paint.Style = PaintStyle.Stroke;
             paint.StrokeWidth = StrokeWidth;
-            
+
             canvas.DrawPath(Path, paint);
         }
 
@@ -105,8 +105,14 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
         }
     }
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
-        return new VectorPath(Path);
+        VectorPath newPath = new VectorPath(Path);
+        if (transformed)
+        {
+            newPath.Transform(TransformationMatrix);
+        }
+
+        return newPath;
     }
 }

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -76,15 +76,20 @@ public class PointsVectorData : ShapeVectorData
         }
     }
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
         VectorPath path = new VectorPath();
-        
+
         foreach (VecD point in Points)
         {
             path.LineTo((VecF)point);
         }
-        
+
+        if (transformed)
+        {
+            path.Transform(TransformationMatrix);
+        }
+
         return path;
     }
 }

+ 7 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -34,7 +34,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         Center = center;
         Size = size;
     }
-    
+
     public RectangleVectorData(double x, double y, double width, double height)
     {
         Center = new VecD(x + width / 2, y + height / 2);
@@ -95,10 +95,15 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         return HashCode.Combine(Center, Size);
     }
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
         VectorPath path = new VectorPath();
         path.AddRect(RectD.FromCenterAndSize(Center, Size));
+        if (transformed)
+        {
+            path.Transform(TransformationMatrix);
+        }
+
         return path;
     }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -81,5 +81,5 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
         return GetCacheHash();
     }
 
-    public abstract VectorPath ToPath();
+    public abstract VectorPath ToPath(bool transformed = false);
 }

+ 8 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs

@@ -88,12 +88,12 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
             lastBounds = richText.MeasureBounds(Font);
         }
     }
-    
+
     public bool AntiAlias { get; set; } = true;
 
     protected override void OnStrokeWidthChanged()
     {
-        if(richText == null)
+        if (richText == null)
         {
             return;
         }
@@ -135,7 +135,6 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
 
     public TextVectorData()
     {
-
     }
 
     public TextVectorData(string text)
@@ -144,11 +143,16 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
     }
 
 
-    public override VectorPath ToPath()
+    public override VectorPath ToPath(bool transformed = false)
     {
         var path = richText.ToPath(Font);
         path.Offset(Position);
 
+        if (transformed)
+        {
+            path.Transform(TransformationMatrix);
+        }
+
         return path;
     }
 

+ 10 - 20
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -79,8 +79,6 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         ColorFilter = ColorFilter.CreateCompose(Nodes.Filters.AlphaGrayscaleFilter, Nodes.Filters.MaskFilter)
     };
 
-    private int maskCacheHash = 0;
-
     protected StructureNode()
     {
         Painter filterlessPainter = new Painter(OnFilterlessPaint);
@@ -211,16 +209,9 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
     }
 
-    protected override bool CacheChanged(RenderContext context)
-    {
-        int cacheHash = EmbeddedMask?.GetCacheHash() ?? 0;
-        return base.CacheChanged(context) || maskCacheHash != cacheHash;
-    }
-
-    protected override void UpdateCache(RenderContext context)
+    protected override int GetContentCacheHash()
     {
-        base.UpdateCache(context);
-        maskCacheHash = EmbeddedMask?.GetCacheHash() ?? 0;
+        return HashCode.Combine(base.GetContentCacheHash(), EmbeddedMask?.GetCacheHash() ?? 0, ClipToPreviousMember ? 1 : 0);
     }
 
     public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processingColorSpace)
@@ -297,10 +288,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
     }
 
-    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target,
-        IReadOnlyDictionary<string, object> data)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target,
+        IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
-        base.DeserializeAdditionalData(target, data);
+        base.DeserializeAdditionalData(target, data, infos);
+
         bool hasMask = data.ContainsKey("embeddedMask");
         if (hasMask)
         {
@@ -309,16 +301,14 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
             EmbeddedMask?.Dispose();
             EmbeddedMask = mask;
 
-            return new List<IChangeInfo> { new StructureMemberMask_ChangeInfo(Id, mask != null) };
+            infos.Add(new StructureMemberMask_ChangeInfo(Id, mask != null));
         }
         
-        if (data.ContainsKey("clipToPreviousMember"))
+        if (data.TryGetValue("clipToPreviousMember", out var clip))
         {
-            ClipToPreviousMember = (bool)data["clipToPreviousMember"];
-            return new List<IChangeInfo> { new StructureMemberClipToMemberBelow_ChangeInfo(Id, ClipToPreviousMember) };
+            ClipToPreviousMember = (bool)clip;
+            infos.Add(new StructureMemberClipToMemberBelow_ChangeInfo(Id, ClipToPreviousMember));
         }
-
-        return new None();
     }
 
     public override RectD? GetPreviewBounds(int frame, string elementFor = "")

+ 38 - 28
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -9,6 +9,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -18,6 +19,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable
 {
     public OutputProperty<ShapeVectorData> Shape { get; }
+
     public Matrix3X3 TransformationMatrix
     {
         get => ShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
@@ -37,10 +39,9 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         get => Shape.Value;
         set => Shape.Value = value;
     }
-    IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
 
+    IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
 
-    private int lastCacheHash;
 
     public override VecD GetScenePosition(KeyFrameTime time) => ShapeData?.TransformedAABB.Center ?? VecD.Zero;
     public override VecD GetSceneSize(KeyFrameTime time) => ShapeData?.TransformedAABB.Size ?? VecD.Zero;
@@ -50,11 +51,6 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         AllowHighDpiRendering = true;
         Shape = CreateOutput<ShapeVectorData>("Shape", "SHAPE", null);
     }
-    
-    protected override VecI GetTargetSize(RenderContext ctx)
-    {
-        return ctx.DocumentSize;
-    }
 
     protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
         Paint paint)
@@ -63,7 +59,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         {
             return;
         }
-        
+
         Rasterize(workingSurface, paint);
     }
 
@@ -73,7 +69,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         {
             return;
         }
-        
+
         Rasterize(workingSurface, paint);
     }
 
@@ -87,13 +83,18 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         {
             return ShapeData?.TransformedVisualAABB;
         }
-        
+
         return null;
     }
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            return base.RenderPreview(renderOn, context, elementToRenderName);
+        }
+
         if (ShapeData == null)
         {
             return false;
@@ -106,16 +107,30 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         VecI translation = new VecI(
             (int)Math.Max(ShapeData.TransformedAABB.TopLeft.X, 0),
             (int)Math.Max(ShapeData.TransformedAABB.TopLeft.Y, 0));
-        
+
         VecI size = tightBoundsSize + translation;
-        
+
         if (size.X == 0 || size.Y == 0)
         {
             return false;
         }
 
         Matrix3X3 matrix = ShapeData.TransformationMatrix;
-        Rasterize(renderOn, paint);
+
+        if (!context.ProcessingColorSpace.IsSrgb)
+        {
+            int saved = renderOn.Canvas.Save();
+            Texture tex = Texture.ForProcessing(renderOn, ColorSpace.CreateSrgb());
+            renderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+            Rasterize(tex.DrawingSurface, paint);
+            renderOn.Canvas.DrawSurface(tex.DrawingSurface, 0, 0);
+            renderOn.Canvas.RestoreToCount(saved);
+        }
+        else
+        {
+            Rasterize(renderOn, paint);
+        }
+
         return true;
     }
 
@@ -125,32 +140,26 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         additionalData["ShapeData"] = ShapeData;
     }
 
-    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target,
-        IReadOnlyDictionary<string, object> data)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target,
+        IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
-        base.DeserializeAdditionalData(target, data);
+        base.DeserializeAdditionalData(target, data, infos);
         ShapeData = (ShapeVectorData)data["ShapeData"];
 
         if (ShapeData == null)
         {
-            return new None();
+            return;
         }
-        
+
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             (RectI)ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
 
-        return new VectorShape_ChangeInfo(Id, affected);
-    }
-
-    protected override bool CacheChanged(RenderContext context)
-    {
-        return base.CacheChanged(context) || (ShapeData?.GetCacheHash() ?? -1) != lastCacheHash;
+        infos.Add(new VectorShape_ChangeInfo(Id, affected));
     }
 
-    protected override void UpdateCache(RenderContext context)
+    protected override int GetContentCacheHash()
     {
-        base.UpdateCache(context);
-        lastCacheHash = ShapeData?.GetCacheHash() ?? -1;
+        return HashCode.Combine(base.GetContentCacheHash(), ShapeData?.GetCacheHash() ?? 0);
     }
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
@@ -167,7 +176,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     {
         int layer = surface.Canvas.SaveLayer(paint);
         ShapeData?.RasterizeTransformed(surface.Canvas);
-        
+
         surface.Canvas.RestoreToCount(layer);
     }
 
@@ -177,6 +186,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         {
             ShapeData = (ShapeVectorData?)ShapeData?.Clone(),
             ClipToPreviousMember = this.ClipToPreviousMember,
+            EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted(),
             AllowHighDpiRendering = this.AllowHighDpiRendering
         };
     }

+ 5 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -205,7 +205,8 @@ internal class CombineStructureMembersOnto_Change : Change
             if (targetData == null)
             {
                 targetData = vectorNode.ShapeData;
-                targetPath = path;
+                targetPath = new VectorPath();
+                targetPath.AddPath(path, vectorNode.ShapeData.TransformationMatrix, AddPathMode.Append);
 
                 if (originalPaths.ContainsKey(frame))
                     originalPaths[frame].Dispose();
@@ -214,7 +215,7 @@ internal class CombineStructureMembersOnto_Change : Change
             }
             else
             {
-                targetPath.AddPath(path, AddPathMode.Append);
+                targetPath.AddPath(path, vectorNode.ShapeData.TransformationMatrix, AddPathMode.Append);
                 path.Dispose();
             }
         }
@@ -230,12 +231,13 @@ internal class CombineStructureMembersOnto_Change : Change
                 FillPaintable = shape.FillPaintable,
                 StrokeWidth = shape.StrokeWidth,
                 Fill = shape.Fill,
-                TransformationMatrix = shape.TransformationMatrix,
+                TransformationMatrix = Matrix3X3.Identity
             };
         }
         else
         {
             data = vectorData;
+            data.TransformationMatrix = Matrix3X3.Identity;
             data.Path = targetPath;
         }
 

+ 4 - 3
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs

@@ -22,10 +22,11 @@ internal class DeserializeNodeAdditionalData_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         Node node = target.FindNode<Node>(nodeId);
-        
-        var changeInfos = node.DeserializeAdditionalData(target, data);
+
+        List<IChangeInfo> infos = new();
+        node.DeserializeAdditionalData(target, data, infos);
         ignoreInUndo = false;
-        return changeInfos;
+        return infos;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs

@@ -26,6 +26,7 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     {
         if (target.TryFindNode<VectorLayerNode>(TargetId, out var node))
         {
+            // TODO: Add is identical check
             originalData = (ShapeVectorData?)node.ShapeData?.Clone();
             return true;
         }

+ 3 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/PreferencesConstants.cs

@@ -27,4 +27,7 @@ public static class PreferencesConstants
 
     public const string LastCrashFile = "LastCrashFile";
     public const string NextSessionFiles = "NextSessionFiles";
+
+    public const string OpenDirectoryOnExport = "OpenDirectoryOnExport";
+    public const bool OpenDirectoryOnExportDefault = true;
 }

+ 2 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -922,5 +922,6 @@
   "AUTOSAVE_SETTINGS_SAVE_USER_FILE": "Autosave to selected file",
   "LOAD_LAZY_FILE_MESSAGE": "To improve startup time, PixiEditor didn't load this file. Click the button below to load it.",
   "EASING_NODE": "Easing",
-  "EASING_TYPE": "Easing Type"
+  "EASING_TYPE": "Easing Type",
+  "OPEN_DIRECTORY_ON_EXPORT": "Open directory on export"
 }

+ 0 - 8
src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json

@@ -359,14 +359,6 @@
       },
       "Parameters": []
     },
-    "": {
-      "Command": "PixiEditor.Restart",
-      "DefaultShortcut": {
-        "key": "None",
-        "modifiers": null
-      },
-      "Parameters": []
-    },
     "ShowGrid": {
       "Command": "PixiEditor.View.ToggleGrid",
       "DefaultShortcut": {

+ 3 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -540,6 +540,8 @@ internal class DocumentOperationsModule : IDocumentOperations
 
         Guid newGuid = Guid.NewGuid();
 
+        Internals.ActionAccumulator.StartChangeBlock();
+
         //make a new layer, put combined image onto it, delete layers that were merged
         bool allVectorNodes = members.All(x => Document.StructureHelper.Find(x) is IVectorLayerHandler);
         Type layerToCreate = allVectorNodes ? typeof(VectorLayerNode) : typeof(ImageLayerNode);
@@ -549,7 +551,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid));
         foreach (var member in members)
             Internals.ActionAccumulator.AddActions(new DeleteStructureMember_Action(member));
-        Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
+        Internals.ActionAccumulator.EndChangeBlock();
     }
 
     /// <summary>

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

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

+ 2 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -947,6 +947,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public void UpdateSavedState()
     {
         OnPropertyChanged(nameof(AllChangesSaved));
+        OnPropertyChanged(nameof(HasSavedUndo));
+        OnPropertyChanged(nameof(HasSavedRedo));
     }
 
     private void ExtractSelectedLayers(IFolderHandler folder, HashSet<Guid> list,

+ 1 - 0
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -270,6 +270,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
             new("GENERAL"),
             new("DISCORD"),
             new("KEY_BINDINGS"),
+            new("EXPORT"),
         };
 
         ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;

+ 4 - 1
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -599,7 +599,10 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                     {
                         Dispatcher.UIThread.Post(() =>
                         {
-                            IOperatingSystem.Current.OpenFolder(result.finalPath);
+                            if (IPreferences.Current.GetPreference<bool>(PreferencesConstants.OpenDirectoryOnExport))
+                            {
+                                IOperatingSystem.Current.OpenFolder(result.finalPath);
+                            }
                         });
                     }
                     else

+ 51 - 36
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -50,7 +50,9 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             OnPropertyChanged(nameof(UpdateReadyToInstall));
             if (value)
             {
-                VersionText = new LocalizedString("TO_INSTALL_UPDATE", UpdateChecker.LatestReleaseInfo.TagName); // Button shows "Restart" before this text
+                VersionText =
+                    new LocalizedString("TO_INSTALL_UPDATE",
+                        UpdateChecker.LatestReleaseInfo.TagName); // Button shows "Restart" before this text
             }
         }
     }
@@ -73,22 +75,23 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     public async Task<bool> CheckForUpdate()
     {
-        if(!IOperatingSystem.Current.IsWindows)
+        if (!IOperatingSystem.Current.IsWindows)
         {
             return false;
         }
-        
+
         bool updateAvailable = await UpdateChecker.CheckUpdateAvailable();
-        if(!UpdateChecker.LatestReleaseInfo.WasDataFetchSuccessful || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName))
+        if (!UpdateChecker.LatestReleaseInfo.WasDataFetchSuccessful ||
+            string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName))
         {
             return false;
         }
-        
+
         bool updateCompatible = await UpdateChecker.IsUpdateCompatible();
         bool autoUpdateFailed = CheckAutoupdateFailed();
         bool updateFileDoesNotExists = !File.Exists(
             Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
-        
+
         bool updateExeDoesNotExists = !File.Exists(
             Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe"));
         if (updateAvailable && (updateFileDoesNotExists && updateExeDoesNotExists) || autoUpdateFailed)
@@ -117,7 +120,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                 NoticeDialog.Show("FAILED_DOWNLOADING", "FAILED_DOWNLOADING_TITLE");
                 return false;
             }
-            catch(TaskCanceledException ex)
+            catch (TaskCanceledException ex)
             {
                 return false;
             }
@@ -139,7 +142,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         string dir = AppDomain.CurrentDomain.BaseDirectory;
 
         UpdateDownloader.CreateTempDirectory();
-        if(UpdateChecker.LatestReleaseInfo == null || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
+        if (UpdateChecker.LatestReleaseInfo == null ||
+            string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
         bool updateFileExists = File.Exists(
             Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
         string exePath = Path.Join(UpdateDownloader.DownloadLocation,
@@ -147,7 +151,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
         bool updateExeExists = File.Exists(exePath);
 
-        if (updateExeExists && !UpdateChecker.VersionDifferent(UpdateChecker.LatestReleaseInfo.TagName, UpdateChecker.CurrentVersionTag))
+        if (updateExeExists &&
+            !UpdateChecker.VersionDifferent(UpdateChecker.LatestReleaseInfo.TagName, UpdateChecker.CurrentVersionTag))
         {
             File.Delete(exePath);
             updateExeExists = false;
@@ -176,8 +181,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             CreateUpdateInfo(UpdateChecker.LatestReleaseInfo.TagName);
             return;
         }
-        
-        if(!CanInstallUpdate(UpdateChecker.LatestReleaseInfo.TagName, info) && !updateExeExists)
+
+        if (!CanInstallUpdate(UpdateChecker.LatestReleaseInfo.TagName, info) && !updateExeExists)
         {
             return;
         }
@@ -225,8 +230,15 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private static void RestartToUpdate(string updateExeFile)
     {
-        Process.Start(updateExeFile);
-        Shutdown();
+        try
+        {
+            IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updateExeFile);
+            Shutdown();
+        }
+        catch (Win32Exception)
+        {
+            NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
+        }
     }
 
     private static void Shutdown()
@@ -235,12 +247,13 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             desktop.Shutdown();
     }
 
-    [Command.Internal("PixiEditor.Restart")]
-    public static void RestartApplication()
+    [Command.Internal("PixiEditor.RestartToUpdate")]
+    public static void RestartApplicationToUpdate()
     {
         try
         {
-            ProcessHelper.RunAsAdmin(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "PixiEditor.UpdateInstaller.exe"));
+            ProcessHelper.RunAsAdmin(Path.Join(AppDomain.CurrentDomain.BaseDirectory,
+                "PixiEditor.UpdateInstaller.exe"));
             Shutdown();
         }
         catch (Win32Exception)
@@ -262,7 +275,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             try
             {
                 await CheckForUpdate();
-                if(UpdateChecker.LatestReleaseInfo != null && UpdateChecker.LatestReleaseInfo.TagName == VersionHelpers.GetCurrentAssemblyVersionString())
+                if (UpdateChecker.LatestReleaseInfo != null && UpdateChecker.LatestReleaseInfo.TagName ==
+                    VersionHelpers.GetCurrentAssemblyVersionString())
                 {
                     EnsureUpdateFilesDeleted();
                 }
@@ -280,17 +294,17 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             Install();
         }
     }
-    
+
     private bool OsSupported()
     {
         return IOperatingSystem.Current.IsWindows;
     }
-    
+
     private bool UpdateInfoExists()
     {
         return File.Exists(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
     }
-    
+
     private void CreateUpdateInfo(string targetVersion)
     {
         StringBuilder sb = new StringBuilder();
@@ -298,7 +312,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         sb.AppendLine("0");
         File.WriteAllText(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), sb.ToString());
     }
-    
+
     private string[] IncrementUpdateInfo()
     {
         string[] lines = File.ReadAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
@@ -306,53 +320,54 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         count++;
         lines[1] = count.ToString();
         File.WriteAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), lines);
-        
+
         return lines;
     }
-    
+
     private void EnsureUpdateFilesDeleted()
     {
         string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
-        if(File.Exists(path))
+        if (File.Exists(path))
         {
             File.Delete(path);
         }
     }
-    
+
     private bool CanInstallUpdate(string forVersion, string[] lines)
     {
         if (lines.Length != 2) return false;
-        
+
         if (lines[0] != forVersion) return false;
-        
+
         return int.TryParse(lines[1], out int count) && count < MaxRetryCount;
     }
-    
+
     private bool UpdateInfoValid(string forVersion, string[] lines)
     {
         if (lines.Length != 2) return false;
-        
+
         if (lines[0] != forVersion) return false;
-        
+
         return int.TryParse(lines[1], out _);
     }
-    
+
     private bool CheckAutoupdateFailed()
     {
         string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
         if (!File.Exists(path)) return false;
-        
+
         string[] lines = File.ReadAllLines(path);
         if (lines.Length != 2) return false;
-        
+
         if (!int.TryParse(lines[1], out int count)) return false;
-        
+
         return count >= MaxRetryCount;
     }
-    
+
     private void RemoveZipIfExists()
     {
-        string zipPath = Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
+        string zipPath = Path.Join(UpdateDownloader.DownloadLocation,
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
         if (File.Exists(zipPath))
         {
             File.Delete(zipPath);

+ 4 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -18,7 +18,10 @@ internal abstract class Toolbar : ObservableObject, IToolbar
     {
         setting.ValueChanged += (sender, args) =>
         {
-            SettingChanged?.Invoke(setting.Name, setting.Value);
+            if (args.OldValue != args.NewValue)
+            {
+                SettingChanged?.Invoke(setting.Name, setting.Value);
+            }
         };
         
         settings.Add(setting);

+ 6 - 3
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -133,7 +133,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
         foreach (var type in SupportedLayerTypes)
         {
-            if (type.IsInstanceOfType(layer) || IsFolderAndRasterSupported(layer))
+            if (type.IsInstanceOfType(layer) || IsMaskSelectedAndRasterSupported(layer))
             {
                 CanBeUsedOnActiveLayer = true;
                 return;
@@ -143,9 +143,12 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         CanBeUsedOnActiveLayer = false;
     }
 
-    private bool IsFolderAndRasterSupported(IStructureMemberHandler layer)
+    private bool IsMaskSelectedAndRasterSupported(IStructureMemberHandler layer)
     {
-        return SupportedLayerTypes.Contains(typeof(IRasterLayerHandler)) && layer is IFolderHandler;
+        return SupportedLayerTypes.Contains(typeof(IRasterLayerHandler)) && layer is IFolderHandler or ILayerHandler
+        {
+            ShouldDrawOnMask: true
+        };
     }
 
     private void OnLanguageChanged(Language obj)

+ 7 - 0
src/PixiEditor/ViewModels/UserPreferences/Settings/FileSettings.cs

@@ -82,4 +82,11 @@ internal class FileSettings : SettingsGroup
         get => autosaveToDocumentPath;
         set => RaiseAndUpdatePreference(ref autosaveToDocumentPath, value);
     }
+
+    private bool openDirectoryOnExport = GetPreference(PreferencesConstants.OpenDirectoryOnExport, PreferencesConstants.OpenDirectoryOnExportDefault);
+    public bool OpenDirectoryOnExport
+    {
+        get => openDirectoryOnExport;
+        set => RaiseAndUpdatePreference(ref openDirectoryOnExport, value);
+    }
 }

+ 1 - 1
src/PixiEditor/Views/Main/ActionDisplayBar.axaml

@@ -58,7 +58,7 @@
             <Button
                 IsVisible="{Binding DataContext.UpdateSubViewModel.UpdateReadyToInstall, FallbackValue=False, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
                 Background="{DynamicResource ThemeAccentBrush}"
-                Command="{xaml:Command PixiEditor.Restart}" ui:Translator.Key="RESTART" />
+                Command="{xaml:Command PixiEditor.RestartToUpdate}" ui:Translator.Key="RESTART" />
             <TextBlock
                 VerticalAlignment="Center"
                 Padding="10, 0"

+ 18 - 0
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -294,6 +294,24 @@
 
                         <settings:ShortcutsBinder Grid.Row="2" />
                     </Grid>
+
+                    <ScrollViewer>
+                        <ScrollViewer.IsVisible>
+                            <Binding Path="CurrentPage" Converter="{converters:IsEqualConverter}">
+                                <Binding.ConverterParameter>
+                                    <sys:Int32>3</sys:Int32>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </ScrollViewer.IsVisible>
+                        <!--Background="{StaticResource AccentColor}"-->
+                        <controls:FixedSizeStackPanel Orientation="Vertical" ChildSize="32"
+                                                      VerticalChildrenAlignment="Center" Margin="12">
+
+                            <CheckBox Classes="leftOffset" Width="200" HorizontalAlignment="Left"
+                                      ui:Translator.Key="OPEN_DIRECTORY_ON_EXPORT"
+                                      IsChecked="{Binding SettingsSubViewModel.File.OpenDirectoryOnExport}" />
+                        </controls:FixedSizeStackPanel>
+                    </ScrollViewer>
                 </Grid>
             </Border>
         </DockPanel>