Browse Source

Clipboard and command changes

flabbet 6 months ago
parent
commit
5887951b83
26 changed files with 443 additions and 227 deletions
  1. 1 1
      src/Drawie
  2. 3 1
      src/PixiEditor.Desktop/Program.cs
  3. 1 1
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  4. 9 2
      src/PixiEditor/Models/Commands/CommandController.cs
  5. 1 0
      src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs
  6. 32 33
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  7. 0 5
      src/PixiEditor/Models/IO/Importer.cs
  8. 1 0
      src/PixiEditor/Styles/PixiEditorPopupTemplate.axaml
  9. 4 2
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  10. 2 1
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  11. 247 129
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  12. 6 4
      src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs
  13. 1 1
      src/PixiEditor/ViewModels/SubViewModels/ExtensionsViewModel.cs
  14. 15 11
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  15. 28 7
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  16. 4 1
      src/PixiEditor/ViewModels/SubViewModels/SearchViewModel.cs
  17. 11 3
      src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs
  18. 5 2
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  19. 8 2
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  20. 2 2
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  21. 4 4
      src/PixiEditor/ViewModels/ViewModelMain.cs
  22. 29 0
      src/PixiEditor/Views/Main/CommandSearch/SearchResultControl.axaml.cs
  23. 0 5
      src/PixiEditor/Views/MainView.axaml
  24. 7 3
      src/PixiEditor/Views/MainView.axaml.cs
  25. 1 1
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml
  26. 21 6
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 24a557b27c764d89bc2c0f9cbf287150cdfce5f9
+Subproject commit 306b9f1b786af58a147166436eb521979f71f18a

+ 3 - 1
src/PixiEditor.Desktop/Program.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia;
+using Avalonia.Logging;
 using Drawie.Interop.VulkanAvalonia;
 
 namespace PixiEditor.Desktop;
@@ -25,8 +26,9 @@ public class Program
             .With(new X11PlatformOptions()
             {
                 RenderingMode = new X11RenderingMode[] { X11RenderingMode.Vulkan, X11RenderingMode.Glx },
-                OverlayPopups = true
+                OverlayPopups = true,
             })
             .WithDrawie()
+            .LogToTrace(LogEventLevel.Verbose, "Vulkan")
             .LogToTrace();
 }

+ 1 - 1
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -177,7 +177,7 @@ internal class ClassicDesktopEntry
             StartupArgs.Args = args;
             StartupArgs.Args.Add("--openedInExisting");
             ViewModels_ViewModelMain viewModel = (ViewModels_ViewModelMain)mainWindow.DataContext;
-            viewModel.StartupCommand.Execute(null);
+            viewModel.OnStartup();
         }
     }
 

+ 9 - 2
src/PixiEditor/Models/Commands/CommandController.cs

@@ -178,8 +178,7 @@ internal class CommandController
     {
         foreach (var evaluator in objectsToInvokeOn)
         {
-            //TODO: Check if performance is better with or without this
-            /*if (evaluator.Methods.CanExecuteEvaluator.DependentOn != null && evaluator.Methods.CanExecuteEvaluator.DependentOn.Contains(propertyName))*/
+            if (evaluator.Methods.CanExecuteEvaluator.DependentOn != null && evaluator.Methods.CanExecuteEvaluator.DependentOn.Contains(propertyName))
             {
                 evaluator.OnCanExecuteChanged();
             }
@@ -673,4 +672,12 @@ internal class CommandController
 
         shortcutFile.SaveShortcuts();
     }
+
+    public static void CanExecuteChanged(string commandPattern)
+    {
+        foreach (var command in Current.Commands.Where(x => x.InternalName.StartsWith(commandPattern)))
+        {
+            command.OnCanExecuteChanged();
+        }
+    }
 }

+ 1 - 0
src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs

@@ -20,6 +20,7 @@ internal class CommandSearchResult : SearchResult
     public CommandSearchResult(Command command)
     {
         Command = command;
+        Command.CanExecuteChanged += () => OnPropertyChanged(nameof(CanExecute));
     }
 
     public override void Execute()

+ 32 - 33
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -161,7 +161,7 @@ internal static class ClipboardController
 
         await Clipboard.SetDataObjectAsync(data);
     }
