Kaynağa Gözat

Merge branch 'master' into text

Krzysztof Krysiński 5 ay önce
ebeveyn
işleme
c5e38e6053

+ 2 - 2
README.md

@@ -12,9 +12,9 @@
 
 ### Check out our website [pixieditor.net](https://pixieditor.net) and [PixiEditor Forum](https://forum.pixieditor.net/)
 
-# Contributions temporarily freezed!
+# Feature Contributions temporarily freezed!
 
-PixiEditor is undergoing massive changes, master branch is unstable. We will not accept any contributions at the moment, until version 2.0 comes out.
+PixiEditor is undergoing massive changes, master branch is unstable. We will not accept any feature contributions at the moment, until version 2.0 comes out. Feel free to fix bugs though. But before you do, let us know on [Discord](https://discord.gg/qSRMYmq), since we already might've fixed them.
 
 ## About PixiEditor
 

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -54,12 +54,12 @@ public class MergeNode : RenderNode
         if (Bottom.Value != null && Top.Value != null)
         {
             int saved = target.Canvas.SaveLayer();
-            Bottom.Value.Paint(context, target);
+            Bottom.Value?.Paint(context, target);
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             
-            Top.Value.Paint(context, target);
+            Top.Value?.Paint(context, target);
             target.Canvas.RestoreToCount(saved);
             return;
         }

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

@@ -3,6 +3,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using Drawie.Numerics.Helpers;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
@@ -47,8 +48,7 @@ public class PointsVectorData : ShapeVectorData
             canvas.SetMatrix(final);
         }
 
-        canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((float)p.X, (float)p.Y)).ToArray(),
-            paint);
+        canvas.DrawPoints(PointMode.Points, Points.ToVecFArray(), paint);
 
         if (applyTransform)
         {

+ 2 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -26,6 +26,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
         var data = Input.Value;
 
         var distance = MinDistance.Value;
+        var minDistanceSquared = distance * distance;
 
         if (distance == 0 || data == null || data.Points == null)
         {
@@ -34,9 +35,6 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 
         var availablePoints = data.Points.Distinct().ToList();
         List<VecD> newPoints = new List<VecD>();
-        
-        var minDistance = MinDistance.Value;
-        var documentSize = context.DocumentSize;
 
         var random = new Random(Seed.Value);
         while (availablePoints.Count > 1)
@@ -55,7 +53,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
             continue;
 
             bool InRange(VecD other) =>
-                (other - point).Length <= minDistance;
+                (other - point).LengthSquared <= minDistanceSquared;
         }
 
         if (availablePoints.Count == 1)

+ 2 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -848,5 +848,6 @@
   "TEXT_TOOL_TOOLTIP": "Create text ({0}).",
   "BOLD_TOOLTIP": "Bold",
   "ITALIC_TOOLTIP": "Italic",
-  "CUSTOM_FONT": "Custom font"
+  "CUSTOM_FONT": "Custom font",
+  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics"
 }

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

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

+ 97 - 43
src/PixiEditor/ViewModels/SubViewModels/DebugViewModel.cs

@@ -7,6 +7,10 @@ using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform.Storage;
+using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.Debug;
+using Drawie.Interop.Avalonia.Core;
+using DrawiEngine;
 using Newtonsoft.Json;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
@@ -26,7 +30,8 @@ using PixiEditor.Views.Dialogs.Debugging.Localization;
 
 namespace PixiEditor.ViewModels.SubViewModels;
 
-[Command.Group("PixiEditor.Debug", "DEBUG", IsVisibleMenuProperty = $"{nameof(ViewModelMain.DebugSubViewModel)}.{nameof(UseDebug)}")]
+[Command.Group("PixiEditor.Debug", "DEBUG",
+    IsVisibleMenuProperty = $"{nameof(ViewModelMain.DebugSubViewModel)}.{nameof(UseDebug)}")]
 internal class DebugViewModel : SubViewModel<ViewModelMain>
 {
     public static bool IsDebugBuild { get; set; }
@@ -57,7 +62,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     }
 
     private bool forceOtherFlowDirection;
-    
+
     public bool ForceOtherFlowDirection
     {
         get => forceOtherFlowDirection;
@@ -89,19 +94,25 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
 
         IOperatingSystem.Current.OpenFolder(path);
     }
