Browse Source

Copy pasting layers

flabbet 10 months ago
parent
commit
21f4780899

+ 5 - 3
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -2,7 +2,9 @@
 
 public static class ClipboardDataFormats
 {
-    public static string Dib = "DeviceIndependentBitmap";
-    public static string Bitmap = "Bitmap";
-    public static string Png = "PNG";
+    public const string Dib = "DeviceIndependentBitmap";
+    public const string Bitmap = "Bitmap";
+    public const string Png = "PNG";
+    public const string LayerIdList = "PixiEditor.LayerIdList";
+    public const string PositionFormat = "PixiEditor.Position";
 }

+ 92 - 26
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -15,6 +15,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Constants;
 using PixiEditor.Models.Clipboard;
@@ -32,9 +33,7 @@ namespace PixiEditor.Models.Controllers;
 internal static class ClipboardController
 {
     public static IClipboard Clipboard { get; private set; }
-
-    private const string PositionFormat = "PixiEditor.Position";
-
+    
     public static void Initialize(IClipboard clipboard)
     {
         Clipboard = clipboard;
@@ -46,24 +45,70 @@ internal static class ClipboardController
         "Copied.png");
 
     /// <summary>
-    ///     Copies the selection to clipboard in PNG, Bitmap and DIB formats.
+    ///     Copies the document elements to clipboard like selection on PNG, Bitmap and DIB formats.
+    ///     Data that is copied:
+    ///     1. General image data (PNG, Bitmap, DIB), either selection or selected layers of tight bounds size
+    /// 
+    ///     PixiEditor specific stuff: 
+    ///     2. Position of the copied area
+    ///     3. Layers guid, this is used to duplicate the layer when pasting
     /// </summary>
     public static async Task CopyToClipboard(DocumentViewModel document)
     {
         await Clipboard.ClearAsync();
 
-        var surface = document.MaybeExtractSelectedArea();
-        if (surface.IsT0)
-            return;
-        if (surface.IsT1)
+        DataObject data = new DataObject();
+
+        Surface surfaceToCopy = null;
+        RectI copyArea = RectI.Empty;
+
+        if (!document.SelectionPathBindable.IsEmpty)
+        {
+            var surface = document.TryExtractArea((RectI)document.SelectionPathBindable.TightBounds);
+            if (surface.IsT0)
+                return;
+
+            if (surface.IsT1)
+            {
+                NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
+                return;
+            }
+            
+            (surfaceToCopy, copyArea) = surface.AsT2;
+        }
+        else if(document.TransformViewModel.TransformActive)
+        {
+            var surface = document.TryExtractArea((RectI)document.TransformViewModel.Corners.AABBBounds.RoundOutwards());
+            if (surface.IsT0 || surface.IsT1)
+                return;
+            
+            (surfaceToCopy, copyArea) = surface.AsT2;
+        }
+
+        if (surfaceToCopy == null)
         {
-            NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
             return;
         }
+        
+        await AddImageToClipboard(surfaceToCopy, data);
 
-        var (actuallySurface, area) = surface.AsT2;
-        DataObject data = new DataObject();
+        if (copyArea.Size != document.SizeBindable && copyArea.Pos != VecI.Zero && copyArea != RectI.Empty)
+        {
+            data.SetVecI(ClipboardDataFormats.PositionFormat, copyArea.Pos);
+        }
+        
+        string[] layerIds = document.GetSelectedMembers().Select(x => x.ToString()).ToArray();
+        string layerIdsString = string.Join(";", layerIds);
+        
+        byte[] layerIdsBytes = System.Text.Encoding.UTF8.GetBytes(layerIdsString);
+        
+        data.Set(ClipboardDataFormats.LayerIdList, layerIdsBytes);
+
+        await Clipboard.SetDataObjectAsync(data);
+    }
 
+    private static async Task AddImageToClipboard(Surface actuallySurface, DataObject data)
+    {
         using (ImgData pngData = actuallySurface.DrawingSurface.Snapshot().Encode())
         {
             using MemoryStream pngStream = new MemoryStream();
@@ -81,13 +126,6 @@ internal static class ClipboardController
         WriteableBitmap finalBitmap = actuallySurface.ToWriteableBitmap();
         data.Set(ClipboardDataFormats.Bitmap, finalBitmap); // Bitmap, no transparency
         data.Set(ClipboardDataFormats.Dib, finalBitmap); // DIB format, no transparency
-
-        if (area.Size != document.SizeBindable && area.Pos != VecI.Zero)
-        {
-            data.SetVecI(PositionFormat, area.Pos);
-        }
-
-        await Clipboard.SetDataObjectAsync(data);
     }
 
     /// <summary>
@@ -95,6 +133,18 @@ internal static class ClipboardController
     /// </summary>
     public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
     {
+        Guid[] layerIds = GetLayerIds(data);
+
+        if (layerIds != null && layerIds.Length > 0)
+        {
+            foreach (var layerId in layerIds)
+            {
+                document.Operations.DuplicateLayer(layerId);
+            }
+            
+            return true;
+        }
+        
         List<DataImage> images = GetImage(data);
         if (images.Count == 0)
             return false;
@@ -111,7 +161,7 @@ internal static class ClipboardController
 
             if (pasteAsNew)
             {
-                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer, "New Layer", false);
+                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer, new LocalizedString("NEW_LAYER"), false);
 
                 if (guid == null)
                 {
@@ -133,6 +183,21 @@ internal static class ClipboardController
         return true;
     }
 
+    private static Guid[] GetLayerIds(IEnumerable<IDataObject> data)
+    {
+        foreach (var dataObject in data)
+        {
+            if (dataObject.Contains(ClipboardDataFormats.LayerIdList))
+            {
+                byte[] layerIds = (byte[])dataObject.Get(ClipboardDataFormats.LayerIdList);
+                string layerIdsString = System.Text.Encoding.UTF8.GetString(layerIds);
+                return layerIdsString.Split(';').Select(Guid.Parse).ToArray();
+            }
+        }
+
+        return [];
+    }
+
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
@@ -163,7 +228,7 @@ internal static class ClipboardController
 
             DataObject data = new DataObject();
             data.Set(format, obj);
-            
+
             dataObjects.Add(data);
         }
 
@@ -185,20 +250,21 @@ internal static class ClipboardController
 
         if (data == null)
             return surfaces;
-        
+
         VecI pos = VecI.NegativeOne;
 
         foreach (var dataObject in data)
         {
             if (TryExtractSingleImage(dataObject, out var singleImage))
             {
-                surfaces.Add(new DataImage(singleImage, dataObject.Contains(PositionFormat) ? dataObject.GetVecI(PositionFormat) : pos));
+                surfaces.Add(new DataImage(singleImage,
+                    dataObject.Contains(ClipboardDataFormats.PositionFormat) ? dataObject.GetVecI(ClipboardDataFormats.PositionFormat) : pos));
                 continue;
             }
 
-            if (dataObject.Contains(PositionFormat))
+            if (dataObject.Contains(ClipboardDataFormats.PositionFormat))
             {
-                pos = dataObject.GetVecI(PositionFormat);
+                pos = dataObject.GetVecI(ClipboardDataFormats.PositionFormat);
                 for (var i = 0; i < surfaces.Count; i++)
                 {
                     var surface = surfaces[i];
@@ -237,7 +303,7 @@ internal static class ClipboardController
                     }
 
                     string filename = Path.GetFullPath(path);
-                    surfaces.Add(new DataImage(filename, imported, dataObject.GetVecI(PositionFormat)));
+                    surfaces.Add(new DataImage(filename, imported, dataObject.GetVecI(ClipboardDataFormats.PositionFormat)));
                 }
                 catch
                 {
@@ -365,7 +431,7 @@ internal static class ClipboardController
             }
             else if (HasData(data, ClipboardDataFormats.Dib, ClipboardDataFormats.Bitmap))
             {
-                var imgs = GetImage(new [] { data });
+                var imgs = GetImage(new[] { data });
                 if (imgs == null || imgs.Count == 0)
                 {
                     result = null;

+ 21 - 12
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -558,27 +558,31 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// Takes the selected area and converts it into a surface
     /// </summary>
     /// <returns><see cref="Error"/> on error, <see cref="None"/> for empty <see cref="Surface"/>, <see cref="Surface"/> otherwise.</returns>
-    public OneOf<Error, None, (Surface, RectI)> MaybeExtractSelectedArea(
+    public OneOf<Error, None, (Surface, RectI)> TryExtractArea(RectI bounds,
         IStructureMemberHandler? layerToExtractFrom = null)
     {
         layerToExtractFrom ??= SelectedStructureMember;
         if (layerToExtractFrom is not ILayerHandler layerVm)
             return new Error();
-        if (SelectionPathBindable.IsEmpty)
+        if (bounds.IsZeroOrNegativeArea)
             return new None();
 
-        //TODO: Make sure it's not needed for other layer types
-        IReadOnlyImageNode? layer = (IReadOnlyImageNode?)Internals.Tracker.Document.FindMember(layerVm.Id);
+        IReadOnlyStructureNode? layer = Internals.Tracker.Document.FindMember(layerVm.Id);
         if (layer is null)
             return new Error();
-
-        RectI bounds = (RectI)SelectionPathBindable.TightBounds;
+        
         RectI? memberImageBounds;
         try
         {
-            // TODO: Make sure it must be GetLayerImageAtFrame rather than Rasterize()
-            memberImageBounds = layer.GetLayerImageAtFrame(AnimationDataViewModel.ActiveFrameBindable)
-                .FindChunkAlignedMostUpToDateBounds();
+            if (layer is IReadOnlyImageNode imgNode)
+            {
+                memberImageBounds = imgNode.GetLayerImageAtFrame(AnimationDataViewModel.ActiveFrameBindable)
+                    .FindChunkAlignedMostUpToDateBounds();
+            }
+            else
+            {
+                memberImageBounds = (RectI?)layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime);
+            }
         }
         catch (ObjectDisposedException)
         {
@@ -597,11 +601,16 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         VectorPath clipPath = new VectorPath(SelectionPathBindable) { FillType = PathFillType.EvenOdd };
         clipPath.Transform(Matrix3X3.CreateTranslation(-bounds.X, -bounds.Y));
         output.DrawingSurface.Canvas.Save();
-        output.DrawingSurface.Canvas.ClipPath(clipPath);
+        if (!clipPath.IsEmpty)
+        {
+            output.DrawingSurface.Canvas.ClipPath(clipPath);
+        }
+
         try
         {
-            layer.GetLayerImageAtFrame(AnimationDataViewModel.ActiveFrameBindable)
-                .DrawMostUpToDateRegionOn(bounds, ChunkResolution.Full, output.DrawingSurface, VecI.Zero);
+            using Texture rendered = Renderer.RenderLayer(layerVm.Id, ChunkResolution.Full, AnimationDataViewModel.ActiveFrameTime);
+            using Image snapshot = rendered.DrawingSurface.Snapshot(bounds);
+            output.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0);
         }
         catch (ObjectDisposedException)
         {

+ 80 - 22
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -14,7 +14,9 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Search;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
 using PixiEditor.Numerics;
 using PixiEditor.UI.Common.Fonts;
 
@@ -32,7 +34,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         });
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Cut", "CUT", "CUT_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.X, Modifiers = KeyModifiers.Control,
+    [Command.Basic("PixiEditor.Clipboard.Cut", "CUT", "CUT_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty",
+        Key = Key.X, Modifiers = KeyModifiers.Control,
         MenuItemPath = "EDIT/CUT", MenuItemOrder = 2, Icon = PixiPerfectIcons.Scissors, AnalyticsTrack = true)]
     public async Task Cut()
     {
@@ -43,24 +46,51 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteSelectedPixels(doc.AnimationDataViewModel.ActiveFrameBindable, true);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
+    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
         MenuItemPath = "EDIT/PASTE", MenuItemOrder = 4, Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
-    [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,
+    [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,
         Icon = PixiPerfectIcons.PasteAsNewLayer, AnalyticsTrack = true)]
     public void Paste(bool pasteAsNewLayer)
     {
-        if (Owner.DocumentManagerSubViewModel.ActiveDocument is null) 
+        if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
             return;
-        ClipboardController.TryPasteFromClipboard(Owner.DocumentManagerSubViewModel.ActiveDocument, pasteAsNewLayer);
+
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+
+        Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
+        ClipboardController.TryPasteFromClipboard(doc, pasteAsNewLayer);
+
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            Guid[] newGuids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
+
+            var diff = newGuids.Except(guids).ToArray();
+            if (diff.Length > 0)
+            {
+                doc.Operations.ClearSoftSelectedMembers();
+                doc.Operations.SetSelectedMember(diff[0]);
+
+                for (int i = 1; i < diff.Length; i++)
+                {
+                    doc.Operations.AddSoftSelectedMember(diff[i]);
+                }
+            }
+        });
     }
-    
-    [Command.Basic("PixiEditor.Clipboard.PasteReferenceLayer", "PASTE_REFERENCE_LAYER", "PASTE_REFERENCE_LAYER_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste",
+
+    [Command.Basic("PixiEditor.Clipboard.PasteReferenceLayer", "PASTE_REFERENCE_LAYER",
+        "PASTE_REFERENCE_LAYER_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste",
         Icon = PixiPerfectIcons.PasteReferenceLayer, AnalyticsTrack = true)]
     public async Task PasteReferenceLayer(IDataObject data)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
-        DataImage imageData = (data == null ? await ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(new [] { data })).First();
+        DataImage imageData =
+            (data == null
+                ? await ClipboardController.GetImagesFromClipboard()
+                : ClipboardController.GetImage(new[] { data })).First();
         using var surface = imageData.Image;
 
         var bitmap = imageData.Image.ToWriteableBitmap();
@@ -76,7 +106,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             desktop.MainWindow!.Activate();
         }
     }
-    
+
     [Command.Internal("PixiEditor.Clipboard.PasteReferenceLayerFromPath")]
     public void PasteReferenceLayer(string path)
     {
@@ -91,11 +121,16 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             new VecI(bitmap.Size.X, bitmap.Size.Y));
     }
 
-    [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "PASTE_COLOR", "PASTE_COLOR_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon", AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "PASTE_COLOR_SECONDARY", "PASTE_COLOR_SECONDARY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon", AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "PASTE_COLOR", "PASTE_COLOR_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon",
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "PASTE_COLOR_SECONDARY",
+        "PASTE_COLOR_SECONDARY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor",
+        IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon", AnalyticsTrack = true)]
     public async Task PasteColor(bool secondary)
     {
-        if (!ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out var result))
+        if (!ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty,
+                out var result))
         {
             return;
         }
@@ -110,21 +145,30 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.C, Modifiers = KeyModifiers.Control,
+    [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
+        Key = Key.C, Modifiers = KeyModifiers.Control,
         MenuItemPath = "EDIT/COPY", MenuItemOrder = 3, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
     public async Task Copy()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
+
         await ClipboardController.CopyToClipboard(doc);
     }
 
-    [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", "COPY_COLOR_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "COPY_COLOR_SECONDARY_HEX", "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "COPY_COLOR_SECONDARY_RGB", "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
-    [Command.Filter("PixiEditor.Clipboard.CopyColorToClipboard", "COPY_COLOR_TO_CLIPBOARD", "COPY_COLOR", Key = Key.C, Modifiers = KeyModifiers.Shift, AnalyticsTrack = true)]
+    [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",
+        "COPY_COLOR_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "COPY_COLOR_SECONDARY_HEX",
+        "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon",
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "COPY_COLOR_SECONDARY_RGB",
+        "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon",
+        AnalyticsTrack = true)]
+    [Command.Filter("PixiEditor.Clipboard.CopyColorToClipboard", "COPY_COLOR_TO_CLIPBOARD", "COPY_COLOR", Key = Key.C,
+        Modifiers = KeyModifiers.Shift, AnalyticsTrack = true)]
     public async Task CopyColorAsHex(CopyColor color)
     {
         var targetColor = color switch
@@ -149,18 +193,32 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPaste")]
     public bool CanPaste(object parameter)
     {
-        return Owner.DocumentIsNotNull(null) && parameter is IDataObject data ? ClipboardController.IsImage(data) : ClipboardController.IsImageInClipboard().Result;
+        return Owner.DocumentIsNotNull(null) && parameter is IDataObject data
+            ? ClipboardController.IsImage(data)
+            : ClipboardController.IsImageInClipboard().Result;
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
-    public static async Task<bool> CanPasteColor() => ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
+    public static async Task<bool> CanPasteColor() =>
+        ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
+
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopy")]
+    public bool CanCopy()
+    {
+        return Owner.DocumentManagerSubViewModel.ActiveDocument != null &&
+               (Owner.SelectionSubViewModel.SelectionIsNotEmpty() ||
+                Owner.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel.TransformActive);
+    }
 
     [Evaluator.Icon("PixiEditor.Clipboard.PasteColorIcon")]
     public static async Task<IImage> GetPasteColorIcon()
     {
         Color color;
 
-        color = ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out var result) ? result.Value.ToOpaqueMediaColor() : Colors.Transparent;
+        color = ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty,
+            out var result)
+            ? result.Value.ToOpaqueMediaColor()
+            : Colors.Transparent;
 
         return ColorSearchResult.GetIcon(color.ToOpaqueColor());
     }
@@ -183,7 +241,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         {
             throw new ArgumentException("data must be of type CopyColor, BasicCommand or CommandSearchResult");
         }
-        
+
         var targetColor = color switch
         {
             CopyColor.PrimaryHEX or CopyColor.PrimaryRGB => Owner.ColorsSubViewModel.PrimaryColor,