diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b1396e207..aab3caa40 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin } } - InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface()); - InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService().Language); - if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index e2a66656a..ffa17ab4d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource return DefaultLanguageCode; } - internal void AddPluginLanguageDirectories(IEnumerable plugins) + private void AddPluginLanguageDirectories() { - foreach (var plugin in plugins) + foreach (var plugin in PluginManager.GetPluginsForInterface()) { var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; var dir = Path.GetDirectoryName(location); @@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource _oldResources.Clear(); } + /// + /// Initialize language. Will change app language and plugin language based on settings. + /// + public async Task InitializeLanguageAsync() + { + // Get actual language + var languageCode = _settings.Language; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Add plugin language directories first so that we can load language files from plugins + AddPluginLanguageDirectories(); + + // Change language + await ChangeLanguageAsync(language); + } + + /// + /// Change language during runtime. Will change app language and plugin language & save settings. + /// + /// public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); @@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); - ChangeLanguage(language, isSystem); + + // Change language + _ = ChangeLanguageAsync(language); + + // Save settings + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } private Language GetLanguageByLanguageCode(string languageCode) @@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource } } - private void ChangeLanguage(Language language, bool isSystem) + private async Task ChangeLanguageAsync(Language language) { - language = language.NonNull(); - + // Remove old language files and load language RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } + // Culture of main thread // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; - // Raise event after culture is set - _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; - _ = Task.Run(() => - { - UpdatePluginMetadataTranslations(); - }); + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); } public bool PromptShouldUsePinyin(string languageCodeToSet) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index f080f24de..363ecb9d0 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 8dbe3f7e9..7a3a0c36e 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 @@ namespace Flow.Launcher.Infrastructure } /// - /// + /// /// /// /// DoNotRound, Round, RoundSmall, Default @@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure } #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/App.xaml.cs b/Flow.Launcher/App.xaml.cs index d78c6c47b..833c63ddf 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -152,14 +152,19 @@ namespace Flow.Launcher AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future - Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); - PluginManager.LoadPlugins(_settings.PluginSettings); + // Register ResultsUpdated event after all plugins are loaded + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); + Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(); + + // Change language after all plugins are initialized because we need to update plugin title based on their api + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + await imageLoadertask; _mainWindow = new MainWindow(); diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index c2b35ed12..33654c4cf 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -104,6 +104,9 @@ namespace Flow.Launcher { _settings.FirstLaunch = false; App.API.SaveAppAllSettings(); + /* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */ + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + _settings.BackdropType = BackdropTypes.Acrylic; var WelcomeWindow = new WelcomeWindow(); WelcomeWindow.Show(); } @@ -146,7 +149,9 @@ namespace Flow.Launcher // Since the default main window visibility is visible, so we need set focus during startup QueryTextBox.Focus(); - + // Set the initial state of the QueryTextBoxCursorMovedToEnd property + // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. + _viewModel.QueryTextCursorMovedToEnd = false; _viewModel.PropertyChanged += (o, e) => { switch (e.PropertyName) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ab67b21bb..4705541ac 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -168,7 +168,6 @@ namespace Flow.Launcher.ViewModel }; RegisterViewUpdate(); - RegisterResultsUpdatedEvent(); _ = RegisterClockAndDateUpdateAsync(); } @@ -213,7 +212,7 @@ namespace Flow.Launcher.ViewModel } } - private void RegisterResultsUpdatedEvent() + public void RegisterResultsUpdatedEvent() { foreach (var pair in PluginManager.GetPluginsForInterface()) { @@ -1373,6 +1372,11 @@ namespace Flow.Launcher.ViewModel MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + if (StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } }); } @@ -1441,7 +1445,12 @@ namespace Flow.Launcher.ViewModel // 📌 Apply DWM Cloak (Completely hide the window) Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - + + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + await Task.Delay(50); // Update WPF properties diff --git a/README.md b/README.md index 6611f55dc..cbb553bd1 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea ## 📦 Plugins -- Support wide range of plugins. Visit [here](https://flowlauncher.com/docs/#/plugins) for our plugin portfolio. +- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio. - Publish your own plugin to flow! Create plugins in: