Browse Source

Merge pull request #882 from PixiEditor/fixes/7.04.2025

Fixes/7.04.2025
Krzysztof Krysiński 3 tháng trước cách đây
mục cha
commit
1f85ceef5c
44 tập tin đã thay đổi với 648 bổ sung110 xóa
  1. 1 1
      src/Drawie
  2. 2 0
      src/PixiEditor.ChangeableDocument.Gen/Helpers.cs
  3. 11 1
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  4. 58 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentMemoryPipe.cs
  5. 21 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentNodePipe.cs
  6. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyFolderNode.cs
  7. 6 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  8. 19 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/BoolOperationNode.cs
  9. 19 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  10. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/ICrossDocumentPipe.cs
  11. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  12. 22 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  13. 12 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  14. 168 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs
  15. 112 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs
  16. 0 5
      src/PixiEditor.Linux/LinuxProcessUtility.cs
  17. 1 10
      src/PixiEditor.MacOs/MacOsProcessUtility.cs
  18. 0 1
      src/PixiEditor.OperatingSystem/IProcessUtility.cs
  19. 1 10
      src/PixiEditor.Windows/WindowsProcessUtility.cs
  20. 3 2
      src/PixiEditor/Data/Localization/Languages/en.json
  21. 1 0
      src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs
  22. 0 5
      src/PixiEditor/Helpers/ProcessHelper.cs
  23. 1 0
      src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs
  24. 1 0
      src/PixiEditor/Models/Commands/CommandController.cs
  25. 1 0
      src/PixiEditor/Models/Commands/Commands/ToolCommand.cs
  26. 21 14
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  27. 1 1
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  28. 35 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  29. 3 0
      src/PixiEditor/Models/Handlers/IDocument.cs
  30. 1 0
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  31. 6 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  32. 1 1
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  33. 32 8
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  34. 1 1
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  35. 1 1
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  36. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  37. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs
  38. 2 1
      src/PixiEditor/Views/Layers/LayersManager.axaml.cs
  39. 13 4
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  40. 8 4
      src/PixiEditor/Views/Overlays/Overlay.cs
  41. 38 21
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  42. 5 2
      src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs
  43. 1 0
      src/PixiEditor/Views/Rendering/Scene.cs
  44. 5 0
      tests/PixiEditor.Backend.Tests/MockDocument.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 7b79ed74f73de883c3dbea383f0a410d52ebdba6
+Subproject commit 676738f1cb90e799f574851ad93171e18e434434

+ 2 - 0
src/PixiEditor.ChangeableDocument.Gen/Helpers.cs

@@ -25,6 +25,7 @@ internal static class Helpers
         StringBuilder sb = new();
          
         sb.AppendLine("using Drawie.Backend.Core.Numerics;");
+        sb.AppendLine("using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;");
         sb.AppendLine("namespace PixiEditor.ChangeableDocument.Actions.Generated;\n");
         sb.AppendLine("[System.Runtime.CompilerServices.CompilerGenerated]");
         sb.AppendLine($"public record class {actionName} : PixiEditor.ChangeableDocument.Actions.IMakeChangeAction");
@@ -59,6 +60,7 @@ internal static class Helpers
         StringBuilder sb = new();
 
         sb.AppendLine("using Drawie.Backend.Core.Numerics;");
+        sb.AppendLine("using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;");
         sb.AppendLine("namespace PixiEditor.ChangeableDocument.Actions.Generated;");
         sb.AppendLine($"public record class {actionName} : PixiEditor.ChangeableDocument.Actions.IStartOrUpdateChangeAction" + (isCancelable ? ", PixiEditor.ChangeableDocument.Actions.ICancelableAction" : ""));
         sb.AppendLine("{");

+ 11 - 1
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -48,6 +48,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     public bool VerticalSymmetryAxisEnabled { get; set; }
     public double HorizontalSymmetryAxisY { get; set; }
     public double VerticalSymmetryAxisX { get; set; }
+    public bool IsDisposed { get; private set; }
 
     public Document()
     {
@@ -57,6 +58,9 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     public void Dispose()
     {
+        if (IsDisposed) return;
+
+        IsDisposed = true;
         NodeGraph.Dispose();
         Selection.Dispose();
     }
@@ -154,7 +158,8 @@ internal class Document : IChangeable, IReadOnlyDocument
         List<IReadOnlyStructureNode> parents = new();
         childNode.TraverseForwards((node, input) =>
         {
-            if (node is IReadOnlyStructureNode parent && input is { InternalPropertyName: FolderNode.ContentInternalName })
+            if (node is IReadOnlyStructureNode parent &&
+                input is { InternalPropertyName: FolderNode.ContentInternalName })
                 parents.Add(parent);
             return true;
         });
@@ -162,6 +167,11 @@ internal class Document : IChangeable, IReadOnlyDocument
         return parents;
     }
 