-    
+
     public static async Task<string> GetTextFromClipboard()
     {
         return await Clipboard.GetTextAsync();
@@ -269,13 +269,13 @@ internal static class ClipboardController
 
             var layer = doc.StructureHelper.Find(layerId);
 
-            if(layer == null) return false;
+            if (layer == null) return false;
 
-            if(tightBounds == null)
+            if (tightBounds == null)
             {
                 tightBounds = layer.TightBounds;
             }
-            else if(layer.TightBounds.HasValue)
+            else if (layer.TightBounds.HasValue)
             {
                 tightBounds = tightBounds.Value.Union(layer.TightBounds.Value);
             }
@@ -434,7 +434,30 @@ internal static class ClipboardController
         return surfaces;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Clipboard.HasImageInClipboard")]
+    public static bool IsImage(IDataObject? dataObject)
+    {
+        if (dataObject == null)
+            return false;
+
+        try
+        {
+            var files = dataObject.GetFileDropList();
+            if (files != null)
+            {
+                if (IsImageFormat(files.Select(x => x.Path.LocalPath).ToArray()))
+                {
+                    return true;
+                }
+            }
+        }
+        catch (COMException)
+        {
+            return false;
+        }
+
+        return HasData(dataObject, ClipboardDataFormats.Png, ClipboardDataFormats.ImageSlashPng);
+    }
+
     public static async Task<bool> IsImageInClipboard()
     {
         var formats = await Clipboard.GetFormatsAsync();
@@ -458,7 +481,7 @@ internal static class ClipboardController
         {
             if (format == DataFormats.Text)
             {
-                string text = await Clipboard.GetTextAsync();
+                string text = await ClipboardController.GetTextFromClipboard();
                 if (Importer.IsSupportedFile(text))
                 {
                     return text;
@@ -466,7 +489,7 @@ internal static class ClipboardController
             }
             else if (format == DataFormats.Files)
             {
-                var files = await Clipboard.GetDataAsync(format);
+                var files = await ClipboardController.Clipboard.GetDataAsync(format);
                 if (files is IEnumerable<IStorageItem> storageFiles)
                 {
                     foreach (var file in storageFiles)
@@ -490,31 +513,7 @@ internal static class ClipboardController
         return string.Empty;
     }
 
-    public static bool IsImage(IDataObject? dataObject)
-    {
-        if (dataObject == null)
-            return false;
-
-        try
-        {
-            var files = dataObject.GetFileDropList();
-            if (files != null)
-            {
-                if (IsImageFormat(files.Select(x => x.Path.LocalPath).ToArray()))
-                {
-                    return true;
-                }
-            }
-        }
-        catch (COMException)
-        {
-            return false;
-        }
-
-        return HasData(dataObject, ClipboardDataFormats.Png, ClipboardDataFormats.ImageSlashPng);
-    }
-
-    private static bool IsImageFormat(string[] formats)
+    public static bool IsImageFormat(string[] formats)
     {
         foreach (var format in formats)
         {
@@ -601,7 +600,7 @@ internal static class ClipboardController
     {
         return await GetIds(ClipboardDataFormats.CelIdList);
     }
-    
+
     public static async Task<Guid[]> GetIds(string format)
     {
         var data = await TryGetDataObject();

+ 0 - 5
src/PixiEditor/Models/IO/Importer.cs

@@ -77,11 +77,6 @@ internal class Importer : ObservableObject
         }
     }
 
-    public static WriteableBitmap ImportWriteableBitmap(string path)
-    {
-        return ImportBitmap(path).ToWriteableBitmap();
-    }
-
     public static DocumentViewModel ImportDocument(string path, bool associatePath = true)
     {
         try

+ 1 - 0
src/PixiEditor/Styles/PixiEditorPopupTemplate.axaml

@@ -12,6 +12,7 @@
     <Style Selector="controls|PixiEditorPopup">
         <Setter Property="WindowStartupLocation" Value="CenterOwner" />
         <Setter Property="TransparencyLevelHint" Value="Transparent" />
+        <Setter Property="SystemDecorations" Value="Full" />
         <Setter Property="ExtendClientAreaChromeHints">
             <Setter.Value>
                 <OnPlatform>

+ 4 - 2
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -261,9 +261,11 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.Operations.UseSrgbProcessing();
     }
 
-    [Evaluator.CanExecute("PixiEditor.DocumentUsesSrgbBlending", nameof(ActiveDocument))]
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesSrgbBlending", nameof(ActiveDocument),
+        nameof(ActiveDocument.UsesSrgbBlending))]
     public bool DocumentUsesSrgbBlending() => ActiveDocument?.UsesSrgbBlending ?? false;
 
-    [Evaluator.CanExecute("PixiEditor.DocumentUsesLinearBlending", nameof(ActiveDocument))]
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesLinearBlending", nameof(ActiveDocument),
+        nameof(ActiveDocument.UsesSrgbBlending))]
     public bool DocumentUsesLinearBlending() => !ActiveDocument?.UsesSrgbBlending ?? true;
 }

+ 2 - 1
src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs

@@ -5,6 +5,7 @@ using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Data;
+using Avalonia.Threading;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Commands.XAML;
 using PixiEditor.Extensions.Common.Localization;
@@ -89,7 +90,7 @@ internal class MenuBarViewModel : PixiObservableObject
             {
                 builder.ModifyMenuTree(nativeMenuItems);
             }
-            
+
             NativeMenu = [];
             foreach (var item in nativeMenuItems)
             {

+ 247 - 129
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -1,10 +1,13 @@
-using System.Collections.Immutable;
+using System.Collections.Concurrent;
+using System.Collections.Immutable;
 using System.Linq;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
 using PixiEditor.Helpers.Extensions;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
@@ -18,6 +21,8 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
+using PixiEditor.Helpers.Constants;
+using PixiEditor.Models.Commands;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Dock;
 using PixiEditor.ViewModels.Document;
@@ -27,6 +32,13 @@ namespace PixiEditor.ViewModels.SubViewModels;
 [Command.Group("PixiEditor.Clipboard", "CLIPBOARD")]
 internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 {
+    private ConcurrentDictionary<string, Task> clipboardTasks = new();
+    private bool canPasteImage;
+    private string lastTextInClipboard;
+    private bool areNodesInClipboard;
+    private bool areCelsInClipboard;
+    private bool hasImageInClipboard;
+
     public ClipboardViewModel(ViewModelMain owner)
         : base(owner)
     {
@@ -62,52 +74,58 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
-        Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
-        ClipboardController.TryPasteFromClipboard(doc, pasteAsNewLayer);
-
-        doc.Operations.InvokeCustomAction(() =>
+        Dispatcher.UIThread.InvokeAsync(async () =>
         {
-            Guid[] newGuids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
+            Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
+            await ClipboardController.TryPasteFromClipboard(doc, pasteAsNewLayer);
 
-            var diff = newGuids.Except(guids).ToArray();
-            if (diff.Length > 0)
+            doc.Operations.InvokeCustomAction(() =>
             {
-                doc.Operations.ClearSoftSelectedMembers();
-                doc.Operations.SetSelectedMember(diff[0]);
+                Guid[] newGuids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
 
-                for (int i = 1; i < diff.Length; i++)
+                var diff = newGuids.Except(guids).ToArray();
+                if (diff.Length > 0)
                 {
-                    doc.Operations.AddSoftSelectedMember(diff[i]);
+                    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",
         Icon = PixiPerfectIcons.PasteReferenceLayer, AnalyticsTrack = true)]
-    public async Task PasteReferenceLayer(IDataObject data)
+    public void PasteReferenceLayer(IDataObject data)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
-        DataImage imageData =
-            (data == null
-                ? await ClipboardController.GetImagesFromClipboard()
-                : ClipboardController.GetImage(new[] { data })).First();
-        using var surface = imageData.Image;
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            DataImage imageData =
+                (data == null
+                    ? await ClipboardController.GetImagesFromClipboard()
+                    : ClipboardController.GetImage(new[] { data })).First();
+            using var surface = imageData.Image;
 
-        var bitmap = imageData.Image.ToWriteableBitmap();
+            var bitmap = imageData.Image.ToWriteableBitmap();
 
-        byte[] pixels = bitmap.ExtractPixels();
+            byte[] pixels = bitmap.ExtractPixels();
 
-        doc.Operations.ImportReferenceLayer(
-            pixels.ToImmutableArray(),
-            imageData.Image.Size);
+            doc.Operations.ImportReferenceLayer(
+                pixels.ToImmutableArray(),
+                imageData.Image.Size);
 
-        if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-        {
-            desktop.MainWindow!.Activate();
-        }
+            if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            {
+                desktop.MainWindow!.Activate();
+            }
+        });
     }
 
     [Command.Internal("PixiEditor.Clipboard.PasteReferenceLayerFromPath")]
@@ -130,143 +148,153 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     [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)
+    public void PasteColor(bool secondary)
     {
-        if (!ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty,
-                out var result))
+        Dispatcher.UIThread.InvokeAsync(async () =>
         {
-            return;
-        }
+            if (!ColorHelper.ParseAnyFormat(
+                    (await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty,
+                    out var result))
+            {
+                return;
+            }
 
-        if (!secondary)
-        {
-            Owner.ColorsSubViewModel.PrimaryColor = result.Value;
-        }
-        else
-        {
-            Owner.ColorsSubViewModel.SecondaryColor = result.Value;
-        }
+            if (!secondary)
+            {
+                Owner.ColorsSubViewModel.PrimaryColor = result.Value;
+            }
+            else
+            {
+                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()
+    public void PasteNodes()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
 
-        Guid[] toDuplicate = await ClipboardController.GetNodeIds();
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            Guid[] toDuplicate = await ClipboardController.GetNodeIds();
 
-        List<Guid> newIds = new();
+            List<Guid> newIds = new();
 
-        Dictionary<Guid, Guid> nodeMapping = new();
+            Dictionary<Guid, Guid> nodeMapping = new();
 
-        using var block = doc.Operations.StartChangeBlock();
+            using var block = doc.Operations.StartChangeBlock();
 
-        foreach (var nodeId in toDuplicate)
-        {
-            Guid? newId = doc.Operations.DuplicateNode(nodeId);
-            if (newId != null)
+            foreach (var nodeId in toDuplicate)
             {
-                newIds.Add(newId.Value);
-                nodeMapping.Add(nodeId, newId.Value);
+                Guid? newId = doc.Operations.DuplicateNode(nodeId);
+                if (newId != null)
+                {
+                    newIds.Add(newId.Value);
+                    nodeMapping.Add(nodeId, newId.Value);
+                }
             }
-        }
 
-        if (newIds.Count == 0)
-            return;
+            if (newIds.Count == 0)
+                return;
 
-        await block.ExecuteQueuedActions();
+            await block.ExecuteQueuedActions();
 
-        ConnectRelatedNodes(doc, nodeMapping);
+            ConnectRelatedNodes(doc, nodeMapping);
 
-        doc.Operations.InvokeCustomAction(() =>
-        {
-            foreach (var node in doc.NodeGraph.AllNodes)
+            doc.Operations.InvokeCustomAction(() =>
             {
-                node.IsNodeSelected = false;
-            }
+                foreach (var node in doc.NodeGraph.AllNodes)
+                {
+                    node.IsNodeSelected = false;
+                }
 
-            foreach (var node in newIds)
-            {
-                var nodeInstance = doc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == node);
-                if (nodeInstance != null)
+                foreach (var node in newIds)
                 {
-                    nodeInstance.IsNodeSelected = true;
+                    var nodeInstance = doc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == node);
+                    if (nodeInstance != null)
+                    {
+                        nodeInstance.IsNodeSelected = true;
+                    }
                 }
-            }
+            });
         });
     }
 
     [Command.Basic("PixiEditor.Clipboard.PasteCels", "PASTE_CELS", "PASTE_CELS_DESCRIPTIVE",
         CanExecute = "PixiEditor.Clipboard.CanPasteCels", Key = Key.V, Modifiers = KeyModifiers.Control,
         ShortcutContexts = [typeof(TimelineDockViewModel)], Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
-    public async Task PasteCels()
+    public void PasteCels()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
 
-        var cels = await ClipboardController.GetCelIds();
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            var cels = await ClipboardController.GetCelIds();
 
-        if (cels.Length == 0)
-            return;
+            if (cels.Length == 0)
+                return;
 
-        using var block = doc.Operations.StartChangeBlock();
+            using var block = doc.Operations.StartChangeBlock();
 
-        List<Guid> newCels = new();
-        List<ICelHandler> celsToSelect = new();
+            List<Guid> newCels = new();
+            List<ICelHandler> celsToSelect = new();
 
-        int minStartFrame = int.MaxValue;
-        
-        foreach (var cel in cels)
-        {
-            var foundCel = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
-            if (foundCel == null)
-                continue;
-            
-            celsToSelect.Add(foundCel);
-            minStartFrame = Math.Min(minStartFrame, foundCel.StartFrameBindable);
-        }
-        
-        int delta = doc.AnimationDataViewModel.ActiveFrameBindable - minStartFrame;
+            int minStartFrame = int.MaxValue;
 
-        foreach (var cel in celsToSelect)
-        {
-            int celFrame = cel.StartFrameBindable + delta;
-            Guid? newCel = doc.AnimationDataViewModel.CreateCel(cel.LayerGuid,
-                celFrame, cel.LayerGuid,
-                cel.StartFrameBindable);
-            if (newCel != null)
+            foreach (var cel in cels)
             {
-                int duration = cel.DurationBindable;
-                doc.Operations.ChangeCelLength(newCel.Value, celFrame, duration);
-                newCels.Add(newCel.Value);
+                var foundCel = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
+                if (foundCel == null)
+                    continue;
+
+                celsToSelect.Add(foundCel);
+                minStartFrame = Math.Min(minStartFrame, foundCel.StartFrameBindable);
             }
-        }
 
-        doc.Operations.InvokeCustomAction(() =>
-        {
-            foreach (var cel in doc.AnimationDataViewModel.AllCels)
+            int delta = doc.AnimationDataViewModel.ActiveFrameBindable - minStartFrame;
+
+            foreach (var cel in celsToSelect)
             {
-                cel.IsSelected = false;
+                int celFrame = cel.StartFrameBindable + delta;
+                Guid? newCel = doc.AnimationDataViewModel.CreateCel(cel.LayerGuid,
+                    celFrame, cel.LayerGuid,
+                    cel.StartFrameBindable);
+                if (newCel != null)
+                {
+                    int duration = cel.DurationBindable;
+                    doc.Operations.ChangeCelLength(newCel.Value, celFrame, duration);
+                    newCels.Add(newCel.Value);
+                }
             }
 
-            foreach (var cel in newCels)
+            doc.Operations.InvokeCustomAction(() =>
             {
-                var celInstance = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
-                if (celInstance != null)
+                foreach (var cel in doc.AnimationDataViewModel.AllCels)
                 {
-                    celInstance.IsSelected = true;
+                    cel.IsSelected = false;
                 }
-            }
+
+                foreach (var cel in newCels)
+                {
+                    var celInstance = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
+                    if (celInstance != null)
+                    {
+                        celInstance.IsSelected = true;
+                    }
+                }
+            });
         });
     }
 
-
-    [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
+    [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)]
@@ -332,13 +360,16 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         "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",
+    [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",
+    [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,
+    [Command.Filter("PixiEditor.Clipboard.CopyColorToClipboard", "COPY_COLOR_TO_CLIPBOARD", "COPY_COLOR",
+        Key = Key.C,
         Modifiers = KeyModifiers.Shift | KeyModifiers.Alt, AnalyticsTrack = true)]
     public async Task CopyColorAsHex(CopyColor color)
     {
@@ -364,45 +395,75 @@ 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;
+        if (!Owner.DocumentIsNotNull(null)) return false;
+
+        if (parameter is IDataObject data)
+            return ClipboardController.IsImage(data);
+
+        QueueCheckCanPasteImage();
+        return canPasteImage;
+    }
+
+    [Evaluator.CanExecute("PixiEditor.Clipboard.HasImageInClipboard")]
+    public bool HasImageInClipboard()
+    {
+        QueueHasImageInClipboard();
+        return hasImageInClipboard;
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyCels")]
     public bool CanCopyCels()
     {
         return Owner.DocumentIsNotNull(null) &&
-               Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.AllCels.Any(x => x.IsSelected);
+               Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.AllCels.Any(
+                   x => x.IsSelected);
     }
 
-    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyNodes")]
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyNodes",
+        nameof(Owner.DocumentManagerSubViewModel),
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument), 
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument.NodeGraph))]
     public bool CanCopyNodes()
     {
         return Owner.DocumentIsNotNull(null) &&
                Owner.DocumentManagerSubViewModel.ActiveDocument.NodeGraph.AllNodes.Any(x => x.IsNodeSelected);
     }
 
-    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteNodes")]
+
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteNodes", 
+        nameof(Owner.DocumentManagerSubViewModel),
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument), nameof(Owner.DocumentManagerSubViewModel.ActiveDocument.NodeGraph))]
     public bool CanPasteNodes()
     {
-        return Owner.DocumentIsNotNull(null) && ClipboardController.AreNodesInClipboard().Result;
+        if (!Owner.DocumentIsNotNull(null)) return false;
+
+        QueueCheckNodesInClipboard();
+        return areNodesInClipboard;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteCels")]
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteCels",
+        nameof(Owner.DocumentManagerSubViewModel),
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument), 
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel))]
     public bool CanPasteCels()
     {
-        return Owner.DocumentIsNotNull(null) && ClipboardController.AreCelsInClipboard().Result;
+        if (!Owner.DocumentIsNotNull(null)) return false;
+
+        QueueCheckCelsInClipboard();
+        return areCelsInClipboard;
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
-    public static async Task<bool> CanPasteColor()
+    public bool CanPasteColor()
     {
-        return ColorHelper.ParseAnyFormat(
-            (await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
+        QueueFetchTextFromClipboard();
+        return ColorHelper.ParseAnyFormat(lastTextInClipboard?.Trim() ?? string.Empty, out _);
     }
 
-    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopy")]
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopy",
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel.TransformActive),
+        nameof(Owner.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember))]
     public bool CanCopy()
     {
         return Owner.DocumentManagerSubViewModel.ActiveDocument != null &&
@@ -412,15 +473,15 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     }
 
     [Evaluator.Icon("PixiEditor.Clipboard.PasteColorIcon")]
