Browse Source

Merge pull request #876 from PixiEditor/fixes/03.04.2025

Fixes/03.04.2025
Krzysztof Krysiński 5 tháng trước cách đây
mục cha
commit
d6bfc83111
29 tập tin đã thay đổi với 551 bổ sung154 xóa
  1. 1 1
      src/ColorPicker
  2. 1 1
      src/Directory.Build.props
  3. 1 1
      src/Drawie
  4. 1 1
      src/PixiDocks
  5. 19 1
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  6. 36 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  7. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  8. 6 1
      src/PixiEditor.Extensions.WasmRuntime/Api/FlyUiApi.cs
  9. 31 32
      src/PixiEditor.UI.Common/Controls/Shelf.axaml
  10. 78 0
      src/PixiEditor.UI.Common/Controls/Shelf.cs
  11. 71 0
      src/PixiEditor.UI.Common/Tweening/Tweener.cs
  12. 1 0
      src/PixiEditor/Models/Handlers/IDocument.cs
  13. 2 0
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  14. 2 1
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  15. 41 6
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  16. 25 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  17. 46 23
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  18. 2 10
      src/PixiEditor/Views/Animations/Timeline.cs
  19. 2 1
      src/PixiEditor/Views/Input/EditableTextBlock.axaml
  20. 8 11
      src/PixiEditor/Views/Input/EditableTextBlock.axaml.cs
  21. 3 2
      src/PixiEditor/Views/Layers/FolderControl.axaml
  22. 43 17
      src/PixiEditor/Views/Layers/FolderControl.axaml.cs
  23. 44 21
      src/PixiEditor/Views/Layers/LayerControl.axaml.cs
  24. 1 1
      src/PixiEditor/Views/Layers/LayersManager.axaml
  25. 68 12
      src/PixiEditor/Views/Layers/LayersManager.axaml.cs
  26. 1 1
      src/PixiEditor/Views/Layers/ReferenceLayer.axaml
  27. 10 6
      src/PixiEditor/Views/Layers/ReferenceLayer.axaml.cs
  28. 1 1
      tests/Directory.Build.props
  29. 5 0
      tests/PixiEditor.Backend.Tests/MockDocument.cs

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 172589cc045a0d9b530618897ce570db9056d8c8
+Subproject commit ef170d8a8ace0e58889e6586c1539b6a19ac55aa

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.2.5</AvaloniaVersion>
+		    <AvaloniaVersion>11.2.6</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit cfad269e5518054b43fba83a51d602dfa778c290
+Subproject commit 7b79ed74f73de883c3dbea383f0a410d52ebdba6

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit d4e89ae9349cb7329eb98866541893e1a8510094
+Subproject commit 1420290bb4485a34cede1a5018bb0dc3bf9b8b1f

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

@@ -94,7 +94,8 @@ internal class Document : IChangeable, IReadOnlyDocument
         if (layer is IReadOnlyImageNode imageNode)
         {
             var chunkyImage = imageNode.GetLayerImageAtFrame(frame);
-            using Surface chunkSurface = Surface.ForProcessing(chunkyImage.CommittedSize, chunkyImage.ProcessingColorSpace);
+            using Surface chunkSurface =
+                Surface.ForProcessing(chunkyImage.CommittedSize, chunkyImage.ProcessingColorSpace);
             chunkyImage.DrawCommittedRegionOn(
                 new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 ChunkResolution.Full,
@@ -144,6 +145,23 @@ internal class Document : IChangeable, IReadOnlyDocument
         ProcessingColorSpace = processingColorSpace;
     }
 
+    public List<IReadOnlyStructureNode> GetParents(Guid memberGuid)
+    {
+        var childNode = FindNode<StructureNode>(memberGuid);
+        if (childNode == null)
+            return new List<IReadOnlyStructureNode>();
+
+        List<IReadOnlyStructureNode> parents = new();
+        childNode.TraverseForwards((node, input) =>
+        {
+            if (node is IReadOnlyStructureNode parent && input is { InternalPropertyName: FolderNode.ContentInternalName })
+                parents.Add(parent);
+            return true;
+        });
+
+        return parents;
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>

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

@@ -90,7 +90,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Timeline))
         {
-            changed |= lastFrameTime.Frame != context.FrameTime.Frame || Math.Abs(lastFrameTime.NormalizedTime - context.FrameTime.NormalizedTime) > float.Epsilon;
+            changed |= lastFrameTime.Frame != context.FrameTime.Frame ||
+                       Math.Abs(lastFrameTime.NormalizedTime - context.FrameTime.NormalizedTime) > float.Epsilon;
         }
 
         int contentCacheHash = GetContentCacheHash();