+    public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode
+    {
+        return new DocumentNodePipe<T>(this, layerId);
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>

+ 58 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentMemoryPipe.cs

@@ -0,0 +1,58 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal abstract class DocumentMemoryPipe<T> : ICrossDocumentPipe<T> where T : class
+{
+    protected Document Document { get; }
+    public bool IsOpen { get; private set; }
+    public bool CanOpen { get; private set; } = true;
+
+    public DocumentMemoryPipe(Document document)
+    {
+        Document = document;
+        IsOpen = false;
+    }
+
+    public void Open()
+    {
+        if (!CanOpen)
+            throw new InvalidOperationException("Pipe cannot be opened");
+        IsOpen = true;
+    }
+
+    public void Close()
+    {
+        IsOpen = false;
+    }
+
+    public T? TryAccessData()
+    {
+        if (!IsOpen)
+        {
+            if (!CanOpen)
+            {
+#if DEBUG
+                throw new InvalidOperationException("Trying to open a disposed pipe");
+#endif
+                return null;
+            }
+
+            Open();
+        }
+
+        if (!DocumentValid()) return null;
+
+        return GetData();
+    }
+
+    protected abstract T? GetData();
+
+    public void Dispose()
+    {
+        IsOpen = false;
+        CanOpen = false;
+    }
+
+    private bool DocumentValid() => Document is { IsDisposed: false };
+}

+ 21 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentNodePipe.cs

@@ -0,0 +1,21 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal class DocumentNodePipe<T> : DocumentMemoryPipe<T> where T : class, IReadOnlyNode
+{
+    public Guid NodeGuid { get; }
+    public DocumentNodePipe(Document document, Guid nodeGuid) : base(document)
+    {
+        NodeGuid = nodeGuid;
+    }
+
+    protected override T? GetData()
+    {
+        var foundNode = Document.FindNode(NodeGuid);
+        if (foundNode is T casted) return casted;
+
+        return null;
+    }
+}

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

@@ -6,4 +6,5 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 public interface IReadOnlyFolderNode : IReadOnlyStructureNode
 {
     public HashSet<Guid> GetLayerNodeGuids();
+    public RenderInputProperty Content { get; }
 }

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

@@ -142,23 +142,23 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        RectI? bounds = null;
+        RectD? bounds = null;
         if (Content.Connection != null)
         {
             Content.Connection.Node.TraverseBackwards((n) =>
             {
                 if (n is StructureNode structureNode)
                 {
-                    RectI? imageBounds = (RectI?)structureNode.GetTightBounds(frameTime);
-                    if (imageBounds != null)
+                    RectD? childBounds = structureNode.GetTightBounds(frameTime);
+                    if (childBounds != null)
                     {
                         if (bounds == null)
                         {
-                            bounds = imageBounds;
+                            bounds = childBounds;
                         }
                         else
                         {
-                            bounds = bounds.Value.Union(imageBounds.Value);
+                            bounds = bounds.Value.Union(childBounds.Value);
                         }
                     }
                 }
@@ -166,7 +166,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
                 return true;
             });
 
-            return (RectD?)bounds ?? RectD.Empty;
+            return bounds ?? RectD.Empty;
         }
 
         return null;

+ 19 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/BoolOperationNode.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.Vector;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 
@@ -45,12 +46,29 @@ public class BoolOperationNode : Node
         ShapeVectorData shapeA = ShapeA.Value;
         ShapeVectorData shapeB = ShapeB.Value;
 
+        StrokeCap cap = StrokeCap.Round;
+        StrokeJoin join = StrokeJoin.Round;
+        PathFillType fillType = PathFillType.Winding;
+
+        if (shapeA is PathVectorData pathA)
+        {
+            cap = pathA.StrokeLineCap;
+            join = pathA.StrokeLineJoin;
+        }
+        else if (shapeB is PathVectorData pathB)
+        {
+            cap = pathB.StrokeLineCap;
+            join = pathB.StrokeLineJoin;
+        }
+
         Result.Value = new PathVectorData(shapeA.ToPath(true).Op(shapeB.ToPath(true), Operation.Value))
         {
             Fill = shapeA.Fill,
             Stroke = shapeA.Stroke,
             StrokeWidth = shapeA.StrokeWidth,
             FillPaintable = shapeA.FillPaintable,
+            StrokeLineCap = cap,
+            StrokeLineJoin = join,
         };
     }
 

+ 19 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -15,12 +15,26 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
 
     public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
 
+    public PathFillType FillType
+    {
+        get => Path?.FillType ?? PathFillType.Winding;
+        set
+        {
+            if (Path == null)
+            {
+                return;
+            }
+
+            Path.FillType = value;
+        }
+    }
+
     public PathVectorData(VectorPath path)
     {
         Path = path;
@@ -93,6 +107,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
         hash.Add(Path);
         hash.Add(StrokeLineCap);
         hash.Add(StrokeLineJoin);
+        hash.Add(FillType);
 
         return hash.ToHashCode();
     }
@@ -118,7 +133,8 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
     protected bool Equals(PathVectorData other)
     {
-        return base.Equals(other) && Path.Equals(other.Path) && StrokeLineCap == other.StrokeLineCap && StrokeLineJoin == other.StrokeLineJoin;
+        return base.Equals(other) && Path.Equals(other.Path) && StrokeLineCap == other.StrokeLineCap && StrokeLineJoin == other.StrokeLineJoin
+                && FillType == other.FillType;
     }
 
     public override bool Equals(object? obj)