-    public static async Task<IImage> GetPasteColorIcon()
+    public IImage GetPasteColorIcon()
     {
         Color color;
 
-        color = ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty,
+        QueueFetchTextFromClipboard();
+        color = ColorHelper.ParseAnyFormat(lastTextInClipboard?.Trim() ?? string.Empty,
             out var result)
             ? result.Value.ToOpaqueMediaColor()
             : Colors.Transparent;
-
         return ColorSearchResult.GetIcon(color.ToOpaqueColor());
     }
 
@@ -480,6 +541,63 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         }
     }
 
+    private void QueueHasImageInClipboard()
+    {
+        QueueClipboardTask("HasImageInClipboard", ClipboardController.IsImageInClipboard, hasImageInClipboard,
+            x => hasImageInClipboard = x);
+    }
+
+    private void QueueCheckCanPasteImage()
+    {
+        QueueClipboardTask("CheckCanPasteImage", ClipboardController.IsImageInClipboard, canPasteImage,
+            x => canPasteImage = x);
+    }
+
+    private void QueueFetchTextFromClipboard()
+    {
+        QueueClipboardTask("FetchTextFromClipboard", ClipboardController.GetTextFromClipboard, lastTextInClipboard,
+            x => lastTextInClipboard = x);
+    }
+
+    private void QueueCheckNodesInClipboard()
+    {
+        QueueClipboardTask("CheckNodesInClipboard", ClipboardController.AreNodesInClipboard, areNodesInClipboard,
+            x => areNodesInClipboard = x);
+    }
+
+    private void QueueCheckCelsInClipboard()
+    {
+        QueueClipboardTask("CheckCelsInClipboard", ClipboardController.AreCelsInClipboard, areCelsInClipboard,
+            x => areCelsInClipboard = x);
+    }
+
+    private void QueueClipboardTask<T>(string key, Func<Task<T>> task, T value, Action<T> updateAction)
+    {
+        if (clipboardTasks.TryGetValue(key, out var t))
+        {
+            return;
+        }
+
+        var newTask = Task.Run(
+            async () =>
+            {
+                T result = await task();
+                if (!EqualityComparer<T>.Default.Equals(result, value))
+                {
+                    updateAction(result);
+                    
+                    Dispatcher.UIThread.Invoke(() =>
+                    {
+                        CommandController.CanExecuteChanged("PixiEditor.Clipboard");
+                    });
+                }
+
+                clipboardTasks.Remove(key, out _);
+            });
+
+        clipboardTasks.TryAdd(key, newTask);
+    }
+
     public enum CopyColor
     {
         PrimaryHEX,

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

@@ -24,6 +24,7 @@ using PixiEditor.Models.ExternalServices;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Palettes;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Windows;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
@@ -103,7 +104,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         Owner.OnStartupEvent += OwnerOnStartupEvent;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Colors.CanReplaceColors")]
+    [Evaluator.CanExecute("PixiEditor.Colors.CanReplaceColors", nameof(DocumentManagerViewModel.ActiveDocument))]
     public bool CanReplaceColors()
     {
         return ViewModelMain.Current?.DocumentManagerSubViewModel?.ActiveDocument is not null;
@@ -163,7 +164,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         return new DrawingImage(new DrawingGroup { Children = new DrawingCollection { oldDrawing, newDrawing } });
     }
 
-    private async void OwnerOnStartupEvent(object sender, EventArgs e)
+    private async void OwnerOnStartupEvent()
     {
         await ImportLospecPalette();
     }
@@ -239,7 +240,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         }
     }
 
