Browse Source

Merge pull request #738 from PixiEditor/linux

Linux specific fixes and adjustments
Krzysztof Krysiński 6 months ago
parent
commit
7423e55547
52 changed files with 935 additions and 330 deletions
  1. 39 0
      .env-dev/run-build-bin.nix
  2. 35 0
      .env-dev/run-rider.nix
  3. 34 0
      .env-dev/shell.nix
  4. 1 0
      .gitignore
  5. 1 1
      src/Drawie
  6. 1 1
      src/PixiDocks
  7. 4 1
      src/PixiEditor.ChangeableDocument.Gen/Helpers.cs
  8. 3 1
      src/PixiEditor.ChangeableDocument.Gen/MethodInfo.cs
  9. 6 46
      src/PixiEditor.ChangeableDocument.Gen/PixiEditor.ChangeableDocument.Gen.csproj
  10. 3 1
      src/PixiEditor.ChangeableDocument.Gen/UpdateableChangeActionGenerator.cs
  11. 3 1
      src/PixiEditor.Desktop/Program.cs
  12. 1 1
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  13. 1 1
      src/PixiEditor.Gen/PixiEditor.Gen.csproj
  14. 84 0
      src/PixiEditor.Linux/LinuxInputKeys.cs
  15. 13 6
      src/PixiEditor.Linux/LinuxOperatingSystem.cs
  16. 56 0
      src/PixiEditor.Linux/LinuxProcessUtility.cs
  17. 3 4
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  18. 6 4
      src/PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs
  19. 10 3
      src/PixiEditor/Models/Commands/CommandController.cs
  20. 1 1
      src/PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs
  21. 1 0
      src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs
  22. 32 33
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  23. 13 7
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  24. 0 5
      src/PixiEditor/Models/IO/Importer.cs
  25. 15 15
      src/PixiEditor/PixiEditor.csproj
  26. 12 1
      src/PixiEditor/Styles/PixiEditorPopupTemplate.axaml
  27. 4 2
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  28. 2 1
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  29. 247 129
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  30. 6 4
      src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs
  31. 1 1
      src/PixiEditor/ViewModels/SubViewModels/ExtensionsViewModel.cs
  32. 15 11
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  33. 28 7
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  34. 4 1
      src/PixiEditor/ViewModels/SubViewModels/SearchViewModel.cs
  35. 11 3
      src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs
  36. 5 2
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  37. 8 2
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  38. 2 2
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  39. 5 4
      src/PixiEditor/ViewModels/ViewModelMain.cs
  40. 1 1
      src/PixiEditor/Views/Dialogs/DialogTitleBar.axaml
  41. 1 0
      src/PixiEditor/Views/Dialogs/DialogTitleBar.axaml.cs
  42. 64 4
      src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs
  43. 21 4
      src/PixiEditor/Views/Input/NumberInput.cs
  44. 29 0
      src/PixiEditor/Views/Main/CommandSearch/SearchResultControl.axaml.cs
  45. 1 0
      src/PixiEditor/Views/Main/MainTitleBar.axaml.cs
  46. 0 5
      src/PixiEditor/Views/MainView.axaml
  47. 7 3
      src/PixiEditor/Views/MainView.axaml.cs
  48. 10 0
      src/PixiEditor/Views/MainWindow.axaml
  49. 62 3
      src/PixiEditor/Views/MainWindow.axaml.cs
  50. 1 1
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml
  51. 21 6
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml.cs
  52. 1 1
      src/global.json

+ 39 - 0
.env-dev/run-build-bin.nix

@@ -0,0 +1,39 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+
+(pkgs.buildFHSEnv {
+  name = "pixieditor-env";
+  targetPkgs = pkgs: (with pkgs; [
+    dotnet-sdk
+    avalonia
+    fontconfig
+    alsa-lib
+    glew
+    udev
+    gnumake 
+    vulkan-headers
+    vulkan-loader
+    vulkan-validation-layers
+    vulkan-tools
+    vulkan-tools-lunarg
+    powershell
+  ]) ++ (with pkgs.xorg; [
+   libX11
+    libICE
+    libSM
+    libXi
+    libXcursor
+    libXext
+    libXrandr  ]);
+
+  multiPkgs = pkgs: (with pkgs; [
+   udev
+   alsa-lib
+  ]);
+
+  runScript = "nohup ./PixiEditor &";
+}).env
+
+
+
+

+ 35 - 0
.env-dev/run-rider.nix