@@ -143,6 +159,6 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
     public override int GetHashCode()
     {
-        return HashCode.Combine(base.GetHashCode(), Path, (int)StrokeLineCap, (int)StrokeLineJoin);
+        return HashCode.Combine(base.GetHashCode(), Path, (int)StrokeLineCap, (int)StrokeLineJoin, FillType);
     }
 }

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/ICrossDocumentPipe.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface ICrossDocumentPipe<T> : IDisposable
+{
+    public T? TryAccessData();
+    public bool CanOpen { get; }
+    public bool IsOpen { get; }
+    public void Open();
+    public void Close();
+}

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -105,4 +105,5 @@ public interface IReadOnlyDocument : IDisposable
     public ColorSpace ProcessingColorSpace { get; }
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
     public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
+    public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode;
 }

+ 22 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -33,7 +33,21 @@ internal class CreateStructureMember_Change : Change
             !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
             return false;
 
-        return target.TryFindNode<Node>(parentGuid, out _);
+        if (!target.TryFindNode<Node>(parentGuid, out Node parent))
+        {
+            return false;
+        }
+
+        var painterInput = parent.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter)) as InputProperty<Painter>;
+
+        if (painterInput == null)
+        {
+            FailedMessage = "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER";
+            return false;
+        }
+
+        return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
@@ -49,7 +63,13 @@ internal class CreateStructureMember_Change : Change
         InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
 
-        var previouslyConnected = targetInput.Connection;
+        if (targetInput == null)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        var previouslyConnected = targetInput?.Connection;
 
         if (member is FolderNode folder)
         {

+ 12 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -28,7 +28,17 @@ internal class DuplicateLayer_Change : Change
             return false;
         
         connectionsData = NodeOperations.CreateConnectionsData(layer);
-        
+
+        var targetInput = layer.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter) &&
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+
+        if (targetInput == null)
+        {
+            FailedMessage = "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER";
+            return false;
+        }
+
         return true;
     }
 
@@ -45,7 +55,7 @@ internal class DuplicateLayer_Change : Change
             x.ValueType == typeof(Painter) &&
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
         
-        var previousConnection = targetInput.Connection;
+        var previousConnection = targetInput?.Connection;
 
         List<IChangeInfo> operations = new();
 

+ 168 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs

@@ -0,0 +1,168 @@
+using System.Collections.Immutable;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class ImportFolder_Change : Change
+{
+    private ICrossDocumentPipe<IReadOnlyFolderNode> sourcefolderPipe;
+    private Guid duplicateGuid;
+    private Guid[] contentGuids;
+    private Guid[] contentDuplicateGuids;
+
+    private Guid[]? childGuidsToUse;
+
+    private ConnectionsData? connectionsData;
+    private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+    private Dictionary<Guid, VecD> originalPositions;
+
+    [GenerateMakeChangeAction]
+    public ImportFolder_Change(ICrossDocumentPipe<IReadOnlyFolderNode> pipe, Guid newGuid, ImmutableList<Guid>? childGuids)
+    {
+        sourcefolderPipe = pipe;
+        duplicateGuid = newGuid;
+        childGuidsToUse = childGuids?.ToArray();
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (sourcefolderPipe is not { CanOpen: true } || target.NodeGraph.OutputNode == null)
+            return false;
+
+        var folder = sourcefolderPipe.TryAccessData();
+
+        connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
+
+        List<Guid> contentGuidList = new();
+
+        folder.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            contentGuidList.Add(x.Id);
+            contentConnectionsData[x.Id] = NodeOperations.CreateConnectionsData(x);
+            return true;
+        });
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        var readOnlyFolderNode = sourcefolderPipe.TryAccessData();
+
+        if (readOnlyFolderNode is not FolderNode folderNode || target.NodeGraph.OutputNode == null)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        FolderNode clone = (FolderNode)folderNode.Clone();
+        clone.Id = duplicateGuid;
+
+        InputProperty<Painter?> targetInput = target.NodeGraph.OutputNode.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+
+        var previousConnection = targetInput.Connection;
+
+        operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
+        DuplicateContent(target, clone, folderNode, operations);
+
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (contentDuplicateGuids is not null)
+        {
+            foreach (Guid contentGuid in contentDuplicateGuids)
+            {
+                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
+                changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
+
+                target.NodeGraph.RemoveNode(contentNode);
+                contentNode.Dispose();
+            }
+        }
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.NodeGraph.OutputNode;
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
+        return changes;
+    }
+
+    private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
+        List<IChangeInfo> operations)
+    {
+        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+
+        nodeMap[existingLayer.Id] = clone.Id;
+        int counter = 0;
+        List<Guid> contentGuidList = new();
+
+        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            if (x is not Node targetNode)
+                return false;
+
+            Node? node = targetNode.Clone();
+
+            if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+            {
+                node.Id = childGuidsToUse[counter];
+                counter++;
+            }
+
+            nodeMap[x.Id] = node.Id;
+            contentGuidList.Add(node.Id);
+
+            target.NodeGraph.AddNode(node);
+
+            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+            return true;
+        });
+
+        foreach (var data in contentConnectionsData)
+        {
+            var updatedData = data.Value.WithUpdatedIds(nodeMap);
+            Guid targetNodeId = nodeMap[data.Key];
+            operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
+                target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
+        }
+
+        contentDuplicateGuids = contentGuidList.ToArray();
+    }
+}

