Browse Source

Merge branch 'master' into patch-1

Krzysztof Krysiński 3 weeks ago
parent
commit
5771c332ea
37 changed files with 407 additions and 138 deletions
  1. 1 1
      src/ColorPicker
  2. 1 1
      src/Drawie
  3. 1 1
      src/PixiDocks
  4. 1 1
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  5. 2 2
      src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj
  6. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  7. 6 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/BlurNode.cs
  8. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs
  9. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  10. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/FilterNode.cs
  11. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  12. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/InvertFilterNode.cs
  13. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs
  14. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/SepiaFilterNode.cs
  15. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ShadowNode.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  17. 3 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  18. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  19. 1 1
      src/PixiEditor.Extensions/PixiEditor.Extensions.csproj
  20. 2 2
      src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs
  21. 13 5
      src/PixiEditor/Helpers/Extensions/DataObjectExtensions.cs
  22. 12 2
      src/PixiEditor/Models/Commands/XAML/NativeMenu.cs
  23. 227 81
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  24. 62 0
      src/PixiEditor/Models/Controllers/IImportObject.cs
  25. 2 2
      src/PixiEditor/PixiEditor.csproj
  26. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  27. 5 1
      src/PixiEditor/ViewModels/Document/CelViewModel.cs
  28. 4 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  29. 1 1
      src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs
  30. 2 2
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  31. 1 1
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  32. 3 1
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  33. 8 4
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  34. 2 1
      src/PixiEditor/Views/Layers/LayersManager.axaml.cs
  35. 6 2
      src/PixiEditor/Views/MainView.axaml.cs
  36. 2 2
      src/PixiEditor/Views/Nodes/Properties/NodePropertyView.cs
  37. 10 2
      src/PixiEditor/Views/Overlays/Handles/Handle.cs

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit db8aaff273239b21bf4c6f99b0162dcd1d742533
+Subproject commit 943e9abbb60b73c4965b947e987dc2696e0b08f8

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 46be7a7eb2a93fc0fade23c8dcca20558209be6f
+Subproject commit 1be85ac9f4bc6b584e6a3a5a3d0287201c6a5f03

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 87c3164a739b529dbbce199a7af761cbd11e5a58
+Subproject commit 6e745d0309ad7a00a53f62f2aa362be77903a5fd

+ 1 - 1
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -24,7 +24,7 @@ public class FFMpegRenderer : IAnimationRenderer
     {
     {
         string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
         string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
 
-        string binaryPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), path);
+        string binaryPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), path);
 
 
         GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = binaryPath });
         GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = binaryPath });
 
 

+ 2 - 2
src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj

@@ -32,13 +32,13 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64'">
   <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64'">
-    <Content Include="ThirdParty\MacOS\ffmpeg\**">
+    <Content Include="ThirdParty/MacOS/ffmpeg/**">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
   <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
-    <Content Include="ThirdParty\MacOS\ffmpeg\**">
+    <Content Include="ThirdParty/MacOS/ffmpeg/**">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
   </ItemGroup>
   </ItemGroup>

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -65,10 +65,15 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
                 else if (sourceObj is Expression expression)
                 else if (sourceObj is Expression expression)
                 {
                 {
                     ShaderExpressionVariable shaderExpressionVariable = (ShaderExpressionVariable)toReturn;
                     ShaderExpressionVariable shaderExpressionVariable = (ShaderExpressionVariable)toReturn;
-                    shaderExpressionVariable.OverrideExpression = Adjust(expression, toReturn, out var adjustNested);
+                    var adjusted = Adjust(expression, toReturn, out var adjustNested);
                     if (adjustNested)
                     if (adjustNested)
                     {
                     {
                         AdjustNested(((IMultiValueVariable)toReturn), expression);
                         AdjustNested(((IMultiValueVariable)toReturn), expression);
+                        shaderExpressionVariable.OverrideExpression = ((IMultiValueVariable)toReturn).GetWholeNestedExpression();
+                    }
+                    else
+                    {
+                        shaderExpressionVariable.OverrideExpression = adjusted;
                     }
                     }
                 }
                 }
 
 
@@ -91,7 +96,6 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         if (toReturn is IMultiValueVariable)
         if (toReturn is IMultiValueVariable)
         {
         {
             adjustNestedVariables = true;
             adjustNestedVariables = true;
-            return expression;
         }
         }
 
 
         return expression;
         return expression;

+ 6 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/BlurNode.cs

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -11,16 +12,18 @@ public class BlurNode : FilterNode
     public InputProperty<bool> PreserveAlpha { get; }
     public InputProperty<bool> PreserveAlpha { get; }
     
     
     public InputProperty<VecD> Radius { get; }
     public InputProperty<VecD> Radius { get; }
-    
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
     public BlurNode()
     public BlurNode()
     {
     {
         PreserveAlpha = CreateInput("PreserveAlpha", "PRESERVE_ALPHA", true);
         PreserveAlpha = CreateInput("PreserveAlpha", "PRESERVE_ALPHA", true);
         Radius = CreateInput("Radius", "RADIUS", new VecD(1, 1)).WithRules(x => x.Min(new VecD(0, 0)));
         Radius = CreateInput("Radius", "RADIUS", new VecD(1, 1)).WithRules(x => x.Min(new VecD(0, 0)));
     }
     }
 
 
