Jelajahi Sumber

Fixed and commented more commands stuff

Krzysztof Krysiński 2 tahun lalu
induk
melakukan
f4074ecbda
21 mengubah file dengan 528 tambahan dan 42 penghapusan
  1. 17 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/InputKeyHelpers.cs
  2. 2 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandController.cs
  3. 44 6
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Command.cs
  4. 22 19
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ContextMenu.cs
  5. 3 2
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Menu.cs
  6. 17 4
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ShortcutBinding.cs
  7. 19 6
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/NoticeDialog.cs
  8. 8 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/ICommandsHandler.cs
  9. 0 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/ISearchHandler.cs
  10. 0 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolHandler.cs
  11. 0 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolsHandler.cs
  12. 0 4
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Input/KeyCombination.cs
  13. 1 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/PixiEditor.Avalonia.csproj
  14. 1 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/NoticePopup.axaml.cs
  15. 11 0
      src/PixiEditor.OperatingSystem/IInputKeys.cs
  16. 13 0
      src/PixiEditor.OperatingSystem/IOperatingSystem.cs
  17. 4 0
      src/PixiEditor.OperatingSystem/PixiEditor.OperatingSystem.csproj
  18. 4 0
      src/PixiEditor.Windows/PixiEditor.Windows.csproj
  19. 281 0
      src/PixiEditor.Windows/Win32.cs
  20. 78 0
      src/PixiEditor.Windows/WindowsInputKeys.cs
  21. 3 0
      src/PixiEditor.Windows/WindowsOperatingSystem.cs

+ 17 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/InputKeyHelpers.cs

@@ -0,0 +1,17 @@
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Windows.Input;
+using Avalonia.Input;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Helpers;
+
+internal static class InputKeyHelpers
+{
+    /// <summary>
+    /// Returns the character of the <paramref name="key"/> mapped to the users keyboard layout
+    /// </summary>
+    public static string GetKeyboardKey(Key key, bool forceInvariant = false) =>
+        IOperatingSystem.Current.InputKeys.GetKeyboardKey(key, forceInvariant);
+}

+ 2 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/CommandController.cs