-    [Evaluator.CanExecute("PixiEditor.Colors.CanImportPalette")]
+    [Evaluator.CanExecute("PixiEditor.Colors.CanImportPalette", nameof(DocumentManagerViewModel.ActiveDocument))]
     public bool CanImportPalette(List<PaletteColor> paletteColors)
     {
         return paletteColors is not null && Owner.DocumentIsNotNull(paletteColors) && paletteColors.Count > 0;
@@ -261,7 +262,8 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         }
     }
 
-    [Evaluator.CanExecute("PixiEditor.Colors.CanSelectPaletteColor")]
+    [Evaluator.CanExecute("PixiEditor.Colors.CanSelectPaletteColor", nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.Palette))]
     public bool CanSelectPaletteColor(int index)
     {
         var document = Owner.DocumentManagerSubViewModel.ActiveDocument;

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

@@ -24,7 +24,7 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
         windowProvider?.RegisterWindow<PalettesBrowser>();
     }
 
-    private void Owner_OnStartupEvent(object sender, EventArgs e)
+    private void Owner_OnStartupEvent()
     {
         ExtensionLoader.InitializeExtensions(new ExtensionServices(Owner.Services));
     }

+ 15 - 11
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -101,10 +101,11 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
     private void OpenHelloTherePopup()
     {
-        new HelloTherePopup(this).Show();
+        var popup = new HelloTherePopup(this);
+        popup.Show();
     }
 
