Browse Source

Added duplicate folder

flabbet 7 months ago
parent
commit
74a9e9e6ab

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs

@@ -10,4 +10,50 @@ public class ConnectionsData
         this.originalOutputConnections = originalOutputConnections;
         this.originalInputConnections = originalInputConnections;
     }
+
+    public ConnectionsData WithUpdatedIds(Dictionary<Guid,Guid> nodeMap)
+    {
+        Dictionary<PropertyConnection, List<PropertyConnection>> newOutputConnections = new();
+        foreach (var (key, value) in originalOutputConnections)
+        {
+            Guid? sourceNodeId = key.NodeId;
+            if (sourceNodeId.HasValue)
+            {
+                sourceNodeId = nodeMap[sourceNodeId.Value];
+            }
+            
+            var valueCopy = new List<PropertyConnection>();
+            foreach (var connection in value)
+            {
+                Guid? targetNodeId = connection.NodeId;
+                if (targetNodeId.HasValue)
+                {
+                    targetNodeId = nodeMap[targetNodeId.Value];
+                }
+                valueCopy.Add(connection with { NodeId = targetNodeId });
+            }
+            
+            newOutputConnections.Add(key with { NodeId = sourceNodeId }, valueCopy);
+        }
+        
+        List<(PropertyConnection, PropertyConnection?)> newInputConnections = new();
+        foreach (var (input, output) in originalInputConnections)
+        {
+            Guid? inputNodeId = input.NodeId;
+            if (inputNodeId.HasValue)
+            {
+                inputNodeId = nodeMap[inputNodeId.Value];
+            }
+            
+            Guid? outputNodeId = output?.NodeId;
+            if (outputNodeId.HasValue)
+            {
+                outputNodeId = nodeMap[outputNodeId.Value];
+            }
+            
+            newInputConnections.Add((input with { NodeId = inputNodeId }, new PropertyConnection(outputNodeId, output?.PropertyName)));
+        }
+        
+        return new ConnectionsData(newOutputConnections, newInputConnections);
+    }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -133,7 +133,7 @@ public static class NodeOperations
         return changes;
     }
 
-    public static ConnectionsData CreateConnectionsData(Node node)
+    public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
 

+ 139 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -0,0 +1,139 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class DuplicateFolder_Change : Change
+{
+    private readonly Guid folderGuid;
+    private Guid duplicateGuid;
+    private Guid[] contentGuids;
+    private Guid[] contentDuplicateGuids;
+
+    private ConnectionsData? connectionsData;
+    private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+
+    [GenerateMakeChangeAction]
+    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid)
+    {
+        this.folderGuid = folderGuid;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (!target.TryFindMember<FolderNode>(folderGuid, out FolderNode? folder))
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(folder);
+
+        List<Guid> contentGuidList = new();
+
+        folder.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            contentGuidList.Add(x.Id);
+            contentConnectionsData[x.Id] = NodeOperations.CreateConnectionsData(x);
+            return true;
+        });
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        (FolderNode existingLayer, Node parent) = ((FolderNode, Node))target.FindChildAndParentOrThrow(folderGuid);
+
+        FolderNode clone = (FolderNode)existingLayer.Clone();
+        clone.Id = duplicateGuid;
+
+        InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter) &&
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+        
+        operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+
+        DuplicateContent(target, clone, existingLayer, operations);
+        
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (contentDuplicateGuids is not null)
+        {
+            foreach (Guid contentGuid in contentDuplicateGuids)
+            {
+                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
+                changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
+                
+                target.NodeGraph.RemoveNode(contentNode);
+                contentNode.Dispose();
+            }
+        }
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.FindNodeOrThrow<Node>(folderGuid);
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        return changes;
+    }
+
+    private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
+        List<IChangeInfo> operations)
+    {
+        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+
+        nodeMap[existingLayer.Id] = clone.Id;
+        List<Guid> contentGuidList = new();
+
+        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            if (x is not Node targetNode)
+                return false;
+
+            Node? node = targetNode.Clone();
+            nodeMap[x.Id] = node.Id;
+            contentGuidList.Add(node.Id);
+
+            target.NodeGraph.AddNode(node);
+
+            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+            return true;
+        });
+
+        foreach (var data in contentConnectionsData)
+        {
+            var updatedData = data.Value.WithUpdatedIds(nodeMap);
+            Guid targetNodeId = nodeMap[data.Key];
+            operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
+                target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
+        }
+        
+        contentDuplicateGuids = contentGuidList.ToArray();
+    }
+}

+ 1 - 1
src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json