+ 112 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs

@@ -0,0 +1,112 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class ImportLayer_Change : Change
+{
+    private ICrossDocumentPipe<IReadOnlyLayerNode> sourceDocumentPipe;
+    private Dictionary<Guid, VecD> originalPositions;
+    private ConnectionsData? connectionsData;
+
+    private Guid duplicateGuid;
+
+    [GenerateMakeChangeAction]
+    public ImportLayer_Change(ICrossDocumentPipe<IReadOnlyLayerNode> pipe, Guid newGuid)
+    {
+        sourceDocumentPipe = pipe;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (sourceDocumentPipe is not { CanOpen: true })
+            return false;
+
+        if (!sourceDocumentPipe.IsOpen)
+        {
+            sourceDocumentPipe.Open();
+        }
+
+        IReadOnlyLayerNode? layer = sourceDocumentPipe.TryAccessData();
+        if (layer == null || target.NodeGraph.OutputNode == null)
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
+
+        if (target.NodeGraph.OutputNode == null) return false;
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+
+        var layer = sourceDocumentPipe.TryAccessData();
+        if (layer is not LayerNode layerNode)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        var clone = (LayerNode)layerNode.Clone();
+        clone.Id = duplicateGuid;
+
+        var targetInput = target.NodeGraph.OutputNode?.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
+
+        var previousConnection = targetInput?.Connection;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+
+        operations.Add(CreateLayer_ChangeInfo.FromLayer(clone));
+
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.NodeGraph.OutputNode;
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
+        return changes;
+    }
+
+    public override void Dispose()
+    {
+        sourceDocumentPipe?.Dispose();
+    }
+}
+

+ 0 - 5
src/PixiEditor.Linux/LinuxProcessUtility.cs

@@ -12,11 +12,6 @@ public class LinuxProcessUtility : IProcessUtility
         throw new NotImplementedException("Running as admin is not supported on Linux");
     }
 
