2
0
Эх сурвалжийг харах

Merge pull request #631 from PixiEditor/fixes-29-08

Batch of fixes
Krzysztof Krysiński 11 сар өмнө
parent
commit
5ff660d1f6

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

@@ -192,14 +192,6 @@
       "Parameters": []
     },
     "RemoveLayer": {
-      "Command": "PixiEditor.Layer.DeleteSelected",
-      "DefaultShortcut": {
-        "key": "None",
-        "modifiers": null
-      },
-      "Parameters": []
-    },
-    "": {
       "Command": "PixiEditor.Layer.DeleteAllSelected",
       "DefaultShortcut": {
         "key": "None",

+ 12 - 1
src/PixiEditor/Models/Clipboard/DataImage.cs

@@ -5,7 +5,18 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.Clipboard;
 
-public record struct DataImage(string? name, Surface image, VecI position)
+public record struct DataImage
 {
+    public string? Name { get; set; }
+    public Surface Image { get; set; }
+    public VecI Position { get; set; }
+    
     public DataImage(Surface image, VecI position) : this(null, image, position) { }
+
+    public DataImage(string? name, Surface image, VecI position)
+    {
+        Name = name;
+        Image = image;
+        Position = position;
+    }
 }

+ 17 - 5
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -102,7 +102,7 @@ internal static class ClipboardController
         if (images.Count == 1)
         {
             var dataImage = images[0];
-            var position = dataImage.position;
+            var position = dataImage.Position;
 
             if (document.SizeBindable.X < position.X || document.SizeBindable.Y < position.Y)
             {
@@ -119,11 +119,11 @@ internal static class ClipboardController
                 }
 
                 document.Operations.SetSelectedMember(guid.Value);
-                document.Operations.PasteImageWithTransform(dataImage.image, position, guid.Value, false);
+                document.Operations.PasteImageWithTransform(dataImage.Image, position, guid.Value, false);
             }
             else
             {
-                document.Operations.PasteImageWithTransform(dataImage.image, position);
+                document.Operations.PasteImageWithTransform(dataImage.Image, position);
             }
 
             return true;
@@ -185,15 +185,27 @@ internal static class ClipboardController
 
         if (data == null)
             return surfaces;
+        
+        VecI pos = VecI.NegativeOne;
 
         foreach (var dataObject in data)
         {
             if (TryExtractSingleImage(dataObject, out var singleImage))
             {
-                surfaces.Add(new DataImage(singleImage, dataObject.GetVecI(PositionFormat)));
+                surfaces.Add(new DataImage(singleImage, dataObject.Contains(PositionFormat) ? dataObject.GetVecI(PositionFormat) : pos));
                 continue;
             }
 
+            if (dataObject.Contains(PositionFormat))
+            {
+                pos = dataObject.GetVecI(PositionFormat);
+                for (var i = 0; i < surfaces.Count; i++)
+                {
+                    var surface = surfaces[i];
+                    surfaces[i] = surface with { Position = pos };
+                }
+            }
+
             var paths = dataObject.GetFileDropList().Select(x => x.Path.LocalPath).ToList();
             if (paths != null && dataObject.TryGetRawTextPath(out string? textPath))
             {
@@ -360,7 +372,7 @@ internal static class ClipboardController
                     return false;
                 }
 
-                result = imgs[0].image;
+                result = imgs[0].Image;
                 return true;
             }
             else

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

@@ -3,6 +3,7 @@ using System.Linq;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.DrawingApi.Core.Bridge;

+ 142 - 56
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,15 +1,11 @@
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.IO;
-using System.Linq;
-using ChunkyImageLib;
+using System.Collections.Immutable;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Models.Clipboard;
@@ -96,13 +92,15 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <summary>
     /// Adds a new viewport or updates a existing one
     /// </summary>
-    public void AddOrUpdateViewport(ViewportInfo info) => Internals.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(info));
+    public void AddOrUpdateViewport(ViewportInfo info) =>
+        Internals.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(info));
 
     /// <summary>
     /// Deletes the viewport with the <paramref name="viewportGuid"/>
     /// </summary>
     /// <param name="viewportGuid">The Guid of the viewport to remove</param>
-    public void RemoveViewport(Guid viewportGuid) => Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
+    public void RemoveViewport(Guid viewportGuid) =>
+        Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
 
     /// <summary>
     /// Delete the whole undo stack