-    private void Owner_OnStartupEvent(object sender, System.EventArgs e)
+    private void Owner_OnStartupEvent()
     {
         List<string> args = StartupArgs.Args;
         string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
@@ -158,20 +159,23 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.File.OpenFileFromClipboard", "OPEN_FILE_FROM_CLIPBOARD",
         "OPEN_FILE_FROM_CLIPBOARD_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.HasImageInClipboard",
         AnalyticsTrack = true)]
-    public async Task OpenFromClipboard()
+    public void OpenFromClipboard()
     {
-        var images = await ClipboardController.GetImagesFromClipboard();
-
-        foreach (var dataImage in images)
+        Dispatcher.UIThread.InvokeAsync(async () =>
         {
-            if (File.Exists(dataImage.Name))
+            var images = await ClipboardController.GetImagesFromClipboard();
+
+            foreach (var dataImage in images)
             {
+                if (File.Exists(dataImage.Name))
+                {
+                    OpenRegularImage(dataImage.Image, null);
+                    continue;
+                }
+
                 OpenRegularImage(dataImage.Image, null);
-                continue;
             }
-
-            OpenRegularImage(dataImage.Image, null);
-        }
+        });
     }
 
     private bool MakeExistingDocumentActiveIfOpened(string path)

+ 28 - 7
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -49,7 +49,9 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return false;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.CanDeleteSelected")]
+    [Evaluator.CanExecute("PixiEditor.Layer.CanDeleteSelected",
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember))]
     public bool CanDeleteSelected()
     {
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
@@ -58,7 +60,10 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return true;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.HasSelectedMembers")]
+    [Evaluator.CanExecute("PixiEditor.Layer.HasSelectedMembers",
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(DocumentManagerViewModel.ActiveDocument.SoftSelectedStructureMembers))]
     public bool HasSelectedMembers()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -67,7 +72,10 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return doc.SelectedStructureMember is not null || doc.SoftSelectedStructureMembers.Count > 0;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.HasMultipleSelectedMembers")]
+    [Evaluator.CanExecute("PixiEditor.Layer.HasMultipleSelectedMembers",
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(DocumentManagerViewModel.ActiveDocument.SoftSelectedStructureMembers))]
     public bool HasMultipleSelectedMembers()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -255,11 +263,17 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasMask")]
+    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasMask",
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
     public bool ActiveMemberHasMask() =>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false;
 
-    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasNoMask")]
+    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasNoMask",
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
     public bool ActiveLayerHasNoMask() =>
         !Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false;
 
@@ -381,11 +395,18 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         Icon = PixiPerfectIcons.Merge, AnalyticsTrack = true)]
     public void MergeWithBelow() => MergeSelectedWith(false);
 