-    public Process RunAsAdmin(string path, string args, bool createWindow)
-    {
-        throw new NotImplementedException("Running as admin is not supported on Linux");
-    }
-
     public bool IsRunningAsAdministrator()
     {
         return Environment.IsPrivilegedProcess;

+ 1 - 10
src/PixiEditor.MacOs/MacOsProcessUtility.cs

@@ -6,22 +6,13 @@ namespace PixiEditor.MacOs;
 internal class MacOsProcessUtility : IProcessUtility
 {
     public Process RunAsAdmin(string path, string args)
-    {
-        return RunAsAdmin(path, args, true);
-    }
-
-    public Process RunAsAdmin(string path, string args, bool createWindow)
     {
         ProcessStartInfo startInfo = new ProcessStartInfo
         {
             FileName = path,
             Verb = "runas",
             Arguments = args,
-            UseShellExecute = createWindow,
-            CreateNoWindow = !createWindow,
-            RedirectStandardOutput = !createWindow,
-            RedirectStandardError = !createWindow,
-            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
+            UseShellExecute = true
         };
 
         Process p = new Process();

+ 0 - 1
src/PixiEditor.OperatingSystem/IProcessUtility.cs

@@ -5,7 +5,6 @@ namespace PixiEditor.OperatingSystem;
 public interface IProcessUtility
 {
     public Process RunAsAdmin(string path, string? args);
-    public Process RunAsAdmin(string path, string? args, bool createWindow);
     public bool IsRunningAsAdministrator();
     public Process ShellExecute(string toExecute);
     public Process ShellExecute(string toExecute, string args);

+ 1 - 10
src/PixiEditor.Windows/WindowsProcessUtility.cs

@@ -8,22 +8,13 @@ namespace PixiEditor.Windows;
 public class WindowsProcessUtility : IProcessUtility
 {
     public Process RunAsAdmin(string path, string args)
-    {
-        return RunAsAdmin(path, args, true);
-    }
-
-    public Process RunAsAdmin(string path, string args, bool createWindow)
     {
         ProcessStartInfo startInfo = new ProcessStartInfo
         {
             FileName = path,
             Verb = "runas",
             Arguments = args,
-            UseShellExecute = createWindow,
-            CreateNoWindow = !createWindow,
-            RedirectStandardOutput = !createWindow,
-            RedirectStandardError = !createWindow,
-            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
+            UseShellExecute = true
         };
 
         Process p = new Process();

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

@@ -757,7 +757,7 @@
   "MODULO": "Modulo",
   "STEP": "Step",
   "SMOOTH_STEP": "Smoothstep",
-  "PIXEL_PERFECT_TOOLSET": "Pixel Perfect",
+  "PIXEL_PERFECT_TOOLSET": "Pixel Art",
   "VECTOR_TOOLSET": "Vector",
   "VECTOR_LAYER": "Vector Layer",
   "STROKE_COLOR_LABEL": "Stroke",
@@ -1009,5 +1009,6 @@
   "XOR_VECTOR_PATH_OP": "XOR",
   "REVERSE_DIFFERENCE_VECTOR_PATH_OP": "Reverse Difference",
   "NO_DOCUMENT_OPEN": "Nothing's here",
-  "EMPTY_DOCUMENT_ACTION_BTN": "Start creating"
+  "EMPTY_DOCUMENT_ACTION_BTN": "Start creating",
+  "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER": "Current Node Graph setup disallows creation of a new layer next to the selected one."
 }

+ 1 - 0
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -9,4 +9,5 @@ public static class ClipboardDataFormats
     public const string DocumentFormat = "PixiEditor.Document";
     public const string NodeIdList = "PixiEditor.NodeIdList";
     public const string CelIdList = "PixiEditor.CelIdList";
+    public const string PixiVectorData = "PixiEditor.VectorData";
 }

+ 0 - 5
src/PixiEditor/Helpers/ProcessHelper.cs

@@ -14,9 +14,4 @@ internal static class ProcessHelper
     {
         return IOperatingSystem.Current.ProcessUtility.IsRunningAsAdministrator();
     }
-
-    public static void RunAsAdmin(string path, string? args, bool showWindow)
-    {
-        IOperatingSystem.Current.ProcessUtility.RunAsAdmin(path, args, showWindow);
-    }
 }

+ 1 - 0
src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs

@@ -8,6 +8,7 @@ internal partial class Command
     internal class ToolAttribute : CommandAttribute
     {
         public Key Transient { get; set; }
+        public bool TransientImmediate { get; set; } = false;
 
         public ToolAttribute() : base(null, null, null)
         {

+ 1 - 0
src/PixiEditor/Models/Commands/CommandController.cs

@@ -214,6 +214,7 @@ internal class CommandController
                 Icon = toolInstance.DefaultIcon,
                 IconEvaluator = IconEvaluator.Default,
                 TransientKey = toolAttr.Transient,
+                TransientImmediate = toolAttr.TransientImmediate,
                 DefaultShortcut = toolAttr.GetShortcut(),
                 Shortcut = GetShortcut(internalName, toolAttr.GetShortcut(), template),
                 ToolType = type,

+ 1 - 0
src/PixiEditor/Models/Commands/Commands/ToolCommand.cs

@@ -10,6 +10,7 @@ internal partial class Command
         public Type ToolType { get; init; }
 
         public Key TransientKey { get; init; }
+        public bool TransientImmediate { get; init; } = false;
 
         public override object GetParameter() => ToolType;
     }

+ 21 - 14
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -27,6 +27,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels.Document;
@@ -189,23 +190,28 @@ internal static class ClipboardController
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
+    public static bool TryPaste(DocumentViewModel document, DocumentManagerViewModel manager, IEnumerable<IDataObject>
+        data, bool pasteAsNew = false)
     {
-        Guid sourceDocument = GetSourceDocument(data);
+        Guid sourceDocument = GetSourceDocument(data, document.Id);
         Guid[] layerIds = GetLayerIds(data);
 
-        if (sourceDocument != document.Id)
-        {
-            layerIds = [];
-        }
-
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
 
-        if (pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
+        IDocument? targetDoc = manager.Documents.FirstOrDefault(x => x.Id == sourceDocument);
+
+        if (targetDoc != null && pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, targetDoc)))
         {
             foreach (var layerId in layerIds)
             {
-                document.Operations.DuplicateMember(layerId);
+                if (sourceDocument == document.Id)
+                {
+                    document.Operations.DuplicateMember(layerId);
+                }
+                else
+                {
+                    document.Operations.ImportMember(layerId, targetDoc);
+                }
             }
 
             return true;
@@ -250,7 +256,7 @@ internal static class ClipboardController
         return true;
     }
 
-    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, DocumentViewModel doc)
+    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, IDocument doc)
     {
         var dataObjects = data as IDataObject[] ?? data.ToArray();
 
@@ -299,7 +305,7 @@ internal static class ClipboardController
         return [];
     }
 
-    private static Guid GetSourceDocument(IEnumerable<IDataObject> data)
+    private static Guid GetSourceDocument(IEnumerable<IDataObject> data, Guid fallback)
     {
         foreach (var dataObject in data)
         {
@@ -311,19 +317,20 @@ internal static class ClipboardController
             }
         }
 
-        return Guid.Empty;
+        return fallback;
     }
 
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, bool pasteAsNew = false)
+    public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, DocumentManagerViewModel manager,
+        bool pasteAsNew = false)
     {
         var data = await TryGetDataObject();
         if (data == null)
             return false;
 
-        return TryPaste(document, data, pasteAsNew);
+        return TryPaste(document, manager, data, pasteAsNew);
     }
 
     private static async Task<List<DataObject?>> TryGetDataObject()

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

@@ -164,7 +164,7 @@ internal class ActionAccumulator
 #if DEBUG
             Console.WriteLine(e);
 #endif
-            CrashHelper.SendExceptionInfoAsync(e);
+            CrashHelper.SendExceptionInfo(e);
             throw;
         }
 

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