@@ -268,6 +269,39 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+    public void TraverseForwards(Func<IReadOnlyNode, IInputProperty, IOutputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IInputProperty, IOutputProperty)>();
+        queueNodes.Enqueue((this, null, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2, node.Item3))
+            {
+                return;
+            }
+
+            foreach (var outputProperty in node.Item1.OutputProperties)
+            {
+                foreach (var connection in outputProperty.Connections)
+                {
+                    if (connection.Connection != null)
+                    {
+                        queueNodes.Enqueue((connection.Node, connection, outputProperty));
+                    }
+                }
+            }
+        }
+    }
+
     public void RemoveKeyFrame(Guid keyFrameId)
     {
         keyFrames.RemoveAll(x => x.KeyFrameGuid == keyFrameId);
@@ -365,7 +399,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected void RemoveInputProperty(InputProperty property)
     {
-        if(inputs.Remove(property))
+        if (inputs.Remove(property))
         {
             property.ConnectionChanged -= InvokeConnectionsChanged;
         }
@@ -527,7 +561,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
     internal virtual void DeserializeAdditionalData(IReadOnlyDocument target,
         IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
-
     }
 
     private void InvokeConnectionsChanged()

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

@@ -104,4 +104,5 @@ public interface IReadOnlyDocument : IDisposable
     public DocumentRenderer Renderer { get; }
     public ColorSpace ProcessingColorSpace { get; }
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
+    public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
 }

+ 6 - 1
src/PixiEditor.Extensions.WasmRuntime/Api/FlyUiApi.cs

@@ -8,6 +8,9 @@ internal class FlyUIApi : ApiGroupHandler
     [ApiFunction("subscribe_to_event")]
     public void SubscribeToEvent(int controlId, string eventName)
     {
+        if (!LayoutBuilder.ManagedElements.ContainsKey(controlId))
+            return;
+
         LayoutBuilder.ManagedElements[controlId].AddEvent(eventName, (args) =>
         {
             var action = Instance.GetAction<int, int>("raise_element_event");
@@ -21,7 +24,9 @@ internal class FlyUIApi : ApiGroupHandler
     [ApiFunction("state_changed")]
     public void StateChanged(int controlId, Span<byte> bodySpan)
     {
-        var element = LayoutBuilder.ManagedElements[controlId];
+        if (!LayoutBuilder.ManagedElements.TryGetValue(controlId, out var element))
+            return;
+
         var body = LayoutBuilder.Deserialize(bodySpan, DuplicateResolutionTactic.ReplaceRemoveChildren);
 
         Dispatcher.UIThread.InvokeAsync(() =>

+ 31 - 32
src/PixiEditor.UI.Common/Controls/Shelf.axaml

@@ -2,37 +2,40 @@
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls">
     <ControlTheme TargetType="controls:Shelf" x:Key="{x:Type controls:Shelf}">
+        <Setter Property="Transitions">
+            <Transitions>
+                <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3" />
+            </Transitions>
+        </Setter>
         <Setter Property="Template">
             <ControlTemplate>
                 <Border CornerRadius="{DynamicResource ControlCornerRadiusTop}"
                         Background="{DynamicResource ThemeBackgroundBrush1}">
-                    <Border.Transitions>
-                        <Transitions>
-                            <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3"/>
-                        </Transitions>
-                    </Border.Transitions>
                     <DockPanel LastChildFill="True">
-                        <CheckBox IsChecked="{TemplateBinding IsOpen, Mode=TwoWay}" Focusable="False" ZIndex="10" Name="PART_VisibilityCheckbox" Height="16" HorizontalAlignment="Right"
+                        <CheckBox IsChecked="{TemplateBinding IsOpen, Mode=TwoWay}" Focusable="False" ZIndex="10"
+                                  Name="PART_VisibilityCheckbox" Height="16" HorizontalAlignment="Right"
                                   DockPanel.Dock="Right" VerticalAlignment="Top" Margin="0,5,5,0">
-                        <CheckBox.Transitions>
-                            <Transitions>
-                                <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform" Duration="0:0:0.3"/>
-                            </Transitions>
-                        </CheckBox.Transitions>
-                    <CheckBox.Template>
-                        <ControlTemplate TargetType="{x:Type CheckBox}">
-                            <StackPanel Orientation="Horizontal" Focusable="False">
-                                <Image Focusable="False" Width="14" Cursor="Hand" Name="PART_CheckboxImage" Source="avares://PixiEditor.UI.Common/Assets/ChevronDown.png"/>
-                                <ContentPresenter Focusable="False"/>
-                            </StackPanel>
-                        </ControlTemplate>
-                    </CheckBox.Template>
-                </CheckBox>
+                            <CheckBox.Transitions>
+                                <Transitions>
+                                    <TransformOperationsTransition Easing="SineEaseInOut" Property="RenderTransform"
+                                                                   Duration="0:0:0.3" />
+                                </Transitions>
+                            </CheckBox.Transitions>
+                            <CheckBox.Template>
+                                <ControlTemplate TargetType="{x:Type CheckBox}">
+                                    <StackPanel Orientation="Horizontal" Focusable="False">
+                                        <Image Focusable="False" Width="14" Cursor="Hand" Name="PART_CheckboxImage"
+                                               Source="avares://PixiEditor.UI.Common/Assets/ChevronDown.png" />
+                                        <ContentPresenter Focusable="False" />
+                                    </StackPanel>
+                                </ControlTemplate>
+                            </CheckBox.Template>
+                        </CheckBox>
                         <ContentPresenter Content="{TemplateBinding Content}">
                             <ContentPresenter.Transitions>
                                 <Transitions>
-                                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.3"/>
-                                    <DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
+                                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.3" />
+                                    <DoubleTransition Property="Opacity" Duration="0:0:0.3" />
                                 </Transitions>
                             </ContentPresenter.Transitions>
                         </ContentPresenter>
@@ -42,21 +45,17 @@
         </Setter>
 
         <Style Selector="^ /template/ CheckBox">
-            <Setter Property="RenderTransformOrigin" Value="7, 7"/>
-            <Setter Property="RenderTransform" Value="rotate(0deg)"/>
+            <Setter Property="RenderTransformOrigin" Value="7, 7" />
+            <Setter Property="RenderTransform" Value="rotate(0deg)" />
         </Style>
 
         <Style Selector="^:not(:isOpen) /template/ CheckBox">
-            <Setter Property="RenderTransform" Value="rotate(180deg)"/>
-        </Style>
-
-        <Style Selector="^:not(:isOpen) /template/ Border">
-            <Setter Property="RenderTransform" Value="translateY(20px)"/>
+            <Setter Property="RenderTransform" Value="rotate(180deg)" />
         </Style>
 
         <Style Selector="^:not(:isOpen) /template/ ContentPresenter">
-            <Setter Property="Opacity" Value="0"/>
-            <Setter Property="IsHitTestVisible" Value="False"/>
+            <Setter Property="Opacity" Value="0" />
+            <Setter Property="IsHitTestVisible" Value="False" />
         </Style>
     </ControlTheme>
-</ResourceDictionary>
+</ResourceDictionary>

+ 78 - 0
src/PixiEditor.UI.Common/Controls/Shelf.cs

@@ -1,7 +1,11 @@
 using Avalonia;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
+using Avalonia.Interactivity;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
+using PixiEditor.UI.Common.Tweening;
 
 namespace PixiEditor.UI.Common.Controls;
 
@@ -11,12 +15,44 @@ public class Shelf : ContentControl
     public static readonly StyledProperty<bool> IsOpenProperty = AvaloniaProperty.Register<Shelf, bool>(
         nameof(IsOpen), true);
 
+    public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent =
+        RoutedEvent.Register<Shelf, RoutedEventArgs>(nameof(Opened), RoutingStrategies.Bubble);
+
+    public event EventHandler<RoutedEventArgs> Opened
+    {
+        add => AddHandler(OpenedEvent, value);
+        remove => RemoveHandler(OpenedEvent, value);
+    }
+
+    public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent =
+        RoutedEvent.Register<Shelf, RoutedEventArgs>(nameof(Closed), RoutingStrategies.Bubble);
+
+    public event EventHandler<RoutedEventArgs> Closed
+    {
+        add => AddHandler(ClosedEvent, value);
+        remove => RemoveHandler(ClosedEvent, value);
+    }
+
     public bool IsOpen
     {
         get => GetValue(IsOpenProperty);
         set => SetValue(IsOpenProperty, value);
     }
 
+    public static readonly StyledProperty<Control?> ControlToCollapseProperty =
+        AvaloniaProperty.Register<Shelf, Control?>(
+            nameof(ControlToCollapse));
+
+    public Control? ControlToCollapse
+    {
+        get => GetValue(ControlToCollapseProperty);
+        set => SetValue(ControlToCollapseProperty, value);
+    }
+
+    private double originalControlHeight;
+
+    private Tweener<double> tween;
+
     static Shelf()
     {
         IsOpenProperty.Changed.Subscribe(OnIsOpenChanged);
@@ -31,5 +67,47 @@ public class Shelf : ContentControl
     {
         var shelf = (Shelf)e.Sender;
         shelf.PseudoClasses.Set(":isOpen", (bool)e.NewValue);
+        if (e.NewValue is bool isOpen)
+        {
+            if (isOpen)
+            {
+                shelf.RaiseEvent(new RoutedEventArgs(OpenedEvent));
+                shelf.ExpandControl();
+            }
+            else
+            {
+                shelf.RaiseEvent(new RoutedEventArgs(ClosedEvent));
+                shelf.CollapseControl();
+            }
+        }
+    }
+
+    private void CollapseControl()
+    {
+        if (ControlToCollapse == null) ControlToCollapse = this;
+        originalControlHeight = ControlToCollapse.Bounds.Height;
+        ControlToCollapse.Height = originalControlHeight;
+
+        tween?.Stop();
+        tween = Tween.Double(
+            Control.HeightProperty,
+            ControlToCollapse,
+            originalControlHeight - 20,
+            300,
+            new SineEaseInOut()
+        ).Run();
+    }
+
+    private void ExpandControl()
+    {
+        if (ControlToCollapse == null) ControlToCollapse = this;
+        tween?.Stop();
+        tween = Tween.Double(
+            Control.HeightProperty,
+            ControlToCollapse,
+            originalControlHeight,
+            300,
+            new SineEaseInOut()
+        ).Run();
     }
 }

+ 71 - 0
src/PixiEditor.UI.Common/Tweening/Tweener.cs

@@ -0,0 +1,71 @@
+using Avalonia;
+using Avalonia.Animation.Easings;
+using Avalonia.Controls;
+using Avalonia.Threading;
+
+namespace PixiEditor.UI.Common.Tweening;
+
+public static class Tween
+{
+    public static Tweener<double> Double(AvaloniaProperty<double> property, Control control,
+        double endValue, double duration, IEasing easing = null)
+    {
+        return new Tweener<double>(property, control, endValue, duration,
+            (start, end, t) => start + (end - start) * easing?.Ease(t) ?? t);
+    }
+}
+
+public class Tweener<T>
+{
+    public AvaloniaProperty<T> Property { get; }
+
+    public T StartValue { get; private set; }
+    public T EndValue { get; }
+    public double DurationMs { get; }
+
+    public Func<T, T, double, T> Interpolator { get; }
+
+    public Control Control { get; set; }
+
+    private DispatcherTimer timer;
+
+    public Tweener(AvaloniaProperty<T> property, Control control, T endValue, double durationMs,
+        Func<T, T, double, T> interpolator)
+    {
+        Property = property;
+        EndValue = endValue;
+        DurationMs = durationMs;
+        Interpolator = interpolator;
+        Control = control;
+    }
+
+    public Tweener<T> Run()
+    {
+        timer = new DispatcherTimer(DispatcherPriority.Default) { Interval = TimeSpan.FromMilliseconds(16) };
+        DateTime startTime = DateTime.Now;
+        StartValue = (T)Control.GetValue(Property);
+        timer.Tick += (sender, args) =>
+        {
+            double elapsed = (DateTime.Now - startTime).TotalMilliseconds;
+            if (elapsed >= DurationMs)
+            {
+                timer.Stop();
+                Control.SetValue(Property, EndValue);
+                return;
+            }
+
+            double t = elapsed / DurationMs;
+            T value = Interpolator(StartValue, EndValue, t);
+            Control.SetValue(Property, value);
+        };
+
+        timer.Start();
+        return this;
+    }
+
+    public void Stop()
+    {
+        timer.Stop();
+        Control.SetValue(Property, EndValue);
+    }
+}

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

@@ -64,6 +64,7 @@ internal interface IDocument : IHandler
         bool includeCanvas, int frame, bool isTopMost, string? customOutput);
 
     public HashSet<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
+    public List<Guid> GetSelectedMembersInOrder(bool includeNested = false);
     public void UpdateSavedState();
 
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument;
+using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Layers;
 
 namespace PixiEditor.Models.Handlers;
@@ -13,4 +14,5 @@ internal interface IDocumentOperations
     public void ClearSoftSelectedMembers();
     public Guid? CreateStructureMember(Type type, ActionSource source, string? name = null);
     public void InvokeCustomAction(Action action, bool stopActiveExecutor = true);
+    public ChangeBlock StartChangeBlock();
 }

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

@@ -332,7 +332,8 @@ internal class AffectedAreasGatherer
 
     private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     {
-        var path = tracker.Document.FindMemberPath(memberGuid);
+        var path = tracker.Document.GetParents(memberGuid);
+        path.Insert(0, tracker.Document.FindMember(memberGuid));
         for (int i = ignoreSelf ? 1 : 0; i < path.Count; i++)
         {
             var member = path[i];

+ 41 - 6
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -140,7 +140,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
             return newCelGuid;
         }
-        
+
         return null;
     }
 
@@ -230,7 +230,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         {
             cachedFirstFrame = null;
             cachedLastFrame = null;
-            
+
             keyFrame.SetStartFrame(newStartFrame);
             keyFrame.SetDuration(newDuration);
             keyFrames.NotifyCollectionChanged();
@@ -274,10 +274,10 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
 
         SortByLayers();
-        
+
         cachedFirstFrame = null;
         cachedLastFrame = null;
-        
+
         OnPropertyChanged(nameof(FirstFrame));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
@@ -304,7 +304,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         });
 
         allCels.RemoveAll(x => x.Id == keyFrameId);
-        
+
         cachedFirstFrame = null;
         cachedLastFrame = null;
 
@@ -398,8 +398,12 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     public void SortByLayers()
     {
         var allLayers = Document.StructureHelper.GetAllLayers();
+
+        if (!OrderDifferent(keyFrames, allLayers)) return;
+
         var unsortedKeyFrames = keyFrames.ToList();
         var layerKeyFrames = new List<CelGroupViewModel>();
+
         foreach (var layer in allLayers)
         {
             var group = unsortedKeyFrames.FirstOrDefault(x =>
@@ -429,11 +433,42 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public int GetLastVisibleFrame()
     {
-        return keyFrames.Count > 0 ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable) : 0;
+        return keyFrames.Count > 0
+            ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable)
+            : 0;
     }
 
     public int GetVisibleFramesCount()
     {
         return GetLastVisibleFrame() - GetFirstVisibleFrame();
     }
+
+    private static bool OrderDifferent(IReadOnlyCollection<ICelHandler> keyFrames,
+        IReadOnlyCollection<ILayerHandler> allLayers)
+    {
+        List<ICelGroupHandler> groups = new List<ICelGroupHandler>();
+
+        foreach (var keyFrame in keyFrames)
+        {
+            if (keyFrame is ICelGroupHandler group)
+            {
+                groups.Add(group);
+            }
+        }
+
+        if (groups.Count != allLayers.Count)
+        {
+            return true;
+        }
+
+        for (int i = 0; i < groups.Count; i++)
+        {
+            if (groups[i].LayerGuid != allLayers.ElementAt(i).Id)
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }

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

@@ -945,6 +945,31 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return layerGuids;
     }
 
+
+    public List<Guid> GetSelectedMembersInOrder(bool includeNested = false)
+    {
+        var selectedMembers = GetSelectedMembers();
+        List<Guid> orderedMembers = new List<Guid>();
+        var allMembers = StructureHelper.TraverseAllMembers();
+
+        for (var index = 0; index < allMembers.Count; index++)
+        {
+            var member = allMembers[index];
+            if (selectedMembers.Contains(member.Id))
+            {
+                if (!includeNested)
+                {
+                    var parents = StructureHelper.GetParents(member.Id);
+                    if(parents.Any(x => selectedMembers.Contains(x.Id)))
+                        continue;
+                }
+                orderedMembers.Add(member.Id);
+            }
+        }
+
+        return orderedMembers;
+    }
+
     /// <summary>
     ///     Gets all selected layers extracted from selected folders.
     /// </summary>

+ 46 - 23
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -40,15 +40,6 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     {
     }
 
-    public void CreateFolderFromActiveLayers()
-    {
-    }
-
-    public bool CanCreateFolderFromSelected()
-    {
-        return false;
-    }
-
     [Evaluator.CanExecute("PixiEditor.Layer.CanDeleteSelected",
         nameof(DocumentManagerViewModel.ActiveDocument),
         nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember))]
@@ -122,7 +113,39 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
             return;
-        doc.Operations.CreateStructureMember(StructureMemberType.Folder);
+
+        using var block = doc.Operations.StartChangeBlock();
+        Guid? guid = doc.Operations.CreateStructureMember(StructureMemberType.Folder);
+        if(doc.SoftSelectedStructureMembers.Count == 0)
+            return;
+        var selectedInOrder = doc.GetSelectedMembersInOrder();
+        selectedInOrder.Reverse();
+        block.ExecuteQueuedActions();
+
+        if (guid is null)
+            return;
+
+        if (selectedInOrder.Count > 0)
+        {
+            Guid lastMovedMember = guid.Value;
+            StructureMemberPlacement placement = StructureMemberPlacement.Inside;
+
+            foreach (Guid memberGuid in selectedInOrder)
+            {
+                doc.Operations.MoveStructureMember(memberGuid, lastMovedMember, placement);
+                lastMovedMember = memberGuid;
+                if (placement == StructureMemberPlacement.Inside)
+                {
+                    placement = StructureMemberPlacement.Below;
+                }
+
+                block.ExecuteQueuedActions();
+            }
+
+            doc.Operations.ClearSoftSelectedMembers();
+        }
+
+        doc.Operations.SetSelectedMember(guid.Value);
     }
 
     [Command.Basic("PixiEditor.Layer.NewLayer", "NEW_LAYER", "CREATE_NEW_LAYER",
@@ -133,7 +156,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
             return;
-        
+
         doc.Operations.CreateStructureMember(StructureMemberType.Layer);
     }
 
@@ -141,7 +164,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
             return null;
-        
+
         return doc.Operations.CreateStructureMember(layerType, source, name);
     }
 
@@ -184,7 +207,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     public void OpacitySliderSet(double value)
     {
         var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        
+
         if (document?.SelectedStructureMember != null)
         {
             document.Operations.SetMemberOpacity(document.SelectedStructureMember.Id, (float)value);
@@ -201,7 +224,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         member.Document.Operations.DuplicateMember(member.Id);
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsLayer", 
+    [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsLayer",
         nameof(DocumentManagerViewModel.ActiveDocument), nameof(DocumentViewModel.SelectedStructureMember))]
     public bool SelectedMemberIsLayer(object property)
     {
@@ -216,7 +239,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
         return member is ILayerHandler && member is not IRasterLayerHandler;
     }
-    
+
     [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsVectorLayer",
         nameof(DocumentManagerViewModel.ActiveDocument), nameof(DocumentViewModel.SelectedStructureMember))]
     public bool SelectedMemberIsVectorLayer(object property)
@@ -341,14 +364,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         member.IsVisibleBindable = !member.IsVisibleBindable;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.HasMemberAbove", 
-        nameof(DocumentManagerViewModel.ActiveDocument), 
+    [Evaluator.CanExecute("PixiEditor.Layer.HasMemberAbove",
+        nameof(DocumentManagerViewModel.ActiveDocument),
         nameof(DocumentViewModel.SelectedStructureMember), nameof(DocumentViewModel.AllChangesSaved))]
     public bool HasMemberAbove(object property) => HasSelectedMember(true);
 
     [Evaluator.CanExecute("PixiEditor.Layer.HasMemberBelow",
-    nameof(DocumentManagerViewModel.ActiveDocument), 
-    nameof(DocumentViewModel.SelectedStructureMember), nameof(DocumentViewModel.AllChangesSaved))]
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentViewModel.SelectedStructureMember), nameof(DocumentViewModel.AllChangesSaved))]
     public bool HasMemberBelow(object property) => HasSelectedMember(false);
 
     [Command.Basic("PixiEditor.Layer.MoveSelectedMemberUpwards", "MOVE_MEMBER_UP", "MOVE_MEMBER_UP_DESCRIPTIVE",
@@ -410,7 +433,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     public bool ReferenceLayerExists() =>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.ReferenceLayerViewModel.ReferenceTexture is not null;
 
-    [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExist", 
+    [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExist",
         nameof(ViewModelMain.DocumentManagerSubViewModel),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceTexture))]
@@ -441,14 +464,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show(title: "ERROR", message: e.DisplayMessage);
             return;
         }