@@ -126,7 +124,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         RectI maxSize = new RectI(VecI.Zero, Document.SizeBindable);
         foreach (var imageWithName in images)
         {
-            maxSize = maxSize.Union(new RectI(imageWithName.position, imageWithName.image.Size));
+            maxSize = maxSize.Union(new RectI(imageWithName.Position, imageWithName.Image.Size));
         }
 
         if (maxSize.Size != Document.SizeBindable)
@@ -134,10 +132,13 @@ internal class DocumentOperationsModule : IDocumentOperations
 
         foreach (var imageWithName in images)
         {
-            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, Path.GetFileName(imageWithName.name));
-            DrawImage(imageWithName.image, new ShapeCorners(new RectD(imageWithName.position, imageWithName.image.Size)),
+            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer,
+                Path.GetFileName(imageWithName.Name));
+            DrawImage(imageWithName.Image,
+                new ShapeCorners(new RectD(imageWithName.Position, imageWithName.Image.Size)),
                 layerGuid, true, false, frame, false);
         }
+
         Internals.ActionAccumulator.AddFinishedActions();
     }
 
@@ -180,11 +181,28 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Deletes all members with the <paramref name="guids"/>
     /// </summary>
     /// <param name="guids">The Guids of the layers to delete</param>
-    public void DeleteStructureMembers(IReadOnlyList<Guid> guids)
+    public void DeleteStructureMembers(IReadOnlyList<Guid> guids, bool selectNext = true)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        Internals.ActionAccumulator.AddFinishedActions(guids.Select(static guid => new DeleteStructureMember_Action(guid)).ToArray());
+
+        Guid closestMember = FindClosestMember(guids);
+        
+        IAction[] actions = new IAction[guids.Count + (selectNext ? 1 : 0)];
+        for (int i = 0; i < guids.Count; i++)
+        {
+            actions[i] = new DeleteStructureMember_Action(guids[i]);
+        }
+
+        if (selectNext)
+        {
+            if (closestMember != Guid.Empty)
+            {
+                actions[^1] = new SetSelectedMember_PassthroughAction(closestMember);
+            }
+        }
+
+        Internals.ActionAccumulator.AddFinishedActions(actions);
     }
 
     /// <summary>
@@ -194,7 +212,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="anchor">Where the existing content should be put</param>
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     {
-        if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
+        if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 ||
+            newSize.Y < 1)
             return;
 
         if (Document.ReferenceLayerHandler.ReferenceBitmap is not null)
@@ -208,7 +227,8 @@ internal class DocumentOperationsModule : IDocumentOperations
                 BottomLeft = curShape.BottomLeft + offset,
                 BottomRight = curShape.BottomRight + offset,
             };
-            Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners), new EndTransformReferenceLayer_Action());
+            Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners),
+                new EndTransformReferenceLayer_Action());
         }
 
         Internals.ActionAccumulator.AddFinishedActions(new ResizeCanvas_Action(newSize, anchor));
@@ -221,7 +241,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="resampling">The resampling method to use</param>
     public void ResizeImage(VecI newSize, ResamplingMethod resampling)
     {
-        if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
+        if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 ||
+            newSize.Y < 1)
             return;
 
         if (Document.ReferenceLayerHandler.ReferenceBitmap is not null)
@@ -235,7 +256,8 @@ internal class DocumentOperationsModule : IDocumentOperations
                 BottomLeft = curShape.BottomLeft.Multiply(scale),
                 BottomRight = curShape.BottomRight.Multiply(scale),
             };
-            Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners), new EndTransformReferenceLayer_Action());
+            Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners),
+                new EndTransformReferenceLayer_Action());
         }
 
         Internals.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(newSize, resampling));
@@ -250,17 +272,18 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
             return;
-        
-        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor(), frame));
+
+        Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor(),
+            frame));
         ReplaceInPalette(oldColor, newColor);
     }
 
     private void ReplaceInPalette(PaletteColor oldColor, PaletteColor newColor)
     {
         int indexOfOldColor = Document.Palette.IndexOf(oldColor);
-        if(indexOfOldColor == -1)
+        if (indexOfOldColor == -1)
             return;
-        
+
         Document.Palette.RemoveAt(indexOfOldColor);
         Document.Palette.Insert(indexOfOldColor, newColor);
     }
@@ -286,7 +309,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.Id));
     }
-    
+
     /// <summary>
     /// Applies the mask to the image
     /// </summary>
@@ -294,39 +317,49 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        
-        Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.Id, frame), new DeleteStructureMemberMask_Action(member.Id));
+
+        Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.Id, frame),
+            new DeleteStructureMemberMask_Action(member.Id));
     }
 
     /// <summary>
     /// Sets the selected structure memeber
     /// </summary>
     /// <param name="memberGuid">The Guid of the member to select</param>