-    
 
-    [Command.Debug("PixiEditor.Debug.IO.OpenLocalAppDataDirectory", @"PixiEditor", "OPEN_LOCAL_APPDATA_DIR", "OPEN_LOCAL_APPDATA_DIR",
-        MenuItemPath = "DEBUG/OPEN_LOCAL_APPDATA_DIR", MenuItemOrder = 5, Icon = PixiPerfectIcons.Folder, AnalyticsTrack = true)]
-    [Command.Debug("PixiEditor.Debug.IO.OpenCrashReportsDirectory", @"PixiEditor\crash_logs", "OPEN_CRASH_REPORTS_DIR", "OPEN_CRASH_REPORTS_DIR",
-        MenuItemPath = "DEBUG/OPEN_CRASH_REPORTS_DIR", MenuItemOrder = 6, Icon = PixiPerfectIcons.Folder, AnalyticsTrack = true)]
+
+    [Command.Debug("PixiEditor.Debug.IO.OpenLocalAppDataDirectory", @"PixiEditor", "OPEN_LOCAL_APPDATA_DIR",
+        "OPEN_LOCAL_APPDATA_DIR",
+        MenuItemPath = "DEBUG/OPEN_LOCAL_APPDATA_DIR", MenuItemOrder = 5, Icon = PixiPerfectIcons.Folder,
+        AnalyticsTrack = true)]
+    [Command.Debug("PixiEditor.Debug.IO.OpenCrashReportsDirectory", @"PixiEditor\crash_logs", "OPEN_CRASH_REPORTS_DIR",
+        "OPEN_CRASH_REPORTS_DIR",
+        MenuItemPath = "DEBUG/OPEN_CRASH_REPORTS_DIR", MenuItemOrder = 6, Icon = PixiPerfectIcons.Folder,
+        AnalyticsTrack = true)]
     public static void OpenLocalAppDataFolder(string subDirectory)
     {
-        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subDirectory);
+        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+            subDirectory);
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.IO.OpenRoamingAppDataDirectory", @"PixiEditor", "OPEN_ROAMING_APPDATA_DIR", "OPEN_ROAMING_APPDATA_DIR", Icon = PixiPerfectIcons.Folder,
+    [Command.Debug("PixiEditor.Debug.IO.OpenRoamingAppDataDirectory", @"PixiEditor", "OPEN_ROAMING_APPDATA_DIR",
+        "OPEN_ROAMING_APPDATA_DIR", Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "DEBUG/OPEN_ROAMING_APPDATA_DIR", MenuItemOrder = 7)]
     public static void OpenAppDataFolder(string subDirectory)
     {
@@ -109,7 +120,8 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.IO.OpenTempDirectory", @"PixiEditor", "OPEN_TEMP_DIR", "OPEN_TEMP_DIR", Icon = PixiPerfectIcons.Folder,
+    [Command.Debug("PixiEditor.Debug.IO.OpenTempDirectory", @"PixiEditor", "OPEN_TEMP_DIR", "OPEN_TEMP_DIR",
+        Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "DEBUG/OPEN_TEMP_DIR", MenuItemOrder = 8, AnalyticsTrack = true)]
     public static void OpenTempFolder(string subDirectory)
     {
@@ -117,14 +129,40 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.DumpAllCommands", "DUMP_ALL_COMMANDS", "DUMP_ALL_COMMANDS_DESCRIPTIVE", AnalyticsTrack = true)]
+    [Command.Debug("PixiEditor.Debug.DumpGPUDiagnostics", "DUMP_GPU_DIAGNOSTICS", "DUMP_GPU_DIAGNOSTICS",
+        AnalyticsTrack = true)]
+    public async Task DumpGpuDiagnostics()
+    {
+        await Application.Current.ForDesktopMainWindowAsync(async desktop =>
+        {
+            FilePickerSaveOptions options = new FilePickerSaveOptions();
+            options.DefaultExtension = "txt";
+            options.FileTypeChoices =
+                new FilePickerFileType[] { new FilePickerFileType("Text") { Patterns = new[] { "*.txt" } } };
+            var pickedFile = desktop.StorageProvider.SaveFilePickerAsync(options).Result;
+
+            if (pickedFile != null)
+            {
+                GpuDiagnostics diagnostics = IDrawieInteropContext.Current.GetGpuDiagnostics();
+
+                await using StreamWriter writer = new StreamWriter(pickedFile.Path.LocalPath);
+                await writer.WriteAsync(diagnostics.ToString());
+
+                IOperatingSystem.Current.OpenFolder(pickedFile.Path.LocalPath);
+            }
+        });
+    }
+
+    [Command.Debug("PixiEditor.Debug.DumpAllCommands", "DUMP_ALL_COMMANDS", "DUMP_ALL_COMMANDS_DESCRIPTIVE",
+        AnalyticsTrack = true)]
     public async Task DumpAllCommands()
     {
         await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
             FilePickerSaveOptions options = new FilePickerSaveOptions();
             options.DefaultExtension = "txt";
-            options.FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("Text") {Patterns = new [] {"*.txt"}} };
+            options.FileTypeChoices =
+                new FilePickerFileType[] { new FilePickerFileType("Text") { Patterns = new[] { "*.txt" } } };
             var pickedFile = desktop.StorageProvider.SaveFilePickerAsync(options).Result;
 
             if (pickedFile != null)
@@ -142,15 +180,17 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
             }
         });
     }