-        catch(ArgumentException e)
+        catch (ArgumentException e)
         {
             NoticeDialog.Show(title: "ERROR", message: e.Message);
             return;
         }
 
         byte[] bytes = bitmap.ToByteArray();
-        
+
         bitmap.Dispose();
 
         VecI size = new VecI(bitmap.Size.X, bitmap.Size.Y);
@@ -535,7 +558,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
         doc!.Operations.Rasterize(member.Id);
     }
-    
+
     [Command.Basic("PixiEditor.Layer.ConvertToCurve", "CONVERT_TO_CURVE", "CONVERT_TO_CURVE_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.SelectedMemberIsVectorLayer")]
     public void ConvertActiveLayerToCurve()

+ 2 - 10
src/PixiEditor/Views/Animations/Timeline.cs

@@ -457,18 +457,10 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
         double mouseXInViewport = e.GetPosition(_timelineKeyFramesScroll).X;
 
-        double currentFrameUnderMouse = towardsFrame;
+        double currentFrameUnderMouse = towardsFrame - 1;
         double newOffsetX = currentFrameUnderMouse * newScale - mouseXInViewport + MinLeftOffset;
 
-        if (towardsFrame * MarginMultiplier > KeyFrames.FrameCount)
-        {
-            // 50 is a magic number I found working ok, for bigger frames it is a bit too big, maybe find a better way to calculate this?
-            extendingElement.Margin = new Thickness(newOffsetX * 50, 0, 0, 0);
-        }
-        else
-        {
-            extendingElement.Margin = new Thickness(_timelineKeyFramesScroll.Viewport.Width, 0, 0, 0);
-        }
+        extendingElement.Margin = new Thickness(_timelineKeyFramesScroll.Viewport.Width + newOffsetX * 1.1f, 0, 0, 0);
 
         Dispatcher.UIThread.Post(
             () =>

+ 2 - 1
src/PixiEditor/Views/Input/EditableTextBlock.axaml

@@ -8,7 +8,8 @@
              mc:Ignorable="d" x:Name="etb"
              d:DesignHeight="60" d:DesignWidth="100" Focusable="True">
     <Grid>
-        <TextBlock Foreground="{Binding ElementName=etb, Path=Foreground}" PointerPressed="TextBlock_MouseDown"
+        <TextBlock Foreground="{Binding ElementName=etb, Path=Foreground}"
+                   DoubleTapped="OnDoubleTapped"
                    TextTrimming="CharacterEllipsis" Name="textBlock"
                    IsVisible="{Binding Path=TextBlockVisibility, ElementName=etb}"
                    Text="{Binding Path=Text, ElementName=etb, Mode=TwoWay}" />

+ 8 - 11
src/PixiEditor/Views/Input/EditableTextBlock.axaml.cs

@@ -36,7 +36,7 @@ internal partial class EditableTextBlock : UserControl
 
     public static readonly StyledProperty<SolidColorBrush> ForegroundProperty =
         AvaloniaProperty.Register<EditableTextBlock, SolidColorBrush>(
-        nameof(Foreground), new SolidColorBrush(Brushes.White.Color));
+            nameof(Foreground), new SolidColorBrush(Brushes.White.Color));
 
     public SolidColorBrush Foreground
     {
@@ -82,10 +82,10 @@ internal partial class EditableTextBlock : UserControl
 
         Dispatcher.UIThread.Post(
             () =>
-        {
-            textBox.Focus();
-            textBox.SelectAll();
-        }, DispatcherPriority.Input);
+            {
+                textBox.Focus();
+                textBox.SelectAll();
+            }, DispatcherPriority.Input);
     }
 
     public void DisableEditing()