@@ -23,6 +23,7 @@ using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -90,7 +91,8 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
             return;
-        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id, Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id,
+            Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
@@ -225,6 +227,38 @@ internal class DocumentOperationsModule : IDocumentOperations
         }
     }
 
+    public void ImportMember(Guid layerId, IDocument sourceDocument)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        if (sourceDocument == this.Document)
+        {
+            DuplicateMember(layerId);
+            return;
+        }
+
+        if (!sourceDocument.StructureHelper.TryFindNode(layerId, out IStructureMemberHandler? member))
+            return;
+
+        if (member is ILayerHandler layer)
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(layer.Id), newGuid),
+                new CreateAnimationDataFromLayer_Action(newGuid));
+        }
+        else if (member is IFolderHandler folder)
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
+                new SetSelectedMember_PassthroughAction(newGuid));
+        }
+    }
+
     /// <summary>
     /// Delete the member with the <paramref name="guidValue"/>
     /// </summary>

+ 3 - 0
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -16,6 +16,8 @@ using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.Models.DocumentPassthroughActions;
 
 namespace PixiEditor.Models.Handlers;
@@ -69,4 +71,5 @@ internal interface IDocument : IHandler
 
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
     internal void InternalMarkSaveState(DocumentMarkType type);
+    public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode;
 }