-    
-    [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "GENERATE_KEY_BINDINGS_TEMPLATE", "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE", AnalyticsTrack = true)]
+
+    [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "GENERATE_KEY_BINDINGS_TEMPLATE",
+        "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE", AnalyticsTrack = true)]
     public async Task GenerateKeysTemplate()
     {
         await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
             FilePickerSaveOptions options = new FilePickerSaveOptions();
             options.DefaultExtension = "json";
-            options.FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("Json") {Patterns = new [] {"*.json"}} };
+            options.FileTypeChoices =
+                new FilePickerFileType[] { new FilePickerFileType("Json") { Patterns = new[] { "*.json" } } };
             var pickedFile = await desktop.StorageProvider.SaveFilePickerAsync(options);
 
             if (pickedFile != null)
@@ -161,9 +201,11 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
                 Dictionary<string, KeyDefinition> keyDefinitions = new Dictionary<string, KeyDefinition>();
                 foreach (var command in commands)
                 {
-                    if(command.IsDebug)
+                    if (command.IsDebug)
                         continue;
-                    keyDefinitions.Add($"(provider).{command.InternalName}", new KeyDefinition(command.InternalName, new HumanReadableKeyCombination("None"), Array.Empty<string>()));
+                    keyDefinitions.Add($"(provider).{command.InternalName}",
+                        new KeyDefinition(command.InternalName, new HumanReadableKeyCombination("None"),
+                            Array.Empty<string>()));
                 }
 
                 writer.Write(JsonConvert.SerializeObject(keyDefinitions, Formatting.Indented));
@@ -171,7 +213,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
                 string file = await File.ReadAllTextAsync(pickedFile.Path.LocalPath);
                 foreach (var command in commands)
                 {
-                    if(command.IsDebug)
+                    if (command.IsDebug)
                         continue;
                     file = file.Replace($"(provider).{command.InternalName}", "");
                 }
@@ -182,19 +224,21 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         });
     }
 
-    [Command.Debug("PixiEditor.Debug.ValidateShortcutMap", "VALIDATE_SHORTCUT_MAP", "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE", AnalyticsTrack = true)]
+    [Command.Debug("PixiEditor.Debug.ValidateShortcutMap", "VALIDATE_SHORTCUT_MAP", "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE",
+        AnalyticsTrack = true)]
     public async Task ValidateShortcutMap()
     {
         await Application.Current.ForDesktopMainWindowAsync(async desktop =>
         {
             FilePickerOpenOptions options = new FilePickerOpenOptions
-                {
-                    SuggestedStartLocation =
-                        await desktop.StorageProvider.TryGetFolderFromPathAsync(
-                            Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data",
-                                "ShortcutActionMaps")),
-                    FileTypeFilter = new FilePickerFileType[] { new FilePickerFileType("Json") {Patterns = new [] {"*.json"}} }
-                };
+            {
+                SuggestedStartLocation =
+                    await desktop.StorageProvider.TryGetFolderFromPathAsync(
+                        Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data",
+                            "ShortcutActionMaps")),
+                FileTypeFilter =
+                    new FilePickerFileType[] { new FilePickerFileType("Json") { Patterns = new[] { "*.json" } } }
+            };
             var pickedFile = desktop.StorageProvider.OpenFilePickerAsync(options).Result.FirstOrDefault();
 
             if (pickedFile != null)
@@ -212,7 +256,8 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
                     }
                 }
 