@@ -105,13 +105,10 @@ internal partial class EditableTextBlock : UserControl
         }
     }
 
-    private void TextBlock_MouseDown(object? sender, PointerPressedEventArgs e)
+    private void OnDoubleTapped(object sender, TappedEventArgs e)
     {
-        if (e.ClickCount == 2)
-        {
-            EnableEditing();
-            e.Handled = true;
-        }
+        EnableEditing();
+        e.Handled = true;
     }
 
     private void TextBox_KeyDown(object sender, KeyEventArgs e)

+ 3 - 2
src/PixiEditor/Views/Layers/FolderControl.axaml

@@ -40,9 +40,9 @@
                 <RowDefinition Height="16" />
                 <RowDefinition Height="10" />
             </Grid.RowDefinitions>
-            <Grid DragDrop.AllowDrop="True" Name="TopDropGrid" Grid.Row="0" Grid.ColumnSpan="3"
+            <Grid DragDrop.AllowDrop="True" IsVisible="False" Name="TopDropGrid" Grid.Row="0" Grid.ColumnSpan="3"
                   Background="Transparent" Panel.ZIndex="3" />
-            <Grid IsVisible="True" Margin="20, 0, 0,0" x:Name="middleDropGrid" Grid.Row="1" DragDrop.AllowDrop="True"
+            <Grid IsVisible="False" Margin="20, 0, 0,0" x:Name="middleDropGrid" Grid.Row="1" DragDrop.AllowDrop="True"
                   Panel.ZIndex="2" Background="Transparent" />
             <Grid x:Name="centerGrid" Grid.Row="0" Grid.RowSpan="3" Background="Transparent">
                 <Grid.ColumnDefinitions>