-    [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerExists")]
+    [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerExists",
+        nameof(ViewModelMain.DocumentManagerSubViewModel),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceTexture))]
     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))]
     public bool ReferenceLayerDoesntExist() =>
         Owner.DocumentManagerSubViewModel.ActiveDocument is not null &&
         Owner.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceTexture is null;

+ 4 - 1
src/PixiEditor/ViewModels/SubViewModels/SearchViewModel.cs

@@ -33,7 +33,10 @@ internal class SearchViewModel : SubViewModel<ViewModelMain>, ISearchHandler
     public SearchViewModel(ViewModelMain owner) : base(owner)
     { }
 
-    [Evaluator.CanExecute("PixiEditor.Search.CanOpenSearchWindow")]
+    [Evaluator.CanExecute("PixiEditor.Search.CanOpenSearchWindow", 
+        nameof(ViewModelMain.DocumentManagerSubViewModel),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.Busy))]
     public bool CanToggleSearchWindow() => !ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Busy ?? true;
 
     [Command.Basic("PixiEditor.Search.Toggle", "", "COMMAND_SEARCH", "OPEN_COMMAND_SEARCH", Key = Key.K, Modifiers = KeyModifiers.Control, CanExecute = "PixiEditor.Search.CanOpenSearchWindow", AnalyticsTrack = true)]

+ 11 - 3
src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.ViewModels.SubViewModels;
 
@@ -45,13 +46,19 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.InvertSelection();
     }
 