-    public void SetSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
+    public void SetSelectedMember(Guid memberGuid) =>
+        Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
 
     /// <summary>
     /// Adds a member to the soft selection
     /// </summary>
     /// <param name="memberGuid">The Guid of the member to add</param>
-    public void AddSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));
+    public void AddSoftSelectedMember(Guid memberGuid) =>
+        Internals.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));
 
     /// <summary>
     /// Removes a member from the soft selection
     /// </summary>
     /// <param name="memberGuid">The Guid of the member to remove</param>
-    public void RemoveSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new RemoveSoftSelectedMember_PassthroughAction(memberGuid));
+    public void RemoveSoftSelectedMember(Guid memberGuid) =>
+        Internals.ActionAccumulator.AddActions(new RemoveSoftSelectedMember_PassthroughAction(memberGuid));
 
     /// <summary>
     /// Clears the soft selection
     /// </summary>
-    public void ClearSoftSelectedMembers() => Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
-    
-    public void SetActiveFrame(int newFrame) => Internals.ActionAccumulator.AddActions(new SetActiveFrame_PassthroughAction(newFrame));
-    public void AddSelectedKeyFrame(Guid keyFrameGuid) => Internals.ActionAccumulator.AddActions(new AddSelectedKeyFrame_PassthroughAction(keyFrameGuid));
-    
-    public void RemoveSelectedKeyFrame(Guid keyFrameGuid) => Internals.ActionAccumulator.AddActions(new RemoveSelectedKeyFrame_PassthroughAction(keyFrameGuid));
-    
-    public void ClearSelectedKeyFrames() => Internals.ActionAccumulator.AddActions(new ClearSelectedKeyFrames_PassthroughAction());
+    public void ClearSoftSelectedMembers() =>
+        Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
+
+    public void SetActiveFrame(int newFrame) =>
+        Internals.ActionAccumulator.AddActions(new SetActiveFrame_PassthroughAction(newFrame));
+
+    public void AddSelectedKeyFrame(Guid keyFrameGuid) =>
+        Internals.ActionAccumulator.AddActions(new AddSelectedKeyFrame_PassthroughAction(keyFrameGuid));
+
+    public void RemoveSelectedKeyFrame(Guid keyFrameGuid) =>
+        Internals.ActionAccumulator.AddActions(new RemoveSelectedKeyFrame_PassthroughAction(keyFrameGuid));
+
+    public void ClearSelectedKeyFrames() =>
+        Internals.ActionAccumulator.AddActions(new ClearSelectedKeyFrames_PassthroughAction());
 
     /// <summary>
     /// Undo last change
@@ -338,6 +371,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ChangeController.MidChangeUndoInlet();
             return;
         }
+
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
 
@@ -351,6 +385,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ChangeController.MidChangeRedoInlet();
             return;
         }
+
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 
@@ -359,7 +394,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         if (Internals.ChangeController.IsChangeActive)
         {
             Internals.ChangeController.SelectedObjectNudgedInlet(distance);
-        }    
+        }
     }
 
     /// <summary>
@@ -368,7 +403,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="memberToMove">The member to move</param>
     /// <param name="memberToMoveIntoOrNextTo">The target member</param>
     /// <param name="placement">Where to place the <paramref name="memberToMove"/></param>
-    public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
+    public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo,
+        StructureMemberPlacement placement)
     {
         if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
             return;
@@ -384,7 +420,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
 
         IStructureMemberHandler? node = Document.StructureHelper.FindNode<IStructureMemberHandler>(members[0]);
-        
+
         if (node is null)
             return;
 
@@ -397,20 +433,21 @@ internal class DocumentOperationsModule : IDocumentOperations
                 parent = traversedNode;
                 return false;
             }
-            
+
             return true;
         });
-        
+
         if (parent is null)
             return;
-        
+
         Guid newGuid = Guid.NewGuid();
 
         //make a new layer, put combined image onto it, delete layers that were merged
         Internals.ActionAccumulator.AddActions(
             new CreateStructureMember_Action(parent.Id, newGuid, StructureMemberType.Layer),
             new StructureMemberName_Action(newGuid, node.NodeNameBindable),
-            new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid, Document.AnimationHandler.ActiveFrameBindable));
+            new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid,
+                Document.AnimationHandler.ActiveFrameBindable));
         foreach (var member in members)
             Internals.ActionAccumulator.AddActions(new DeleteStructureMember_Action(member));
         Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
@@ -460,7 +497,8 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ChangeController.TryStopActiveExecutor();
     }
 