@@ -140,6 +140,7 @@
             <Grid
                 Grid.Row="2"
                 Name="BottomDropGrid"
+                IsVisible="False"
                 DragDrop.AllowDrop="True"
                 Grid.ColumnSpan="2" Background="Transparent" />
         </Grid>

+ 43 - 17
src/PixiEditor/Views/Layers/FolderControl.axaml.cs

@@ -12,7 +12,6 @@ namespace PixiEditor.Views.Layers;
 #nullable enable
 internal partial class FolderControl : UserControl
 {
-
     public static readonly StyledProperty<FolderNodeViewModel> FolderProperty =
         AvaloniaProperty.Register<FolderControl, FolderNodeViewModel>(nameof(Folder));
 
@@ -22,8 +21,6 @@ internal partial class FolderControl : UserControl
         set => SetValue(FolderProperty, value);
     }
 
-    public static string? FolderControlDataName = typeof(FolderControl).FullName;
-
     public static readonly StyledProperty<LayersManager> ManagerProperty =
         AvaloniaProperty.Register<FolderControl, LayersManager>(nameof(Manager));
 
@@ -35,7 +32,7 @@ internal partial class FolderControl : UserControl
 
     private readonly IBrush? highlightColor;
 
-    
+
     private MouseUpdateController? mouseUpdateController;
 
     public FolderControl()