-                NoticeDialog.Show(new LocalizedString("VALIDATION_KEYS_NOTICE_DIALOG", emptyKeys, unknownCommands), "RESULT");
+                NoticeDialog.Show(new LocalizedString("VALIDATION_KEYS_NOTICE_DIALOG", emptyKeys, unknownCommands),
+                    "RESULT");
             }
         });
     }
@@ -239,20 +284,22 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         new PointerDebugPopup().Show();
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenLocalizationDebugWindow", "OPEN_LOCALIZATION_DEBUG_WINDOW", "OPEN_LOCALIZATION_DEBUG_WINDOW",
+    [Command.Debug("PixiEditor.Debug.OpenLocalizationDebugWindow", "OPEN_LOCALIZATION_DEBUG_WINDOW",
+        "OPEN_LOCALIZATION_DEBUG_WINDOW",
         MenuItemPath = "DEBUG/OPEN_LOCALIZATION_DEBUG_WINDOW", MenuItemOrder = 2, AnalyticsTrack = true)]
     public void OpenLocalizationDebugWindow()
     {
         if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
-            var window = desktop.Windows.OfType<LocalizationDebugWindow>().FirstOrDefault(new LocalizationDebugWindow());
+            var window = desktop.Windows.OfType<LocalizationDebugWindow>()
+                .FirstOrDefault(new LocalizationDebugWindow());
             window.Show();
             window.Activate();
         }
-
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenPerformanceDebugWindow", "Open Performance Debug Window", "Open Performance Debug Window",
+    [Command.Debug("PixiEditor.Debug.OpenPerformanceDebugWindow", "Open Performance Debug Window",
+        "Open Performance Debug Window",
         MenuItemPath = "DEBUG/Open Performance Debug Window", MenuItemOrder = 4, AnalyticsTrack = true)]
     public void OpenPerformanceDebugWindow()
     {
@@ -275,7 +322,10 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
                     await desktop.StorageProvider.TryGetFolderFromPathAsync(
                         Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Data",
                             "Languages")),
-                FileTypeFilter = new FilePickerFileType[] { new FilePickerFileType("key-value json") {Patterns = new [] {"*.json"}} }
+                FileTypeFilter = new FilePickerFileType[]
+                {
+                    new FilePickerFileType("key-value json") { Patterns = new[] { "*.json" } }
+                }
             };
             var pickedFile = desktop.StorageProvider.OpenFilePickerAsync(options).Result.FirstOrDefault();
 
@@ -289,7 +339,8 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         });
     }
 