-    public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, int frame) =>
+    public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc,
+        bool drawOnMask, int frame) =>
         DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, frame, true);
 
     /// <summary>
@@ -472,7 +510,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="ignoreClipSymmetriesEtc">Ignore selection clipping and symmetry (See DrawingChangeHelper.ApplyClipsSymmetriesEtc of UpdateableDocument)</param>
     /// <param name="drawOnMask">Draw on the mask or on the image</param>
     /// <param name="finish">Is this a finished action</param>
-    private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, int atFrame, bool finish)
+    private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc,
+        bool drawOnMask, int atFrame, bool finish)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
@@ -490,7 +529,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action(Document.AnimationHandler.ActiveFrameBindable));
+        Internals.ActionAccumulator.AddFinishedActions(
+            new ClipCanvas_Action(Document.AnimationHandler.ActiveFrameBindable));
     }
 
     /// <summary>
@@ -505,7 +545,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, frame, membersToFlip));
     }
 
@@ -523,10 +563,10 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate, frame));
     }
-    
+
     /// <summary>
     /// Puts the content of the image in the middle of the canvas
     /// </summary>
@@ -547,9 +587,11 @@ internal class DocumentOperationsModule : IDocumentOperations
         if (Internals.ChangeController.IsChangeActive)
             return;
 
-        RectD referenceImageRect = new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, imageSize));
+        RectD referenceImageRect =
+            new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, imageSize));
         ShapeCorners corners = new ShapeCorners(referenceImageRect);
-        Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imageBgra8888Bytes, imageSize));
+        Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imageBgra8888Bytes,
+            imageSize));
     }
 
     /// <summary>
@@ -582,14 +624,14 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
 
 
-
-        VecD size = new(Document.ReferenceLayerHandler.ReferenceBitmap.Size.X, Document.ReferenceLayerHandler.ReferenceBitmap.Size.Y);
+        VecD size = new(Document.ReferenceLayerHandler.ReferenceBitmap.Size.X,
+            Document.ReferenceLayerHandler.ReferenceBitmap.Size.Y);
         RectD referenceImageRect = new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, size));
         ShapeCorners corners = new ShapeCorners(referenceImageRect);
         Internals.ActionAccumulator.AddFinishedActions(
             new TransformReferenceLayer_Action(corners),
             new EndTransformReferenceLayer_Action()
-            );
+        );
     }
 
     public void SelectionToMask(SelectionMode mode, int frame)
@@ -601,7 +643,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         {
             Internals.ActionAccumulator.AddActions(new CreateStructureMemberMask_Action(member.Id));
         }
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new SelectionToMask_Action(member.Id, mode, frame));
     }
 
@@ -622,13 +664,57 @@ internal class DocumentOperationsModule : IDocumentOperations
             Internals.ActionAccumulator.AddFinishedActions();
         }
     }
-    
+
     public void InvertSelection()
     {
         var selection = Document.SelectionPathBindable;
         var inverse = new VectorPath();
         inverse.AddRect(new RectI(new(0, 0), Document.SizeBindable));
 
-        Internals.ActionAccumulator.AddFinishedActions(new SetSelection_Action(inverse.Op(selection, VectorPathOp.Difference)));
+        Internals.ActionAccumulator.AddFinishedActions(
+            new SetSelection_Action(inverse.Op(selection, VectorPathOp.Difference)));
+    }
+
+    private Guid FindClosestMember(IReadOnlyList<Guid> guids)
+    {
+        IStructureMemberHandler? firstNode = Document.StructureHelper.FindNode<IStructureMemberHandler>(guids[0]);
+        if (firstNode is null)
+            return Guid.Empty;
+
+        INodeHandler? parent = null;
+
+        firstNode.TraverseForwards(traversedNode =>
+        {
+            if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
+            {
+                parent = traversedNode;
+                return false;
+            }
+
+            return true;
+        });
+
+        if (parent is null)
+        {
+            var lastNode = Document.StructureHelper.FindNode<IStructureMemberHandler>(guids[^1]);
+            if (lastNode is null)
+                return Guid.Empty;
+            
+            lastNode.TraverseBackwards(traversedNode =>
+            {
+                if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
+                {
+                    parent = traversedNode;
+                    return false;
+                }
+
+                return true;
+            });
+        }
+        
+        if (parent is null)
+            return Guid.Empty;
+
+        return parent.Id;
     }
 }

+ 4 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -3,6 +3,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Tools;
 using PixiEditor.Numerics;
