diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index f080f24de15..363ecb9d002 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -46,4 +46,16 @@ GetMonitorInfo MONITORINFOEXW WM_ENTERSIZEMOVE -WM_EXITSIZEMOVE \ No newline at end of file +WM_EXITSIZEMOVE + +GetKeyboardLayout +GetWindowThreadProcessId +ActivateKeyboardLayout +GetKeyboardLayoutList +PostMessage +WM_INPUTLANGCHANGEREQUEST +INPUTLANGCHANGE_FORWARD +LOCALE_TRANSIENT_KEYBOARD1 +LOCALE_TRANSIENT_KEYBOARD2 +LOCALE_TRANSIENT_KEYBOARD3 +LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 8dbe3f7e9eb..7a3a0c36e26 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,14 +1,18 @@ using System; using System.ComponentModel; +using System.Globalization; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using System.Windows.Media; +using Flow.Launcher.Infrastructure.UserSettings; +using Microsoft.Win32; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; -using Flow.Launcher.Infrastructure.UserSettings; +using Point = System.Windows.Point; namespace Flow.Launcher.Infrastructure { @@ -63,7 +67,7 @@ public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMod } /// - /// + /// /// /// /// DoNotRound, Round, RoundSmall, Default @@ -317,5 +321,172 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false) } #endregion + + #region Keyboard Layout + + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private const string EnglishLanguageTag = "en"; + + private static readonly string[] ImeLanguageTags = + { + "zh", // Chinese + "ja", // Japanese + "ko", // Korean + }; + + private const uint KeyboardLayoutLoWord = 0xFFFF; + + // Store the previous keyboard layout + private static HKL _previousLayout; + + /// + /// Switches the keyboard layout to English if available. + /// + /// If true, the current keyboard layout will be stored for later restoration. + /// Thrown when there's an error getting the window thread process ID. + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) + { + // Find an installed English layout + var enHKL = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (enHKL == HKL.Null) return; + + // Get the current foreground window + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + // Get the current foreground window thread ID + var threadId = PInvoke.GetWindowThreadProcessId(hwnd); + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + + // If the current layout has an IME mode, disable it without switching to another layout. + // This is needed because for languages with IME mode, Flow Launcher just temporarily disables + // the IME mode instead of switching to another layout. + var currentLayout = PInvoke.GetKeyboardLayout(threadId); + var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; + foreach (var langTag in ImeLanguageTags) + { + if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + + // Backup current keyboard layout + if (backupPrevious) _previousLayout = currentLayout; + + // Switch to English layout + PInvoke.ActivateKeyboardLayout(enHKL, 0); + } + + /// + /// Restores the previously backed-up keyboard layout. + /// If it wasn't backed up or has already been restored, this method does nothing. + /// + public static void RestorePreviousKeyboardLayout() + { + if (_previousLayout == HKL.Null) return; + + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + PInvoke.PostMessage( + hwnd, + PInvoke.WM_INPUTLANGCHANGEREQUEST, + PInvoke.INPUTLANGCHANGE_FORWARD, + _previousLayout.Value + ); + + _previousLayout = HKL.Null; + } + + /// + /// Finds an installed English keyboard layout. + /// + /// + /// + private static unsafe HKL FindEnglishKeyboardLayout() + { + // Get the number of keyboard layouts + int count = PInvoke.GetKeyboardLayoutList(0, null); + if (count <= 0) return HKL.Null; + + // Get all keyboard layouts + var handles = new HKL[count]; + fixed (HKL* h = handles) + { + var result = PInvoke.GetKeyboardLayoutList(count, h); + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Look for any English keyboard layout + foreach (var hkl in handles) + { + // The lower word contains the language identifier + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; + var langTag = GetLanguageTag(langId); + + // Check if it's an English layout + if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase)) + { + return hkl; + } + } + + return HKL.Null; + } + + /// + /// Returns the + /// + /// BCP 47 language tag + /// + /// of the current input language. + /// + /// + /// Edited from: https://github.com/dotnet/winforms + /// + private static string GetLanguageTag(uint langId) + { + // We need to convert the language identifier to a language tag, because they are deprecated and may have a + // transient value. + // https://learn.microsoft.com/globalization/locale/other-locale-names#lcid + // https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks + // + // It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect + // language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID" + // instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet). + // + // Try to extract proper language tag from registry as a workaround approved by a Windows team. + // https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949 + // + // NOTE: this logic may break in future versions of Windows since it is not documented. + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + { + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key?.GetValue("Languages") is string[] languages) + { + foreach (string language in languages) + { + using var subKey = key.OpenSubKey(language); + if (subKey?.GetValue("TransientLangId") is int transientLangId + && transientLangId == langId) + { + return language; + } + } + } + } + + return CultureInfo.GetCultureInfo((int)langId).Name; + } + + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a13f..59af95da539 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1373,6 +1373,11 @@ public void Show() MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + if (StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } }); } @@ -1441,7 +1446,12 @@ public async void Hide() // 📌 Apply DWM Cloak (Completely hide the window) Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - + + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + await Task.Delay(50); // Update WPF properties