@@ -224,7 +224,7 @@
       "Parameters": []
     },
     "DuplicateLayer": {
-      "Command": "PixiEditor.Layer.DuplicateSelectedLayer",
+      "Command": "PixiEditor.Layer.DuplicateSelectedMember",
       "DefaultShortcut": {
         "key": "None",
         "modifiers": null

+ 1 - 1
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -200,7 +200,7 @@ internal static class ClipboardController
         {
             foreach (var layerId in layerIds)
             {
-                document.Operations.DuplicateLayer(layerId);
+                document.Operations.DuplicateMember(layerId);
             }
 
             return true;

+ 22 - 10
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -34,7 +34,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         Document = document;
         Internals = internals;
     }
-    
+
     public ChangeBlock StartChangeBlock()
     {
         return new ChangeBlock(Internals.ActionAccumulator);
@@ -196,17 +196,28 @@ internal class DocumentOperationsModule : IDocumentOperations
     }
 
     /// <summary>
-    /// Duplicates the layer with the <paramref name="guidValue"/>
+    /// Duplicates the member with the <paramref name="guidValue"/>
     /// </summary>
-    /// <param name="guidValue">The Guid of the layer</param>
-    public void DuplicateLayer(Guid guidValue)
+    /// <param name="guidValue">The Guid of the member</param>
+    public void DuplicateMember(Guid guidValue)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
 
-        Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+        bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
+        if (!isFolder)
+        {
+            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+        }
+        else
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new DuplicateFolder_Action(guidValue, newGuid),
+                new SetSelectedMember_PassthroughAction(newGuid));
+        }
     }
 
     /// <summary>
@@ -452,7 +463,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         IMidChangeUndoableExecutor executor =
             Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
-        if (executor is { CanRedo: true }) 
+        if (executor is { CanRedo: true })
         {
             executor.OnMidChangeRedo();
             return;
@@ -850,14 +861,15 @@ internal class DocumentOperationsModule : IDocumentOperations
             return null;
 
         Internals.ChangeController.TryStopActiveExecutor();
-        
-        if(!Document.StructureHelper.TryFindNode(nodeId, out INodeHandler node) || node.InternalName == OutputNode.UniqueName)
+
+        if (!Document.StructureHelper.TryFindNode(nodeId, out INodeHandler node) ||
+            node.InternalName == OutputNode.UniqueName)
             return null;
 
         Guid newGuid = Guid.NewGuid();
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId, newGuid));
-        
+
         return newGuid;
     }
 }

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

