Skip to content

Commit 334a27f

Browse files
authored
Merge branch 'dev' into graceful_shutdown
2 parents 93ccdee + 298af4d commit 334a27f

File tree

8 files changed

+252
-26
lines changed

8 files changed

+252
-26
lines changed

Flow.Launcher.Core/Plugin/PluginManager.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,6 @@ public static async Task InitializePluginsAsync()
205205
}
206206
}
207207

208-
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
209-
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
210-
211208
if (failedPlugins.Any())
212209
{
213210
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));

Flow.Launcher.Core/Resource/Internationalization.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ private static string GetSystemLanguageCodeAtStartup()
6767
return DefaultLanguageCode;
6868
}
6969

70-
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
70+
private void AddPluginLanguageDirectories()
7171
{
72-
foreach (var plugin in plugins)
72+
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
7373
{
7474
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
7575
var dir = Path.GetDirectoryName(location);
@@ -96,6 +96,32 @@ private void LoadDefaultLanguage()
9696
_oldResources.Clear();
9797
}
9898

99+
/// <summary>
100+
/// Initialize language. Will change app language and plugin language based on settings.
101+
/// </summary>
102+
public async Task InitializeLanguageAsync()
103+
{
104+
// Get actual language
105+
var languageCode = _settings.Language;
106+
if (languageCode == Constant.SystemLanguageCode)
107+
{
108+
languageCode = SystemLanguageCode;
109+
}
110+
111+
// Get language by language code and change language
112+
var language = GetLanguageByLanguageCode(languageCode);
113+
114+
// Add plugin language directories first so that we can load language files from plugins
115+
AddPluginLanguageDirectories();
116+
117+
// Change language
118+
await ChangeLanguageAsync(language);
119+
}
120+
121+
/// <summary>
122+
/// Change language during runtime. Will change app language and plugin language & save settings.
123+
/// </summary>
124+
/// <param name="languageCode"></param>
99125
public void ChangeLanguage(string languageCode)
100126
{
101127
languageCode = languageCode.NonNull();
@@ -110,7 +136,12 @@ public void ChangeLanguage(string languageCode)
110136

111137
// Get language by language code and change language
112138
var language = GetLanguageByLanguageCode(languageCode);
113-
ChangeLanguage(language, isSystem);
139+
140+
// Change language
141+
_ = ChangeLanguageAsync(language);
142+
143+
// Save settings
144+
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
114145
}
115146

116147
private Language GetLanguageByLanguageCode(string languageCode)
@@ -128,26 +159,22 @@ private Language GetLanguageByLanguageCode(string languageCode)
128159
}
129160
}
130161

131-
private void ChangeLanguage(Language language, bool isSystem)
162+
private async Task ChangeLanguageAsync(Language language)
132163
{
133-
language = language.NonNull();
134-
164+
// Remove old language files and load language
135165
RemoveOldLanguageFiles();
136166
if (language != AvailableLanguages.English)
137167
{
138168
LoadLanguage(language);
139169
}
170+
140171
// Culture of main thread
141172
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
142173
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
143174
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
144175

145-
// Raise event after culture is set
146-
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
147-
_ = Task.Run(() =>
148-
{
149-
UpdatePluginMetadataTranslations();
150-
});
176+
// Raise event for plugins after culture is set
177+
await Task.Run(UpdatePluginMetadataTranslations);
151178
}
152179

153180
public bool PromptShouldUsePinyin(string languageCodeToSet)

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/App.xaml.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,19 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
152152

153153
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
154154

155-
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
156-
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
157-
158155
PluginManager.LoadPlugins(_settings.PluginSettings);
159156

157+
// Register ResultsUpdated event after all plugins are loaded
158+
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
159+
160160
Http.Proxy = _settings.Proxy;
161161

162162
await PluginManager.InitializePluginsAsync();
163+
164+
// Change language after all plugins are initialized because we need to update plugin title based on their api
165+
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
166+
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
167+
163168
await imageLoadertask;
164169

165170
_mainWindow = new MainWindow();

Flow.Launcher/MainWindow.xaml.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ private async void OnLoaded(object sender, RoutedEventArgs _)
104104
{
105105
_settings.FirstLaunch = false;
106106
App.API.SaveAppAllSettings();
107+
/* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */
108+
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
109+
_settings.BackdropType = BackdropTypes.Acrylic;
107110
var WelcomeWindow = new WelcomeWindow();
108111
WelcomeWindow.Show();
109112
}
@@ -146,7 +149,9 @@ private async void OnLoaded(object sender, RoutedEventArgs _)
146149

147150
// Since the default main window visibility is visible, so we need set focus during startup
148151
QueryTextBox.Focus();
149-
152+
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
153+
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
154+
_viewModel.QueryTextCursorMovedToEnd = false;
150155
_viewModel.PropertyChanged += (o, e) =>
151156
{
152157
switch (e.PropertyName)

0 commit comments

Comments
 (0)