@@ -51,7 +52,9 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         }
 
         ShapeCorners corners = new(new RectD(pos, image.Size));
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document.AnimationHandler.ActiveFrameBindable, default));
+        internals!.ActionAccumulator.AddActions(
+            new ClearSelection_Action(),
+            new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document.AnimationHandler.ActiveFrameBindable, default));
         document.TransformHandler.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, true);
 
         return ExecutionState.Success;

+ 1 - 1
src/PixiEditor/Models/Files/ImageFileType.cs

@@ -38,7 +38,7 @@ internal abstract class ImageFileType : IoFileType
                 return SaveResult.ConcurrencyError;
 
             finalSurface = maybeBitmap.AsT1;
-            if (maybeBitmap.AsT1.Size != exportConfig.ExportSize)
+            if (maybeBitmap.AsT1.Size != exportConfig.ExportSize && exportConfig.ExportSize.X > 0 && exportConfig.ExportSize.Y > 0)
             {
                 finalSurface = finalSurface.ResizeNearestNeighbor(exportConfig.ExportSize);
                 maybeBitmap.AsT1.Dispose();

+ 1 - 0
src/PixiEditor/Models/Handlers/IDocumentOperations.cs

@@ -4,4 +4,5 @@ public interface IDocumentOperations
 {
     public void DeleteStructureMember(Guid memberGuidValue);
     public void DuplicateLayer(Guid memberGuidValue);
+    public void AddSoftSelectedMember(Guid memberGuidValue);
 }

+ 11 - 13
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Input;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Helpers;
+using PixiEditor.Helpers.UI;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Numerics;
@@ -91,18 +92,6 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         set => SetProperty(ref coverWholeScreen, value);
     }
 
-    private ShapeCorners requestedCorners;
-    public ShapeCorners RequestedCorners
-    {
-        get => requestedCorners;
-        set
-        {
-            // The event must be raised even if the value hasn't changed, so I'm not using SetProperty
-            requestedCorners = value;
-            OnPropertyChanged(nameof(RequestedCorners));
-        }
-    }
-
     private ShapeCorners corners;
     public ShapeCorners Corners
     {
@@ -113,6 +102,13 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             TransformMoved?.Invoke(this, value);
         }
     }
+    
+    private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
+    public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
+    {
+        get => requestedCornersExecutor;
+        set => SetProperty(ref requestedCornersExecutor, value);
+    }
 
     private ICommand? actionCompletedCommand = null;
     public ICommand? ActionCompletedCommand
@@ -139,6 +135,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
             undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
         });
+
+        RequestCornersExecutor = new ExecutionTrigger<ShapeCorners>();
     }
 
     public bool Undo()
@@ -194,12 +192,12 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         CornerFreedom = TransformCornerFreedom.Scale;
         SideFreedom = TransformSideFreedom.Stretch;
         LockRotation = mode == DocumentTransformMode.Scale_NoRotate_NoShear_NoPerspective;
-        RequestedCorners = initPos;
         CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         ShowTransformControls = showApplyButton;
 
         undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Initial);
+        RequestCornersExecutor?.Execute(this, initPos);
     }
 
     public void KeyModifiersInlet(bool isShiftDown, bool isCtrlDown, bool isAltDown)

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

@@ -61,15 +61,15 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
         DataImage imageData = (data == null ? await ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(new [] { data })).First();
-        using var surface = imageData.image;
+        using var surface = imageData.Image;
 
-        var bitmap = imageData.image.ToWriteableBitmap();
+        var bitmap = imageData.Image.ToWriteableBitmap();
 
         byte[] pixels = bitmap.ExtractPixels();
 
         doc.Operations.ImportReferenceLayer(
             pixels.ToImmutableArray(),
-            imageData.image.Size);
+            imageData.Image.Size);
 
         if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {

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

@@ -169,13 +169,13 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
         foreach (var dataImage in images)
         {
-            if (File.Exists(dataImage.name))
+            if (File.Exists(dataImage.Name))
             {
-                OpenRegularImage(dataImage.image, null);
+                OpenRegularImage(dataImage.Image, null);
                 continue;
             }
 
-            OpenRegularImage(dataImage.image, null);
+            OpenRegularImage(dataImage.Image, null);
         }
     }
 

+ 4 - 15
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -55,20 +55,6 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return true;
     }
 
