Browse Source

Copy pasting nodes work

flabbet 7 months ago
parent
commit
ef68041347

+ 14 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -10,11 +10,13 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("Output")]
 public class OutputNode : Node, IRenderInput, IPreviewRenderable
 {
+    public const string UniqueName = "PixiEditor.Output";
     public const string InputPropertyName = "Background";
 
-    public RenderInputProperty Input { get; } 
-    
+    public RenderInputProperty Input { get; }
+
     private VecI? lastDocumentSize;
+
     public OutputNode()
     {
         Input = new RenderInputProperty(this, InputPropertyName, "BACKGROUND", null);
@@ -28,26 +30,27 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
-        if(!string.IsNullOrEmpty(context.TargetOutput)) return;
-        
+        if (!string.IsNullOrEmpty(context.TargetOutput)) return;
+
         lastDocumentSize = context.DocumentSize;
-        
+
         int saved = context.RenderSurface.Canvas.Save();
         context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
         Input.Value?.Paint(context, context.RenderSurface);
-        
+
         context.RenderSurface.Canvas.RestoreToCount(saved);
     }
 
     RenderInputProperty IRenderInput.Background => Input;
+
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         if (lastDocumentSize == null)
         {
             return null;
         }
-        
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
+
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
     }
 
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
@@ -56,12 +59,12 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
         {
             return false;
         }
-        
+
         int saved = renderOn.Canvas.Save();
         Input.Value.Paint(context, renderOn);
-        
+
         renderOn.Canvas.RestoreToCount(saved);
-        
+
         return true;
     }
 }

+ 52 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DuplicateNode_Change.cs

@@ -0,0 +1,52 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class DuplicateNode_Change : Change
+{
+    private Guid nodeGuid;
+    
+    private Guid createdNodeGuid;
+
+    [GenerateMakeChangeAction]
+    public DuplicateNode_Change(Guid nodeGuid)
+    {
+        this.nodeGuid = nodeGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindNode(nodeGuid, out Node node) && node.GetNodeTypeUniqueName() != OutputNode.UniqueName;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        Node existingNode = target.FindNode(nodeGuid);
+        Node clone = existingNode.Clone();
+        
+        createdNodeGuid = clone.Id;
+
+        target.NodeGraph.AddNode(clone);
+
+        ignoreInUndo = false;
+
+        return CreateNode_ChangeInfo.CreateFromNode(clone);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode(createdNodeGuid);
+        target.NodeGraph.RemoveNode(node);
+        
+        node.Dispose();
+
+        return new DeleteNode_ChangeInfo(node.Id);
+    }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is DuplicateNode_Change;
+    }
+}

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

@@ -6,5 +6,6 @@ public static class ClipboardDataFormats
     public const string LayerIdList = "PixiEditor.LayerIdList";
     public const string PositionFormat = "PixiEditor.Position";
     public const string ImageSlashPng = "image/png";
-    public const string DocumentFormat = "PixiEditor.Document"; 
+    public const string DocumentFormat = "PixiEditor.Document";
+    public const string NodeIdList = "PixiEditor.NodeIdList";
 }

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -17,7 +17,7 @@ internal partial class Command
 
         public string CanExecute { get; set; }
         
-        public Type? ShortcutContext { get; set; }
+        public Type[]? ShortcutContexts { get; set; }
 
         /// <summary>
         /// Gets or sets the default shortcut key for this command

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

@@ -260,7 +260,7 @@ internal class CommandController
                                 Parameter = basic.Parameter,
                                 MenuItemPath = basic.MenuItemPath,
                                 MenuItemOrder = basic.MenuItemOrder,
-                                ShortcutContext = basic.ShortcutContext
+                                ShortcutContexts = basic.ShortcutContexts
                             });
                     }
                     else if (attribute is Attributes.Commands.Command.FilterAttribute menu)

+ 1 - 1
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -42,7 +42,7 @@ internal abstract partial class Command : PixiObservableObject
         }
     }
     
-    public Type? ShortcutContext { get; init; }
+    public Type[]? ShortcutContexts { get; init; }
 
     public string? MenuItemPath { get; init; }
 