+ 1 - 0
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -337,6 +337,7 @@ internal class AffectedAreasGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count; i++)
         {
             var member = path[i];
+            if(member == null) continue;
             if (!ImagePreviewAreas.ContainsKey(member.Id))
             {
                 ImagePreviewAreas[member.Id] = new AffectedArea(area);

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

@@ -599,6 +599,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
     }
 
+    public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode
+    {
+        return Internals.Tracker.Document.CreateNodePipe<T>(layerId);
+    }
+
     public OneOf<Error, Surface> TryRenderWholeImage(KeyFrameTime frameTime, VecI renderSize)
     {
         try
@@ -1148,6 +1153,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public void Dispose()
     {
+        Internals.ChangeController.TryStopActiveExecutor();
         Internals.Tracker.Dispose();
         Internals.Tracker.Document.Dispose();
     }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -77,7 +77,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         Dispatcher.UIThread.InvokeAsync(async () =>
         {
             Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
-            await ClipboardController.TryPasteFromClipboard(doc, pasteAsNewLayer);
+            await ClipboardController.TryPasteFromClipboard(doc, Owner.DocumentManagerSubViewModel, pasteAsNewLayer);
 
             doc.Operations.InvokeCustomAction(() =>
             {

+ 32 - 8
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -32,6 +32,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
     private bool? drawingWithRight;
     private bool startedWithEraser;
 
+    private Key? queuedTransientKey;
+
     public RelayCommand<MouseOnCanvasEventArgs> MouseMoveCommand { get; set; }
     public RelayCommand<MouseOnCanvasEventArgs> MouseDownCommand { get; set; }
     public RelayCommand PreviewMouseMiddleButtonCommand { get; set; }
@@ -125,19 +127,28 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.EventInlet.OnKeyDown(args.Key);
     }
 
-    private void HandleTransientKey(Key transientKey)
+    private bool HandleTransientKey(Key transientKey, bool executeOnlyImmediate)
     {
         if (ShortcutController.ShortcutExecutionBlocked)
         {
-            return;
+            return false;
         }
 
         var tool = GetTransientTool(transientKey);
 
-        if (tool is not null)
+        if (tool is null)
+        {
+            return false;
+        }
+
+        if (!tool.TransientImmediate && executeOnlyImmediate)
         {
-            Owner.ToolsSubViewModel.SetActiveTool(tool.ToolType, true);
+            return false;
         }
+
+        Owner.ToolsSubViewModel.SetActiveTool(tool.ToolType, true);
+
+        return true;
     }
 
     private static Command.ToolCommand? GetTransientTool(Key transientKey)
@@ -152,11 +163,18 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
     {
         if (argsModifiers == KeyModifiers.None && !isRepeat)
         {
-            HandleTransientKey(key);
+            if (!HandleTransientKey(key, true))
+            {
+                queuedTransientKey = key;
+            }
+        }
+        else
+        {
+            queuedTransientKey = null;
         }
 
         if (isRepeat && Owner.ShortcutController.LastCommands != null &&
-            Owner.ShortcutController.LastCommands.Any(x => x is Command.ToolCommand))
+            Owner.ShortcutController.LastCommands.Any(x => x is Command.ToolCommand cmd && cmd.Shortcut == new KeyCombination(key, argsModifiers)))
         {
             Owner.ToolsSubViewModel.HandleToolRepeatShortcutDown();
         }
@@ -186,6 +204,12 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
     private void OnMouseDown(object? sender, MouseOnCanvasEventArgs args)
     {
+        if (args.Button == MouseButton.Left && queuedTransientKey != null)
+        {
+            HandleTransientKey(queuedTransientKey.Value, false);
+            queuedTransientKey = null;
+        }
+
         if (drawingWithRight != null || args.Button is not (MouseButton.Left or MouseButton.Right))
             return;
 
@@ -199,8 +223,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
         drawingWithRight = args.Button == MouseButton.Right;
         activeDocument.EventInlet.OnCanvasLeftMouseButtonDown(args);
-        if(args.Handled) return;
-        
+        if (args.Handled) return;
+
         Owner.ToolsSubViewModel.UseToolEventInlet(args.PositionOnCanvas, args.Button);
 
         if (args.Button == MouseButton.Right)

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -99,7 +99,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
-        var selected = GetSelected();
+        var selected = doc.ExtractSelectedLayers(true).Concat(doc.SelectedMembers).Distinct().ToList();
         if (selected.Count > 0)
         {
             doc.Operations.DeleteStructureMembers(selected);

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

@@ -272,7 +272,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
         try
         {
-            ProcessHelper.RunAsAdmin(updaterPath, startAfterUpdate ? "--startOnSuccess" : null, false);
+            ProcessHelper.RunAsAdmin(updaterPath, startAfterUpdate ? "--startOnSuccess" : null);
             Shutdown();
         }
         catch (Win32Exception)

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -13,7 +13,7 @@ using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.V, Transient = Key.Space)]
+[Command.Tool(Key = Key.V)]
 internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 {
     private string defaultActionDisplay = "MOVE_TOOL_ACTION_DISPLAY";

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs

@@ -6,7 +6,7 @@ using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.H, Transient = Key.Space)]
+[Command.Tool(Key = Key.H, Transient = Key.Space, TransientImmediate = true)]
 internal class MoveViewportToolViewModel : ToolViewModel
 {
     public override string ToolNameLocalizationKey => "MOVE_VIEWPORT_TOOL";

+ 2 - 1
src/PixiEditor/Views/Layers/LayersManager.axaml.cs

@@ -22,6 +22,7 @@ internal partial class LayersManager : UserControl
 {
     public const string LayersDataName = "PixiEditor.LayersData";
     public DocumentViewModel ActiveDocument => DataContext is LayersDockViewModel vm ? vm.ActiveDocument : null;
+    public DocumentManagerViewModel ManagerViewModel => DataContext is LayersDockViewModel vm ? vm.DocumentManager : null;
     private readonly IBrush? highlightColor;
 
     public LayersManager()
@@ -180,7 +181,7 @@ internal partial class LayersManager : UserControl
             e.Handled = true;
         }
 
-        if (ClipboardController.TryPaste(ActiveDocument, new[] { (IDataObject)e.Data }, true))
+        if (ClipboardController.TryPaste(ActiveDocument, ManagerViewModel, new[] { (IDataObject)e.Data }, true))
         {
             e.Handled = true;
         }

+ 13 - 4
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -23,6 +23,7 @@ using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.Views.Nodes.Properties;
+using PixiEditor.Zoombox;
 using Point = Avalonia.Point;
 
 namespace PixiEditor.Views.Nodes;
@@ -366,10 +367,18 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         if (e.GetMouseButton(this) == MouseButton.Left)
         {
-            ClearSelection();
-            isSelecting = true;
-            selectionRectangle.IsVisible = true;
-            e.Handled = true;
+            if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+            {
+                ZoomMode = ZoomboxMode.Move;
+            }
+            else
+            {
+                ClearSelection();
+                isSelecting = true;
+                selectionRectangle.IsVisible = true;
+                ZoomMode = ZoomboxMode.Normal;
+                e.Handled = true;
+            }
         }
         else
         {

+ 8 - 4
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -183,14 +183,18 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public void KeyPressed(KeyEventArgs args)
     {
         if(SuppressEvents) return;
-        OnKeyPressed(args.Key, args.KeyModifiers, args.KeySymbol);
+        if (args.Handled) return;
+        OnKeyPressed(args);
+        if (args.Handled) return;
         KeyPressedOverlay?.Invoke(args.Key, args.KeyModifiers);
     }
 
     public void KeyReleased(KeyEventArgs keyEventArgs)
     {
         if(SuppressEvents) return;
-        OnKeyReleased(keyEventArgs.Key, keyEventArgs.KeyModifiers);
+        if (keyEventArgs.Handled) return;
+        OnKeyReleased(keyEventArgs);
+        if (keyEventArgs.Handled) return;
         KeyReleasedOverlay?.Invoke(keyEventArgs.Key, keyEventArgs.KeyModifiers);
     }
 
@@ -314,11 +318,11 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     {
     }
     
-    protected virtual void OnKeyPressed(Key key, KeyModifiers keyModifiers, string? keySymbol)
+    protected virtual void OnKeyPressed(KeyEventArgs args)
     {
     }
     
-    protected virtual void OnKeyReleased(Key key, KeyModifiers keyModifiers)
+    protected virtual void OnKeyReleased(KeyEventArgs args)
     {
     }
 

+ 38 - 21
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -155,7 +155,8 @@ public class VectorPathOverlay : Overlay
             }
         }
 
-        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(transformHandle.Size.X / ZoomScale, transformHandle.Size.Y / ZoomScale);
+        transformHandle.Position = Path.TightBounds.BottomRight +
+                                   new VecD(transformHandle.Size.X / ZoomScale, transformHandle.Size.Y / ZoomScale);
         transformHandle.Draw(context);
     }
 
@@ -361,11 +362,12 @@ public class VectorPathOverlay : Overlay
         isDragging = true;
     }
 
-    protected override void OnKeyPressed(Key key, KeyModifiers keyModifiers, string? symbol)
+    protected override void OnKeyPressed(KeyEventArgs args)
     {
-        if (key == Key.Delete)
+        if (args.Key == Key.Delete)
         {
             DeleteSelectedPoints();
+            args.Handled = true;
         }
     }
 
@@ -376,7 +378,7 @@ public class VectorPathOverlay : Overlay
         {
             return;
         }
-        
+
         int handleAdjustment = 0;
 
         foreach (var handle in selectedHandles)
@@ -393,10 +395,10 @@ public class VectorPathOverlay : Overlay
             {
                 subShapeContainingIndex.RemovePoint(localIndex);
             }
-            
+
             handleAdjustment++;
         }
-        
+
         Path = editableVectorPath.ToVectorPath();
         AddToUndoCommand.Execute(Path);
     }