-    [Command.Basic("PixiEditor.Layer.DeleteSelected", "LAYER_DELETE_SELECTED", 
-        "LAYER_DELETE_SELECTED_DESCRIPTIVE", 
-        CanExecute = "PixiEditor.Layer.CanDeleteSelected", Key = Key.Delete, 
-        ShortcutContext = typeof(LayersDockViewModel),
-        Icon = PixiPerfectIcons.Trash, AnalyticsTrack = true)]
-    public void DeleteSelected()
-    {
-        var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
-        if (member is null)
-            return;
-
-        member.Document.Operations.DeleteStructureMember(member.Id);
-    }
-
     [Evaluator.CanExecute("PixiEditor.Layer.HasSelectedMembers")]
     public bool HasSelectedMembers()
     {
@@ -103,7 +89,8 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Layer.DeleteAllSelected", "LAYER_DELETE_ALL_SELECTED", "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasSelectedMembers", 
-        Icon = PixiPerfectIcons.Trash, AnalyticsTrack = true)]
+        Icon = PixiPerfectIcons.Trash, AnalyticsTrack = true, Key = Key.Delete, 
+        ShortcutContext = typeof(LayersDockViewModel))]
     public void DeleteAllSelected()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -111,7 +98,9 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
             return;
         var selected = GetSelected();
         if (selected.Count > 0)
+        {
             doc.Operations.DeleteStructureMembers(selected);
+        }
     }
 
     [Command.Basic("PixiEditor.Layer.NewFolder", "NEW_FOLDER", "CREATE_NEW_FOLDER", CanExecute = "PixiEditor.Layer.CanCreateNewMember",

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

@@ -44,7 +44,7 @@
                     Content="{DynamicResource icon-folder-plus}"
                     FlowDirection="LeftToRight"/>
                 <Button 
-                    Command="{xaml:Command PixiEditor.Layer.DeleteSelected}" Height="24" Width="24" uiExt:Translator.TooltipKey="LAYER_DELETE_ALL_SELECTED"
+                    Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}" Height="24" Width="24" uiExt:Translator.TooltipKey="LAYER_DELETE_ALL_SELECTED"
                     Cursor="Hand"
                     HorizontalAlignment="Stretch" Margin="0,0,5,0"
                     DockPanel.Dock="Left"

+ 49 - 40
src/PixiEditor/Views/Layers/LayersManager.axaml.cs

@@ -5,10 +5,12 @@ using Avalonia.Media;
 using Avalonia.Threading;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.Dock;
 using PixiEditor.ViewModels.Document;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Views.Layers;
 #nullable enable
@@ -16,6 +18,7 @@ internal partial class LayersManager : UserControl
 {
     public DocumentViewModel ActiveDocument => DataContext is LayersDockViewModel vm ? vm.ActiveDocument : null;
     private readonly IBrush? highlightColor;
+
     public LayersManager()
     {
         InitializeComponent();
@@ -43,7 +46,7 @@ internal partial class LayersManager : UserControl
             }
         }
     }
-    
+
     public void LayerControl_MouseMove(PointerEventArgs e)
     {
         if (e is null)
@@ -66,7 +69,7 @@ internal partial class LayersManager : UserControl
 
         e.Pointer.Capture(null);
     }
-    
+
     private void FolderControl_MouseDown(object sender, PointerPressedEventArgs e)
     {
         FolderControl control = (FolderControl)sender;
@@ -99,7 +102,7 @@ internal partial class LayersManager : UserControl
             Dispatcher.UIThread.InvokeAsync(() => DragDrop.DoDragDrop(e, data, DragDropEffects.Move));
         }
     }
-    
+
     private void FolderControl_MouseUp(object sender, PointerReleasedEventArgs e)
     {
         if (sender is not FolderControl folderControl)
@@ -121,7 +124,7 @@ internal partial class LayersManager : UserControl
     private void Grid_Drop(object sender, DragEventArgs e)
     {
         ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = null;
-        
+
         if (ActiveDocument == null)
         {
             return;
@@ -137,7 +140,7 @@ internal partial class LayersManager : UserControl
             e.Handled = true;
         }
 
-        if (ClipboardController.TryPaste(ActiveDocument, new [] { (IDataObject)e.Data }, true))
+        if (ClipboardController.TryPaste(ActiveDocument, new[] { (IDataObject)e.Data }, true))
         {
             e.Handled = true;
         }
@@ -149,7 +152,7 @@ internal partial class LayersManager : UserControl
         {
             return;
         }
-        
+
         var member = LayerControl.ExtractMemberGuid(e.Data);
 
         if (member == null)
@@ -166,7 +169,7 @@ internal partial class LayersManager : UserControl
         {
             e.DragEffects = DragDropEffects.Move;
         }
-        
+
         ((Border)sender).BorderBrush = highlightColor;
         e.Handled = true;
     }
@@ -177,32 +180,6 @@ internal partial class LayersManager : UserControl
         ((Border)sender).BorderBrush = Brushes.Transparent;
     }
 