+ 71 - 15
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -27,6 +27,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels.Document;
 using Bitmap = Avalonia.Media.Imaging.Bitmap;
@@ -126,7 +127,7 @@ internal static class ClipboardController
 
         await Clipboard.SetDataObjectAsync(data);
     }
-    
+
     public static async Task CopyVisibleToClipboard(DocumentViewModel document)
     {
         await Clipboard.ClearAsync();
@@ -134,7 +135,7 @@ internal static class ClipboardController
         DataObject data = new DataObject();
 
         RectD copyArea = new RectD(VecD.Zero, document.SizeBindable);
-        
+
         if (!document.SelectionPathBindable.IsEmpty)
         {
             copyArea = document.SelectionPathBindable.TightBounds;
@@ -145,15 +146,16 @@ internal static class ClipboardController
         }
 
         using Surface documentSurface = new Surface(document.SizeBindable);
-        
-        document.Renderer.RenderDocument(documentSurface.DrawingSurface, document.AnimationDataViewModel.ActiveFrameTime);
-        
+
+        document.Renderer.RenderDocument(documentSurface.DrawingSurface,
+            document.AnimationDataViewModel.ActiveFrameTime);
+
         Surface surfaceToCopy = new Surface((VecI)copyArea.Size.Ceiling());
         using Paint paint = new Paint();
-        
+
         surfaceToCopy.DrawingSurface.Canvas.DrawImage(
-        documentSurface.DrawingSurface.Snapshot(),
-        copyArea, new RectD(0, 0, copyArea.Size.X, copyArea.Size.Y), paint);
+            documentSurface.DrawingSurface.Snapshot(),
+            copyArea, new RectD(0, 0, copyArea.Size.X, copyArea.Size.Y), paint);
 
         await AddImageToClipboard(surfaceToCopy, data);
 
@@ -184,9 +186,9 @@ internal static class ClipboardController
     /// </summary>
     public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
     {
-        Guid sourceDocument = GetSourceDocument(data); 
+        Guid sourceDocument = GetSourceDocument(data);
         Guid[] layerIds = GetLayerIds(data);
-        
+
         if (sourceDocument != document.Id)
         {
             layerIds = [];
@@ -242,11 +244,11 @@ internal static class ClipboardController
         document.Operations.PasteImagesAsLayers(images, document.AnimationDataViewModel.ActiveFrameBindable);
         return true;
     }
-    
+
     private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, DocumentViewModel doc)
     {
         var dataObjects = data as IDataObject[] ?? data.ToArray();
-        
+
         var dataObjectWithPos = dataObjects.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
         VecD pos = VecD.Zero;
 
@@ -254,7 +256,7 @@ internal static class ClipboardController
         {
             pos = dataObjectWithPos.GetVecD(ClipboardDataFormats.PositionFormat);
         }
-        
+
         for (var i = 0; i < layerIds.Length; i++)
         {
             var layerId = layerIds[i];
@@ -263,7 +265,7 @@ internal static class ClipboardController
             if (layer is not { TightBounds: not null } || !layer.TightBounds.Value.Pos.AlmostEquals(pos))
                 return false;
         }
-        
+
         return true;
     }
 
@@ -281,7 +283,7 @@ internal static class ClipboardController
 
         return [];
     }
-    
+
     private static Guid GetSourceDocument(IEnumerable<IDataObject> data)
     {
         foreach (var dataObject in data)
@@ -569,4 +571,58 @@ internal static class ClipboardController
         result = null;
         return false;
     }