@@ -146,6 +146,7 @@ internal class CommandController
     private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,
         ShortcutsTemplate template)
     {
+        IToolsHandler toolsHandler = serviceProvider.GetRequiredService<IToolsHandler>();
         foreach (var toolInstance in serviceProvider.GetServices<IToolHandler>())
         {
             var type = toolInstance.GetType();
@@ -161,7 +162,7 @@ internal class CommandController
 
             LocalizedString displayName = new("SELECT_TOOL", toolInstance.DisplayName);
 
-            var command = new Command.ToolCommand()
+            var command = new Command.ToolCommand(toolsHandler)
             {
                 InternalName = internalName,
                 DisplayName = displayName,

+ 44 - 6
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Command.cs

@@ -1,4 +1,7 @@
-using Avalonia.Controls;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
@@ -53,20 +56,48 @@ internal class Command : MarkupExtension
 
     class ProvidedICommand : IReactiveCommand
     {
-        // TODO: Implement with ReactiveUI
+        //TODO: Not found in Avalonia
+
         /*public event EventHandler CanExecuteChanged
         {
             add => CommandManager.RequerySuggested += value;
             remove => CommandManager.RequerySuggested -= value;
-        }
+        }*/
 
         public Commands.Command Command { get; init; }
 
         public bool UseProvidedParameter { get; init; }
 
-        public bool CanExecute(object parameter) => UseProvidedParameter ? Command.Methods.CanExecute(parameter) : Command.CanExecute();
+        public IObservable<Exception> ThrownExceptions { get; }
+
+        public IObservable<bool> IsExecuting { get; }
+
+        public IObservable<bool> CanExecute { get; }
+
+        public ProvidedICommand()
+        {
+            ReactiveCommand<object, Unit> reactiveCommand = ReactiveCommand.Create<object, Unit>(Execute, CanExecuteCommand());
+        }
+
+
+        public IObservable<bool> CanExecuteCommand()
+        {
+            return this.WhenAnyValue(x => x.Command, x => x.UseProvidedParameter, (command, useProvidedParameter) =>
+            {
+                if (useProvidedParameter)
+                {
+                    return command.CanExecute();
+                    //return command.CanExecute(parameter); // Should be this, but idk how to make it properly, I think whole logic should be changed so it fits
+                    // reactiveUI
+                }
+                else
+                {
+                    return command.CanExecute();
+                }
+            });
+        }
 
-        public void Execute(object parameter)
+        public Unit Execute(object parameter)
         {
             if (UseProvidedParameter)
             {
@@ -76,6 +107,13 @@ internal class Command : MarkupExtension
             {
                 Command.Execute();
             }
-        }*/
+
+            return Unit.Default;
+        }
+
+        public void Dispose()
+        {
+
+        }
     }
 }

+ 22 - 19
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ContextMenu.cs

@@ -1,33 +1,35 @@
-using System.ComponentModel;
-using System.Windows;
-using System.Windows.Controls;
+using Avalonia;
+using Avalonia.Controls;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands.XAML;
 
-internal class ContextMenu : ContextMenu
+internal class ContextMenu : global::Avalonia.Controls.ContextMenu
 {
-    public static readonly DependencyProperty CommandNameProperty =
-        DependencyProperty.RegisterAttached(
-            "Command",
-            typeof(string),
-            typeof(ContextMenu),
-            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, CommandChanged)
-        );
+    public static readonly DirectProperty<ContextMenu, string> CommandNameProperty;
 
-    public static string GetCommand(UIElement target) => (string)target.GetValue(CommandNameProperty);
+    static ContextMenu()
+    {
+        CommandNameProperty = AvaloniaProperty.RegisterDirect<ContextMenu, string>(
+            nameof(Command),
+            GetCommand,
+            SetCommand);
+        CommandNameProperty.Changed.Subscribe(CommandChanged);
+    }
+
+    public static string GetCommand(ContextMenu target) => (string)target.GetValue(CommandNameProperty);
 
-    public static void SetCommand(UIElement target, string value) => target.SetValue(CommandNameProperty, value);
+    public static void SetCommand(ContextMenu target, string value) => target.SetValue(CommandNameProperty, value);
 
-    public static void CommandChanged(object sender, DependencyPropertyChangedEventArgs e)
+    public static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
     {
-        if (e.NewValue is not string value || sender is not MenuItem item)
+        if (e.NewValue is not string value || e.Sender is not MenuItem item)
         {
             throw new InvalidOperationException($"{nameof(ContextMenu)}.Command only works for MenuItem's");
         }
 
-        if (DesignerProperties.GetIsInDesignMode(sender as DependencyObject))
+        if (Design.IsDesignMode)
         {
             HandleDesignMode(item, value);
             return;
@@ -35,13 +37,14 @@ internal class ContextMenu : ContextMenu
 
         var command = CommandController.Current.Commands[value];
 
-        item.Command = Command.GetICommand(command, false);
-        item.SetBinding(MenuItem.InputGestureTextProperty, ShortcutBinding.GetBinding(command, null));
+        //TODO: Same story as in Menu.cs
+        //item.Command = Command.GetICommand(command, false);
+        item.Bind(MenuItem.InputGestureProperty, ShortcutBinding.GetBinding(command, null));
     }
 
     private static void HandleDesignMode(MenuItem item, string name)
     {
         var command = DesignCommandHelpers.GetCommandAttribute(name);
-        item.InputGestureText = new KeyCombination(command.Key, command.Modifiers).ToString();
+        item.InputGesture = new KeyCombination(command.Key, command.Modifiers).ToKeyGesture();
     }
 }

+ 3 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/Menu.cs

@@ -41,7 +41,7 @@ internal class Menu : global::Avalonia.Controls.Menu
         var command = CommandController.Current.Commands[value];
 
         var icon = new Image
-        { 
+        {
             Source = command.GetIcon(), 
             Width = IconDimensions, Height = IconDimensions,
             Opacity = command.CanExecute() ? 1 : 0.75
@@ -53,7 +53,8 @@ internal class Menu : global::Avalonia.Controls.Menu
 
         });
 
-        item.Command = Command.GetICommand(command, false);
+        //TODO: This, some ReactiveUI shit should be here, https://docs.avaloniaui.net/docs/next/concepts/reactiveui/reactive-command
+        //item.Command = Command.GetICommand(command, false);
         item.Icon = icon;
         item.Bind(MenuItem.InputGestureProperty, ShortcutBinding.GetBinding(command, null));
     }

+ 17 - 4
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/XAML/ShortcutBinding.cs

@@ -1,7 +1,11 @@
-using Avalonia.Data;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
 using Avalonia.Data.Converters;
 using Avalonia.Markup.Xaml;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Containers;
 using PixiEditor.Models.DataHolders;
 using ActualCommand = PixiEditor.Models.Commands.Commands.Command;
 
@@ -21,14 +25,23 @@ internal class ShortcutBinding : MarkupExtension
 
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
-        if (ViewModelMain.Current == null)
+        if (Design.IsDesignMode)
         {
             var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
             return new KeyCombination(attribute.Key, attribute.Modifiers).ToString();
         }
 
-        commandController ??= ViewModelMain.Current.CommandController;
-        return GetBinding(commandController.Commands[Name], Converter).ProvideValue(serviceProvider);
+        ICommandsHandler? handler = serviceProvider.GetService<ICommandsHandler>();
+        commandController ??= handler.CommandController;
+        var binding = GetBinding(commandController.Commands[Name], Converter);
+
+        var targetValue = serviceProvider.GetService<IProvideValueTarget>();
+        var targetObject = targetValue.TargetObject as AvaloniaObject;
+        var targetProperty = targetValue.TargetProperty as AvaloniaProperty;
+
+        var instancedBinding = binding.Initiate(targetObject, targetProperty);
+
+        return instancedBinding; //TODO: This won't work, leaving it for now
     }
 
     public static Binding GetBinding(ActualCommand command, IValueConverter converter) => new Binding