@@ -48,22 +45,27 @@ internal partial class FolderControl : UserControl
 
         Loaded += OnLoaded;
         Unloaded += OnUnloaded;
-        
+
         AddHandler(DragDrop.DragEnterEvent, FolderControl_DragEnter);
         AddHandler(DragDrop.DragLeaveEvent, FolderControl_DragLeave);
-        
+
         TopDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         TopDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         TopDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Top);
-        
+
         BottomDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         BottomDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         BottomDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Bottom);
-        
+
         middleDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_CenterEnter);
         middleDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_CenterLeave);
         middleDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Center);
-        
+
+        DisableDropPanels();
+    }
+
+    private void DisableDropPanels()
+    {
         TopDropGrid.IsVisible = false;
         middleDropGrid.IsVisible = false;
         BottomDropGrid.IsVisible = false;
@@ -101,30 +103,54 @@ internal partial class FolderControl : UserControl
         LayerControl.RemoveDragEffect(centerGrid);
     }
 
-    private void HandleDrop(IDataObject dataObj, StructureMemberPlacement placement)
+    private bool HandleDrop(IDataObject dataObj, StructureMemberPlacement placement)
     {
-        Guid? droppedMemberGuid = LayerControl.ExtractMemberGuid(dataObj);
-        if (droppedMemberGuid is null)
-            return;
-        Folder.Document.Operations.MoveStructureMember((Guid)droppedMemberGuid, Folder.Id, placement);
+        DisableDropPanels();
+        Guid[]? droppedGuids = LayerControl.ExtractMemberGuids(dataObj);
+        if (droppedGuids is null)
+            return false;
+
+        var document = Folder.Document;
+        if (placement is StructureMemberPlacement.Below or StructureMemberPlacement.BelowOutsideFolder or StructureMemberPlacement.Inside)
+        {
+            droppedGuids = droppedGuids.Reverse().ToArray();
+        }
+
+        using var block = document.Operations.StartChangeBlock();
+        Guid lastMovedMember = Folder.Id;
+
+        foreach (Guid memberGuid in droppedGuids)
+        {
+            document.Operations.MoveStructureMember(memberGuid, lastMovedMember,
+                placement);
+            lastMovedMember = memberGuid;
+            if (placement == StructureMemberPlacement.Inside)
+            {
+                placement = StructureMemberPlacement.Below;
+            }
+
+            block.ExecuteQueuedActions();
+        }
+
+        return true;
     }
 
     private void Grid_Drop_Top(object sender, DragEventArgs e)
     {
         LayerControl.RemoveDragEffect((Grid)sender);
-        HandleDrop(e.Data, StructureMemberPlacement.Above);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.Above);
     }
 
     private void Grid_Drop_Center(object sender, DragEventArgs e)
     {
         LayerControl.RemoveDragEffect(centerGrid);
-        HandleDrop(e.Data, StructureMemberPlacement.Inside);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.Inside);
     }
 
     private void Grid_Drop_Bottom(object sender, DragEventArgs e)
     {
         LayerControl.RemoveDragEffect((Grid)sender);
-        HandleDrop(e.Data, StructureMemberPlacement.Below);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.Below);
     }
 
     private void FolderControl_DragEnter(object sender, DragEventArgs e)

+ 44 - 21
src/PixiEditor/Views/Layers/LayerControl.axaml.cs

@@ -16,8 +16,6 @@ namespace PixiEditor.Views.Layers;
 #nullable enable
 internal partial class LayerControl : UserControl
 {
-    public static string? LayerControlDataName = typeof(LayerControl).FullName;
-
     public static readonly StyledProperty<ILayerHandler> LayerProperty =
         AvaloniaProperty.Register<LayerControl, ILayerHandler>(nameof(Layer));
 
@@ -87,7 +85,7 @@ internal partial class LayerControl : UserControl
         {
             highlightColor = value as IBrush;
         }
-     
+
         TopGrid.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         TopGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         TopGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Top);
@@ -98,9 +96,9 @@ internal partial class LayerControl : UserControl
         thirdDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         thirdDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Bottom);
     }
-    
+
     private void LayerControl_Unloaded(object? sender, RoutedEventArgs e)
-    { 
+    {
         mouseUpdateController?.Dispose();
     }
 
@@ -138,43 +136,68 @@ internal partial class LayerControl : UserControl
             RemoveDragEffect(item);
     }
 
-    public static Guid? ExtractMemberGuid(IDataObject droppedMemberDataObject)
+    public static Guid[]? ExtractMemberGuids(IDataObject droppedMemberDataObject)
     {
-        object droppedLayer = droppedMemberDataObject.Get(LayerControlDataName);
-        object droppedFolder = droppedMemberDataObject.Get(FolderControl.FolderControlDataName);
-        if (droppedLayer is LayerControl layer)
-            return layer.Layer.Id;
-        else if (droppedFolder is FolderControl folder)
-            return folder.Folder.Id;
+        object droppedLayer = droppedMemberDataObject.Get(LayersManager.LayersDataName);
+        if (droppedLayer is null)
+            return null;
+
+        if (droppedLayer is Guid droppedLayerGuid)
+            return new[] { droppedLayerGuid };
+
+        if (droppedLayer is Guid[] droppedLayerGuids)
+        {
+            return droppedLayerGuids;
+        }
+
         return null;
     }
 
