11using System ;
22using System . ComponentModel ;
3- using System . Linq ;
3+ using System . Globalization ;
44using System . Runtime . InteropServices ;
55using System . Windows ;
66using System . Windows . Interop ;
77using System . Windows . Media ;
8+ using Flow . Launcher . Infrastructure . UserSettings ;
9+ using Microsoft . Win32 ;
810using Windows . Win32 ;
911using Windows . Win32 . Foundation ;
1012using Windows . Win32 . Graphics . Dwm ;
1113using Windows . Win32 . UI . Input . KeyboardAndMouse ;
1214using Windows . Win32 . UI . WindowsAndMessaging ;
13- using Flow . Launcher . Infrastructure . UserSettings ;
1415using Point = System . Windows . Point ;
1516
1617namespace Flow . Launcher . Infrastructure
@@ -323,50 +324,23 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false)
323324
324325 #region Keyboard Layout
325326
327+ private const string UserProfileRegistryPath = @"Control Panel\International\User Profile" ;
328+
326329 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
327- private static readonly uint [ ] EnglishLanguageIds =
328- {
329- 0x0009 , 0x0409 , 0x0809 , 0x0C09 , 0x1009 , 0x1409 , 0x1809 , 0x1C09 , 0x2009 , 0x2409 , 0x2809 , 0x2C09 , 0x3009 ,
330- 0x3409 , 0x3C09 , 0x4009 , 0x4409 , 0x4809 , 0x4C09 ,
331- } ;
330+ private const string EnglishLanguageTag = "en" ;
332331
333- private static readonly uint [ ] ImeLanguageIds =
332+ private static readonly string [ ] ImeLanguageTags =
334333 {
335- 0x0004 , 0x7804 , 0x0804 , 0x1004 , 0x7C04 , 0x0C04 , 0x1404 , 0x0404 , 0x0011 , 0x0411 , 0x0012 , 0x0412 ,
334+ "zh" , // Chinese
335+ "ja" , // Japanese
336+ "ko" , // Korean
336337 } ;
337338
338339 private const uint KeyboardLayoutLoWord = 0xFFFF ;
339340
340341 // Store the previous keyboard layout
341342 private static HKL _previousLayout ;
342343
343- private static unsafe HKL FindEnglishKeyboardLayout ( )
344- {
345- // Get the number of keyboard layouts
346- int count = PInvoke . GetKeyboardLayoutList ( 0 , null ) ;
347- if ( count <= 0 ) return HKL . Null ;
348-
349- // Get all keyboard layouts
350- var handles = new HKL [ count ] ;
351- fixed ( HKL * h = handles )
352- {
353- var result = PInvoke . GetKeyboardLayoutList ( count , h ) ;
354- if ( result == 0 ) throw new Win32Exception ( Marshal . GetLastWin32Error ( ) ) ;
355- }
356-
357- // Look for any English keyboard layout
358- foreach ( var hkl in handles )
359- {
360- // The lower word contains the language identifier
361- var langId = ( uint ) hkl . Value & KeyboardLayoutLoWord ;
362-
363- // Check if it's an English layout
364- if ( EnglishLanguageIds . Contains ( langId ) ) return hkl ;
365- }
366-
367- return HKL . Null ;
368- }
369-
370344 /// <summary>
371345 /// Switches the keyboard layout to English if available.
372346 /// </summary>
@@ -392,8 +366,14 @@ public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
392366 // This is needed because for languages with IME mode, Flow Launcher just temporarily disables
393367 // the IME mode instead of switching to another layout.
394368 var currentLayout = PInvoke . GetKeyboardLayout ( threadId ) ;
395- var currentLayoutCode = ( uint ) currentLayout . Value & KeyboardLayoutLoWord ;
396- if ( ImeLanguageIds . Contains ( currentLayoutCode ) ) return ;
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+ }
397377
398378 // Backup current keyboard layout
399379 if ( backupPrevious ) _previousLayout = currentLayout ;
@@ -423,6 +403,91 @@ public static void RestorePreviousKeyboardLayout()
423403 _previousLayout = HKL . Null ;
424404 }
425405
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 ( int ) PInvoke . LOCALE_TRANSIENT_KEYBOARD1
468+ or ( int ) PInvoke . LOCALE_TRANSIENT_KEYBOARD2
469+ or ( int ) PInvoke . LOCALE_TRANSIENT_KEYBOARD3
470+ or ( int ) PInvoke . LOCALE_TRANSIENT_KEYBOARD4 )
471+ {
472+ using RegistryKey key = Registry . CurrentUser . OpenSubKey ( UserProfileRegistryPath ) ;
473+ if ( key is not null && key . GetValue ( "Languages" ) is string [ ] languages )
474+ {
475+ foreach ( string language in languages )
476+ {
477+ using RegistryKey subKey = key . OpenSubKey ( language ) ;
478+ if ( subKey is not null
479+ && subKey . GetValue ( "TransientLangId" ) is int transientLangId
480+ && transientLangId == langId )
481+ {
482+ return language ;
483+ }
484+ }
485+ }
486+ }
487+
488+ return CultureInfo . GetCultureInfo ( ( int ) langId ) . Name ;
489+ }
490+
426491 #endregion
427492 }
428493}
0 commit comments