@@ -0,0 +1,35 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+
+(pkgs.buildFHSEnv {
+  name = "rider-env";
+  targetPkgs = pkgs: (with pkgs; [
+    dotnet-sdk
+    avalonia
+    fontconfig
+    alsa-lib
+    glew
+    udev
+    gnumake 
+    vulkan-headers
+    vulkan-loader
+    vulkan-validation-layers
+    vulkan-tools
+    vulkan-tools-lunarg
+    powershell
+  ]) ++ (with pkgs.xorg; [
+   libX11
+    libICE
+    libSM
+    libXi
+    libXcursor
+    libXext
+    libXrandr  ]);
+
+  multiPkgs = pkgs: (with pkgs; [
+   udev
+   alsa-lib
+  ]);
+
+  runScript = "nohup rider &";
+}).env

+ 34 - 0
.env-dev/shell.nix

@@ -0,0 +1,34 @@
+{ pkgs ? import <nixpkgs> { } }:
+
+with pkgs;
+let
+
+dotnet = dotnet-sdk; 
+
+in mkShell {
+  name = "avalonia-env";
+  packages = (with pkgs; [
+    dotnet
+    avalonia
+    fontconfig
+    alsa-lib
+    glew
+    udev
+    gnumake 
+    vulkan-headers
+    vulkan-loader
+    vulkan-validation-layers
+    vulkan-tools
+    vulkan-tools-lunarg
+    powershell
+  ]) ++ (with pkgs.xorg; [
+   libX11
+    libICE
+    libSM
+    libXi
+    libXcursor
+    libXext
+    libXrandr  ]);
+
+    DOTNET_ROOT = "${dotnet}";
+}

+ 1 - 0
.gitignore

@@ -340,3 +340,4 @@ GitIgnore
 
 Cache/
 .DS_Store
+nohup.out

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit dc3886c56d8df2596f01098622f18a859e2a82a4
+Subproject commit 306b9f1b786af58a147166436eb521979f71f18a

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 47107d7dc284e04ed92e4c470a6ed2f972e5d9cd
+Subproject commit 17c222b262301f0f38687dfbb7545e58d880b9f3

+ 4 - 1
src/PixiEditor.ChangeableDocument.Gen/Helpers.cs

@@ -1,4 +1,7 @@
-using System.Text;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 

+ 3 - 1
src/PixiEditor.ChangeableDocument.Gen/MethodInfo.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.ChangeableDocument.Gen
+using System.Collections.Generic;
+
+namespace PixiEditor.ChangeableDocument.Gen
 {
     internal record struct MethodInfo(string Name, List<TypeWithName> Arguments, NamespacedType ContainingClass);
 }

+ 6 - 46
src/PixiEditor.ChangeableDocument.Gen/PixiEditor.ChangeableDocument.Gen.csproj

@@ -1,57 +1,17 @@
 <Project Sdk="Microsoft.NET.Sdk">
-
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
-    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
-    <ImplicitUsings>true</ImplicitUsings>
-    <LangVersion>Latest</LangVersion>
-    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
-    <Platforms>AnyCPU</Platforms>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">
-    <Optimize>True</Optimize>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|x64'">
-    <Optimize>True</Optimize>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|x86'">
-    <Optimize>True</Optimize>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x64' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
+    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
   </ItemGroup>
+
   <ItemGroup>
     <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
   </ItemGroup>
-</Project>
+</Project>

+ 3 - 1
src/PixiEditor.ChangeableDocument.Gen/UpdateableChangeActionGenerator.cs

@@ -1,4 +1,6 @@
-using Microsoft.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Microsoft.CodeAnalysis.Text;
 

+ 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.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -45,7 +45,7 @@
   <Target Name="GenerateProtoContracts" BeforeTargets="BeforeCompile"
           Inputs="$(MSBuildProjectDirectory)\DataContracts\*.proto"
           Outputs="$(MSBuildProjectDirectory)\ProtoAutogen\*.cs">
-    <Exec Command="dotnet tool run --allow-roll-forward protogen --csharp_out=ProtoAutogen --proto_path=DataContracts +listset=yes *.proto"/>
+    <Exec Command="dotnet tool run protogen --csharp_out=ProtoAutogen --proto_path=DataContracts +listset=yes *.proto"/>
 
     <ItemGroup>
       <Compile Include="ProtoAutogen\*.cs" KeepDuplicates="false"/>

+ 1 - 1
src/PixiEditor.Gen/PixiEditor.Gen.csproj

@@ -4,7 +4,7 @@
     <TargetFramework>netstandard2.0</TargetFramework>
     <IncludeBuildOutput>true</IncludeBuildOutput>
     <Nullable>enable</Nullable>