+
+    public static async Task CopyNodes(Guid[] nodeIds)
+    {
+        await Clipboard.ClearAsync();
+
+        DataObject data = new DataObject();
+
+        byte[] nodeIdsBytes = Encoding.UTF8.GetBytes(string.Join(";", nodeIds.Select(x => x.ToString())));
+
+        data.Set(ClipboardDataFormats.NodeIdList, nodeIdsBytes);
+
+        await Clipboard.SetDataObjectAsync(data);
+    }
+
+    public static async Task PasteNodes(DocumentViewModel document)
+    {
+        var data = await TryGetDataObject();
+        var nodeIds = GetNodeIds(data);
+
+        foreach (var nodeId in nodeIds)
+        {
+            document.Operations.DuplicateNode(nodeId);
+        }
+    }
+
+    public static async Task<Guid[]> GetNodesFromClipboard()
+    {
+        var data = await TryGetDataObject();
+        return GetNodeIds(data);
+    }
+
+    private static Guid[] GetNodeIds(IEnumerable<IDataObject?> data)
+    {
+        foreach (var dataObject in data)
+        {
+            if (dataObject.Contains(ClipboardDataFormats.NodeIdList))
+            {
+                byte[] nodeIds = (byte[])dataObject.Get(ClipboardDataFormats.NodeIdList);
+                string nodeIdsString = System.Text.Encoding.UTF8.GetString(nodeIds);
+                return nodeIdsString.Split(';').Select(Guid.Parse).ToArray();
+            }
+        }
+
+        return [];
+    }
+
+    public static async Task<bool> AreNodesInClipboard()
+    {
+        var formats = await Clipboard.GetFormatsAsync();
+        if (formats == null || formats.Length == 0)
+            return false;
+        
+        return formats.Contains(ClipboardDataFormats.NodeIdList);
+    }
 }

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

@@ -57,7 +57,7 @@ internal class ShortcutController
             return;
         }
 
-        var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContext is null || x.ShortcutContext == ActiveContext).ToList();
+        var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContexts is null || x.ShortcutContexts.Contains(ActiveContext)).ToList();
 
         if (!commands.Any())
         {

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

@@ -837,4 +837,14 @@ internal class DocumentOperationsModule : IDocumentOperations
         Internals.ActionAccumulator.AddFinishedActions(
             new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgb()));
     }
+
+    public void DuplicateNode(Guid nodeId)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId));
+    }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -162,7 +162,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
 
     [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", 
         CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
-        ShortcutContext = typeof(ViewportWindowViewModel),
+        ShortcutContexts = [typeof(ViewportWindowViewModel)],
         Icon = PixiPerfectIcons.Eraser,
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
     public void DeletePixels()

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -151,7 +151,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Animation.ToggleOnionSkinning", "TOGGLE_ONION_SKINNING",
         "TOGGLE_ONION_SKINNING_DESCRIPTIVE",
-        ShortcutContext = typeof(TimelineDockViewModel), Key = Key.O, AnalyticsTrack = true)]
+        ShortcutContexts = [typeof(TimelineDockViewModel)], Key = Key.O, AnalyticsTrack = true)]
     public void ToggleOnionSkinning(bool value)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
@@ -161,7 +161,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Animation.DeleteCels", "DELETE_CELS", "DELETE_CELS_DESCRIPTIVE",
-        ShortcutContext = typeof(TimelineDockViewModel), Key = Key.Delete, AnalyticsTrack = true)]
+        ShortcutContexts = [typeof(TimelineDockViewModel)], Key = Key.Delete, AnalyticsTrack = true)]
     public void DeleteCels()
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;

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

@@ -19,6 +19,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Dock;
 
 namespace PixiEditor.ViewModels.SubViewModels;
 #nullable enable
@@ -48,6 +49,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "PASTE_AS_NEW_LAYER", "PASTE_AS_NEW_LAYER_DESCRIPTIVE",
         CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(LayersDockViewModel)],
         Icon = PixiPerfectIcons.PasteAsNewLayer, AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
         CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
@@ -144,9 +146,22 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             Owner.ColorsSubViewModel.SecondaryColor = result.Value;
         }
     }
