diff --git a/src/ManagedShell.Common/Helpers/KeyboardLayoutHelper.cs b/src/ManagedShell.Common/Helpers/KeyboardLayoutHelper.cs index a14afe95..32b33925 100644 --- a/src/ManagedShell.Common/Helpers/KeyboardLayoutHelper.cs +++ b/src/ManagedShell.Common/Helpers/KeyboardLayoutHelper.cs @@ -1,18 +1,51 @@ using ManagedShell.Common.Structs; using ManagedShell.Interop; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; +using static ManagedShell.Interop.NativeMethods; namespace ManagedShell.Common.Helpers { public static class KeyboardLayoutHelper { + private static uint GetFocusedThreadId() + { + var gti = new GUITHREADINFO(); + gti.cbSize = Marshal.SizeOf(typeof(GUITHREADINFO)); + + // Some apps (e.g. Electron/WebView2 apps such as Teams, and WinUI3 apps such as the + // modern Notepad) keep their real keyboard focus on a child window that belongs to a + // different thread than the top-level foreground window. GetGUIThreadInfo(0, ...) + // resolves the true focused window first. + // idThread 0 asks for the foreground thread's queue; hwndFocus within it is the + // window that truly owns keyboard focus, which may belong to a different thread + // than the top-level foreground window itself. + if (GetGUIThreadInfo(0, ref gti)) + { + IntPtr focusedWindow = gti.hwndFocus != IntPtr.Zero ? gti.hwndFocus : gti.hwndActive; + + if (focusedWindow != IntPtr.Zero) + { + uint focusedThreadId = GetWindowThreadProcessId(focusedWindow, out _); + + if (focusedThreadId != 0) + { + return focusedThreadId; + } + } + } + + return GetWindowThreadProcessId(GetForegroundWindow(), out _); + } + public static KeyboardLayout GetKeyboardLayout(bool currentThread = false) { uint threadId = 0; if (!currentThread) - threadId = NativeMethods.GetWindowThreadProcessId(NativeMethods.GetForegroundWindow(), out _); + threadId = GetFocusedThreadId(); var layout = NativeMethods.GetKeyboardLayout(threadId); return new KeyboardLayout() @@ -39,10 +72,10 @@ public static List GetKeyboardLayoutList() public static bool SetKeyboardLayout(int layoutId) { - return NativeMethods.PostMessage(0xffff, - (uint) NativeMethods.WM.INPUTLANGCHANGEREQUEST, + return PostMessage(0xffff, + (uint) WM.INPUTLANGCHANGEREQUEST, 0, - (long)NativeMethods.LoadKeyboardLayout(layoutId.ToString("x8"), (uint)(NativeMethods.KLF.SUBSTITUTE_OK | NativeMethods.KLF.ACTIVATE))); + (long)LoadKeyboardLayout(layoutId.ToString("x8"), (uint)(KLF.SUBSTITUTE_OK | KLF.ACTIVATE))); } } } diff --git a/src/ManagedShell.Interop/NativeMethods.User32.cs b/src/ManagedShell.Interop/NativeMethods.User32.cs index d69a3ebf..1d2d8bff 100644 --- a/src/ManagedShell.Interop/NativeMethods.User32.cs +++ b/src/ManagedShell.Interop/NativeMethods.User32.cs @@ -3857,5 +3857,22 @@ public struct DEV_BROADCAST_HANDLE [DllImport(User32_DllName)] public static extern bool UnregisterDeviceNotification(IntPtr handle); + + [StructLayout(LayoutKind.Sequential)] + public struct GUITHREADINFO + { + public int cbSize; + public uint flags; + public IntPtr hwndActive; + public IntPtr hwndFocus; + public IntPtr hwndCapture; + public IntPtr hwndMenuOwner; + public IntPtr hwndMoveSize; + public IntPtr hwndCaret; + public Rect rcCaret; + } + + [DllImport(User32_DllName, SetLastError = true)] + public static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO lpgui); } }