Skip to content

Commit ddec1bd

Browse files
authored
Merge pull request #3366 from Flow-Launcher/always-use-english
Implement auto-switching to English when the option is enabled
2 parents 6fbde2f + 4f2a951 commit ddec1bd

File tree

3 files changed

+197
-4
lines changed

3 files changed

+197
-4
lines changed

Flow.Launcher.Infrastructure/NativeMethods.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,16 @@ GetMonitorInfo
4646
MONITORINFOEXW
4747

4848
WM_ENTERSIZEMOVE
49-
WM_EXITSIZEMOVE
49+
WM_EXITSIZEMOVE
50+
51+
GetKeyboardLayout
52+
GetWindowThreadProcessId
53+
ActivateKeyboardLayout
54+
GetKeyboardLayoutList
55+
PostMessage
56+
WM_INPUTLANGCHANGEREQUEST
57+
INPUTLANGCHANGE_FORWARD
58+
LOCALE_TRANSIENT_KEYBOARD1
59+
LOCALE_TRANSIENT_KEYBOARD2
60+
LOCALE_TRANSIENT_KEYBOARD3
61+
LOCALE_TRANSIENT_KEYBOARD4

Flow.Launcher.Infrastructure/Win32Helper.cs

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Globalization;
34
using System.Runtime.InteropServices;
45
using System.Windows;
56
using System.Windows.Interop;
67
using System.Windows.Media;
8+
using Flow.Launcher.Infrastructure.UserSettings;
9+
using Microsoft.Win32;
710
using Windows.Win32;
811
using Windows.Win32.Foundation;
912
using Windows.Win32.Graphics.Dwm;
13+
using Windows.Win32.UI.Input.KeyboardAndMouse;
1014
using Windows.Win32.UI.WindowsAndMessaging;
11-
using Flow.Launcher.Infrastructure.UserSettings;
15+
using Point = System.Windows.Point;
1216

1317
namespace Flow.Launcher.Infrastructure
1418
{
@@ -63,7 +67,7 @@ public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMod
6367
}
6468

6569
/// <summary>
66-
///
70+
///
6771
/// </summary>
6872
/// <param name="window"></param>
6973
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
@@ -317,5 +321,172 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false)
317321
}
318322

319323
#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
320491
}
321492
}

Flow.Launcher/ViewModel/MainViewModel.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,11 @@ public void Show()
13721372
MainWindowOpacity = 1;
13731373
MainWindowVisibilityStatus = true;
13741374
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
1375+
1376+
if (StartWithEnglishMode)
1377+
{
1378+
Win32Helper.SwitchToEnglishKeyboardLayout(true);
1379+
}
13751380
});
13761381
}
13771382

@@ -1440,7 +1445,12 @@ public async void Hide()
14401445
// 📌 Apply DWM Cloak (Completely hide the window)
14411446
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
14421447
}
1443-
1448+
1449+
if (StartWithEnglishMode)
1450+
{
1451+
Win32Helper.RestorePreviousKeyboardLayout();
1452+
}
1453+
14441454
await Task.Delay(50);
14451455

14461456
// Update WPF properties

0 commit comments

Comments
 (0)