@@ -556,7 +558,7 @@ public class VectorPathOverlay : Overlay
             Path = editableVectorPath.ToVectorPath();
             SelectAnchor(anchorHandles.Last());
         }
-        
+
         return true;
     }
 
@@ -615,28 +617,43 @@ public class VectorPathOverlay : Overlay
             return;
         }
 
-        var index = anchorHandles.IndexOf(handle);
+        bool isDraggingControlPoints = args.Modifiers.HasFlag(KeyModifiers.Control);
+
+        var selectedAnchors = isDraggingControlPoints
+            ? new List<AnchorHandle>() { handle }
+            : anchorHandles.Where(h => h.IsSelected).OrderByDescending(h => h == handle).ToList();
 
         var targetPos = ApplySnapping(args.Point);
+        VecF delta = VecF.Zero;
 
-        SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
+        for (int i = 0; i < selectedAnchors.Count; i++)
+        {
+            var anchor = selectedAnchors[i];
+            var index = anchorHandles.IndexOf(anchor);
 
-        int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
+            SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
 
-        bool isDraggingControlPoints = args.Modifiers.HasFlag(KeyModifiers.Control);
+            int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
-        if (isDraggingControlPoints)
-        {
-            var newPath = ConvertTouchingVerbsToCubic(handle);
+            if (isDraggingControlPoints)
+            {
+                var newPath = ConvertTouchingVerbsToCubic(anchor);
 
-            subShapeContainingIndex = newPath.GetSubShapeContainingIndex(index);
-            localIndex = newPath.GetSubShapePointIndex(index, subShapeContainingIndex);
+                subShapeContainingIndex = newPath.GetSubShapeContainingIndex(index);
+                localIndex = newPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
-            HandleContinousCubicDrag(targetPos, handle, subShapeContainingIndex, localIndex, true);
-        }
-        else
-        {
-            subShapeContainingIndex.SetPointPosition(localIndex, (VecF)targetPos, true);
+                HandleContinousCubicDrag(targetPos, anchor, subShapeContainingIndex, localIndex, true);
+            }
+            else
+            {
+                VecF pos = (VecF)subShapeContainingIndex.Points[localIndex].Position;
+                if (i == 0)
+                {
+                    delta = (VecF)targetPos - pos;
+                }
+                VecF newPos = i == 0 ? (VecF)targetPos : pos + delta;
+                subShapeContainingIndex.SetPointPosition(localIndex, newPos, true);
+            }
         }
 
         Path = editableVectorPath.ToVectorPath();

+ 5 - 2
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -443,12 +443,15 @@ internal class TextOverlay : Overlay
         return indexOfClosest;
     }
 
-    protected override void OnKeyPressed(Key key, KeyModifiers keyModifiers, string? keySymbol)
+    protected override void OnKeyPressed(KeyEventArgs args)
     {
         if (!IsEditing) return;
 
         ShortcutController.BlockShortcutExecution(nameof(TextOverlay));
 
+        var key = args.Key;
+        var keyModifiers = args.KeyModifiers;
+
         if (IsUndoRedoShortcut(key, keyModifiers))
         {
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
@@ -461,7 +464,7 @@ internal class TextOverlay : Overlay
             return;
         }
 
-        InsertChar(key, keySymbol);
+        InsertChar(key, args.KeySymbol);
     }
 
     private bool IsUndoRedoShortcut(Key key, KeyModifiers keyModifiers)

+ 1 - 0
src/PixiEditor/Views/Rendering/Scene.cs

@@ -472,6 +472,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
             foreach (Overlay overlay in AllOverlays)
             {
                 if (!overlay.IsVisible) continue;
+
                 overlay.KeyPressed(e);
             }
         }

+ 5 - 0
tests/PixiEditor.Backend.Tests/MockDocument.cs

@@ -84,4 +84,9 @@ public class MockDocument : IReadOnlyDocument
     {
         throw new NotImplementedException();
     }
+
+    public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode
+    {
+        throw new NotImplementedException();
+    }
 }