-    [Command.Debug("PixiEditor.Debug.IO.OpenInstallDirectory", "OPEN_INSTALLATION_DIR", "OPEN_INSTALLATION_DIR", Icon = PixiPerfectIcons.Folder,
+    [Command.Debug("PixiEditor.Debug.IO.OpenInstallDirectory", "OPEN_INSTALLATION_DIR", "OPEN_INSTALLATION_DIR",
+        Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "DEBUG/OPEN_INSTALLATION_DIR", MenuItemOrder = 9, AnalyticsTrack = true)]
     public static void OpenInstallLocation()
     {
@@ -314,11 +365,14 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%/PixiEditor/user_preferences.json", "DELETE_USR_PREFS", "DELETE_USR_PREFS",
+    [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%/PixiEditor/user_preferences.json",
+        "DELETE_USR_PREFS", "DELETE_USR_PREFS",
         MenuItemPath = "DEBUG/DELETE/USER_PREFS", MenuItemOrder = 11, AnalyticsTrack = true)]
-    [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%/PixiEditor/shortcuts.json", "DELETE_SHORTCUT_FILE", "DELETE_SHORTCUT_FILE",
+    [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%/PixiEditor/shortcuts.json",
+        "DELETE_SHORTCUT_FILE", "DELETE_SHORTCUT_FILE",
         MenuItemPath = "DEBUG/DELETE/SHORTCUT_FILE", MenuItemOrder = 12, AnalyticsTrack = true)]
-    [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%/PixiEditor/editor_data.json", "DELETE_EDITOR_DATA", "DELETE_EDITOR_DATA",
+    [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%/PixiEditor/editor_data.json",
+        "DELETE_EDITOR_DATA", "DELETE_EDITOR_DATA",
         MenuItemPath = "DEBUG/DELETE/EDITOR_DATA", MenuItemOrder = 13, AnalyticsTrack = true)]
     public static async Task DeleteFile(string path)
     {
@@ -326,7 +380,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
             return;
         string[] parts = path.Split('/');
         path = Path.Combine(parts); // os specific path
-        
+
         string file = Environment.ExpandEnvironmentVariables(path);
         if (!File.Exists(file))
         {
@@ -339,12 +393,12 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
             }
         }
 
-        OptionsDialog<string> dialog = new("ARE_YOU_SURE", new LocalizedString("ARE_YOU_SURE_PATH_FULL_PATH", path, file), MainWindow.Current)
-        {
-            // TODO: seems like this should be localized strings
-            { new LocalizedString("YES"), x => File.Delete(file) },
-            new LocalizedString("CANCEL")
-        };
+        OptionsDialog<string> dialog =
+            new("ARE_YOU_SURE", new LocalizedString("ARE_YOU_SURE_PATH_FULL_PATH", path, file), MainWindow.Current)
+            {
+                // TODO: seems like this should be localized strings
+                { new LocalizedString("YES"), x => File.Delete(file) }, new LocalizedString("CANCEL")
+            };
 
         await dialog.ShowDialog();
     }

+ 1 - 18
src/PixiEditor/Views/Dock/ColorPickerDockView.axaml.cs

@@ -19,23 +19,6 @@ public partial class ColorPickerDockView : UserControl
         base.OnLoaded(e);
         var textBoxes = this.GetVisualDescendants().OfType<TextBox>().ToArray();
 
-        foreach (var textBox in textBoxes)
-        {
-            var existingBehaviors = Interaction.GetBehaviors(textBox);
-            if(existingBehaviors.Any(x => x is GlobalShortcutFocusBehavior)) continue;
-            bool attach = false;
-            if (existingBehaviors == null)
-            {
-                attach = true;
-                existingBehaviors = new BehaviorCollection();
-            }
-
-            existingBehaviors.Add(new GlobalShortcutFocusBehavior());
-
-            if (attach)
-            {
-                Interaction.SetBehaviors(textBox, existingBehaviors);
-            }
-        }
+        ColorSlidersDockView.AttachBehavioursToTextBoxes(textBoxes); 
     }
 }

+ 41 - 1
src/PixiEditor/Views/Dock/ColorSlidersDockView.axaml.cs

@@ -1,6 +1,10 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using Avalonia.Xaml.Interactivity;
+using PixiEditor.Helpers.Behaviours;
 
 namespace PixiEditor.Views.Dock;
 
@@ -10,5 +14,41 @@ public partial class ColorSlidersDockView : UserControl
     {
         InitializeComponent();
     }
-}
 
+    protected override void OnLoaded(RoutedEventArgs e)
+    {
+        base.OnLoaded(e);
+        var textBoxes = this.GetVisualDescendants().OfType<TextBox>().ToArray();
+
+        AttachBehavioursToTextBoxes(textBoxes);
+    }
+
+    internal static void AttachBehavioursToTextBoxes(TextBox[] textBoxes)
+    {
+        foreach (var textBox in textBoxes)
+        {
+            var existingBehaviors = Interaction.GetBehaviors(textBox);
+            if (existingBehaviors.Any(x => x is GlobalShortcutFocusBehavior)) continue;
+            bool attach = false;
+            if (existingBehaviors == null)
+            {
+                attach = true;
+                existingBehaviors = new BehaviorCollection();
+            }
+
+            try
+            {
+                existingBehaviors.Add(new GlobalShortcutFocusBehavior());
+
+                if (attach)
+                {
+                    Interaction.SetBehaviors(textBox, existingBehaviors);
+                }
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                // avalonia's bug
+            }
+        }
+    }
+}