+ 19 - 6
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/NoticeDialog.cs

@@ -1,4 +1,7 @@
-using PixiEditor.Extensions.Common.Localization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Localization;
 using PixiEditor.Views.Dialogs;
 
@@ -13,12 +16,22 @@ internal static class NoticeDialog
     /// <param name="title">Localized string key for title.</param>
     public static void Show(LocalizedString message, LocalizedString title)
     {
-        NoticePopup popup = new()
+        if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime lifetime)
         {
-            Body = message,
-            Title = title
-        };
+            NoticePopup popup = new()
+            {
+                Body = message,
+                Title = title
+            };
 
-        popup.ShowDialog();
+            if(lifetime.MainWindow is not null)
+            {
+                popup.ShowDialog(lifetime.MainWindow);
+            }
+        }
+        else
+        {
+            throw new InvalidOperationException("NoticeDialog can only be shown in ClassicDesktopStyleApplicationLifetime");
+        }
     }
 }

+ 8 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/ICommandsHandler.cs

@@ -0,0 +1,8 @@
+using PixiEditor.Models.Commands;
+
+namespace PixiEditor.Models.Containers;
+
+internal interface ICommandsHandler
+{
+    public CommandController CommandController { get; }
+}

+ 0 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Descriptors/ISearchHandler.cs → src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/ISearchHandler.cs


+ 0 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Descriptors/IToolHandler.cs → src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolHandler.cs


+ 0 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Descriptors/IToolsHandler.cs → src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/IToolsHandler.cs


+ 0 - 4
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Input/KeyCombination.cs

@@ -1,14 +1,10 @@
 using PixiEditor.Helpers;
-using PixiEditor.Helpers.Extensions;
 using System.Diagnostics;
-using System.Globalization;
 using System.Linq;
 using System.Text;