-    [Evaluator.CanExecute("PixiEditor.Selection.IsNotEmpty")]
+    [Evaluator.CanExecute("PixiEditor.Selection.IsNotEmpty",
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectionPathBindable),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectionPathBindable.IsEmpty))]
     public bool SelectionIsNotEmpty()
     {
         return !Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectionPathBindable?.IsEmpty ?? false;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Selection.IsNotEmptyAndHasMask")]
+    [Evaluator.CanExecute("PixiEditor.Selection.IsNotEmptyAndHasMask", 
+        nameof(DocumentManagerViewModel.ActiveDocument),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(DocumentManagerViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
     public bool SelectionIsNotEmptyAndHasMask()
     {
         return SelectionIsNotEmpty() && (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false);
@@ -104,7 +111,8 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
         document!.Operations.CropToSelection(document.AnimationDataViewModel.ActiveFrameBindable);
     }
 
-    [Evaluator.CanExecute("PixiEditor.Selection.CanNudgeSelectedObject")]
+    [Evaluator.CanExecute("PixiEditor.Selection.CanNudgeSelectedObject",
+        nameof(DocumentManagerViewModel.ActiveDocument))]
     public bool CanNudgeSelectedObject(int[] dist) => Owner.DocumentManagerSubViewModel.ActiveDocument
         ?.IsChangeFeatureActive<ITransformableExecutor>() ?? false;
 }

+ 5 - 2
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -203,7 +203,9 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         SetActiveToolSet(AllToolSets.ElementAt(nextIndex));
     }
 
-    [Evaluator.CanExecute("PixiEditor.HasNextToolSet")]
+    [Evaluator.CanExecute("PixiEditor.HasNextToolSet",
+        nameof(ActiveToolSet),
+        nameof(AllToolSets))]
     public bool HasNextToolSet(bool next)
     {
         int currentIndex = AllToolSets.IndexOf(ActiveToolSet);
@@ -320,7 +322,8 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
             toolbar.ToolSize = newSize;
     }
 
-    [Evaluator.CanExecute("PixiEditor.Tools.CanChangeToolSize")]
+    [Evaluator.CanExecute("PixiEditor.Tools.CanChangeToolSize",
+        nameof(ActiveTool))]
     public bool CanChangeToolSize() => Owner.ToolsSubViewModel.ActiveTool?.Toolbar is IToolSizeToolbar
                                        && Owner.ToolsSubViewModel.ActiveTool is not PenToolViewModel
                                        {

+ 8 - 2
src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs

@@ -71,7 +71,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     /// </summary>
     /// <param name="property">CommandParameter.</param>
     /// <returns>True if can undo.</returns>
-    [Evaluator.CanExecute("PixiEditor.Undo.CanUndo")]
+    [Evaluator.CanExecute("PixiEditor.Undo.CanUndo", 
+        nameof(ViewModelMain.DocumentManagerSubViewModel), 
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument), 
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.HasSavedUndo))]
     public bool CanUndo()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -88,7 +91,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     /// </summary>
     /// <param name="property">CommandProperty.</param>
     /// <returns>True if can redo.</returns>
-    [Evaluator.CanExecute("PixiEditor.Undo.CanRedo")]
+    [Evaluator.CanExecute("PixiEditor.Undo.CanRedo", 
+        nameof(ViewModelMain.DocumentManagerSubViewModel), 
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument), 
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.HasSavedRedo))]
     public bool CanRedo()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

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

@@ -73,7 +73,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     public async Task<bool> CheckForUpdate()
     {
-        if(IOperatingSystem.Current.Name != "Windows")
+        if(!IOperatingSystem.Current.IsWindows)
         {
             return false;
         }
@@ -249,7 +249,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    private void Owner_OnStartupEvent(object sender, EventArgs e)
+    private void Owner_OnStartupEvent()
     {
         ConditionalUPDATE();
     }

+ 4 - 4
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.Extensions.DependencyInjection;
 using Drawie.Backend.Core.ColorsImpl;
@@ -31,7 +32,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     public IServiceProvider Services { get; private set; }
 
     public Action CloseAction { get; set; }
-    public event EventHandler OnStartupEvent;
+    public event Action OnStartupEvent;
     public FileViewModel FileSubViewModel { get; set; }
     public UpdateViewModel UpdateSubViewModel { get; set; }
     public IToolsHandler ToolsSubViewModel { get; set; }
@@ -277,10 +278,9 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         return false;
     }
 
-    [RelayCommand]
-    private void OnStartup(object parameter)
+    public void OnStartup()
     {
-        OnStartupEvent?.Invoke(this, EventArgs.Empty);
+        OnStartupEvent?.Invoke();
     }
 
     private void OnActiveDocumentChanged(object sender, DocumentChangedEventArgs e)

+ 29 - 0
src/PixiEditor/Views/Main/CommandSearch/SearchResultControl.axaml.cs

@@ -37,6 +37,11 @@ internal partial class SearchResultControl : UserControl, INotifyPropertyChanged
 
     public new event PropertyChangedEventHandler? PropertyChanged;
 
+    static SearchResultControl()
+    {
+        ResultProperty.Changed.Subscribe(ResultChanged);
+    }
+    
     public SearchResultControl()
     {
         InitializeComponent();
@@ -62,4 +67,28 @@ internal partial class SearchResultControl : UserControl, INotifyPropertyChanged
         EvaluatedIcon = icon;
         PropertyChanged?.Invoke(this, new(nameof(EvaluatedIcon)));
     }
+    
+    private void OnResultPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(SearchResult.CanExecute))
+        {
+            EvaluateCanExecute();
+            EvaluateIcon();
+        }
+    }
+    
+    private static void ResultChanged(AvaloniaPropertyChangedEventArgs<SearchResult> e)
+    {
+        if (e.Sender is SearchResultControl control)
+        {
+            if (e.OldValue.Value != null)
+            {
+                e.OldValue.Value.PropertyChanged -= control.OnResultPropertyChanged;
+            }
+            if (e.NewValue.Value != null)
+            {
+                e.NewValue.Value.PropertyChanged += control.OnResultPropertyChanged;
+            }
+        }
+    }
 }