@@ -6,7 +6,7 @@ namespace PixiEditor.Models.Handlers;
 internal interface IDocumentOperations
 {
     public void DeleteStructureMember(Guid memberGuidValue);
-    public void DuplicateLayer(Guid memberGuidValue);
+    public void DuplicateMember(Guid memberGuidValue);
     public void AddSoftSelectedMember(Guid memberGuidValue);
     public void MoveStructureMember(Guid memberGuidValue, Guid target, StructureMemberPlacement placement);
     public void SetSelectedMember(Guid memberId);

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

@@ -587,8 +587,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Surface output = new(finalBounds.Size);
 
         VectorPath clipPath = new VectorPath(SelectionPathBindable) { FillType = PathFillType.EvenOdd };
-        clipPath.Transform(Matrix3X3.CreateTranslation(-bounds.X, -bounds.Y));
+        //clipPath.Transform(Matrix3X3.CreateTranslation(-bounds.X, -bounds.Y));
         output.DrawingSurface.Canvas.Save();
+        output.DrawingSurface.Canvas.Translate(-finalBounds.X, -finalBounds.Y);
         if (!clipPath.IsEmpty)
         {
             output.DrawingSurface.Canvas.ClipPath(clipPath);
@@ -601,7 +602,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             try
             {
                 Renderer.RenderLayers(output.DrawingSurface, selectedLayers.ToHashSet(),
-                    AnimationDataViewModel.ActiveFrameBindable, ChunkResolution.Full, finalBounds.Size);
+                    AnimationDataViewModel.ActiveFrameBindable, ChunkResolution.Full, SizeBindable);
             }
             catch (ObjectDisposedException)
             {
@@ -830,13 +831,16 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         var allLayers = StructureHelper.GetAllMembers();
         foreach (var member in allLayers)
         {
-            if (member is ILayerHandler && selectedMembers.Contains(member.Id))
+            if(!selectedMembers.Contains(member.Id))
+                continue;
+            
+            if (member is ILayerHandler)
             {
                 result.Add(member.Id);
             }
             else if (member is IFolderHandler folder)
             {
-                if (includeFoldersWithMask && folder.HasMaskBindable && selectedMembers.Contains(folder.Id))
+                if (includeFoldersWithMask && folder.HasMaskBindable)
                     result.Add(folder.Id);
 
                 ExtractSelectedLayers(folder, result, includeFoldersWithMask);
@@ -856,11 +860,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         foreach (var member in folder.Children)
         {
-            if (member is ImageLayerNodeViewModel layer && !list.Contains(layer.Id))
+            if (member is ILayerHandler layer && !list.Contains(layer.Id))
             {
                 list.Add(layer.Id);
             }
-            else if (member is FolderNodeViewModel childFolder)
+            else if (member is IFolderHandler childFolder)
             {
                 if (includeFoldersWithMask && childFolder.HasMaskBindable && !list.Contains(childFolder.Id))
                     list.Add(childFolder.Id);

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

@@ -181,16 +181,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Layer.DuplicateSelectedLayer", "DUPLICATE_SELECTED_LAYER", "DUPLICATE_SELECTED_LAYER",
-        CanExecute = "PixiEditor.Layer.SelectedMemberIsLayer",
+    [Command.Basic("PixiEditor.Layer.DuplicateSelectedMember", "DUPLICATE_SELECTED_LAYER", "DUPLICATE_SELECTED_LAYER",
         Icon = PixiPerfectIcons.DuplicateFile, MenuItemPath = "EDIT/DUPLICATE", MenuItemOrder = 5,
         AnalyticsTrack = true)]
-    public void DuplicateLayer()
+    public void DuplicateMember()
     {
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
-        if (member is not ILayerHandler)
-            return;
-        member.Document.Operations.DuplicateLayer(member.Id);
+
+        member.Document.Operations.DuplicateMember(member.Id);
     }
 
     [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsLayer")]

+ 76 - 63
src/PixiEditor/Views/Layers/FolderControl.axaml

@@ -14,65 +14,71 @@
              xmlns:input1="clr-namespace:PixiEditor.Views.Input;assembly=PixiEditor.UI.Common"
              xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
              xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
-             mc:Ignorable="d" 
+             mc:Ignorable="d"
              Focusable="True"
-             d:DesignHeight="35" 
-             d:DesignWidth="250" 
+             d:DesignHeight="35"
+             d:DesignWidth="250"
              x:Name="folderControl">
-    <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True" Tag="{Binding ElementName=folderControl}">
+    <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True"
+            Tag="{Binding ElementName=folderControl}">
         <Border.Background>
             <Binding ElementName="folderControl" Path="Folder.Selection">
                 <Binding.Converter>
                     <converters:StructureMemberSelectionTypeToColorConverter
                         SoftColor="{StaticResource SoftSelectedLayerBrush}"
                         HardColor="{StaticResource SelectedLayerBrush}"
-                        NoneColor="Transparent"
-                        />
+                        NoneColor="Transparent" />
                 </Binding.Converter>
             </Binding>
         </Border.Background>
         <Interaction.Behaviors>
-            <behaviours:ClearFocusOnClickBehavior/>
+            <behaviours:ClearFocusOnClickBehavior />
         </Interaction.Behaviors>
         <Grid>
             <Grid.RowDefinitions>
-                <RowDefinition Height="10"/>
-                <RowDefinition Height="16"/>
-                <RowDefinition Height="10"/>
+                <RowDefinition Height="10" />
+                <RowDefinition Height="16" />
+                <RowDefinition Height="10" />
             </Grid.RowDefinitions>
-            <Grid DragDrop.AllowDrop="True" 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" Panel.ZIndex="2" Background="Transparent"  />
+            <Grid DragDrop.AllowDrop="True" 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"
+                  Panel.ZIndex="2" Background="Transparent" />
             <Grid x:Name="centerGrid" Grid.Row="0" Grid.RowSpan="3" Background="Transparent">
                 <Grid.ColumnDefinitions>
-                    <ColumnDefinition Width="24"/>
-                    <ColumnDefinition Width="*"/>
+                    <ColumnDefinition Width="24" />
+                    <ColumnDefinition Width="*" />
                 </Grid.ColumnDefinitions>
                 <CheckBox Classes="ImageCheckBox" VerticalAlignment="Center"
-                      IsThreeState="False" HorizontalAlignment="Center"
-                      IsChecked="{Binding Path=Folder.IsVisibleBindable, ElementName=folderControl, Mode=TwoWay}" Grid.Column="0" Height="16"/>
+                          IsThreeState="False" HorizontalAlignment="Center"
+                          IsChecked="{Binding Path=Folder.IsVisibleBindable, ElementName=folderControl, Mode=TwoWay}"
+                          Grid.Column="0" Height="16" />
 
                 <StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left">
-                    <Rectangle Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent" StrokeThickness="0"/>
-                    <Border 
+                    <Rectangle
+                        Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}"
+                        Fill="Transparent" StrokeThickness="0" />
+                    <Border
                         IsVisible="{Binding Folder.ClipToMemberBelowEnabledBindable, ElementName=folderControl}"
-                        Background="{DynamicResource ThemeAccentBrush}" Width="3" Margin="1,1,2,1" CornerRadius="1"/>
+                        Background="{DynamicResource ThemeAccentBrush}" Width="3" Margin="1,1,2,1" CornerRadius="1" />
                     <StackPanel Grid.Row="1" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
-                        <Border Width="32" Height="32" BorderThickness="1" BorderBrush="Black" RenderOptions.BitmapInterpolationMode="None">
+                        <Border Width="32" Height="32" BorderThickness="1" BorderBrush="Black"
+                                RenderOptions.BitmapInterpolationMode="None">
                             <Border.Background>
                                 <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
                                     <ImageBrush.Transform>
-                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4"/>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:PreviewPainterControl 
+                            <visuals:PreviewPainterControl
                                 PreviewPainter="{Binding Folder.PreviewPainter, ElementName=folderControl}"
                                 ClipToBounds="True"
                                 FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
-                                Width="30" Height="30" RenderOptions.BitmapInterpolationMode="None"/>
+                                Width="30" Height="30" RenderOptions.BitmapInterpolationMode="None" />
                         </Border>
-                        <Border 
-                            Width="32" Height="32" 
+                        <Border
+                            Width="32" Height="32"
                             BorderThickness="1"
                             Margin="3,0,0,0"
                             RenderOptions.BitmapInterpolationMode="None"
@@ -81,24 +87,25 @@
                             <Border.Background>
                                 <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
                                     <ImageBrush.Transform>
-                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4"/>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
                             <Grid IsHitTestVisible="False">
-                                <visuals:PreviewPainterControl 
-                                    PreviewPainter="{Binding Folder.MaskPreviewPainter, ElementName=folderControl}" 
+                                <visuals:PreviewPainterControl
+                                    PreviewPainter="{Binding Folder.MaskPreviewPainter, ElementName=folderControl}"
                                     Width="30" Height="30"
                                     ClipToBounds="True"
                                     FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
-                                    RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
-                                <Path 
-                                Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z" 
-                                Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                IsVisible="{Binding !Folder.MaskIsVisibleBindable, ElementName=folderControl}"/>
+                                    RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
+                                <Path
+                                    Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"
+                                    Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Center"
+                                    VerticalAlignment="Center"
+                                    IsVisible="{Binding !Folder.MaskIsVisibleBindable, ElementName=folderControl}" />
                             </Grid>
                         </Border>
-                        <StackPanel Orientation="Vertical"  Margin="3,0,5,0">
+                        <StackPanel Orientation="Vertical" Margin="3,0,5,0">
                             <input:EditableTextBlock
                                 x:Name="editableTextBlock"
                                 d:Text="New Folder" FontSize="14"
@@ -108,7 +115,8 @@
                             <StackPanel Orientation="Horizontal">
                                 <TextBlock d:Text="100" Foreground="White" FontSize="11">
                                     <TextBlock.Text>
-                                        <Binding ElementName="folderControl" Path="Folder.OpacityBindable" Converter="{converters:MultiplyConverter}" StringFormat="N0">
+                                        <Binding ElementName="folderControl" Path="Folder.OpacityBindable"
+                                                 Converter="{converters:MultiplyConverter}" StringFormat="N0">
                                             <Binding.ConverterParameter>
                                                 <sys:Double>100.0</sys:Double>
                                             </Binding.ConverterParameter>
@@ -116,46 +124,51 @@
                                     </TextBlock.Text>
                                 </TextBlock>
                                 <TextBlock Foreground="White" FontSize="11">%</TextBlock>
-                                <TextBlock 
-                                Margin="5,0,0,0" 
-                                d:Text="Normal" 
-                                Foreground="White"
-                                FontSize="11"
-                                Text="{Binding Folder.BlendModeBindable, ElementName=folderControl, Converter={converters:BlendModeToStringConverter}}"/>
+                                <TextBlock
+                                    Margin="5,0,0,0"
+                                    d:Text="Normal"
+                                    Foreground="White"
+                                    FontSize="11"
+                                    Text="{Binding Folder.BlendModeBindable, ElementName=folderControl, Converter={converters:BlendModeToStringConverter}}" />
                             </StackPanel>
                         </StackPanel>
                     </StackPanel>
-                    <TextBlock Text="{DynamicResource icon-folder}" Classes="pixi-icon" FontSize="20" Margin="0,0,10,0" HorizontalAlignment="Right"/>
+                    <TextBlock Text="{DynamicResource icon-folder}" Classes="pixi-icon" FontSize="20" Margin="0,0,10,0"
+                               HorizontalAlignment="Right" />
                 </StackPanel>
             </Grid>
             <Grid
-                  Grid.Row="2"
-                  Name="BottomDropGrid"
-                  DragDrop.AllowDrop="True"
-                  Grid.ColumnSpan="2" Background="Transparent"/>
+                Grid.Row="2"
+                Name="BottomDropGrid"
+                DragDrop.AllowDrop="True"
+                Grid.ColumnSpan="2" Background="Transparent" />
         </Grid>
         <Border.ContextMenu>
             <ContextMenu>
-                <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}"/>
-                <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click"/>
+                <MenuItem ui:Translator.Key="DUPLICATE"
+                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedMember}" />
+                <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}" />
+                <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click" />
                 <controls:ToggleableMenuItem
-                    IsChecked="{Binding $parent[UserControl].Folder.ClipToMemberBelowEnabledBindable, Mode=TwoWay}" 
-                    ui:Translator.Key="CLIP_TO_BELOW"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="MOVE_UPWARDS" Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberUpwards}"/>
-                <MenuItem ui:Translator.Key="MOVE_DOWNWARDS" Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberDownwards}"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="CREATE_MASK" Command="{xaml:Command PixiEditor.Layer.CreateMask}"/>
-                <MenuItem ui:Translator.Key="DELETE_MASK" Command="{xaml:Command PixiEditor.Layer.DeleteMask}"/>
+                    IsChecked="{Binding $parent[UserControl].Folder.ClipToMemberBelowEnabledBindable, Mode=TwoWay}"
+                    ui:Translator.Key="CLIP_TO_BELOW" />
+                <Separator />
+                <MenuItem ui:Translator.Key="MOVE_UPWARDS"
+                          Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberUpwards}" />
+                <MenuItem ui:Translator.Key="MOVE_DOWNWARDS"
+                          Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberDownwards}" />
+                <Separator />
+                <MenuItem ui:Translator.Key="CREATE_MASK" Command="{xaml:Command PixiEditor.Layer.CreateMask}" />
+                <MenuItem ui:Translator.Key="DELETE_MASK" Command="{xaml:Command PixiEditor.Layer.DeleteMask}" />
                 <controls:ToggleableMenuItem