-    private void HandleDrop(IDataObject dataObj, StructureMemberPlacement placement)
+    private bool HandleDrop(IDataObject dataObj, StructureMemberPlacement placement)
     {
         if (placement == StructureMemberPlacement.Inside)
-            return;
-        Guid? droppedMemberGuid = ExtractMemberGuid(dataObj);
-        if (droppedMemberGuid is null)
-            return;
-        Layer.Document.Operations.MoveStructureMember((Guid)droppedMemberGuid, Layer.Id, placement);
+            return false;
+        Guid[]? droppedMemberGuids = ExtractMemberGuids(dataObj);
+        if (droppedMemberGuids is null)
+            return false;
+        if (Layer is null)
+            return false;
+
+        if(placement is StructureMemberPlacement.Below or StructureMemberPlacement.BelowOutsideFolder)
+        {
+            droppedMemberGuids = droppedMemberGuids.Reverse().ToArray();
+        }
+
+        var document = Layer.Document;
+
+        using var block = Layer.Document.Operations.StartChangeBlock();
+        Guid lastMovedMember = Layer.Id;
+        foreach (Guid memberGuid in droppedMemberGuids)
+        {
+            document.Operations.MoveStructureMember(memberGuid, lastMovedMember, placement);
+            lastMovedMember = memberGuid;
+            block.ExecuteQueuedActions();
+        }
+
+        return true;
     }
 
     private void Grid_Drop_Top(object sender, DragEventArgs e)
     {
         RemoveDragEffect((Grid)sender);
-        HandleDrop(e.Data, StructureMemberPlacement.Above);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.Above);
     }
 
     private void Grid_Drop_Bottom(object sender, DragEventArgs e)
     {
         RemoveDragEffect((Grid)sender);
-        HandleDrop(e.Data, StructureMemberPlacement.Below);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.Below);
     }
 
     private void Grid_Drop_Below(object sender, DragEventArgs e)
     {
         RemoveDragEffect((Grid)sender);
-        HandleDrop(e.Data, StructureMemberPlacement.BelowOutsideFolder);
+        e.Handled = HandleDrop(e.Data, StructureMemberPlacement.BelowOutsideFolder);
     }
 
     private void RenameMenuItem_Click(object sender, RoutedEventArgs e)

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

@@ -21,7 +21,7 @@
     <UserControl.Resources>
         <sys:Double x:Key="OpacityMultiplier">100</sys:Double>
     </UserControl.Resources>
-    <Grid>
+    <Grid DragDrop.AllowDrop="True" Name="RootGrid">
         <Grid.RowDefinitions>
             <RowDefinition Height="58"/>
             <RowDefinition Height="15"/>

+ 68 - 12
src/PixiEditor/Views/Layers/LayersManager.axaml.cs

@@ -1,8 +1,10 @@
-using Avalonia.Controls;
+using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers;
@@ -18,6 +20,7 @@ namespace PixiEditor.Views.Layers;
 #nullable enable
 internal partial class LayersManager : UserControl
 {
+    public const string LayersDataName = "PixiEditor.LayersData";
     public DocumentViewModel ActiveDocument => DataContext is LayersDockViewModel vm ? vm.ActiveDocument : null;
     private readonly IBrush? highlightColor;
 
@@ -29,10 +32,12 @@ internal partial class LayersManager : UserControl
         {
             highlightColor = value as IBrush;
         }
-      
+
         dropBorder.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         dropBorder.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         dropBorder.AddHandler(DragDrop.DropEvent, Grid_Drop);
+        RootGrid.AddHandler(DragDrop.DropEvent, Grid_Drop);
+        treeView.AddHandler(DragDrop.DragOverEvent, TreeView_DragScroll);
     }
 
     private void LayerControl_MouseDown(object sender, PointerPressedEventArgs e)
@@ -63,7 +68,8 @@ internal partial class LayersManager : UserControl
         if (e.Source is LayerControl container && isLeftPressed && Equals(e.Pointer.Captured, container))
         {
             DataObject data = new();
-            data.Set(LayerControl.LayerControlDataName, container);
+            Guid[] selectedGuids = container.Layer.Document.GetSelectedMembersInOrder().ToArray();
+            data.Set(LayersDataName, selectedGuids);
             Dispatcher.UIThread.InvokeAsync(() => DragDrop.DoDragDrop(e, data, DragDropEffects.Move));
         }
     }
@@ -73,6 +79,16 @@ internal partial class LayersManager : UserControl
         if (sender is not LayerControl)
             return;
 
+        if (e is { Source: LayerControl layerControl, InitialPressMouseButton: MouseButton.Left } &&
+            !e.KeyModifiers.HasFlag(KeyModifiers.Control) && !e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+        {
+            if (layerControl.Layer is not null)
+            {
+                layerControl.Layer.Document.Operations.SetSelectedMember(layerControl.Layer.Id);
+                layerControl.Layer.Document.Operations.ClearSoftSelectedMembers();
+            }
+        }
+
         e.Pointer.Capture(null);
     }
 
@@ -104,7 +120,8 @@ internal partial class LayersManager : UserControl
             isLeftPressed && Equals(e.Pointer.Captured, container))
         {
             DataObject data = new();
-            data.Set(FolderControl.FolderControlDataName, container);
+            Guid[] selectedGuids = container.Folder.Document.GetSelectedMembersInOrder().ToArray();
+            data.Set(LayersDataName, selectedGuids);
             Dispatcher.UIThread.InvokeAsync(() => DragDrop.DoDragDrop(e, data, DragDropEffects.Move));
         }
     }
@@ -114,6 +131,16 @@ internal partial class LayersManager : UserControl
         if (sender is not FolderControl folderControl)
             return;
 