+    
+    [Command.Basic("PixiEditor.Clipboard.PasteNodes", "PASTE_NODES", "PASTE_NODES_DESCRIPTIVE",
+        ShortcutContexts = [typeof(NodeGraphDockViewModel)], Key = Key.V, Modifiers = KeyModifiers.Control,
+        CanExecute = "PixiEditor.Clipboard.CanPasteNodes", Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
+    public async Task PasteNodes()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        await ClipboardController.PasteNodes(doc);
+    }
 
     [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
         Key = Key.C, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(LayersDockViewModel)],
         MenuItemPath = "EDIT/COPY", MenuItemOrder = 3, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
     public async Task Copy()
     {
@@ -157,7 +172,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         await ClipboardController.CopyToClipboard(doc);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.CopyVisible", "COPY_VISIBLE", "COPY_VISIBLE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
+    [Command.Basic("PixiEditor.Clipboard.CopyVisible", "COPY_VISIBLE", "COPY_VISIBLE_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanCopy",
         Key = Key.C, Modifiers = KeyModifiers.Shift,
         MenuItemPath = "EDIT/COPY_VISIBLE", MenuItemOrder = 3, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
     public async Task CopyVisible()
@@ -169,6 +185,24 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         await ClipboardController.CopyVisibleToClipboard(doc);
     }
 
+    [Command.Basic("PixiEditor.Clipboard.CopyNodes", "COPY_NODES", "COPY_NODES_DESCRIPTIVE",
+        Key = Key.C, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(NodeGraphDockViewModel)],
+        Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
+    public async Task CopySelectedNodes()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        var selectedNodes = doc.NodeGraph.AllNodes.Where(x => x.IsNodeSelected).Select(x => x.Id).ToArray();
+        if (selectedNodes.Length == 0)
+            return;
+        
+        await ClipboardController.CopyNodes(selectedNodes);
+    }
+    
+    
     [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsHex", CopyColor.PrimaryHEX, "COPY_COLOR_HEX",
         "COPY_COLOR_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsRgb", CopyColor.PrimaryRGB, "COPY_COLOR_RGB",
@@ -209,6 +243,12 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             ? ClipboardController.IsImage(data)
             : ClipboardController.IsImageInClipboard().Result;
     }
+    
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteNodes")]
+    public bool CanPasteNodes()
+    {
+        return Owner.DocumentIsNotNull(null) && ClipboardController.AreNodesInClipboard().Result;
+    }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
     public static async Task<bool> CanPasteColor() =>

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

@@ -92,7 +92,7 @@ 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, Key = Key.Delete,
-        ShortcutContext = typeof(LayersDockViewModel))]
+        ShortcutContexts = [typeof(LayersDockViewModel)])]
     public void DeleteAllSelected()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

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

@@ -15,7 +15,7 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.NodeGraph.DeleteSelectedNodes", "DELETE_NODES", "DELETE_NODES_DESCRIPTIVE", 
-        Key = Key.Delete, ShortcutContext = typeof(NodeGraphDockViewModel), AnalyticsTrack = true)]
+        Key = Key.Delete, ShortcutContexts = [typeof(NodeGraphDockViewModel)], AnalyticsTrack = true)]
     public void DeleteSelectedNodes()
     {
         var nodes = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs

@@ -65,13 +65,13 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectLeft", "NUDGE_SELECTED_LEFT", "NUDGE_SELECTED_LEFT", Key = Key.Left, Parameter = new int[] { -1, 0 }, Icon = PixiPerfectIcons.ChevronLeft, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectRight", "NUDGE_SELECTED_RIGHT", "NUDGE_SELECTED_RIGHT", Key = Key.Right, Parameter = new int[] { 1, 0 }, Icon = PixiPerfectIcons.ChevronRight, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectUp", "NUDGE_SELECTED_UP", "NUDGE_SELECTED_UP", Key = Key.Up, Parameter = new int[] { 0, -1 }, Icon = PixiPerfectIcons.ChevronUp, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectDown", "NUDGE_SELECTED_DOWN", "NUDGE_SELECTED_DOWN", Key = Key.Down, Parameter = new int[] { 0, 1 }, Icon = PixiPerfectIcons.ChevronDown, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     public void NudgeSelectedObject(int[] dist)
     {
         VecI distance = new(dist[0], dist[1]);

+ 1 - 1
src/PixiEditor/Views/Shortcuts/ShortcutBox.cs

@@ -55,7 +55,7 @@ internal class ShortcutBox : ContentControl
 
         if (e != KeyCombination.None)
         {
-            if (controller.Commands[e].Where(x => x.ShortcutContext == null || x.ShortcutContext == Command.ShortcutContext)
+            if (controller.Commands[e].Where(x => x.ShortcutContexts == null || x.ShortcutContexts == Command.ShortcutContexts)
                     .SkipWhile(x => x == Command).FirstOrDefault() is { } oldCommand)
             {
                 var oldShortcut = Command.Shortcut;