-                    IsChecked="{Binding $parent[UserControl].Folder.MaskIsVisibleBindable, Mode=TwoWay}" 
+                    IsChecked="{Binding $parent[UserControl].Folder.MaskIsVisibleBindable, Mode=TwoWay}"
                     IsEnabled="{Binding $parent[UserControl].Folder.HasMaskBindable}"
-                    ui:Translator.Key="ENABLE_MASK"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="MERGE_SELECTED" Command="{xaml:Command PixiEditor.Layer.MergeSelected}"/>
-                <MenuItem ui:Translator.Key="MERGE_WITH_ABOVE" Command="{xaml:Command PixiEditor.Layer.MergeWithAbove}"/>
-                <MenuItem ui:Translator.Key="MERGE_WITH_BELOW" Command="{xaml:Command PixiEditor.Layer.MergeWithBelow}"/>
+                    ui:Translator.Key="ENABLE_MASK" />
+                <Separator />
+                <MenuItem ui:Translator.Key="MERGE_SELECTED" Command="{xaml:Command PixiEditor.Layer.MergeSelected}" />
+                <MenuItem ui:Translator.Key="MERGE_WITH_ABOVE" Command="{xaml:Command PixiEditor.Layer.MergeWithAbove}" />
+                <MenuItem ui:Translator.Key="MERGE_WITH_BELOW" Command="{xaml:Command PixiEditor.Layer.MergeWithBelow}" />
             </ContextMenu>
         </Border.ContextMenu>
     </Border>
-</UserControl>
+</UserControl>

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

@@ -172,7 +172,7 @@
         <Border.ContextMenu>
             <ContextMenu>
                 <MenuItem ui:Translator.Key="DUPLICATE"
-                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedLayer}" />
+                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedMember}" />
                 <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}" />
                 <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click" />
                 <controls:ToggleableMenuItem