|
1 | 1 | using System;
|
2 | 2 | using System.ComponentModel;
|
| 3 | +using System.Globalization; |
3 | 4 | using System.Runtime.InteropServices;
|
4 | 5 | using System.Windows;
|
5 | 6 | using System.Windows.Interop;
|
6 | 7 | using System.Windows.Media;
|
| 8 | +using Flow.Launcher.Infrastructure.UserSettings; |
| 9 | +using Microsoft.Win32; |
7 | 10 | using Windows.Win32;
|
8 | 11 | using Windows.Win32.Foundation;
|
9 | 12 | using Windows.Win32.Graphics.Dwm;
|
| 13 | +using Windows.Win32.UI.Input.KeyboardAndMouse; |
10 | 14 | using Windows.Win32.UI.WindowsAndMessaging;
|
11 |
| -using Flow.Launcher.Infrastructure.UserSettings; |
| 15 | +using Point = System.Windows.Point; |
12 | 16 |
|
13 | 17 | namespace Flow.Launcher.Infrastructure
|
14 | 18 | {
|
@@ -63,7 +67,7 @@ public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMod
|
63 | 67 | }
|
64 | 68 |
|
65 | 69 | /// <summary>
|
66 |
| - /// |
| 70 | + /// |
67 | 71 | /// </summary>
|
68 | 72 | /// <param name="window"></param>
|
69 | 73 | /// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
@@ -317,5 +321,172 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false)
|
317 | 321 | }
|
318 | 322 |
|
319 | 323 | #endregion
|
| 324 | + |
| 325 | + #region Keyboard Layout |
| 326 | + |
| 327 | + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; |
| 328 | + |
| 329 | + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f |
| 330 | + private const string EnglishLanguageTag = "en"; |
| 331 | + |
| 332 | + private static readonly string[] ImeLanguageTags = |
| 333 | + { |
| 334 | + "zh", // Chinese |
| 335 | + "ja", // Japanese |
| 336 | + "ko", // Korean |
| 337 | + }; |
| 338 | + |
| 339 | + private const uint KeyboardLayoutLoWord = 0xFFFF; |
| 340 | + |
| 341 | + // Store the previous keyboard layout |
| 342 | + private static HKL _previousLayout; |
| 343 | + |
| 344 | + /// <summary> |
| 345 | + /// Switches the keyboard layout to English if available. |
| 346 | + /// </summary> |
| 347 | + /// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param> |
| 348 | + /// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception> |
| 349 | + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) |
| 350 | + { |
| 351 | + // Find an installed English layout |
| 352 | + var enHKL = FindEnglishKeyboardLayout(); |
| 353 | + |
| 354 | + // No installed English layout found |
| 355 | + if (enHKL == HKL.Null) return; |
| 356 | + |
| 357 | + // Get the current foreground window |
| 358 | + var hwnd = PInvoke.GetForegroundWindow(); |
| 359 | + if (hwnd == HWND.Null) return; |
| 360 | + |
| 361 | + // Get the current foreground window thread ID |
| 362 | + var threadId = PInvoke.GetWindowThreadProcessId(hwnd); |
| 363 | + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); |
| 364 | + |
| 365 | + // If the current layout has an IME mode, disable it without switching to another layout. |
| 366 | + // This is needed because for languages with IME mode, Flow Launcher just temporarily disables |
| 367 | + // the IME mode instead of switching to another layout. |
| 368 | + var currentLayout = PInvoke.GetKeyboardLayout(threadId); |
| 369 | + var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; |
| 370 | + foreach (var langTag in ImeLanguageTags) |
| 371 | + { |
| 372 | + if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase)) |
| 373 | + { |
| 374 | + return; |
| 375 | + } |
| 376 | + } |
| 377 | + |
| 378 | + // Backup current keyboard layout |
| 379 | + if (backupPrevious) _previousLayout = currentLayout; |
| 380 | + |
| 381 | + // Switch to English layout |
| 382 | + PInvoke.ActivateKeyboardLayout(enHKL, 0); |
| 383 | + } |
| 384 | + |
| 385 | + /// <summary> |
| 386 | + /// Restores the previously backed-up keyboard layout. |
| 387 | + /// If it wasn't backed up or has already been restored, this method does nothing. |
| 388 | + /// </summary> |
| 389 | + public static void RestorePreviousKeyboardLayout() |
| 390 | + { |
| 391 | + if (_previousLayout == HKL.Null) return; |
| 392 | + |
| 393 | + var hwnd = PInvoke.GetForegroundWindow(); |
| 394 | + if (hwnd == HWND.Null) return; |
| 395 | + |
| 396 | + PInvoke.PostMessage( |
| 397 | + hwnd, |
| 398 | + PInvoke.WM_INPUTLANGCHANGEREQUEST, |
| 399 | + PInvoke.INPUTLANGCHANGE_FORWARD, |
| 400 | + _previousLayout.Value |
| 401 | + ); |
| 402 | + |
| 403 | + _previousLayout = HKL.Null; |
| 404 | + } |
| 405 | + |
| 406 | + /// <summary> |
| 407 | + /// Finds an installed English keyboard layout. |
| 408 | + /// </summary> |
| 409 | + /// <returns></returns> |
| 410 | + /// <exception cref="Win32Exception"></exception> |
| 411 | + private static unsafe HKL FindEnglishKeyboardLayout() |
| 412 | + { |
| 413 | + // Get the number of keyboard layouts |
| 414 | + int count = PInvoke.GetKeyboardLayoutList(0, null); |
| 415 | + if (count <= 0) return HKL.Null; |
| 416 | + |
| 417 | + // Get all keyboard layouts |
| 418 | + var handles = new HKL[count]; |
| 419 | + fixed (HKL* h = handles) |
| 420 | + { |
| 421 | + var result = PInvoke.GetKeyboardLayoutList(count, h); |
| 422 | + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); |
| 423 | + } |
| 424 | + |
| 425 | + // Look for any English keyboard layout |
| 426 | + foreach (var hkl in handles) |
| 427 | + { |
| 428 | + // The lower word contains the language identifier |
| 429 | + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; |
| 430 | + var langTag = GetLanguageTag(langId); |
| 431 | + |
| 432 | + // Check if it's an English layout |
| 433 | + if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase)) |
| 434 | + { |
| 435 | + return hkl; |
| 436 | + } |
| 437 | + } |
| 438 | + |
| 439 | + return HKL.Null; |
| 440 | + } |
| 441 | + |
| 442 | + /// <summary> |
| 443 | + /// Returns the |
| 444 | + /// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names"> |
| 445 | + /// BCP 47 language tag |
| 446 | + /// </see> |
| 447 | + /// of the current input language. |
| 448 | + /// </summary> |
| 449 | + /// <remarks> |
| 450 | + /// Edited from: https://github.com/dotnet/winforms |
| 451 | + /// </remarks> |
| 452 | + private static string GetLanguageTag(uint langId) |
| 453 | + { |
| 454 | + // We need to convert the language identifier to a language tag, because they are deprecated and may have a |
| 455 | + // transient value. |
| 456 | + // https://learn.microsoft.com/globalization/locale/other-locale-names#lcid |
| 457 | + // https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks |
| 458 | + // |
| 459 | + // It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect |
| 460 | + // language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID" |
| 461 | + // instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet). |
| 462 | + // |
| 463 | + // Try to extract proper language tag from registry as a workaround approved by a Windows team. |
| 464 | + // https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949 |
| 465 | + // |
| 466 | + // NOTE: this logic may break in future versions of Windows since it is not documented. |
| 467 | + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 |
| 468 | + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 |
| 469 | + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 |
| 470 | + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) |
| 471 | + { |
| 472 | + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); |
| 473 | + if (key?.GetValue("Languages") is string[] languages) |
| 474 | + { |
| 475 | + foreach (string language in languages) |
| 476 | + { |
| 477 | + using var subKey = key.OpenSubKey(language); |
| 478 | + if (subKey?.GetValue("TransientLangId") is int transientLangId |
| 479 | + && transientLangId == langId) |
| 480 | + { |
| 481 | + return language; |
| 482 | + } |
| 483 | + } |
| 484 | + } |
| 485 | + } |
| 486 | + |
| 487 | + return CultureInfo.GetCultureInfo((int)langId).Name; |
| 488 | + } |
| 489 | + |
| 490 | + #endregion |
320 | 491 | }
|
321 | 492 | }
|
0 commit comments