-    private static int TraverseRange(Guid bound1, Guid bound2, FolderViewModel root, Action<StructureMemberViewModel> action, int matches = 0)
-    {
-        if (matches == 2)
-            return 2;
-        foreach (StructureMemberViewModel child in root.Children)
-        {
-            if (child is FolderViewModel innerFolder)
-            {
-                matches = TraverseRange(bound1, bound2, innerFolder, action, matches);
-            }
-            if (matches == 1)
-                action(child);
-            if (matches == 2)
-                return 2;
-            if (child.Id == bound1 || child.Id == bound2)
-            {
-                matches++;
-                if (matches == 1)
-                    action(child);
-                if (matches == 2)
-                    return 2;
-            }
-        }
-        return matches;
-    }
-
     private void HandleMouseDown(StructureMemberViewModel memberVM, PointerPressedEventArgs pointerPressedEventArgs)
     {
         if (ActiveDocument is null)
@@ -219,19 +196,20 @@ internal partial class LayersManager : UserControl
         }
         else if (pointerPressedEventArgs.KeyModifiers.HasFlag(KeyModifiers.Shift))
         {
-            // TODO: Implement this
-            /*if (ActiveDocument.SelectedStructureMember is null || ActiveDocument.SelectedStructureMember.GuidValue == memberVM.GuidValue)
+            if (ActiveDocument.SelectedStructureMember is null ||
+                ActiveDocument.SelectedStructureMember.Id == memberVM.Id)
                 return;
             ActiveDocument.Operations.ClearSoftSelectedMembers();
+
             TraverseRange(
-                ActiveDocument.SelectedStructureMember.GuidValue,
-                memberVM.GuidValue,
-                ActiveDocument.NodeGraph,
+                ActiveDocument.SelectedStructureMember.Id,
+                memberVM.Id,
+                ActiveDocument.NodeGraph.StructureTree.Members,
                 static member =>
                 {
                     if (member.Selection == StructureMemberSelectionType.None)
-                        member.Document.Operations.AddSoftSelectedMember(member.GuidValue);
-                });*/
+                        member.Document.Operations.AddSoftSelectedMember(member.Id);
+                });
         }
         else
         {
@@ -239,4 +217,35 @@ internal partial class LayersManager : UserControl
             ActiveDocument.Operations.ClearSoftSelectedMembers();
         }
     }
+
+    private static int TraverseRange(Guid bound1, Guid bound2, IEnumerable<IStructureMemberHandler> root,
+        Action<IStructureMemberHandler> action, int matches = 0)
+    {
+        if (matches == 2)
+            return 2;
+        
+        var reversed = root.Reverse();
+        foreach (var child in reversed)
+        {
+            if (child is FolderViewModel innerFolder)
+            {
+                matches = TraverseRange(bound1, bound2, innerFolder.Children, action, matches);
+            }
+
+            if (matches == 1)
+                action(child);
+            if (matches == 2)
+                return 2;
+            if (child.Id == bound1 || child.Id == bound2)
+            {
+                matches++;
+                if (matches == 1)
+                    action(child);
+                if (matches == 2)
+                    return 2;
+            }
+        }
+
+        return matches;
+    }
 }

+ 2 - 3
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -236,8 +236,7 @@ internal class ViewportOverlays
         Binding requestedCornersBinding = new()
         {
             Source = Viewport,
-            Path = "Document.TransformViewModel.RequestedCorners",
-            Mode = BindingMode.TwoWay
+            Path = "Document.TransformViewModel.RequestCornersExecutor",
         };
 
         Binding cornerFreedomBinding = new()
@@ -292,7 +291,7 @@ internal class ViewportOverlays
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
         transformOverlay.Bind(TransformOverlay.CornersProperty, cornersBinding);
-        transformOverlay.Bind(TransformOverlay.RequestedCornersProperty, requestedCornersBinding);
+        transformOverlay.Bind(TransformOverlay.RequestCornersExecutorProperty, requestedCornersBinding);
         transformOverlay.Bind(TransformOverlay.CornerFreedomProperty, cornerFreedomBinding);
         transformOverlay.Bind(TransformOverlay.SideFreedomProperty, sideFreedomBinding);
         transformOverlay.Bind(TransformOverlay.LockRotationProperty, lockRotationBinding);

+ 29 - 26
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -10,6 +10,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
+using PixiEditor.Helpers.UI;
 using PixiEditor.Numerics;
 using PixiEditor.Views.Overlays.Handles;
 using Point = Avalonia.Point;