-    protected override ImageFilter GetImageFilter()
+    protected override ImageFilter? GetImageFilter(RenderContext context)
     {
     {
-        var sigma = (VecF)Radius.Value;
+        var sigma = (VecF)(Radius.Value * context.ChunkResolution.Multiplier());
         var preserveAlpha = PreserveAlpha.Value;
         var preserveAlpha = PreserveAlpha.Value;
 
 
         var xFilter = GetGaussianFilter(sigma.X, true, preserveAlpha, null, out float[] xKernel);
         var xFilter = GetGaussianFilter(sigma.X, true, preserveAlpha, null, out float[] xKernel);

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -54,7 +55,7 @@ public class ColorAdjustmentsFilterNode : FilterNode
             .WithRules(rules => rules.Min(-180d).Max(180d));
             .WithRules(rules => rules.Min(-180d).Max(180d));
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter()
+    protected override ColorFilter? GetColorFilter(RenderContext context)
     {
     {
         filters.ForEach(filter => filter.Dispose());
         filters.ForEach(filter => filter.Dispose());
         filters.Clear();
         filters.Clear();

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -17,7 +18,7 @@ public class ColorMatrixFilterNode : FilterNode
         Matrix = CreateInput(nameof(Matrix), "MATRIX", ColorMatrix.Identity);
         Matrix = CreateInput(nameof(Matrix), "MATRIX", ColorMatrix.Identity);
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter()
+    protected override ColorFilter? GetColorFilter(RenderContext context)
     {
     {
         if (Matrix.Value.Equals(lastMatrix))
         if (Matrix.Value.Equals(lastMatrix))
         {
         {

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/FilterNode.cs

@@ -20,8 +20,8 @@ public abstract class FilterNode : Node
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
-        var colorFilter = GetColorFilter();
-        var imageFilter = GetImageFilter();
+        var colorFilter = GetColorFilter(context);
+        var imageFilter = GetImageFilter(context);
 
 
         if (colorFilter == null && imageFilter == null)
         if (colorFilter == null && imageFilter == null)
         {
         {
@@ -34,7 +34,7 @@ public abstract class FilterNode : Node
         Output.Value = filter == null ? new Filter(colorFilter, imageFilter) : filter.Add(colorFilter, imageFilter);
         Output.Value = filter == null ? new Filter(colorFilter, imageFilter) : filter.Add(colorFilter, imageFilter);
     }
     }
 
 
-    protected virtual ColorFilter? GetColorFilter() => null;
+    protected virtual ColorFilter? GetColorFilter(RenderContext context) => null;
 
 
-    protected virtual ImageFilter? GetImageFilter() => null;
+    protected virtual ImageFilter? GetImageFilter(RenderContext context) => null;
 }
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -34,7 +35,7 @@ public class GrayscaleNode : FilterNode
         CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new Vec3D(1, 1, 1));
         CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new Vec3D(1, 1, 1));
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter()
+    protected override ColorFilter? GetColorFilter(RenderContext context)
     {
     {
         if (Mode.Value == lastMode 
         if (Mode.Value == lastMode 
             && Factor.Value == lastFactor 
             && Factor.Value == lastFactor 

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/InvertFilterNode.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -20,7 +21,7 @@ public class InvertFilterNode : FilterNode
         filter = ColorFilter.CreateColorMatrix(invertedMatrix);
         filter = ColorFilter.CreateColorMatrix(invertedMatrix);
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter()
+    protected override ColorFilter? GetColorFilter(RenderContext context)
     {
     {
         filter?.Dispose();
         filter?.Dispose();
 
 

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -37,7 +38,7 @@ public class KernelFilterNode : FilterNode
         OnAlpha = CreateInput(nameof(OnAlpha), "ON_ALPHA", false);
         OnAlpha = CreateInput(nameof(OnAlpha), "ON_ALPHA", false);
     }
     }
 
 
-    protected override ImageFilter? GetImageFilter()
+    protected override ImageFilter? GetImageFilter(RenderContext context)
     {
     {
         var kernel = Kernel.Value;
         var kernel = Kernel.Value;
         
         

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/SepiaFilterNode.cs

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -32,7 +33,7 @@ public class SepiaFilterNode : FilterNode
         );
         );
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter()
+    protected override ColorFilter? GetColorFilter(RenderContext context)
     {
     {
         lastFilter?.Dispose();
         lastFilter?.Dispose();
 
 

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ShadowNode.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -18,7 +19,7 @@ public class ShadowNode : FilterNode
         Color = CreateInput("Color", "COLOR", Colors.Black);
         Color = CreateInput("Color", "COLOR", Colors.Black);
     }
     }
 
 
-    protected override ImageFilter? GetImageFilter()
+    protected override ImageFilter? GetImageFilter(RenderContext context)
     {
     {
         return ImageFilter.CreateDropShadow((float)Offset.Value.X, (float)Offset.Value.Y, (float)Sigma.Value.X, (float)Sigma.Value.Y, Color.Value, null);
         return ImageFilter.CreateDropShadow((float)Offset.Value.X, (float)Offset.Value.Y, (float)Sigma.Value.X, (float)Sigma.Value.Y, Color.Value, null);
     }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -16,7 +16,7 @@ public class PointsVectorData : ShapeVectorData
         Points = new List<VecD>(points);
         Points = new List<VecD>(points);
     }
     }
 
 
-    public override RectD GeometryAABB => new RectD(Points.Min(p => p.X), Points.Min(p => p.Y), Points.Max(p => p.X),
+    public override RectD GeometryAABB => Points == null || Points.Count == 0 ? RectD.Empty : new RectD(Points.Min(p => p.X), Points.Min(p => p.Y), Points.Max(p => p.X),
         Points.Max(p => p.Y));
         Points.Max(p => p.Y));
 
 
     public override RectD VisualAABB => GeometryAABB;
     public override RectD VisualAABB => GeometryAABB;

+ 3 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -115,6 +115,9 @@ public static class NodeOperations
     {
     {
         List<IChangeInfo> changes = new();
         List<IChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
         IOutputProperty? previouslyConnected = null;
+
+        if(parentInput == null) return changes;
+
         if (parentInput.Connection != null)
         if (parentInput.Connection != null)
         {
         {
             previouslyConnected = parentInput.Connection;
             previouslyConnected = parentInput.Connection;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -45,8 +45,8 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
         VecI size = (VecI)newBounds.Size.Ceiling();
         VecI size = (VecI)newBounds.Size.Ceiling();
         
         
         target.Size = size;
         target.Size = size;
-        target.VerticalSymmetryAxisX = Math.Clamp(_originalVerAxisX, 0, target.Size.X);
-        target.HorizontalSymmetryAxisY = Math.Clamp(_originalHorAxisY, 0, target.Size.Y);
+        target.VerticalSymmetryAxisX = Math.Clamp(_originalVerAxisX, 0, Math.Max(target.Size.X, 1));
+        target.HorizontalSymmetryAxisY = Math.Clamp(_originalHorAxisY, 0, Math.Max(target.Size.Y, 1));
         
         
         target.ForEveryMember((member) =>
         target.ForEveryMember((member) =>
         {
         {

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

@@ -15,7 +15,7 @@
     <ItemGroup>
     <ItemGroup>
       <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
       <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
       <PackageReference Include="Avalonia.Remote.Protocol" Version="$(AvaloniaVersion)" />
       <PackageReference Include="Avalonia.Remote.Protocol" Version="$(AvaloniaVersion)" />
-      <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.1" />
+      <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.3" />
       <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
       <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

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

@@ -2,12 +2,12 @@
 
 
 public static class ClipboardDataFormats
 public static class ClipboardDataFormats
 {
 {
-    public const string Png = "PNG";
+    public static readonly string[] PngFormats = [ "PNG", "image/png", "public.png" ];
     public const string LayerIdList = "PixiEditor.LayerIdList";
     public const string LayerIdList = "PixiEditor.LayerIdList";
     public const string PositionFormat = "PixiEditor.Position";
     public const string PositionFormat = "PixiEditor.Position";
-    public const string ImageSlashPng = "image/png";
     public const string DocumentFormat = "PixiEditor.Document";
     public const string DocumentFormat = "PixiEditor.Document";
     public const string NodeIdList = "PixiEditor.NodeIdList";
     public const string NodeIdList = "PixiEditor.NodeIdList";
     public const string CelIdList = "PixiEditor.CelIdList";
     public const string CelIdList = "PixiEditor.CelIdList";
     public const string PixiVectorData = "PixiEditor.VectorData";
     public const string PixiVectorData = "PixiEditor.VectorData";
+    public const string UriList = "text/uri-list";
 }
 }

+ 13 - 5
src/PixiEditor/Helpers/Extensions/DataObjectExtensions.cs

@@ -37,14 +37,22 @@ public static class DataObjectExtensions
             return false;
             return false;
         }
         }
 
 
-        string text = data.GetText();
-
-        if (Directory.Exists(text) || File.Exists(text))
+        try
+        {
+            var text = data.GetText();
+            if (Directory.Exists(text) || File.Exists(text))
+            {
+                path = text;
+                return true;
+            }
+        }
+        catch(InvalidCastException ex) // bug on x11
         {
         {
-            path = text;
-            return true;
+            path = null;
+            return false;
         }
         }
 
 
+
         path = null;
         path = null;
         return false;
         return false;
     }
     }

+ 12 - 2
src/PixiEditor/Models/Commands/XAML/NativeMenu.cs

@@ -65,9 +65,19 @@ internal class NativeMenu : global::Avalonia.Controls.Menu
         {
         {
             if (!ShortcutController.ShortcutExecutionBlocked)
             if (!ShortcutController.ShortcutExecutionBlocked)
             {
             {
-                if (iCommand.CanExecute(parameter))
+                if (command?.Shortcut != null && command.Shortcut.Gesture != null && command.Shortcut.Gesture.Key != Key.None || command.Shortcut.Gesture.KeyModifiers != KeyModifiers.None)
                 {
                 {
-                    iCommand.Execute(parameter);
+                     ViewModelMain.Current.ShortcutController.KeyPressed(
+                         false,
+                         command.Shortcut.Key,
+                         command.Shortcut.Modifiers);
+                }
+                else
+                {
+                    if (iCommand.CanExecute(parameter))
+                    {
+                        iCommand.Execute(parameter);
+                    }
                 }
                 }
             }
             }
             else
             else

+ 227 - 81
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -86,7 +86,8 @@ internal static class ClipboardController
         }
         }
         else if (document.TransformViewModel.TransformActive || lastTransform != null)
         else if (document.TransformViewModel.TransformActive || lastTransform != null)
         {
         {
-            RectD transform = document.TransformViewModel.TransformActive ? document.TransformViewModel.Corners.AABBBounds
+            RectD transform = document.TransformViewModel.TransformActive
+                ? document.TransformViewModel.Corners.AABBBounds
                 : lastTransform.Value;
                 : lastTransform.Value;
             var surface =
             var surface =
                 document.TryExtractAreaFromSelected(
                 document.TryExtractAreaFromSelected(
@@ -149,7 +150,7 @@ internal static class ClipboardController
             copyArea = document.TransformViewModel.Corners.AABBBounds;
             copyArea = document.TransformViewModel.Corners.AABBBounds;
         }
         }
 
 
-        if(copyArea.IsZeroOrNegativeArea || copyArea.HasNaNOrInfinity)
+        if (copyArea.IsZeroOrNegativeArea || copyArea.HasNaNOrInfinity)
         {
         {
             NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
             NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
             return;
             return;
@@ -185,8 +186,13 @@ internal static class ClipboardController
             await pngData.AsStream().CopyToAsync(pngStream);
             await pngData.AsStream().CopyToAsync(pngStream);
 
 
             var pngArray = pngStream.ToArray();
             var pngArray = pngStream.ToArray();
-            data.Set(ClipboardDataFormats.Png, pngArray);
-            data.Set(ClipboardDataFormats.ImageSlashPng, pngArray);
+            foreach (string format in ClipboardDataFormats.PngFormats)
+            {
+                if (!data.Contains(format))
+                {
+                    data.Set(format, pngArray);
+                }
+            }
 
 
             pngStream.Position = 0;
             pngStream.Position = 0;
             try
             try
@@ -221,17 +227,19 @@ internal static class ClipboardController
     /// <summary>
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     ///     Pastes image from clipboard into new layer.
     /// </summary>
     /// </summary>
-    public static bool TryPaste(DocumentViewModel document, DocumentManagerViewModel manager, IEnumerable<IDataObject>
-        data, bool pasteAsNew = false)
+    public static async Task<bool> TryPaste(DocumentViewModel document, DocumentManagerViewModel manager,
+        IImportObject[]
+            data, bool pasteAsNew = false)
     {
     {
-        Guid sourceDocument = GetSourceDocument(data, document.Id);
-        Guid[] layerIds = GetLayerIds(data);
+        Guid sourceDocument = await GetSourceDocument(data, document.Id);
+        Guid[] layerIds = await GetLayerIds(data);
 
 
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
 
 
         IDocument? targetDoc = manager.Documents.FirstOrDefault(x => x.Id == sourceDocument);
         IDocument? targetDoc = manager.Documents.FirstOrDefault(x => x.Id == sourceDocument);
 
 
-        if (targetDoc != null && pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, targetDoc)))
+        if (targetDoc != null && pasteAsNew && layerIds is { Length: > 0 } &&
+            (!hasPos || await AllMatchesPos(layerIds, data, targetDoc)))
         {
         {
             foreach (var layerId in layerIds)
             foreach (var layerId in layerIds)
             {
             {
@@ -250,7 +258,7 @@ internal static class ClipboardController
             return true;
             return true;
         }
         }
 
 
-        List<DataImage> images = GetImage(data);
+        List<DataImage> images = await GetImage(data);
         if (images.Count == 0)
         if (images.Count == 0)
             return false;
             return false;
 
 
@@ -291,16 +299,14 @@ internal static class ClipboardController
         return true;
         return true;
     }
     }
 
 
-    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, IDocument doc)
+    private static async Task<bool> AllMatchesPos(Guid[] layerIds, IImportObject[] dataFormats, IDocument doc)
     {
     {
-        var dataObjects = data as IDataObject[] ?? data.ToArray();
-
-        var dataObjectWithPos = dataObjects.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
+        var dataObjectWithPos = dataFormats.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
         VecD pos = VecD.Zero;
         VecD pos = VecD.Zero;
 
 
         if (dataObjectWithPos != null)
         if (dataObjectWithPos != null)
         {
         {
-            pos = dataObjectWithPos.GetVecD(ClipboardDataFormats.PositionFormat);
+            pos = await GetVecD(ClipboardDataFormats.PositionFormat, dataFormats);
         }
         }
 
 
         RectD? tightBounds = null;
         RectD? tightBounds = null;
@@ -325,13 +331,13 @@ internal static class ClipboardController
         return tightBounds.HasValue && tightBounds.Value.Pos.AlmostEquals(pos);
         return tightBounds.HasValue && tightBounds.Value.Pos.AlmostEquals(pos);
     }
     }
 
 
-    private static Guid[] GetLayerIds(IEnumerable<IDataObject> data)
+    private static async Task<Guid[]> GetLayerIds(IImportObject[] formats)
     {
     {
-        foreach (var dataObject in data)
+        foreach (var dataObject in formats)
         {
         {
             if (dataObject.Contains(ClipboardDataFormats.LayerIdList))
             if (dataObject.Contains(ClipboardDataFormats.LayerIdList))
             {
             {
-                byte[] layerIds = (byte[])dataObject.Get(ClipboardDataFormats.LayerIdList);
+                byte[] layerIds = await Clipboard.GetDataAsync(ClipboardDataFormats.LayerIdList) as byte[];
                 string layerIdsString = System.Text.Encoding.UTF8.GetString(layerIds);
                 string layerIdsString = System.Text.Encoding.UTF8.GetString(layerIds);
                 return layerIdsString.Split(';').Select(Guid.Parse).ToArray();
                 return layerIdsString.Split(';').Select(Guid.Parse).ToArray();
             }
             }
@@ -340,13 +346,14 @@ internal static class ClipboardController
         return [];
         return [];
     }
     }
 
 
-    private static Guid GetSourceDocument(IEnumerable<IDataObject> data, Guid fallback)
+    private static async Task<Guid> GetSourceDocument(IImportObject[] formats, Guid fallback)
     {
     {
-        foreach (var dataObject in data)
+        foreach (var dataObject in formats)
         {
         {
             if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
             if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
             {
             {
-                byte[] guidBytes = (byte[])dataObject.Get(ClipboardDataFormats.DocumentFormat);
+                var data = await Clipboard.GetDataAsync(ClipboardDataFormats.DocumentFormat);
+                byte[] guidBytes = (byte[])data;
                 string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
                 string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
                 return Guid.Parse(guidString);
                 return Guid.Parse(guidString);
             }
             }
@@ -361,73 +368,68 @@ internal static class ClipboardController
     public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, DocumentManagerViewModel manager,
     public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, DocumentManagerViewModel manager,
         bool pasteAsNew = false)
         bool pasteAsNew = false)
     {
     {
-        var data = await TryGetDataObject();
+        var data = await TryGetImportObjects();
         if (data == null)
         if (data == null)
             return false;
             return false;
 
 
-        return TryPaste(document, manager, data, pasteAsNew);
+        return await TryPaste(document, manager, data, pasteAsNew);
     }
     }
 
 
-    private static async Task<List<DataObject?>> TryGetDataObject()
+    private static async Task<ClipboardPromiseObject[]> TryGetImportObjects()
     {
     {
         string[] formats = await Clipboard.GetFormatsAsync();
         string[] formats = await Clipboard.GetFormatsAsync();
         if (formats.Length == 0)
         if (formats.Length == 0)
             return null;
             return null;
 
 
-        List<DataObject?> dataObjects = new();
+        List<ClipboardPromiseObject?> dataObjects = new();
 
 
         for (int i = 0; i < formats.Length; i++)
         for (int i = 0; i < formats.Length; i++)
         {
         {
             string format = formats[i];
             string format = formats[i];
-            var obj = await Clipboard.GetDataAsync(format);
-
-            if (obj == null)
-                continue;
-
-            DataObject data = new DataObject();
-            data.Set(format, obj);
-
-            dataObjects.Add(data);
+            dataObjects.Add(new ClipboardPromiseObject(format, Clipboard));
         }
         }
 
 
-        return dataObjects;
+        return dataObjects.ToArray();
     }
     }
 
 
     public static async Task<List<DataImage>> GetImagesFromClipboard()
     public static async Task<List<DataImage>> GetImagesFromClipboard()
     {
     {
-        var dataObj = await TryGetDataObject();
-        return GetImage(dataObj);
+        var dataObj = await TryGetImportObjects();
+        return await GetImage(dataObj);
     }
     }
 
 
     /// <summary>
     /// <summary>
     /// Gets images from clipboard, supported PNG and Bitmap.
     /// Gets images from clipboard, supported PNG and Bitmap.
     /// </summary>
     /// </summary>
-    public static List<DataImage> GetImage(IEnumerable<IDataObject?> data)
+    public static async Task<List<DataImage>> GetImage(IImportObject[] importableObjects)
     {
     {
         List<DataImage> surfaces = new();
         List<DataImage> surfaces = new();
 
 
-        if (data == null)
+        if (importableObjects == null)
             return surfaces;
             return surfaces;
 
 
         VecD pos = VecD.Zero;
         VecD pos = VecD.Zero;
 
 
         string? importingType = null;
         string? importingType = null;
+        bool pngImported = false;
 
 
-        foreach (var dataObject in data)
+        foreach (var dataObject in importableObjects)
         {
         {
-            if (importingType is null or "bytes" && TryExtractSingleImage(dataObject, out var singleImage))
+            var img = await TryExtractSingleImage(dataObject);
+            if (importingType is null or "bytes" && img != null && !pngImported)
             {
             {
-                surfaces.Add(new DataImage(singleImage,
+                surfaces.Add(new DataImage(img,
                     dataObject.Contains(ClipboardDataFormats.PositionFormat)
                     dataObject.Contains(ClipboardDataFormats.PositionFormat)
-                        ? (VecI)dataObject.GetVecD(ClipboardDataFormats.PositionFormat)
+                        ? (VecI)await GetVecD(ClipboardDataFormats.PositionFormat, importableObjects)
                         : (VecI)pos));
                         : (VecI)pos));
                 importingType = "bytes";
                 importingType = "bytes";
+                pngImported = true;
                 continue;
                 continue;
             }
             }
 
 
             if (dataObject.Contains(ClipboardDataFormats.PositionFormat))
             if (dataObject.Contains(ClipboardDataFormats.PositionFormat))
             {
             {
-                pos = dataObject.GetVecD(ClipboardDataFormats.PositionFormat);
+                pos = await GetVecD(ClipboardDataFormats.PositionFormat, importableObjects);
                 for (var i = 0; i < surfaces.Count; i++)
                 for (var i = 0; i < surfaces.Count; i++)
                 {
                 {
                     var surface = surfaces[i];
                     var surface = surfaces[i];
@@ -435,10 +437,11 @@ internal static class ClipboardController
                 }
                 }
             }
             }
 
 
-            var paths = dataObject.GetFileDropList().Select(x => x.Path.LocalPath).ToList();
-            if (paths != null && dataObject.TryGetRawTextPath(out string? textPath))
+            var paths = (await GetFileDropList(dataObject))?.Select(x => x.Path.LocalPath).ToList();
+            string[]? rawPaths = await TryGetRawTextPaths(dataObject);
+            if (paths != null && rawPaths != null)
             {
             {
-                paths.Add(textPath);
+                paths.AddRange(rawPaths);
             }
             }
 
 
             if (paths == null || paths.Count == 0 || (importingType != null && importingType != "files"))
             if (paths == null || paths.Count == 0 || (importingType != null && importingType != "files"))
@@ -467,7 +470,7 @@ internal static class ClipboardController
 
 
                     string filename = Path.GetFullPath(path);
                     string filename = Path.GetFullPath(path);
                     surfaces.Add(new DataImage(filename, imported,
                     surfaces.Add(new DataImage(filename, imported,
-                        (VecI)dataObject.GetVecD(ClipboardDataFormats.PositionFormat)));
+                        (VecI)await GetVecD(ClipboardDataFormats.PositionFormat, importableObjects)));
                     importingType = "files";
                     importingType = "files";
                 }
                 }
                 catch
                 catch
@@ -480,6 +483,104 @@ internal static class ClipboardController
         return surfaces;
         return surfaces;
     }
     }
 
 
+    private static async Task<string[]?> TryGetRawTextPaths(IImportObject importObj)
+    {
+        if (!importObj.Contains(DataFormats.Text) && !importObj.Contains(ClipboardDataFormats.UriList))
+        {
+            return null;
+        }
+
+        string text = null;
+        try
+        {
+            text = await importObj.GetDataAsync(DataFormats.Text) as string;
+        }
+        catch(InvalidCastException ex) // bug on x11
+        {
+        }
+
+        string[] paths = [text];
+        if (text == null)
+        {
+            if (await importObj.GetDataAsync(ClipboardDataFormats.UriList) is byte[] bytes)
+            {
+                paths = Encoding.UTF8.GetString(bytes).Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
+            }
+        }
+
+        if (paths.Length == 0)
+        {
+            return null;
+        }
+
+        List<string> validPaths = new();
+
+        foreach (string path in paths)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+                continue;
+
+            if (Directory.Exists(path) || File.Exists(path))
+            {
+                validPaths.Add(path);
+            }
+            else
+            {
+                try
+                {
+                    Uri uri = new Uri(path);
+                    if (uri.IsAbsoluteUri && (Directory.Exists(uri.LocalPath) || File.Exists(uri.LocalPath)))
+                    {
+                        validPaths.Add(uri.LocalPath);
+                    }
+                }
+                catch (UriFormatException)
+                {
+                    // Ignore invalid URIs
+                }
+            }
+        }
+
+        return validPaths.Count > 0 ? validPaths.ToArray() : null;
+    }
+
+    private static async Task<IEnumerable<IStorageItem>> GetFileDropList(IImportObject obj)
+    {
+        if (!obj.Contains(DataFormats.Files))
+            return [];
+
+        var data = await obj.GetDataAsync(DataFormats.Files);
+        if (data == null)
+            return [];
+
+        if (data is IEnumerable<IStorageItem> storageItems)
+            return storageItems;
+
+        if (data is Task<object> task)
+        {
+            data = await task;
+            if (data is IEnumerable<IStorageItem> storageItemsFromTask)
+                return storageItemsFromTask;
+        }
+
+        return [];
+    }
+
+
+    private static async Task<VecD> GetVecD(string format, IImportObject[] availableFormats)
+    {
+        var firstFormat = availableFormats.FirstOrDefault(x => x.Contains(format));
+        if (firstFormat == null)
+            return new VecD(-1, -1);
+
+        byte[] bytes = (byte[])await firstFormat.GetDataAsync(format);
+
+        if (bytes is { Length: < 16 })
+            return new VecD(-1, -1);
+
+        return VecD.FromBytes(bytes);
+    }
+
     public static bool IsImage(IDataObject? dataObject)
     public static bool IsImage(IDataObject? dataObject)
     {
     {
         if (dataObject == null)
         if (dataObject == null)
@@ -501,7 +602,7 @@ internal static class ClipboardController
             return false;
             return false;
         }
         }
 
 
-        return HasData(dataObject, ClipboardDataFormats.Png, ClipboardDataFormats.ImageSlashPng);
+        return HasData(dataObject, ClipboardDataFormats.PngFormats);
     }
     }
 
 
     public static async Task<bool> IsImageInClipboard()
     public static async Task<bool> IsImageInClipboard()
@@ -515,7 +616,20 @@ internal static class ClipboardController
         if (!isImage)
         if (!isImage)
         {
         {
             string path = await TryFindImageInFiles(formats);
             string path = await TryFindImageInFiles(formats);
-            return Path.Exists(path);
+            try
+            {
+                Uri uri = new Uri(path);
+                return Path.Exists(uri.LocalPath);
+            }
+            catch (UriFormatException)
+            {
+                return false;
+            }
+            catch (Exception ex)
+            {
+                CrashHelper.SendExceptionInfo(ex);
+                return false;
+            }
         }
         }
 
 
         return isImage;
         return isImage;
@@ -525,15 +639,7 @@ internal static class ClipboardController
     {
     {
         foreach (string format in formats)
         foreach (string format in formats)
         {
         {
-            if (format == DataFormats.Text)
-            {
-                string text = await ClipboardController.GetTextFromClipboard();
-                if (Importer.IsSupportedFile(text))
-                {
-                    return text;
-                }
-            }
-            else if (format == DataFormats.Files)
+            if (format == DataFormats.Files || format == ClipboardDataFormats.UriList)
             {
             {
                 var files = await ClipboardController.Clipboard.GetDataAsync(format);
                 var files = await ClipboardController.Clipboard.GetDataAsync(format);
                 if (files is IEnumerable<IStorageItem> storageFiles)
                 if (files is IEnumerable<IStorageItem> storageFiles)
@@ -553,6 +659,28 @@ internal static class ClipboardController
                         }
                         }
                     }
                     }
                 }
                 }
+
+                if (files is byte[] bytes)
+                {
+                    string utf8String = Encoding.UTF8.GetString(bytes);
+                    string[] paths = utf8String.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
+                    foreach (string path in paths)
+                    {
+                        if (Importer.IsSupportedFile(path))
+                        {
+                            return path;
+                        }
+                    }
+                }
+
+                if (format == DataFormats.Text)
+                {
+                    string text = await ClipboardController.GetTextFromClipboard();
+                    if (Importer.IsSupportedFile(text))
+                    {
+                        return text;
+                    }
+                }
             }
             }
         }
         }
 
 
@@ -563,7 +691,7 @@ internal static class ClipboardController
     {
     {
         foreach (var format in formats)
         foreach (var format in formats)
         {
         {
-            if (format == ClipboardDataFormats.Png)
+            if (ClipboardDataFormats.PngFormats.Contains(format, StringComparer.OrdinalIgnoreCase))
             {
             {
                 return true;
                 return true;
             }
             }
@@ -577,38 +705,55 @@ internal static class ClipboardController
         return false;
         return false;
     }
     }
 
 
-    private static Surface FromPNG(IDataObject data)
+    private static async Task<Surface?> FromPNG(IImportObject importObj)
     {
     {
-        object obj = data.Get("PNG");
-        if (obj is byte[] bytes)
+        object? pngData = null;
+        foreach (string format in ClipboardDataFormats.PngFormats)
+        {
+            if (importObj.Contains(format))
+            {
+                object? data = await importObj.GetDataAsync(format);
+                if (data == null)
+                    continue;
+
+                pngData = data;
+                break;
+            }
+        }
+
+        if (pngData is byte[] bytes)
         {
         {
             return Surface.Load(bytes);
             return Surface.Load(bytes);
         }
         }
 
 
-        if (obj is MemoryStream memoryStream)
+        if (pngData is MemoryStream memoryStream)
         {
         {
             bytes = memoryStream.ToArray();
             bytes = memoryStream.ToArray();
             return Surface.Load(bytes);
             return Surface.Load(bytes);
         }
         }
 
 
-        throw new InvalidDataException("PNG data is not in a supported format.");
+        return null;
     }
     }
 
 
     private static bool HasData(IDataObject dataObject, params string[] formats) => formats.Any(dataObject.Contains);
     private static bool HasData(IDataObject dataObject, params string[] formats) => formats.Any(dataObject.Contains);
 
 
-    private static bool TryExtractSingleImage(IDataObject data, [NotNullWhen(true)] out Surface? result)
+    private static async Task<Surface?> TryExtractSingleImage(IImportObject importedObj)
     {
     {
         try
         try
         {
         {
             Surface source;
             Surface source;
-            if (data.Contains(ClipboardDataFormats.Png) || data.Contains(ClipboardDataFormats.ImageSlashPng))
+            bool dataContainsPng = ClipboardDataFormats.PngFormats.Any(importedObj.Contains);
+            if (dataContainsPng)
             {
             {
-                source = FromPNG(data);
+                source = await FromPNG(importedObj);
+                if (source == null)
+                {
+                    return null;
+                }
             }
             }
             else
             else
             {
             {
-                result = null;
-                return false;
+                return null;
             }
             }
 
 
             /*if (source.Format.Value.IsSkiaSupported())
             /*if (source.Format.Value.IsSkiaSupported())
@@ -624,13 +769,11 @@ internal static class ClipboardController
                 result = SurfaceHelpers.FromBitmap(newFormat);
                 result = SurfaceHelpers.FromBitmap(newFormat);
             }*/
             }*/
 
 
-            result = source;
-            return true;
+            return source;
         }
         }
         catch { }
         catch { }
 
 
-        result = null;
-        return false;
+        return null;
     }
     }
 
 
     public static async Task CopyNodes(Guid[] nodeIds, Guid docId)
     public static async Task CopyNodes(Guid[] nodeIds, Guid docId)
@@ -650,17 +793,20 @@ internal static class ClipboardController
 
 
     public static async Task<Guid[]> GetIds(string format)
     public static async Task<Guid[]> GetIds(string format)
     {
     {
-        var data = await TryGetDataObject();
-        return GetIds(data, format);
+        var data = await TryGetImportObjects();
+        return await GetIds(data, format);
     }
     }
 
 
-    private static Guid[] GetIds(IEnumerable<IDataObject?> data, string format)
+
+    private static async Task<Guid[]> GetIds(IEnumerable<IImportObject?> data, string format)
     {
     {
         foreach (var dataObject in data)
         foreach (var dataObject in data)
         {
         {
             if (dataObject.Contains(format))
             if (dataObject.Contains(format))
             {
             {
-                byte[] nodeIds = (byte[])dataObject.Get(format);
+                byte[] nodeIds = await dataObject.GetDataAsync(format) as byte[];
+                if (nodeIds == null || nodeIds.Length == 0)
+                    return [];
                 string nodeIdsString = System.Text.Encoding.UTF8.GetString(nodeIds);
                 string nodeIdsString = System.Text.Encoding.UTF8.GetString(nodeIds);
                 return nodeIdsString.Split(';').Select(Guid.Parse).ToArray();
                 return nodeIdsString.Split(';').Select(Guid.Parse).ToArray();
             }
             }
@@ -702,7 +848,7 @@ internal static class ClipboardController
         data.Set(ClipboardDataFormats.DocumentFormat, Encoding.UTF8.GetBytes(docId.ToString()));
         data.Set(ClipboardDataFormats.DocumentFormat, Encoding.UTF8.GetBytes(docId.ToString()));
 
 
         byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
         byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
-
+        
         data.Set(format, idsBytes);
         data.Set(format, idsBytes);
 
 
         await Clipboard.SetDataObjectAsync(data);
         await Clipboard.SetDataObjectAsync(data);
@@ -710,7 +856,7 @@ internal static class ClipboardController
 
 
     public static async Task<Guid> GetDocumentId()
     public static async Task<Guid> GetDocumentId()
     {
     {
-        var data = await TryGetDataObject();
+        var data = await TryGetImportObjects();
         if (data == null)
         if (data == null)
             return Guid.Empty;
             return Guid.Empty;
 
 
@@ -718,7 +864,7 @@ internal static class ClipboardController
         {
         {
             if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
             if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
             {
             {
-                byte[] guidBytes = (byte[])dataObject.Get(ClipboardDataFormats.DocumentFormat);
+                byte[] guidBytes = (byte[])await dataObject.GetDataAsync(ClipboardDataFormats.DocumentFormat);
                 string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
                 string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
                 return Guid.Parse(guidString);
                 return Guid.Parse(guidString);
             }
             }

+ 62 - 0
src/PixiEditor/Models/Controllers/IImportObject.cs

@@ -0,0 +1,62 @@
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+
+namespace PixiEditor.Models.Controllers;
+
+public interface IImportObject
+{
+    public bool Contains(string format);
+    public Task<object?> GetDataAsync(string format);
+}
+
+public class ImportedObject : IImportObject
+{
+    private readonly IDataObject dataObject;
+
+    public ImportedObject(IDataObject dataObject)
+    {
+        this.dataObject = dataObject;
+    }
+
+    public bool Contains(string format)
+    {
+        return dataObject.Contains(format);
+    }
+
+    public async Task<object?> GetDataAsync(string format)
+    {
+        return Task.FromResult(dataObject.Get(format));
+    }
+}
+
+public class ClipboardPromiseObject : IImportObject
+{
+    public string Format { get; }
+    public IClipboard Clipboard { get; }
+
+    public ClipboardPromiseObject(string format, IClipboard clipboard)
+    {
+        Format = format;
+        Clipboard = clipboard;
+    }
+
+    public bool Contains(string format)
+    {
+        return Format == format;
+    }
+
+    public async Task<object?> GetDataAsync(string format)
+    {
+        if (Format != format)
+        {
+            return null;
+        }
+
+        return await Clipboard.GetDataAsync(format);
+    }
+
+    public override string ToString()
+    {
+        return $"ClipboardPromiseObject: {Format}";
+    }
+}

+ 2 - 2
src/PixiEditor/PixiEditor.csproj

@@ -91,7 +91,7 @@
     <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="Avalonia.Labs.Lottie" Version="11.3.0" />
+    <PackageReference Include="Avalonia.Labs.Lottie" Version="11.3.1" />
     <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)"/>
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@@ -105,7 +105,7 @@
     <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
-    <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.1" />
+    <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.3" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.7")]
-[assembly: AssemblyFileVersion("2.0.1.7")]
+[assembly: AssemblyVersion("2.0.1.9")]
+[assembly: AssemblyFileVersion("2.0.1.9")]

+ 5 - 1
src/PixiEditor/ViewModels/Document/CelViewModel.cs

@@ -91,7 +91,11 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
     public bool IsSelected
     public bool IsSelected
     {
     {
         get => isSelected;
         get => isSelected;
-        set => SetProperty(ref isSelected, value);
+        set
+        {
+            if (StartFrameBindable == 0) return;
+            SetProperty(ref isSelected, value);
+        }
     }
     }
 
 
     protected CelViewModel(int startFrame, int duration, Guid layerGuid, Guid id,
     protected CelViewModel(int startFrame, int duration, Guid layerGuid, Guid id,

+ 4 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -314,7 +314,10 @@ internal partial class DocumentViewModel
         var path = new SvgPath();
         var path = new SvgPath();
         if (data.Path != null)
         if (data.Path != null)
         {
         {
-            string pathData = data.Path.ToSvgPathData();
+            // This is super strange, we got reports that on linux with different locales, numbers are separated by commas
+            // and not dots. Comma separated svg data is invalid. This is raw Skia call, there is no other place where it could be changed.
+            // That's why we replace commas with dots here. I really hope skia never uses commas as a number separator
+            string pathData = data.Path.ToSvgPathData().Replace(",", ".");
             path.PathData.Unit = new SvgStringUnit(pathData);
             path.PathData.Unit = new SvgStringUnit(pathData);
             SvgFillRule fillRule = data.Path.FillType switch
             SvgFillRule fillRule = data.Path.FillType switch
             {
             {

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

@@ -189,7 +189,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     {
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
 
-        if (activeDocument is null)
+        if (activeDocument is null || info == default)
             return;
             return;
 
 
         if (!info.end)
         if (!info.end)

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

@@ -134,7 +134,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             DataImage imageData =
             DataImage imageData =
                 (data == null
                 (data == null
                     ? await ClipboardController.GetImagesFromClipboard()
                     ? await ClipboardController.GetImagesFromClipboard()
-                    : ClipboardController.GetImage(new[] { data })).First();
+                    : ClipboardController.GetImage(new[] { new ImportedObject(data) }).Result).First();
             using var surface = imageData.Image;
             using var surface = imageData.Image;
 
 
             var bitmap = imageData.Image.ToWriteableBitmap();
             var bitmap = imageData.Image.ToWriteableBitmap();
@@ -398,7 +398,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         var selectedNodes = doc.NodeGraph.AllNodes.Where(x => x.IsNodeSelected).Select(x => x.Id).ToArray();
         var selectedNodes = doc.NodeGraph.AllNodes.Where(x => x.IsNodeSelected).Select(x => x.Id).ToArray();
         if (selectedNodes.Length == 0)
         if (selectedNodes.Length == 0)
             return;
             return;
-
+        
         await ClipboardController.CopyNodes(selectedNodes, doc.Id);
         await ClipboardController.CopyNodes(selectedNodes, doc.Id);
 
 
         areNodesInClipboard = true;
         areNodesInClipboard = true;

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

@@ -235,7 +235,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             {
             {
                 if (File.Exists(dataImage.Name))
                 if (File.Exists(dataImage.Name))
                 {
                 {
-                    OpenRegularImage(dataImage.Image, null);
+                    OpenRegularImage(dataImage.Image, dataImage.Name);
                     continue;
                     continue;
                 }
                 }
 
 

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

@@ -235,6 +235,9 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         AnalyticsTrack = true)]
         AnalyticsTrack = true)]
     public void DuplicateMember()
     public void DuplicateMember()
     {
     {
+        if (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember == null)
+            return;
+
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
 
 
         member.Document.Operations.DuplicateMember(member.Id);
         member.Document.Operations.DuplicateMember(member.Id);
@@ -379,7 +382,6 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
-
     public bool ActiveMemberHasApplyableMask() =>
     public bool ActiveMemberHasApplyableMask() =>
         (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false)
         (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false)
         && Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember is IRasterLayerHandler;
         && Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember is IRasterLayerHandler;

+ 8 - 4
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -247,10 +247,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
     protected override void OnClosing(WindowClosingEventArgs e)
     protected override void OnClosing(WindowClosingEventArgs e)
     {
     {
         base.OnClosing(e);
         base.OnClosing(e);
-        videoPreviewTimer.Stop();
-        videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
-        videoPreviewTimer = null;
-        cancellationTokenSource.Cancel();
+        if (videoPreviewTimer != null)
+        {
+            videoPreviewTimer.Stop();
+            videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
+            videoPreviewTimer = null;
+        }
+
+        cancellationTokenSource?.Cancel();
 
 
         ExportPreview?.Dispose();
         ExportPreview?.Dispose();
 
 

+ 2 - 1
src/PixiEditor/Views/Layers/LayersManager.axaml.cs

@@ -181,7 +181,8 @@ internal partial class LayersManager : UserControl
             e.Handled = true;
             e.Handled = true;
         }
         }
 
 
-        if (ClipboardController.TryPaste(ActiveDocument, ManagerViewModel, new[] { (IDataObject)e.Data }, true))
+        // Imported object doesn't do any async operations so .Result is safe to use here.
+        if (ClipboardController.TryPaste(ActiveDocument, ManagerViewModel, new[] { new ImportedObject((IDataObject)e.Data) }, true).Result)
         {
         {
             e.Handled = true;
             e.Handled = true;
         }
         }

+ 6 - 2
src/PixiEditor/Views/MainView.axaml.cs

@@ -75,9 +75,13 @@ public partial class MainView : UserControl
             return;
             return;
         }
         }
 
 
-        if (fileDropList is { Length: > 0 } && Importer.IsSupportedFile(fileDropList[0].Path.LocalPath))
+        if (fileDropList is { Length: > 0 })
         {
         {
-            Context.FileSubViewModel.OpenFromPath(fileDropList[0].Path.LocalPath);
+            foreach (var item in fileDropList)
+            {
+                Importer.IsSupportedFile(item.Path.LocalPath);
+                Context.FileSubViewModel.OpenFromPath(item.Path.LocalPath);
+            }
         }
         }
     }
     }
 
 

+ 2 - 2
src/PixiEditor/Views/Nodes/Properties/NodePropertyView.cs

@@ -91,12 +91,12 @@ public abstract class NodePropertyView : UserControl
 
 
     protected void HideSocket(bool hideInputSocket, bool hideOutputSocket)
     protected void HideSocket(bool hideInputSocket, bool hideOutputSocket)
     {
     {
-        if (hideInputSocket)
+        if (hideInputSocket && InputSocket is not null)
         {
         {
             InputSocket.IsVisible = false;
             InputSocket.IsVisible = false;
         }
         }
 
 
-        if (hideOutputSocket)
+        if (hideOutputSocket && OutputSocket is not null)
         {
         {
             OutputSocket.IsVisible = false;
             OutputSocket.IsVisible = false;
         }
         }

+ 10 - 2
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -1,4 +1,5 @@
-using Avalonia;
+using System;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Shapes;
 using Avalonia.Input;
 using Avalonia.Input;
@@ -76,7 +77,14 @@ public abstract class Handle : IHandle
         {
         {
             if (shape is string path)
             if (shape is string path)
             {
             {
-                return new PathVectorData(VectorPath.FromSvgPath(path));
+                try
+                {
+                    return new PathVectorData(VectorPath.FromSvgPath(path));
+                }
+                catch (Exception ex)
+                {
+                    return new PathVectorData(VectorPath.FromSvgPath("M 0 0 L 1 0 M 0 0 L 0 1"));
+                }
             }
             }
 
 
             if (shape is VectorPathResource resource)
             if (shape is VectorPathResource resource)