Browse Source

Use US layout for Ru, Uk and Ar and reload shortcuts when user switches keyboard layout

CPKreuz 2 years ago
parent
commit
04f4d9e2f8

+ 43 - 13
src/PixiEditor/Helpers/InputKeyHelpers.cs

@@ -7,35 +7,43 @@ namespace PixiEditor.Helpers;
 
 internal static class InputKeyHelpers
 {
+    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 static string GetKeyboardKey(Key key) => GetKeyboardKey(key, CultureInfo.CurrentCulture);
-
-    public static string GetKeyboardKey(Key key, CultureInfo culture) => key switch
+    public static string GetKeyboardKey(Key key, bool forceInvariant = false) => key switch
     {
-        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, culture)}",
+        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, forceInvariant)}",
         Key.Space => nameof(Key.Space),
         Key.Tab => nameof(Key.Tab),
         Key.Return => "Enter",
         Key.Back => "Backspace",
         Key.Escape => "Esc",
-        _ => GetMappedKey(key, culture),
+        _ => GetMappedKey(key, forceInvariant),
     };
 
-    private static string GetMappedKey(Key key, CultureInfo culture)
+    private static string GetMappedKey(Key key, bool forceInvariant)
     {
         int virtualKey = KeyInterop.VirtualKeyFromKey(key);
         byte[] keyboardState = new byte[256];
 
-        uint scanCode = Win32.MapVirtualKeyExW((uint)virtualKey, Win32.MapType.MAPVK_VK_TO_VSC, culture.KeyboardLayoutId);
-        StringBuilder stringBuilder = new(3);
-
-        int result = Win32.ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0);
+        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;
-
-        stringResult = result switch
+        string stringResult = result switch
         {
             0 => key.ToString(),
             -1 => stringBuilder.ToString().ToUpper(),
@@ -44,4 +52,26 @@ internal static class InputKeyHelpers
 
         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;
+    }
 }

+ 24 - 1
src/PixiEditor/Helpers/Win32.cs

@@ -159,11 +159,34 @@ internal class Win32
         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, int hkl);
+    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",

+ 2 - 0
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using System.Windows.Input;
 using System.Windows.Media;
 using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Evaluators;
@@ -47,6 +48,7 @@ internal abstract partial class Command : NotifyableObject
     {
         Methods = new(this, onExecute, canExecute);
         ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+        InputLanguageManager.Current.InputLanguageChanged += (_, _) => RaisePropertyChanged(nameof(Shortcut));
     }
 
     private void OnLanguageChanged(Language obj)

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

@@ -18,10 +18,11 @@ internal class ShortcutBinding : MarkupExtension
 
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
-#if DEBUG
-        var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
-        return new KeyCombination(attribute.Key, attribute.Modifiers).ToString();
-#endif
+        if (ViewModelMain.Current == null)
+        {
+            var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
+            return new KeyCombination(attribute.Key, attribute.Modifiers).ToString();
+        }
 
         commandController ??= ViewModelMain.Current.CommandController;
         return GetBinding(commandController.Commands[Name]).ProvideValue(serviceProvider);

+ 4 - 4
src/PixiEditor/Models/DataHolders/KeyCombination.cs

@@ -13,9 +13,9 @@ public record struct KeyCombination(Key Key, ModifierKeys Modifiers)
 {
     public static KeyCombination None => new(Key.None, ModifierKeys.None);
 
-    public override string ToString() => ToString(CultureInfo.CurrentCulture);
+    public override string ToString() => ToString(false);
 
-    public string ToString(CultureInfo culture)
+    private string ToString(bool forceInvariant)
     {
         StringBuilder builder = new();
 
@@ -36,11 +36,11 @@ public record struct KeyCombination(Key Key, ModifierKeys Modifiers)
 
         if (Key != Key.None)
         {
-            builder.Append(InputKeyHelpers.GetKeyboardKey(Key, culture));
+            builder.Append(InputKeyHelpers.GetKeyboardKey(Key, forceInvariant));
         }
 
         return builder.ToString();
     }
 
-    private string GetDebuggerDisplay() => ToString(CultureInfo.InvariantCulture);
+    private string GetDebuggerDisplay() => ToString(true);
 }

+ 1 - 0
src/PixiEditor/Views/UserControls/KeyCombinationBox.xaml.cs

@@ -44,6 +44,7 @@ internal partial class KeyCombinationBox : UserControl
         UpdateButton();
 
         ViewModelMain.Current.LocalizationProvider.OnLanguageChanged += _ => UpdateText();
+        InputLanguageManager.Current.InputLanguageChanged += (_, _) => UpdateText();
     }
 
     private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)