@@ -18,15 +19,6 @@ namespace PixiEditor.Views.Overlays.TransformOverlay;
 #nullable enable
 internal class TransformOverlay : Overlay
 {
-    public static readonly StyledProperty<ShapeCorners> RequestedCornersProperty =
-        AvaloniaProperty.Register<TransformOverlay, ShapeCorners>(nameof(RequestedCorners), defaultValue: default(ShapeCorners));
-
-    public ShapeCorners RequestedCorners
-    {
-        get => GetValue(RequestedCornersProperty);
-        set => SetValue(RequestedCornersProperty, value);
-    }
-
     public static readonly StyledProperty<ShapeCorners> CornersProperty =
         AvaloniaProperty.Register<TransformOverlay, ShapeCorners>(nameof(Corners), defaultValue: default(ShapeCorners));
 
@@ -99,6 +91,15 @@ internal class TransformOverlay : Overlay
         set => SetValue(CoverWholeScreenProperty, value);
     }
 
+    public static readonly StyledProperty<ExecutionTrigger<ShapeCorners>> RequestCornersExecutorProperty = AvaloniaProperty.Register<TransformOverlay, ExecutionTrigger<ShapeCorners>>(
+        nameof(RequestCornersExecutor));
+
+    public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
+    {
+        get => GetValue(RequestCornersExecutorProperty);
+        set => SetValue(RequestCornersExecutorProperty, value);
+    }
+
     public static readonly StyledProperty<ICommand?> ActionCompletedProperty =
         AvaloniaProperty.Register<TransformOverlay, ICommand?>(nameof(ActionCompleted));
 
@@ -110,14 +111,13 @@ internal class TransformOverlay : Overlay
 
     static TransformOverlay()
     {
-        AffectsRender<TransformOverlay>(RequestedCornersProperty, CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty, LockRotationProperty, SnapToAnglesProperty, InternalStateProperty, ZoomboxAngleProperty, CoverWholeScreenProperty);
+        AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty, LockRotationProperty, SnapToAnglesProperty, InternalStateProperty, ZoomboxAngleProperty, CoverWholeScreenProperty);
 
-        RequestedCornersProperty.Changed.Subscribe(OnRequestedCorners);
+        RequestCornersExecutorProperty.Changed.Subscribe(OnCornersExecutorChanged);
     }
 
     private const int anchorSizeMultiplierForRotation = 15;
 
-    private bool isResettingRequestedCorners = false;
     private bool isMoving = false;
     private VecD mousePosOnStartMove = new();
     private VecD originOnStartMove = new();
@@ -616,23 +616,26 @@ internal class TransformOverlay : Overlay
         return null;
     }
 
-    private static void OnRequestedCorners(AvaloniaPropertyChangedEventArgs<ShapeCorners> args)
+    private void OnRequestedCorners(object sender, ShapeCorners corners)
     {
-        TransformOverlay overlay = (TransformOverlay)args.Sender;
-        if (overlay.isResettingRequestedCorners)
-            return;
-        overlay.isMoving = false;
-        overlay.isRotating = false;
-        overlay.Corners = args.NewValue.Value;
-        overlay.InternalState = new()
+        isMoving = false;
+        isRotating = false;
+        Corners = corners; 
+        InternalState = new()
         {
-            ProportionalAngle1 = (overlay.Corners.BottomRight - overlay.Corners.TopLeft).Angle,
-            ProportionalAngle2 = (overlay.Corners.TopRight - overlay.Corners.BottomLeft).Angle,
+            ProportionalAngle1 = (Corners.BottomRight - Corners.TopLeft).Angle,
+            ProportionalAngle2 = (Corners.TopRight - Corners.BottomLeft).Angle,
             OriginWasManuallyDragged = false,
-            Origin = TransformHelper.OriginFromCorners(overlay.Corners),
+            Origin = TransformHelper.OriginFromCorners(Corners),
         };
-        overlay.isResettingRequestedCorners = true;
-        //overlay.RequestedCorners = new ShapeCorners();
-        overlay.isResettingRequestedCorners = false;
+    }
+    
+    private static void OnCornersExecutorChanged(AvaloniaPropertyChangedEventArgs<ExecutionTrigger<ShapeCorners>> args)
+    {
+        TransformOverlay overlay = (TransformOverlay)args.Sender;
+        if (args.OldValue != null)
+            args.OldValue.Value.Triggered -= overlay.OnRequestedCorners;
+        if (args.NewValue != null)
+            args.NewValue.Value.Triggered += overlay.OnRequestedCorners;
     }
 }