+ 0 - 5
src/PixiEditor/Views/MainView.axaml

@@ -15,11 +15,6 @@
     <Design.DataContext>
         <viewModels1:ViewModelMain />
     </Design.DataContext>
-    <Interaction.Behaviors>
-        <EventTriggerBehavior EventName="Loaded">
-            <InvokeCommandAction Command="{Binding StartupCommand}" />
-        </EventTriggerBehavior>
-    </Interaction.Behaviors>
     <Grid DragDrop.AllowDrop="True" Name="DropGrid">
         <!--A hacky way to fix first element not rendering in OpenGL render api--> 
         <visuals:TextureControl Name="OpenGlInitDummy" IsVisible="False"/>

+ 7 - 3
src/PixiEditor/Views/MainView.axaml.cs

@@ -27,12 +27,11 @@ public partial class MainView : UserControl
         DropGrid.AddHandler(DragDrop.DragEnterEvent, MainView_DragEnter);
         DropGrid.AddHandler(DragDrop.DragLeaveEvent, MainView_DragLeave);
         DropGrid.AddHandler(DragDrop.DropEvent, MainView_Drop);
+        Loaded += OnLoaded;
     }
 
-    protected override void OnLoaded(RoutedEventArgs e)
+    private void OnLoaded(object? sender, RoutedEventArgs e)
     {
-        base.OnLoaded(e);
-
         // hacky way to fix first element not rendering
         // feel free to make a proper fix inside Drawie
         if (IDrawieInteropContext.Current is OpenGlInteropContext)
@@ -47,6 +46,11 @@ public partial class MainView : UserControl
                 OpenGlInitDummy.IsVisible = false;
             });
         }
+
+        if (DataContext is ViewModelMain vm)
+        {
+            vm.OnStartup();
+        }
     }
 
     private void MainView_Drop(object sender, DragEventArgs e)

+ 1 - 1
src/PixiEditor/Views/Windows/HelloTherePopup.axaml

@@ -17,7 +17,7 @@
                          xmlns:windows="clr-namespace:PixiEditor.Views.Windows"
                          mc:Ignorable="d"
                          Title="Hello there!" Height="680" Width="982" MinHeight="500" MinWidth="600"
-                         Loaded="HelloTherePopup_OnLoaded">
+                         >
 
     <Window.Styles>
         <Style Selector="TextBlock">

+ 21 - 6
src/PixiEditor/Views/Windows/HelloTherePopup.axaml.cs

@@ -94,7 +94,7 @@ internal partial class HelloTherePopup : PixiEditorPopup
 
     public AsyncRelayCommand OpenNewFileCommand { get; set; }
 
-    public AsyncRelayCommand NewFromClipboardCommand { get; set; }
+    public RelayCommand NewFromClipboardCommand { get; set; }
 
     public RelayCommand<string> OpenRecentCommand { get; set; }
 
@@ -116,6 +116,8 @@ internal partial class HelloTherePopup : PixiEditorPopup
 #endif
 
     private bool _newsDisabled = false;
+    
+    private bool hasImageInClipboard = false;
 
     public HelloTherePopup(FileViewModel fileViewModel)
     {
@@ -127,7 +129,7 @@ internal partial class HelloTherePopup : PixiEditorPopup
         OpenRecentCommand = new RelayCommand<string>(OpenRecent);
         SetShowAllBetaExamplesCommand = new RelayCommand<bool>(SetShowAllBetaExamples);
         OpenInExplorerCommand = new RelayCommand<string>(OpenInExplorer, CanOpenInExplorer);
-        NewFromClipboardCommand = new AsyncRelayCommand(NewFromClipboard, CanOpenFromClipboard);
+        NewFromClipboardCommand = new RelayCommand(NewFromClipboard, CanOpenFromClipboard);
 
         RecentlyOpenedEmpty = RecentlyOpened.Count == 0;
         RecentlyOpened.CollectionChanged += RecentlyOpened_CollectionChanged;
@@ -136,6 +138,8 @@ internal partial class HelloTherePopup : PixiEditorPopup
 
         NewsProvider = new NewsProvider();
 
+        CheckHasClipboardInImage();
+
         Closing += (_, _) => { IsClosing = true; };
 
         Activated += RefreshClipboardImg;
@@ -217,21 +221,32 @@ internal partial class HelloTherePopup : PixiEditorPopup
 
     private void RefreshClipboardImg(object? sender, EventArgs e)
     {
-        NewFromClipboardCommand.NotifyCanExecuteChanged();
+        CheckHasClipboardInImage();
+    }
+
+    private void CheckHasClipboardInImage()
+    {
+        Task.Run(async () =>
+        {
+            hasImageInClipboard = await ClipboardController.IsImageInClipboard();
+        }).ContinueWith(_ =>
+        {
+            Dispatcher.UIThread.Invoke(NewFromClipboardCommand.NotifyCanExecuteChanged);
+        });
     }
 
 
-    private async Task NewFromClipboard()
+    private void NewFromClipboard()
     {
         Activated -= RefreshClipboardImg;
         Application.Current.ForDesktopMainWindow(mainWindow => mainWindow.Activate());
+        FileViewModel.OpenFromClipboard();
         Close();
-        await FileViewModel.OpenFromClipboard();
     }
 
     private bool CanOpenFromClipboard()
     {
-        return ClipboardController.IsImageInClipboard().Result;
+        return hasImageInClipboard;
     }
 
     private void OpenRecent(string parameter)