-using System.Windows.Input;
 using Avalonia.Input;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Helpers;
-using PixiEditor.Models.Localization;
 
 namespace PixiEditor.Models.DataHolders;
 

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/PixiEditor.Avalonia.csproj

@@ -35,6 +35,7 @@
       <ProjectReference Include="..\..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
       <ProjectReference Include="..\..\PixiEditor.DrawingApi.Skia\PixiEditor.DrawingApi.Skia.csproj" />
       <ProjectReference Include="..\..\PixiEditor.Extensions\PixiEditor.Extensions.csproj" />
+      <ProjectReference Include="..\..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
       <ProjectReference Include="..\..\PixiEditor.Platform.MSStore\PixiEditor.Platform.MSStore.csproj" />
       <ProjectReference Include="..\..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj" />
       <ProjectReference Include="..\..\PixiEditor.Platform.Steam\PixiEditor.Platform.Steam.csproj" />

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

@@ -1,5 +1,6 @@
 using System.Windows;
 using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Interactivity;
 
 namespace PixiEditor.Views.Dialogs;

+ 11 - 0
src/PixiEditor.OperatingSystem/IInputKeys.cs

@@ -0,0 +1,11 @@
+using Avalonia.Input;
+
+namespace PixiEditor.OperatingSystem;
+
+public interface IInputKeys
+{
+    /// <summary>
+    ///     Returns the character of the <paramref name="key"/> mapped to the users keyboard layout
+    /// </summary>
+    public string GetKeyboardKey(Key key, bool forceInvariant = false);
+}

+ 13 - 0
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -2,5 +2,18 @@
 
 public interface IOperatingSystem
 {
+    public static IOperatingSystem Current { get; protected set; }
     public string Name { get; }
+
+    public IInputKeys InputKeys { get; }
+
+    protected static void SetCurrent(IOperatingSystem operatingSystem)
+    {
+        if (Current != null)
+        {
+            throw new InvalidOperationException("Current operating system is already set");
+        }
+
+        Current = operatingSystem;
+    }
 }

+ 4 - 0
src/PixiEditor.OperatingSystem/PixiEditor.OperatingSystem.csproj

@@ -6,4 +6,8 @@
         <Nullable>enable</Nullable>
     </PropertyGroup>
 
+    <ItemGroup>
+      <PackageReference Include="Avalonia" Version="11.0.0" />
+    </ItemGroup>
+
 </Project>

+ 4 - 0
src/PixiEditor.Windows/PixiEditor.Windows.csproj

@@ -10,4 +10,8 @@
       <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
     </ItemGroup>
 
+    <ItemGroup>
+      <PackageReference Include="Avalonia.Win32" Version="11.0.0" />
+    </ItemGroup>
+
 </Project>

+ 281 - 0
src/PixiEditor.Windows/Win32.cs

@@ -0,0 +1,281 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace PixiEditor.Helpers;
+internal class Win32
+{
+    public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
+    public const int WH_MOUSE_LL = 14;
+
+    public const int WM_GETMINMAXINFO = 0x0024;
+    public const int WM_LBUTTONUP = 0x0202;
+    public const int WM_MBUTTONUP = 0x0208;
+    public const int WM_RBUTTONUP = 0x0205;
+    public const int WM_CLOSE = 0x0010;
+    public const int WM_DESTROY = 0x0002;
+
+    public const int ERROR_CLASS_ALREADY_EXISTS = 1410;
+    public const int CW_USEDEFAULT = unchecked((int)0x80000000);
+
+    public const uint WS_CHILD = 0x40000000;
+    public const uint WS_CAPTION = 0x00C00000;
+    public const uint WS_OVERLAPPED = 0x00000000;
+    public const uint WS_SYSMENU = 0x00080000;
+    public const uint WS_THICKFRAME = 0x00040000;
+    public const uint WS_MINIMIZEBOX = 0x00020000;
+    public const uint WS_MAXIMIZEBOX = 0x00010000;
+    public const uint WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
+
+    public delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+    public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    internal struct WNDCLASS
+    {
+        public uint style;
+        public IntPtr lpfnWndProc;
+        public int cbClsExtra;
+        public int cbWndExtra;
+        public IntPtr hInstance;
+        public IntPtr hIcon;
+        public IntPtr hCursor;
+        public IntPtr hbrBackground;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszMenuName;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszClassName;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct MSG
+    {
+        public IntPtr hwnd;
+        public uint message;
+        public IntPtr wParam;
+        public IntPtr lParam;
+        public uint time;
+        public POINT pt;
+        public uint lPrivate;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MSLLHOOKSTRUCT
+    {
+        public POINT Pt;
+        public uint MouseData;
+        public uint Flags;
+        public uint Time;
+        public IntPtr DwExtraInfo;
+    }
+
+    public enum MapType : uint
+    {
+        /// <summary>
+        /// The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VK_TO_VSC = 0x0,
+
+        /// <summary>
+        /// The uCode parameter is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VSC_TO_VK = 0x1,
+
+        /// <summary>
+        /// The uCode parameter is a virtual-key code and is translated into an unshifted character value in the low order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VK_TO_CHAR = 0x2,
+
+        /// <summary>
+        /// The uCode parameter is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VSC_TO_VK_EX = 0x3,
+    }
+
+    [Serializable]
+    [StructLayout(LayoutKind.Sequential)]
+    public struct RECT
+    {
+        public int Left;
+        public int Top;
+        public int Right;
+        public int Bottom;
+
+        public RECT(int left, int top, int right, int bottom)
+        {
+            this.Left = left;
+            this.Top = top;
+            this.Right = right;
+            this.Bottom = bottom;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MONITORINFO
+    {
+        public int cbSize;
+        public RECT rcMonitor;
+        public RECT rcWork;
+        public uint dwFlags;
+    }
+
+    [Serializable]
+    [StructLayout(LayoutKind.Sequential)]
+    public struct POINT
+    {
+        public int X;
+        public int Y;
+
+        public POINT(int x, int y)
+        {
+            this.X = x;
+            this.Y = y;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MINMAXINFO
+    {
+        public POINT ptReserved;
+        public POINT ptMaxSize;
+        public POINT ptMaxPosition;
+        public POINT ptMinTrackSize;
+        public POINT ptMaxTrackSize;
+    }
+
+
+    [DllImport("user32.dll")]
+    public static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
+
+    [DllImport("user32.dll")]
+    public static extern int ToUnicode(
+        uint wVirtKey,
+        uint wScanCode,
+        byte[] lpKeyState,
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder pwszBuff,
+        int cchBuff,
+        uint wFlags);
+
+    [DllImport("user32.dll")]
+    public static extern nint GetKeyboardLayout(
+        uint idThread);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetKeyboardLayoutNameW(
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder klid);
+    
+    [DllImport("user32.dll")]
+    public static extern int ToUnicodeEx(
+        uint wVirtKey,
+        uint wScanCode,
+        byte[] lpKeyState,
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder pwszBuff,
+        int cchBuff,
+        uint wFlags,
+        nint dwhkl);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetKeyboardState(byte[] lpKeyState);
+
+    [DllImport("user32.dll")]
+    public static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, nint hkl);
+    
+    [DllImport("user32.dll")]
+    public static extern IntPtr LoadKeyboardLayoutA(string pwszKLID, uint Flags);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall,
+        SetLastError = true)]
+    public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall,
+        SetLastError = true)]
+    public static extern int UnhookWindowsHookEx(int idHook);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall)]
+    public static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
+
+    [DllImport("kernel32.dll")]
+    public static extern IntPtr GetModuleHandle(string name);
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr CreateWindowExW(
+       UInt32 dwExStyle,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpClassName,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpWindowName,
+       UInt32 dwStyle,
+       Int32 x,
+       Int32 y,
+       Int32 nWidth,
+       Int32 nHeight,
+       IntPtr hWndParent,
+       IntPtr hMenu,
+       IntPtr hInstance,
+       IntPtr lpParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr DefWindowProcW(
+        IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DestroyWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool ShowWindow(
+        IntPtr hWnd,
+        int nCmdShow
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool UpdateWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern int GetMessage(
+        out MSG lpMsg,
+        IntPtr hWnd,
+        uint wMsgFilterMin,
+        uint wMsgFilterMax
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DispatchMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool TranslateMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern System.UInt16 RegisterClassW(
+        [In] ref WNDCLASS lpWndClass
+    );
+
+    [DllImport("Kernel32.dll", SetLastError = true)]
+    public static extern int GetCurrentThreadId();
+
+    [DllImport("user32.dll")]
+    public static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
+}

+ 78 - 0
src/PixiEditor.Windows/WindowsInputKeys.cs

@@ -0,0 +1,78 @@
+using System.Text;
+using Avalonia.Input;
+using Avalonia.Win32.Input;
+using PixiEditor.Helpers;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.Windows;
+
+public class WindowsInputKeys : IInputKeys
+{
+    const string Russian = "00000419";
+    const string Ukrainian = "00000422";
+    const string UkrainianEnhanced = "00020422";
+    const string Arabic1 = "00000401";
+    const string Arabic2 = "00010401";
+    const string Arabic3 = "00020401";
+    private const string InvariantLayoutCode = "00000409"; // Also known as the US Layout
+
+    private static nint? invariantLayout;
+
+    /// <summary>
+    /// Returns the charcter of the <paramref name="key"/> mapped to the users keyboard layout
+    /// </summary>
+    public string GetKeyboardKey(Key key, bool forceInvariant = false) => key switch
+    {
+        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, forceInvariant)}",
+        Key.Space => nameof(Key.Space),
+        Key.Tab => nameof(Key.Tab),
+        Key.Return => "↵",
+        Key.Back => "Backspace",
+        Key.Escape => "Esc",
+        _ => GetMappedKey(key, forceInvariant),
+    };
+
+    private static string GetMappedKey(Key key, bool forceInvariant)
+    {
+        int virtualKey = KeyInterop.VirtualKeyFromKey(key);
+        byte[] keyboardState = new byte[256];
+
+        nint targetLayout = GetLayoutHkl(forceInvariant);
+
+        uint scanCode = Win32.MapVirtualKeyExW((uint)virtualKey, Win32.MapType.MAPVK_VK_TO_VSC, targetLayout);
+
+        StringBuilder stringBuilder = new(5);
+        int result = Win32.ToUnicodeEx((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0, targetLayout);
+
+        string stringResult = result switch
+        {
+            0 => key.ToString(),
+            -1 => stringBuilder.ToString().ToUpper(),
+            _ => stringBuilder[result - 1].ToString().ToUpper()
+        };
+
+        return stringResult;
+    }
+
+    private static nint GetLayoutHkl(bool forceInvariant = false)
+    {
+        if (forceInvariant)
+        {
+            invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+            return invariantLayout.Value;
+        }
+
+        var builder = new StringBuilder(8);
+        bool success = Win32.GetKeyboardLayoutNameW(builder);
+
+        // Fallback to US layout for certain layouts. Do not prepend a 0x and make sure the string is 8 chars long
+        // Layouts can be found here https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values?view=windows-11
+        if (!success || builder.ToString() is not (Russian or Ukrainian or UkrainianEnhanced or Arabic1 or Arabic2 or Arabic3))
+        {
+            return Win32.GetKeyboardLayout(0);
+        }
+
+        invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+        return invariantLayout.Value;
+    }
+}

+ 3 - 0
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -5,4 +5,7 @@ namespace PixiEditor.Windows;
 public class WindowsOperatingSystem : IOperatingSystem
 {
     public string Name => "Windows";
+    public IInputKeys InputKeys { get; } = new WindowsInputKeys();
+
+    public WindowsOperatingSystem() => IOperatingSystem.SetCurrent(this);
 }