-    <ImplicitUsings>enable</ImplicitUsings>
+    <ImplicitUsings>true</ImplicitUsings>
     <LangVersion>latest</LangVersion>
     <RootNamespace>PixiEditorGen</RootNamespace>
   </PropertyGroup>

+ 84 - 0
src/PixiEditor.Linux/LinuxInputKeys.cs

@@ -0,0 +1,84 @@
+using Avalonia.Input;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Linux;
+
+internal class LinuxInputKeys : IInputKeys
+{
+    public string GetKeyboardKey(Key key, bool forceInvariant = false)
+    {
+        return MapKey(key);
+    }
+
+    public bool ModifierUsesSymbol(KeyModifiers modifier) => false;
+
+    private string MapKey(Key key)
+    {
+        // at the moment only latin keys are supported
+
+        return key switch
+        {
+            Key.Back => "Backspace",
+            Key.Tab => "Tab",
+            Key.Return => "↵",
+            Key.CapsLock => "Caps Lock",
+            Key.Escape => "Esc",
+            Key.Space => "Space",
+            Key.PageUp => "Page Up",
+            Key.PageDown => "Page Down",
+            Key.D0 => "0",
+            Key.D1 => "1",
+            Key.D2 => "2",
+            Key.D3 => "3",
+            Key.D4 => "4",
+            Key.D5 => "5",
+            Key.D6 => "6",
+            Key.D7 => "7",
+            Key.D8 => "8",
+            Key.D9 => "9",
+            Key.LWin => "Super",
+            Key.RWin => "Super",
+            Key.NumPad0 => "0",
+            Key.NumPad1 => "1",
+            Key.NumPad2 => "2",
+            Key.NumPad3 => "3",
+            Key.NumPad4 => "4",
+            Key.NumPad5 => "5",
+            Key.NumPad6 => "6",
+            Key.NumPad7 => "7",
+            Key.NumPad8 => "8",
+            Key.NumPad9 => "9",
+            Key.Multiply => "*",
+            Key.Add => "+",
+            Key.Separator => ",",
+            Key.Subtract => "-",
+            Key.Decimal => ".",
+            Key.Divide => "/",
+            Key.NumLock => "Num Lock",
+            Key.LeftShift => "Shift",
+            Key.RightShift => "Shift",
+            Key.LeftCtrl => "Ctrl",
+            Key.RightCtrl => "Ctrl",
+            Key.LeftAlt => "Alt",
+            Key.RightAlt => "Alt",
+            Key.OemSemicolon => ";",
+            Key.OemPlus => "=",
+            Key.OemComma => ",",
+            Key.OemMinus => "-",
+            Key.OemPeriod => ".",
+            Key.OemQuestion => "/",
+            Key.OemTilde => "`",
+            Key.OemOpenBrackets => "[",
+            Key.OemPipe => "\\",
+            Key.OemCloseBrackets => "]",
+            Key.OemQuotes => "'",
+            Key.OemBackslash => "\\",
+            Key.FnLeftArrow => "Left Arrow",
+            Key.FnRightArrow => "Right Arrow",
+            Key.FnUpArrow => "Up Arrow",
+            Key.FnDownArrow => "Down Arrow",
+            Key.MediaHome => "Home",
+            _ => key.ToString()
+        };
+    }
+}

+ 13 - 6
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -10,19 +10,26 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
     public string Name { get; } = "Linux";
     public string AnalyticsId => "Linux";
     public string AnalyticsName => LinuxOSInformation.FromReleaseFile().ToString();
-    public IInputKeys InputKeys { get; }
-    public IProcessUtility ProcessUtility { get; }
+    public IInputKeys InputKeys { get; } = new LinuxInputKeys();
+    public IProcessUtility ProcessUtility { get; } = new LinuxProcessUtility();
 
     public string ExecutableExtension { get; } = string.Empty;
 
     public void OpenUri(string uri)
     {
-        throw new NotImplementedException();
+        ProcessUtility.Execute($"xdg-open", uri);
     }
 
     public void OpenFolder(string path)
     {
-        throw new NotImplementedException();
+        try
+        {
+            ProcessUtility.Execute($"dbus-send", $"--session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\"");
+        }
+        catch (Exception e)
+        {
+            ProcessUtility.Execute($"xdg-open", Path.GetDirectoryName(path));
+        }
     }
 
     public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime)
@@ -32,12 +39,12 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
 
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
     {
-        throw new NotImplementedException();
+        // TODO: Check if this is executed on Linux at all
     }
 
     public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs)
     {
-        throw new NotImplementedException();
+        // TODO: Check if this is executed on Linux at all
     }
 
     class LinuxOSInformation

+ 56 - 0
src/PixiEditor.Linux/LinuxProcessUtility.cs

@@ -0,0 +1,56 @@
+using System.Diagnostics;
+using System.Net;
+using System.Security;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Linux;
+
+public class LinuxProcessUtility : IProcessUtility
+{
+    public Process RunAsAdmin(string path)
+    {
+        throw new NotImplementedException("Running as admin is not supported on Linux");
+    }
+
+    public Process RunAsAdmin(string path, bool createWindow)
+    {
+        throw new NotImplementedException("Running as admin is not supported on Linux");
+    }
+
+    public bool IsRunningAsAdministrator()
+    {
+        return Environment.IsPrivilegedProcess;
+    }
+
+    public Process ShellExecute(string toExecute)
+    {
+        Process process = new Process();
+        process.StartInfo.FileName = toExecute;
+        process.StartInfo.UseShellExecute = true;
+        process.Start();
+        
+        return process;
+    }
+
+    public Process ShellExecute(string toExecute, string args)
+    {
+        Process process = new Process();
+        process.StartInfo.FileName = toExecute;
+        process.StartInfo.Arguments = args;
+        process.StartInfo.UseShellExecute = true;
+        process.Start();
+        
+        return process;
+    }
+
+    public Process Execute(string path, string args)
+    {
+        Process process = new Process();
+        process.StartInfo.FileName = path;
+        process.StartInfo.Arguments = args;
+        process.StartInfo.UseShellExecute = false;
+        process.Start();
+        
+        return process;
+    }
+}

+ 3 - 4
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -59,7 +59,6 @@ internal class ClassicDesktopEntry
         StartupArgs.Args = e.Args.ToList();
         string arguments = string.Join(' ', e.Args);
 
-        Dispatcher dispatcher = Dispatcher.UIThread;
         InitOperatingSystem();
 
         if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
@@ -85,9 +84,9 @@ internal class ClassicDesktopEntry
 
             return;
         }
-
+        
 #if !STEAM && !DEBUG
-        if (!HandleNewInstance(dispatcher))
+        if (!HandleNewInstance(Dispatcher.UIThread))
         {
             return;
         }
@@ -177,7 +176,7 @@ internal class ClassicDesktopEntry
             StartupArgs.Args = args;
             StartupArgs.Args.Add("--openedInExisting");
             ViewModels_ViewModelMain viewModel = (ViewModels_ViewModelMain)mainWindow.DataContext;
-            viewModel.StartupCommand.Execute(null);
+            viewModel.OnStartup();
         }
     }
 

+ 6 - 4
src/PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs

@@ -1,20 +1,22 @@
-namespace PixiEditor.Models.Commands.Attributes.Evaluators;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Models.Commands.Attributes.Evaluators;
 
 internal partial class Evaluator
 {
     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
     internal class CanExecuteAttribute : EvaluatorAttribute
     {
-        //public string[] DependentOn { get; }
+        public string[] DependentOn { get; }
 
         public CanExecuteAttribute([InternalName] string name) : base(name)
         {
-            //DependentOn = new[] { nameof(DocumentManagerViewModel.ActiveDocument) }; // ActiveDocument will be required 99% of the time, so we'll just add it by default
+            DependentOn = new[] { nameof(DocumentManagerViewModel.ActiveDocument) }; // ActiveDocument will be required 99% of the time, so we'll just add it by default
         }
 
         public CanExecuteAttribute([InternalName] string name, params string[] dependentOn) : base(name)
         {
-            //DependentOn = dependentOn;
+            DependentOn = dependentOn;
         }
     }
 }

+ 10 - 3
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();
             }
@@ -603,7 +602,7 @@ internal class CommandController
                                     evaluateFunction => new CanExecuteEvaluator()
                                     {
                                         Name = attribute.Name, Evaluate = evaluateFunction.Invoke,
-                                        /*DependentOn = canExecuteAttribute.DependentOn*/
+                                        DependentOn = canExecuteAttribute.DependentOn
                                     });
                                 break;
                             }
@@ -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 - 1
src/PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs

@@ -7,7 +7,7 @@ internal class CanExecuteEvaluator : Evaluator<bool>
     public static CanExecuteEvaluator AlwaysTrue { get; } = new StaticValueEvaluator(true);
 
     public static CanExecuteEvaluator AlwaysFalse { get; } = new StaticValueEvaluator(false);
-    public string[]? DependentOn { get; set; } // TODO: It is used in CanExecuteChanged event, but it's commented out because it might not impact performance
+    public string[]? DependentOn { get; set; }
 
     private class StaticValueEvaluator : CanExecuteEvaluator
     {

+ 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();

+ 13 - 7
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -31,6 +31,8 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
     private Font? cachedFont;
     private bool isListeningForValidLayer;
     private VectorPath? onPath;
+    
+    private List<Font> fontsToDispose = new();
 
     public override bool BlocksOtherActions => false;
 
@@ -134,6 +136,16 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
     {
         internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
         document.TextOverlayHandler.Hide();
+        
+        foreach (var font in fontsToDispose)
+        {
+            if (font != null && !font.IsDisposed)
+            {
+                font.Dispose();
+            }
+        }
+        
+        fontsToDispose.Clear();
     }
 
     public void OnTextChanged(string text)
@@ -156,12 +168,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
 
         if (name == nameof(ITextToolbar.FontFamily))
         {
-            Font toDispose = cachedFont;
-            Dispatcher.UIThread.Post(() =>
-            {
-                toDispose?.Dispose();
-            });
-
+            fontsToDispose.Add(cachedFont);
             cachedFont = toolbar.ConstructFont();
             document.TextOverlayHandler.Font = cachedFont;
         }
@@ -257,7 +264,6 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
             AntiAlias = toolbar.AntiAliasing,
             Path = onPath,
             // TODO: MaxWidth = toolbar.MaxWidth
-            // TODO: Path
         };
     }
 

+ 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

+ 15 - 15
src/PixiEditor/PixiEditor.csproj

@@ -51,7 +51,7 @@
   <ItemGroup Condition="'$(Configuration)' == 'DevSteam'">
     <ProjectReference Include="..\PixiEditor.Platform.Steam\PixiEditor.Platform.Steam.csproj"/>
   </ItemGroup>
-  
+
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
   </ItemGroup>
@@ -59,15 +59,15 @@
   <ItemGroup Condition=" '$(Configuration)' == 'Release' ">
     <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
   </ItemGroup>
-  
+
   <ItemGroup Condition=" '$(Configuration)' == 'DevRelease' ">
     <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
   </ItemGroup>
-  
+
   <ItemGroup Condition=" '$(Configuration)' == 'MSIX' ">
     <ProjectReference Include="..\PixiEditor.Platform.MSStore\PixiEditor.Platform.MSStore.csproj"/>
   </ItemGroup>
-  
+
   <ItemGroup Condition=" '$(Configuration)' == 'MSIX Debug' ">
     <ProjectReference Include="..\PixiEditor.Platform.MSStore\PixiEditor.Platform.MSStore.csproj"/>
   </ItemGroup>
@@ -83,31 +83,31 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0" />
+    <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Labs.Lottie" Version="11.2.0"/>
     <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0" />
+    <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0"/>
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="ByteSize" Version="2.1.2" />
+    <PackageReference Include="ByteSize" Version="2.1.2"/>
     <PackageReference Include="CLSEncoderDecoder" Version="1.0.0"/>
-    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
+    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2"/>
     <PackageReference Include="DiscordRichPresence" Version="1.2.1.24"/>
-    <PackageReference Include="Hardware.Info" Version="101.0.0" />
-    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
+    <PackageReference Include="Hardware.Info" Version="101.0.0"/>
+    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0"/>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
     <PackageReference Include="PixiEditor.ColorPicker.AvaloniaUI" Version="1.0.6"/>
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\Drawie\src\Drawie.Interop.Avalonia\Drawie.Interop.Avalonia.csproj" />
-    <ProjectReference Include="..\Drawie\src\Drawie.Interop.Avalonia.Core\Drawie.Interop.Avalonia.Core.csproj" />
+    <ProjectReference Include="..\Drawie\src\Drawie.Interop.Avalonia\Drawie.Interop.Avalonia.csproj"/>
+    <ProjectReference Include="..\Drawie\src\Drawie.Interop.Avalonia.Core\Drawie.Interop.Avalonia.Core.csproj"/>
     <ProjectReference Include="..\PixiDocks\src\PixiDocks.Avalonia\PixiDocks.Avalonia.csproj"/>
-    <ProjectReference Include="..\PixiEditor.SVG\PixiEditor.SVG.csproj" />
+    <ProjectReference Include="..\PixiEditor.SVG\PixiEditor.SVG.csproj"/>
     <ProjectReference Include="..\PixiParser\src\PixiParser.Skia\PixiParser.Skia.csproj"/>
     <ProjectReference Include="..\PixiParser\src\PixiParser\PixiParser.csproj"/>
     <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj"/>
@@ -146,7 +146,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <Folder Include="Extensions\" />
+    <Folder Include="Extensions\"/>
   </ItemGroup>
 
 </Project>

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

@@ -12,6 +12,16 @@
     <Style Selector="controls|PixiEditorPopup">
         <Setter Property="WindowStartupLocation" Value="CenterOwner" />
         <Setter Property="TransparencyLevelHint" Value="Transparent" />
+        <Setter Property="SystemDecorations">
+            <OnPlatform>
+                <OnPlatform.Default>
+                    <SystemDecorations>Full</SystemDecorations>
+                </OnPlatform.Default>
+                <OnPlatform.Linux>
+                    <SystemDecorations>None</SystemDecorations>
+                </OnPlatform.Linux>
+            </OnPlatform>
+        </Setter>
         <Setter Property="ExtendClientAreaChromeHints">
             <Setter.Value>
                 <OnPlatform>
@@ -35,9 +45,10 @@
         <Setter Property="Template">
             <ControlTemplate>
                 <VisualLayerManager>
-                    <Panel>
+                    <Panel Name="PART_ResizePanel">
                         <DockPanel>
                             <controls:DialogTitleBar
+                                Name="PART_TitleBar"
                                 DockPanel.Dock="Top" 
                                 CloseCommand="{TemplateBinding CloseCommand}"
                                 CanMinimize="{TemplateBinding CanMinimize}"

+ 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();
     }

+ 5 - 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;
@@ -16,6 +17,7 @@ using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Handlers;
+using PixiEditor.OperatingSystem;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Menu;
 using PixiEditor.ViewModels.SubViewModels;
@@ -31,7 +33,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 +279,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)

+ 1 - 1
src/PixiEditor/Views/Dialogs/DialogTitleBar.axaml

@@ -23,7 +23,7 @@
         <DockPanel IsHitTestVisible="True">
             <CaptionButtons Name="captionButtons" DockPanel.Dock="Right" IsVisible="{OnPlatform macOS=false, Default=true}"/>
             <ContentPresenter DockPanel.Dock="Right" IsVisible="{Binding !!AdditionalElement}" Content="{Binding Path=AdditionalElement}"/>
-            <Control /><!-- dummy control to occupy dockpanel center -->
+            <Panel Background="Transparent" IsHitTestVisible="True" /><!-- dummy control to occupy dockpanel center -->
         </DockPanel>
     </Grid>
 </UserControl>

+ 1 - 0
src/PixiEditor/Views/Dialogs/DialogTitleBar.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Chrome;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Interactivity;
 using PixiEditor.Extensions.UI;
 

+ 64 - 4
src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs

@@ -2,8 +2,14 @@
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Styling;
+using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
+using PixiDocks.Avalonia.Helpers;
 using PixiEditor.Extensions.CommonApi;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Windowing;
@@ -11,6 +17,8 @@ using PixiEditor.Extensions.UI;
 
 namespace PixiEditor.Views.Dialogs;
 
+[TemplatePart("PART_ResizePanel", typeof(Panel))]
+[TemplatePart("Part_TitleBar", typeof(DialogTitleBar))]
 public partial class PixiEditorPopup : Window, IPopupWindow
 {
     public string UniqueId => "PixiEditor.Popup";
@@ -21,8 +29,9 @@ public partial class PixiEditorPopup : Window, IPopupWindow
     public static readonly StyledProperty<bool> CloseIsHideProperty = AvaloniaProperty.Register<PixiEditorPopup, bool>(
         nameof(CloseIsHide), defaultValue: false);
 
-    public static readonly StyledProperty<ICommand> CloseCommandProperty = AvaloniaProperty.Register<PixiEditorPopup, ICommand>(
-        nameof(CloseCommand));
+    public static readonly StyledProperty<ICommand> CloseCommandProperty =
+        AvaloniaProperty.Register<PixiEditorPopup, ICommand>(
+            nameof(CloseCommand));
 
     public ICommand CloseCommand
     {
@@ -42,6 +51,8 @@ public partial class PixiEditorPopup : Window, IPopupWindow
         set => SetValue(CanMinimizeProperty, value);
     }
 
+    private Panel resizePanel;
+
     protected override Type StyleKeyOverride => typeof(PixiEditorPopup);
 
     public PixiEditorPopup()
@@ -52,6 +63,55 @@ public partial class PixiEditorPopup : Window, IPopupWindow
 #endif
     }
 
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        if (System.OperatingSystem.IsLinux())
+        {
+            var titleBar = e.NameScope.Find<DialogTitleBar>("PART_TitleBar");
+            titleBar.PointerPressed += OnTitleBarPressed;
+
+            resizePanel = e.NameScope.Find<Panel>("PART_ResizePanel");
+            resizePanel.AddHandler(PointerPressedEvent, OnResizePanelPressed,
+                RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+            resizePanel.PointerMoved += OnResizePanelMoved;
+        }
+    }
+
+    private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
+    {
+        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            if (e.ClickCount == 2)
+            {
+                WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+            }
+            else
+            {
+                BeginMoveDrag(e);
+                e.Handled = true;
+            }
+        }
+    }
+
+    private void OnResizePanelPressed(object? sender, PointerPressedEventArgs e)
+    {
+        if (WindowState == WindowState.Normal && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && CanResize)
+        {
+            var dir = WindowUtility.GetResizeDirection(e.GetPosition(resizePanel), resizePanel, new Thickness(8));
+            if (dir == null) return;
+
+            BeginResizeDrag(dir.Value, e);
+            e.Handled = true;
+        }
+    }
+
+    private void OnResizePanelMoved(object? sender, PointerEventArgs e)
+    {
+        if (!CanResize || WindowState != WindowState.Normal) return;
+        Cursor = new Cursor(WindowUtility.SetResizeCursor(e, resizePanel, new Thickness(8)));
+    }
+
     public override void Show()
     {
         Show(MainWindow.Current);
@@ -65,7 +125,7 @@ public partial class PixiEditorPopup : Window, IPopupWindow
     [RelayCommand]
     public void SetResultAndCloseCommand()
     {
-        if(CloseIsHide)
+        if (CloseIsHide)
             Hide();
         else
             Close(true);
@@ -73,7 +133,7 @@ public partial class PixiEditorPopup : Window, IPopupWindow
 
     public void ClosePopup()
     {
-        if(CloseIsHide)
+        if (CloseIsHide)
             Hide();
         else
             Close(false);

+ 21 - 4
src/PixiEditor/Views/Input/NumberInput.cs

@@ -135,6 +135,8 @@ internal partial class NumberInput : TextBox
     private double _pressedValue;
     private double _pressedRelativeX;
 
+    private double scrollBuildup;
+
     static NumberInput()
     {
         ValueProperty.Changed.Subscribe(OnValueChanged);
@@ -347,15 +349,29 @@ internal partial class NumberInput : TextBox
             return;
         }
 
-        int step = (int)e.Delta.Y;
+        e.Handled = true;
+        double requiredBuildup = 1;
+        
+        if(Decimals == 0 && e.KeyModifiers.HasFlag(KeyModifiers.Control))
+        {
+            requiredBuildup = 2;
+        }
+        
+        if (Math.Abs(scrollBuildup) < requiredBuildup)
+        {
+            scrollBuildup += e.Delta.Y;
+            return;
+        }
+
+        double step = Math.Sign(e.Delta.Y);
 
         double newValue = Value;
-        if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+        if (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && Min - double.NegativeInfinity > 0.1f && Max - double.PositiveInfinity > 0.1f)
         {
             double multiplier = (Max - Min) * 0.1f;
             newValue += step * multiplier;
         }
-        else if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+        else if (e.KeyModifiers.HasFlag(KeyModifiers.Control) && Decimals > 0)
         {
             newValue += step / 2f;
         }
@@ -365,7 +381,8 @@ internal partial class NumberInput : TextBox
         }
 
         Value = (float)Math.Round(Math.Clamp(newValue, Min, Max), Decimals);
-
+        
+        scrollBuildup = 0;
         OnScrollAction?.Invoke();
     }
 

+ 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;
+            }
+        }
+    }
 }

+ 1 - 0
src/PixiEditor/Views/Main/MainTitleBar.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 using PixiEditor.OperatingSystem;
 using PixiEditor.ViewModels.Menu;

+ 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)

+ 10 - 0
src/PixiEditor/Views/MainWindow.axaml

@@ -17,5 +17,15 @@
         Initialized="MainWindow_Initialized"
         ui:Translator.UseLanguageFlowDirection="True"
         Title="PixiEditor">
+    <Window.SystemDecorations>
+        <OnPlatform>
+            <OnPlatform.Default>
+                <SystemDecorations>Full</SystemDecorations>
+            </OnPlatform.Default>
+            <OnPlatform.Linux>
+                <SystemDecorations>None</SystemDecorations>
+            </OnPlatform.Linux>
+        </OnPlatform> 
+    </Window.SystemDecorations>
     <views1:MainView />
 </Window>

+ 62 - 3
src/PixiEditor/Views/MainWindow.axaml.cs

@@ -2,13 +2,18 @@ using AsyncImageLoader.Loaders;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 using Microsoft.Extensions.DependencyInjection;
 using Drawie.Backend.Core.Bridge;
+using PixiDocks.Avalonia.Helpers;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Helpers;
@@ -18,6 +23,7 @@ using PixiEditor.Models.ExceptionHandling;
 using PixiEditor.Models.IO;
 using PixiEditor.Platform;
 using PixiEditor.ViewModels.SubViewModels;
+using PixiEditor.Views.Main;
 using PixiEditor.Views.Rendering;
 using ViewModels_ViewModelMain = PixiEditor.ViewModels.ViewModelMain;
 
@@ -30,8 +36,10 @@ internal partial class MainWindow : Window
     private readonly IServiceProvider services;
     private static ExtensionLoader extLoader;
 
+    private MainTitleBar titleBar;
+
     public StartupPerformance StartupPerformance { get; } = new();
-    
+
     public new ViewModels_ViewModelMain DataContext
     {
         get => (ViewModels_ViewModelMain)base.DataContext;
@@ -53,7 +61,7 @@ internal partial class MainWindow : Window
     public MainWindow(ExtensionLoader extensionLoader, Guid? analyticsSessionId = null)
     {
         StartupPerformance.ReportToMainWindow();
-        
+
         (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow = this;
         extLoader = extensionLoader;
 
@@ -63,7 +71,8 @@ internal partial class MainWindow : Window
             .AddExtensionServices(extensionLoader)
             .BuildServiceProvider();
 
-        AsyncImageLoader.ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader(Path.Combine(Paths.TempFilesPath, "ImageCache"));
+        AsyncImageLoader.ImageLoader.AsyncImageLoader =
+            new DiskCachedWebImageLoader(Path.Combine(Paths.TempFilesPath, "ImageCache"));
 
         preferences = services.GetRequiredService<IPreferences>();
         platform = services.GetRequiredService<IPlatform>();
@@ -127,12 +136,62 @@ internal partial class MainWindow : Window
     protected override void OnLoaded(RoutedEventArgs e)
     {
         base.OnLoaded(e);
+        
+        titleBar = this.FindDescendantOfType<MainTitleBar>(true);
+        if (System.OperatingSystem.IsLinux())
+        {
+            titleBar.PointerPressed += OnTitleBarPressed;
+            
+            PointerMoved += UpdateResizeCursor;
+            AddHandler(PointerPressedEvent, Pressed, RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+        }
+        
+
         LoadingWindow.Instance?.SafeClose();
         Activate();
         StartupPerformance.ReportToInteractivity();
         Analytics.SendStartup(StartupPerformance);
     }
 
+    private void UpdateResizeCursor(object? sender, PointerEventArgs e)
+    {
+        if(WindowState != WindowState.Normal)
+        {
+            return;
+        }
+        
+        Cursor = new Cursor(WindowUtility.SetResizeCursor(e, this, new Thickness(8)));
+    }
+
+    private void Pressed(object? sender, PointerPressedEventArgs e)
+    {
+        if (WindowState == WindowState.Normal && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            var direction = WindowUtility.GetResizeDirection(e.GetPosition(this), this, new Thickness(8));
+            if(direction == null) return;
+            
+            BeginResizeDrag(direction.Value, e);
+        }
+    }
+
+    private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
+    {
+        bool withinTitleBar = e.GetPosition(this).Y <= titleBar.Bounds.Height;
+        bool sourceIsMenuItem = e.Source is Control ctrl && ctrl.GetLogicalParent() is MenuItem;
+        if (withinTitleBar && !sourceIsMenuItem && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            if(e.ClickCount == 2)
+            {
+                WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+            }
+            else
+            {
+                BeginMoveDrag(e);
+                e.Handled = true;
+            }
+        }
+    }
+
     protected override void OnClosing(WindowClosingEventArgs e)
     {
         if (!DataContext.UserWantsToClose)

+ 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)

+ 1 - 1
src/global.json

@@ -1,6 +1,6 @@
 {
   "sdk": {
-    "version": "8.0.0",
+    "version": "8.0.405",
     "rollForward": "latestMajor",
     "allowPrerelease": false
   }