+        if (e is { Source: FolderControl layerControl, InitialPressMouseButton: MouseButton.Left } &&
+            !e.KeyModifiers.HasFlag(KeyModifiers.Control) && !e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+        {
+            if (layerControl.Folder is not null)
+            {
+                layerControl.Folder.Document.Operations.SetSelectedMember(layerControl.Folder.Id);
+                layerControl.Folder.Document.Operations.ClearSoftSelectedMembers();
+            }
+        }
+
         e.Pointer.Capture(null);
     }
 
@@ -137,12 +164,19 @@ internal partial class LayersManager : UserControl
         }
 
         dropBorder.BorderBrush = Brushes.Transparent;
-        Guid? droppedGuid = LayerControl.ExtractMemberGuid(e.Data);
-
-        if (droppedGuid is not null && ActiveDocument is not null)
+        Guid[]? droppedGuids = LayerControl.ExtractMemberGuids(e.Data);
+        if (droppedGuids != null)
         {
-            ActiveDocument.Operations.MoveStructureMember((Guid)droppedGuid,
-                ActiveDocument.NodeGraph.StructureTree.Members[^1].Id, StructureMemberPlacement.Below);
+            using var block = ActiveDocument.Operations.StartChangeBlock();
+            Guid lastMovedMember = ActiveDocument.NodeGraph.StructureTree.Members[^1].Id;
+
+            foreach (Guid memberGuid in droppedGuids)
+            {
+                ActiveDocument.Operations.MoveStructureMember(memberGuid, lastMovedMember,
+                    StructureMemberPlacement.Below);
+                lastMovedMember = memberGuid;
+            }
+
             e.Handled = true;
         }
 
@@ -159,7 +193,7 @@ internal partial class LayersManager : UserControl
             return;
         }
 
-        var member = LayerControl.ExtractMemberGuid(e.Data);
+        var member = LayerControl.ExtractMemberGuids(e.Data);
 
         if (member == null)
         {
@@ -217,7 +251,7 @@ internal partial class LayersManager : UserControl
                         member.Document.Operations.AddSoftSelectedMember(member.Id);
                 });
         }
-        else
+        else if (!ActiveDocument.SelectedMembers.Contains(memberVM.Id))
         {
             ActiveDocument.Operations.SetSelectedMember(memberVM.Id);
             ActiveDocument.Operations.ClearSoftSelectedMembers();
@@ -229,7 +263,7 @@ internal partial class LayersManager : UserControl
     {
         if (matches == 2)
             return 2;
-        
+
         var reversed = root.Reverse();
         foreach (var child in reversed)
         {
@@ -254,4 +288,26 @@ internal partial class LayersManager : UserControl
 
         return matches;
     }
+
+    private void TreeView_DragScroll(object sender, DragEventArgs e)
+    {
+        if (sender is not TreeView treeView)
+            return;
+
+        var point = e.GetPosition(treeView);
+
+        ScrollViewer scrollViewer = treeView.FindDescendantOfType<ScrollViewer>();
+
+        if (scrollViewer is null)
+            return;
+
+        if (point.Y < 10)
+        {
+            scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollViewer.Offset.Y - 10);
+        }
+        else if (point.Y > treeView.Bounds.Height - 10)
+        {
+            scrollViewer.Offset = new Vector(scrollViewer.Offset.X, scrollViewer.Offset.Y + 10);
+        }
+    }
 }

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

@@ -29,7 +29,7 @@
             <behaviours:ClearFocusOnClickBehavior />
         </Interaction.Behaviors>
         <DockPanel Background="Transparent">
-            <controls:Shelf>
+            <controls:Shelf ControlToCollapse="{Binding RelativeSource={RelativeSource AncestorType=UserControl, Mode=FindAncestor}}">
                 <Grid Height="40" x:Name="mainDockPanel">
                     <Grid
                         IsVisible="{Binding Document.ReferenceLayerViewModel.ReferenceTexture, ElementName=uc, Converter={converters:NullToVisibilityConverter}}"

+ 10 - 6
src/PixiEditor/Views/Layers/ReferenceLayer.axaml.cs

@@ -2,6 +2,8 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.ViewModels;
@@ -13,16 +15,18 @@ namespace PixiEditor.Views.Layers;
 internal partial class ReferenceLayer : UserControl
 {
     private Command command;
+    private Rect originalBounds;
 
-    public static readonly StyledProperty<DocumentViewModel> DocumentProperty = AvaloniaProperty.Register<ReferenceLayer, DocumentViewModel>(
-        nameof(Document));
+    public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
+        AvaloniaProperty.Register<ReferenceLayer, DocumentViewModel>(
+            nameof(Document));
 
     public DocumentViewModel Document
     {
         get => GetValue(DocumentProperty);
         set => SetValue(DocumentProperty, value);
     }
-    
+
     static ReferenceLayer()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChanged);
@@ -32,12 +36,12 @@ internal partial class ReferenceLayer : UserControl
     {
         command = CommandController.Current.Commands["PixiEditor.Clipboard.PasteReferenceLayer"];
         InitializeComponent();
-        
+
         DragBorder.AddHandler(DragDrop.DragEnterEvent, ReferenceLayer_DragEnter);
         DragBorder.AddHandler(DragDrop.DragLeaveEvent, ReferenceLayer_DragLeave);
         DragBorder.AddHandler(DragDrop.DropEvent, ReferenceLayer_Drop);
     }
-    
+
     private static void OnDocumentChanged(AvaloniaPropertyChangedEventArgs<DocumentViewModel> e)
     {
         ReferenceLayer referenceLayer = (ReferenceLayer)e.Sender;
@@ -45,7 +49,7 @@ internal partial class ReferenceLayer : UserControl
         {
             e.OldValue.Value.ReferenceLayerViewModel.PropertyChanged -= referenceLayer.OnDocumentPropertyChanged;
         }
-        
+
         if (e.NewValue.HasValue && e.NewValue.Value != null)
         {
             e.NewValue.Value.ReferenceLayerViewModel.PropertyChanged += referenceLayer.OnDocumentPropertyChanged;

+ 1 - 1
tests/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.2.5</AvaloniaVersion>
+		<AvaloniaVersion>11.2.6</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

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

@@ -79,4 +79,9 @@ public class MockDocument : IReadOnlyDocument
     {
         throw new NotImplementedException();
     }
+
+    public List<IReadOnlyStructureNode> GetParents(Guid memberGuid)
+    {
+        throw new NotImplementedException();
+    }
 }