From a160a781902409d0520cdf5acd154651bbff2538 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 14:46:39 +0800 Subject: [PATCH 01/63] Fix result update interface issue --- Flow.Launcher/App.xaml.cs | 1 + Flow.Launcher/ViewModel/MainViewModel.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 23c77618f..6649a8110 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -131,6 +131,7 @@ namespace Flow.Launcher Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); PluginManager.LoadPlugins(_settings.PluginSettings); + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); Http.Proxy = _settings.Proxy; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..452222d93 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()) { From b75066f52ceb0b2a140b041aa133454ee56e21a1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 15:23:13 +0800 Subject: [PATCH 02/63] Fix change plugin language issue --- Flow.Launcher/App.xaml.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 6649a8110..377d36c48 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -127,12 +127,15 @@ namespace Flow.Launcher AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); + PluginManager.LoadPlugins(_settings.PluginSettings); + + // Register ResultsUpdated event after all plugins are loaded + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); + + // Change language after all plugins are initialized // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); - PluginManager.LoadPlugins(_settings.PluginSettings); - Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); - Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(); From 1b066a572487e21be6d62d1ffebb5064abc9c99c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 16:49:22 +0800 Subject: [PATCH 03/63] Improve code quality --- Flow.Launcher/App.xaml.cs | 3 +- Flow.Launcher/Helper/SingleInstance.cs | 260 ++++++++++++------------- 2 files changed, 122 insertions(+), 141 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 23c77618f..19e932ea8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -28,7 +28,6 @@ namespace Flow.Launcher public partial class App : IDisposable, ISingleInstanceApp { public static IPublicAPI API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; private static bool _disposed; private readonly Settings _settings; @@ -99,7 +98,7 @@ namespace Flow.Launcher [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { using var application = new App(); application.InitializeComponent(); diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index e0e3075f6..76c109a39 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,155 +6,137 @@ using System.Windows; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public interface ISingleInstanceApp { - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } + void OnSecondAppStarted(); +} + +/// +/// This class checks to make sure that only one instance of +/// this application is running at a time. +/// +/// +/// Note: this class should be used with some caution, because it does no +/// security checking. For example, if one instance of an app that uses this class +/// is running as Administrator, any other instance, even if it is not +/// running as Administrator, can activate it with command line arguments. +/// For most apps, this will not be much of an issue. +/// +public static class SingleInstance where TApplication: Application, ISingleInstanceApp +{ + #region Private Fields /// - /// This class checks to make sure that only one instance of - /// this application is running at a time. + /// String delimiter used in channel names. /// - /// - /// Note: this class should be used with some caution, because it does no - /// security checking. For example, if one instance of an app that uses this class - /// is running as Administrator, any other instance, even if it is not - /// running as Administrator, can activate it with command line arguments. - /// For most apps, this will not be much of an issue. - /// - public static class SingleInstance - where TApplication: Application , ISingleInstanceApp - + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() { - #region Private Fields + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; - - /// - /// Application mutex. - /// - internal static Mutex singleInstanceMutex; - - #endregion - - #region Public Methods - - /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. - /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; - - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); - if (firstInstance) - { - _ = CreateRemoteService(channelName); - return true; - } - else - { - _ = SignalFirstInstance(channelName); - return false; - } + _ = CreateRemoteServiceAsync(channelName); + return true; } - - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() + else { - singleInstanceMutex?.ReleaseMutex(); + _ = SignalFirstInstanceAsync(channelName); + return false; } - - #endregion - - #region Private Methods - - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) - { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) - { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } - } - } - - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstance(string channelName) - { - // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } - - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; - } - - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) - { - return; - } - - ((TApplication)Application.Current).OnSecondAppStarted(); - } - - #endregion } + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + SingleInstanceMutex?.ReleaseMutex(); + } + + #endregion + + #region Private Methods + + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) + { + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) + { + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); + } + } + + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); + + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).OnSecondAppStarted(); + } + + #endregion } From 783cef6814c8731580ac99b691ec667ab854011b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 20:55:57 +0800 Subject: [PATCH 04/63] Gracefully shutdown all threads when exiting --- Flow.Launcher/App.xaml.cs | 133 +++++++++++++++++++---- Flow.Launcher/MainWindow.xaml | 1 + Flow.Launcher/MainWindow.xaml.cs | 61 +++++++++-- Flow.Launcher/ViewModel/MainViewModel.cs | 26 ++++- 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 19e932ea8..8f7c8aec9 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,12 +25,29 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { - public partial class App : IDisposable, ISingleInstanceApp + public partial class App : IAsyncDisposable, ISingleInstanceApp { + #region Public Properties + public static IPublicAPI API { get; private set; } + public static CancellationTokenSource NativeThreadCTS { get; private set; } + + #endregion + + #region Private Fields + private static bool _disposed; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; private readonly Settings _settings; + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + public App() { // Initialize settings @@ -78,34 +95,47 @@ namespace Flow.Launcher { API = Ioc.Default.GetRequiredService(); _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); } catch (Exception e) { ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); return; } + + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } } - private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) - { - // Firstly show users the message - MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + #endregion - // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. - Environment.FailFast(message, e); - } + #region Main [STAThread] public static void Main() { + NativeThreadCTS = new CancellationTokenSource(); + if (SingleInstance.InitializeAsFirstInstance()) { - using var application = new App(); + var application = new App(); application.InitializeComponent(); application.Run(); + application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } + #endregion + + #region App Events + #pragma warning disable VSTHRD100 // Avoid async void methods private async void OnStartup(object sender, StartupEventArgs e) @@ -136,11 +166,11 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(); await imageLoadertask; - var window = new MainWindow(); + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; HotKeyMapper.Initialize(); @@ -157,8 +187,7 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info( - "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } @@ -191,7 +220,6 @@ namespace Flow.Launcher } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -209,11 +237,30 @@ namespace Flow.Launcher }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + _ = DisposeAsync(); + }; + + Current.Exit += (s, e) => + { + NativeThreadCTS.Cancel(); + Log.Info("|App.RegisterExitEvents|Application Exit"); + _ = DisposeAsync(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + _ = DisposeAsync(); + }; } /// @@ -234,20 +281,62 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IAsyncDisposable + + protected virtual async ValueTask DisposeAsync(bool disposing) { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + _disposed = true; } + + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + API?.SaveAppAllSettings(); + await PluginManager.DisposePluginsAsync(); + + // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. + await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303ac..f5f3bac84 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -17,6 +17,7 @@ AllowDrop="True" AllowsTransparency="True" Background="Transparent" + Closed="OnClosed" Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95..0af617a77 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -27,7 +27,7 @@ using Screen = System.Windows.Forms.Screen; namespace Flow.Launcher { - public partial class MainWindow + public partial class MainWindow : IDisposable { #region Private Fields @@ -50,13 +50,17 @@ namespace Flow.Launcher private SoundPlayer animationSoundWPF; // Window WndProc + private HwndSource _hwndSource; private int _initialWidth; private int _initialHeight; // Window Animation private const double DefaultRightMargin = 66; //* this value from base.xaml private bool _animating; - private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부 + private bool _isClockPanelAnimating = false; + + // IDisposable + private bool _disposedValue = false; #endregion @@ -85,8 +89,8 @@ namespace Flow.Launcher private void OnSourceInitialized(object sender, EventArgs e) { var handle = Win32Helper.GetWindowHandle(this, true); - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); + _hwndSource = HwndSource.FromHwnd(handle); + _hwndSource.AddHook(WndProc); Win32Helper.HideFromAltTab(this); Win32Helper.DisableControlBox(this); } @@ -227,20 +231,31 @@ namespace Flow.Launcher .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private async void OnClosing(object sender, CancelEventArgs e) + private void OnClosing(object sender, CancelEventArgs e) { + _viewModel.Save(); _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); - Environment.Exit(0); + } + + private void OnClosed(object sender, EventArgs e) + { + try + { + _hwndSource.RemoveHook(WndProc); + } + catch (Exception) + { + // Ignored + } + + _hwndSource = null; } private void OnLocationChanged(object sender, EventArgs e) { - if (_animating) - return; + if (_animating) return; + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { _settings.WindowLeft = Left; @@ -990,5 +1005,29 @@ namespace Flow.Launcher } #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _hwndSource?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..18e61914d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.ViewModel { - public partial class MainViewModel : BaseModel, ISavable + public partial class MainViewModel : BaseModel, ISavable, IDisposable { #region Private Fields @@ -1542,5 +1542,29 @@ namespace Flow.Launcher.ViewModel } #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _updateSource?.Dispose(); + _disposed = true; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } From b71e7226e5bbc1ff852e4a71064c72afae3f7558 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 21:17:31 +0800 Subject: [PATCH 05/63] Improve & Fix --- Flow.Launcher/App.xaml.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 8f7c8aec9..63dcdf353 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -243,23 +243,23 @@ namespace Flow.Launcher private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => + AppDomain.CurrentDomain.ProcessExit += async (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.Exit += (s, e) => + Current.Exit += async (s, e) => { NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.SessionEnding += (s, e) => + Current.SessionEnding += async (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - _ = DisposeAsync(); + await DisposeAsync(); }; } @@ -303,6 +303,8 @@ namespace Flow.Launcher _disposed = true; } + await Task.Delay(10000); + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -313,7 +315,7 @@ namespace Flow.Launcher await PluginManager.DisposePluginsAsync(); // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); _mainVM?.Dispose(); } From 81a46327223596a9cff55bd8ad4e1b332237d997 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 00:27:03 +0600 Subject: [PATCH 06/63] Implement auto-switching to English when the option is enabled --- Flow.Launcher/Helper/KeyboardLayoutHelper.cs | 95 ++++++++++++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 13 ++- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 Flow.Launcher/Helper/KeyboardLayoutHelper.cs diff --git a/Flow.Launcher/Helper/KeyboardLayoutHelper.cs b/Flow.Launcher/Helper/KeyboardLayoutHelper.cs new file mode 100644 index 000000000..db62ef783 --- /dev/null +++ b/Flow.Launcher/Helper/KeyboardLayoutHelper.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Flow.Launcher.Helper; + +public static class KeyboardLayoutHelper +{ + #region Windows API + + [DllImport("user32.dll")] + private static extern IntPtr GetKeyboardLayout(uint idThread); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); + + [DllImport("user32.dll")] + private static extern IntPtr ActivateKeyboardLayout(IntPtr hkl, uint flags); + + [DllImport("user32.dll")] + private static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[] lpList); + + [DllImport("kernel32.dll")] + private static extern int GetLocaleInfoA(uint Locale, uint LCType, StringBuilder lpLCData, int cchData); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + // Used to get the language name of the keyboard layout + private const uint LOCALE_SLANGUAGE = 0x00000002; + + // The string to search for in the language name of a keyboard layout + private const string LAYOUT_ENGLISH_SEARCH = "english"; + + private static IntPtr FindEnglishKeyboardLayout() + { + // Get the number of keyboard layouts + var count = GetKeyboardLayoutList(0, null); + if (count <= 0) return IntPtr.Zero; + + // Get all keyboard layouts + var keyboardLayouts = new IntPtr[count]; + GetKeyboardLayoutList(count, keyboardLayouts); + + // Look for any English keyboard layout + foreach (var layout in keyboardLayouts) + { + // The lower word contains the language identifier + var langId = (uint)layout.ToInt32() & 0xFFFF; + + // Get language name for the layout + var sb = new StringBuilder(256); + GetLocaleInfoA(langId, LOCALE_SLANGUAGE, sb, sb.Capacity); + var langName = sb.ToString().ToLowerInvariant(); + + // Check if it's an English layout + if (langName.Contains(LAYOUT_ENGLISH_SEARCH)) + { + return layout; + } + } + + return IntPtr.Zero; + } + + #endregion + + // Query textbox keyboard layout + private static IntPtr _previousLayout; + + public static void SetEnglishKeyboardLayout() + { + // Find an installed English layout + var englishLayout = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (englishLayout == IntPtr.Zero) return; + + var hwnd = GetForegroundWindow(); + var threadId = GetWindowThreadProcessId(hwnd, IntPtr.Zero); + + // Store current keyboard layout + _previousLayout = GetKeyboardLayout(threadId) & 0xFFFF; + + // Switch to English layout + ActivateKeyboardLayout(englishLayout, 0); + } + + public static void SetPreviousKeyboardLayout() + { + if (_previousLayout == IntPtr.Zero) return; + ActivateKeyboardLayout(_previousLayout, 0); + _previousLayout = IntPtr.Zero; + } +} diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..641825ec6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -14,6 +14,7 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; @@ -1373,6 +1374,11 @@ namespace Flow.Launcher.ViewModel MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + if (StartWithEnglishMode) + { + KeyboardLayoutHelper.SetEnglishKeyboardLayout(); + } }); } @@ -1441,7 +1447,12 @@ namespace Flow.Launcher.ViewModel // 📌 Apply DWM Cloak (Completely hide the window) Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - + + if (StartWithEnglishMode) + { + KeyboardLayoutHelper.SetPreviousKeyboardLayout(); + } + await Task.Delay(50); // Update WPF properties From 5b29dedcbebb255a22a39932049fb8f6df53da3d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:10:52 +0800 Subject: [PATCH 07/63] Remove useless cancellation token source --- Flow.Launcher/App.xaml.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 63dcdf353..7d417e036 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -30,7 +30,6 @@ namespace Flow.Launcher #region Public Properties public static IPublicAPI API { get; private set; } - public static CancellationTokenSource NativeThreadCTS { get; private set; } #endregion @@ -121,8 +120,6 @@ namespace Flow.Launcher [STAThread] public static void Main() { - NativeThreadCTS = new CancellationTokenSource(); - if (SingleInstance.InitializeAsFirstInstance()) { var application = new App(); @@ -251,7 +248,6 @@ namespace Flow.Launcher Current.Exit += async (s, e) => { - NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); await DisposeAsync(); }; From 09bc2bc48b5d0325421a6eab20856c80771dfc7d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:24:34 +0800 Subject: [PATCH 08/63] Cleanup & Improve --- Flow.Launcher/App.xaml.cs | 35 +++++++++++++++----------------- Flow.Launcher/MainWindow.xaml.cs | 3 +-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7d417e036..016c2d06c 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,7 +25,7 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { - public partial class App : IAsyncDisposable, ISingleInstanceApp + public partial class App : IDisposable, ISingleInstanceApp { #region Public Properties @@ -122,10 +122,9 @@ namespace Flow.Launcher { if (SingleInstance.InitializeAsFirstInstance()) { - var application = new App(); + using var application = new App(); application.InitializeComponent(); application.Run(); - application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } @@ -240,22 +239,22 @@ namespace Flow.Launcher private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += async (s, e) => + AppDomain.CurrentDomain.ProcessExit += (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - await DisposeAsync(); + Dispose(); }; - Current.Exit += async (s, e) => + Current.Exit += (s, e) => { Log.Info("|App.RegisterExitEvents|Application Exit"); - await DisposeAsync(); + Dispose(); }; - Current.SessionEnding += async (s, e) => + Current.SessionEnding += (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - await DisposeAsync(); + Dispose(); }; } @@ -279,9 +278,9 @@ namespace Flow.Launcher #endregion - #region IAsyncDisposable + #region IDisposable - protected virtual async ValueTask DisposeAsync(bool disposing) + protected virtual void Dispose(bool disposing) { // Prevent two disposes at the same time. lock (_disposingLock) @@ -299,9 +298,7 @@ namespace Flow.Launcher _disposed = true; } - await Task.Delay(10000); - - await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -310,8 +307,9 @@ namespace Flow.Launcher API?.SaveAppAllSettings(); await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); } @@ -319,10 +317,9 @@ namespace Flow.Launcher }); } - public async ValueTask DisposeAsync() + public void Dispose() { - // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method - await DisposeAsync(disposing: true); + Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0af617a77..e7be15081 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher private readonly ContextMenu contextMenu = new(); private readonly MainViewModel _viewModel; - // Window Event : Key Event + // Window Event: Key Event private bool isArrowKeyPressed = false; // Window Sound Effects @@ -233,7 +233,6 @@ namespace Flow.Launcher private void OnClosing(object sender, CancelEventArgs e) { - _viewModel.Save(); _notifyIcon.Visible = false; Notification.Uninstall(); } From aa3ad10044396092a7e6779542d389e2d76716a5 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 08:32:35 +0600 Subject: [PATCH 09/63] When looking for English keyboard layout, use pre-defined IDs instead of relying on layout name as a string --- Flow.Launcher/Helper/KeyboardLayoutHelper.cs | 23 +++++++------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher/Helper/KeyboardLayoutHelper.cs b/Flow.Launcher/Helper/KeyboardLayoutHelper.cs index db62ef783..e5458f6cd 100644 --- a/Flow.Launcher/Helper/KeyboardLayoutHelper.cs +++ b/Flow.Launcher/Helper/KeyboardLayoutHelper.cs @@ -1,6 +1,6 @@ using System; +using System.Linq; using System.Runtime.InteropServices; -using System.Text; namespace Flow.Launcher.Helper; @@ -20,17 +20,15 @@ public static class KeyboardLayoutHelper [DllImport("user32.dll")] private static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[] lpList); - [DllImport("kernel32.dll")] - private static extern int GetLocaleInfoA(uint Locale, uint LCType, StringBuilder lpLCData, int cchData); - [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); - // Used to get the language name of the keyboard layout - private const uint LOCALE_SLANGUAGE = 0x00000002; - - // The string to search for in the language name of a keyboard layout - private const string LAYOUT_ENGLISH_SEARCH = "english"; + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private static readonly uint[] EnglishLanguageIds = + { + 0x0009, 0x0409, 0x0809, 0x0C09, 0x1000, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, + 0x3009, 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, + }; private static IntPtr FindEnglishKeyboardLayout() { @@ -48,13 +46,8 @@ public static class KeyboardLayoutHelper // The lower word contains the language identifier var langId = (uint)layout.ToInt32() & 0xFFFF; - // Get language name for the layout - var sb = new StringBuilder(256); - GetLocaleInfoA(langId, LOCALE_SLANGUAGE, sb, sb.Capacity); - var langName = sb.ToString().ToLowerInvariant(); - // Check if it's an English layout - if (langName.Contains(LAYOUT_ENGLISH_SEARCH)) + if (EnglishLanguageIds.Contains(langId)) { return layout; } From 4e5584b70920a4269080219dccf55838478d68ce Mon Sep 17 00:00:00 2001 From: DB p Date: Sat, 22 Mar 2025 12:25:02 +0900 Subject: [PATCH 10/63] Enable acrylic effect on first launch if running on Windows 11 --- Flow.Launcher/MainWindow.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95..d2e9dade9 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -98,6 +98,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(); } From 023ab45022fab28b9969ea1df1521c8d2c3912e1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 12:06:44 +0800 Subject: [PATCH 11/63] Use PInvoke instead of DllImport & Several adjustments --- .../NativeMethods.txt | 7 +- Flow.Launcher.Infrastructure/Win32Helper.cs | 79 +++++++++++++++++ Flow.Launcher/Helper/KeyboardLayoutHelper.cs | 88 ------------------- Flow.Launcher/ViewModel/MainViewModel.cs | 5 +- 4 files changed, 87 insertions(+), 92 deletions(-) delete mode 100644 Flow.Launcher/Helper/KeyboardLayoutHelper.cs diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index f080f24de..d26478bbe 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -46,4 +46,9 @@ GetMonitorInfo MONITORINFOEXW WM_ENTERSIZEMOVE -WM_EXITSIZEMOVE \ No newline at end of file +WM_EXITSIZEMOVE + +GetKeyboardLayout +GetWindowThreadProcessId +ActivateKeyboardLayout +GetKeyboardLayoutList \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 8dbe3f7e9..664f428ec 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Linq; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; @@ -7,8 +8,10 @@ using System.Windows.Media; 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 { @@ -317,5 +320,81 @@ namespace Flow.Launcher.Infrastructure } #endregion + + #region Keyboard Layout + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private static readonly uint[] EnglishLanguageIds = + { + 0x0009, 0x0409, 0x0809, 0x0C09, 0x1000, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, + 0x3009, 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, + }; + + // Store the previous keyboard layout + private static HKL _previousLayout; + + 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) + { + _ = PInvoke.GetKeyboardLayoutList(count, h); + } + + // Look for any English keyboard layout + foreach (var hkl in handles) + { + // The lower word contains the language identifier + var langId = (uint)hkl.Value & 0xFFFF; + + // Check if it's an English layout + if (EnglishLanguageIds.Contains(langId)) + { + return hkl; + } + } + + return HKL.Null; + } + + public static unsafe void SetEnglishKeyboardLayout(bool backupPrevious) + { + // Find an installed English layout + var enHKL = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (enHKL == HKL.Null) return; + + // Get the current window thread ID + uint threadId = 0; + var result = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow(), &threadId); + if (result == 0 || threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + + // Backup current keyboard layout + if (backupPrevious) + { + _previousLayout = PInvoke.GetKeyboardLayout(threadId); + } + + // Switch to English layout + PInvoke.ActivateKeyboardLayout(enHKL, 0); + } + + public static void SetPreviousKeyboardLayout() + { + if (_previousLayout != HKL.Null) + { + PInvoke.ActivateKeyboardLayout(_previousLayout, 0); + + _previousLayout = HKL.Null; + } + } + + #endregion } } diff --git a/Flow.Launcher/Helper/KeyboardLayoutHelper.cs b/Flow.Launcher/Helper/KeyboardLayoutHelper.cs deleted file mode 100644 index e5458f6cd..000000000 --- a/Flow.Launcher/Helper/KeyboardLayoutHelper.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.InteropServices; - -namespace Flow.Launcher.Helper; - -public static class KeyboardLayoutHelper -{ - #region Windows API - - [DllImport("user32.dll")] - private static extern IntPtr GetKeyboardLayout(uint idThread); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); - - [DllImport("user32.dll")] - private static extern IntPtr ActivateKeyboardLayout(IntPtr hkl, uint flags); - - [DllImport("user32.dll")] - private static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[] lpList); - - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f - private static readonly uint[] EnglishLanguageIds = - { - 0x0009, 0x0409, 0x0809, 0x0C09, 0x1000, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, - 0x3009, 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, - }; - - private static IntPtr FindEnglishKeyboardLayout() - { - // Get the number of keyboard layouts - var count = GetKeyboardLayoutList(0, null); - if (count <= 0) return IntPtr.Zero; - - // Get all keyboard layouts - var keyboardLayouts = new IntPtr[count]; - GetKeyboardLayoutList(count, keyboardLayouts); - - // Look for any English keyboard layout - foreach (var layout in keyboardLayouts) - { - // The lower word contains the language identifier - var langId = (uint)layout.ToInt32() & 0xFFFF; - - // Check if it's an English layout - if (EnglishLanguageIds.Contains(langId)) - { - return layout; - } - } - - return IntPtr.Zero; - } - - #endregion - - // Query textbox keyboard layout - private static IntPtr _previousLayout; - - public static void SetEnglishKeyboardLayout() - { - // Find an installed English layout - var englishLayout = FindEnglishKeyboardLayout(); - - // No installed English layout found - if (englishLayout == IntPtr.Zero) return; - - var hwnd = GetForegroundWindow(); - var threadId = GetWindowThreadProcessId(hwnd, IntPtr.Zero); - - // Store current keyboard layout - _previousLayout = GetKeyboardLayout(threadId) & 0xFFFF; - - // Switch to English layout - ActivateKeyboardLayout(englishLayout, 0); - } - - public static void SetPreviousKeyboardLayout() - { - if (_previousLayout == IntPtr.Zero) return; - ActivateKeyboardLayout(_previousLayout, 0); - _previousLayout = IntPtr.Zero; - } -} diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 641825ec6..0f355a80f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -14,7 +14,6 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; @@ -1377,7 +1376,7 @@ namespace Flow.Launcher.ViewModel if (StartWithEnglishMode) { - KeyboardLayoutHelper.SetEnglishKeyboardLayout(); + Win32Helper.SetEnglishKeyboardLayout(true); } }); } @@ -1450,7 +1449,7 @@ namespace Flow.Launcher.ViewModel if (StartWithEnglishMode) { - KeyboardLayoutHelper.SetPreviousKeyboardLayout(); + Win32Helper.SetPreviousKeyboardLayout(); } await Task.Delay(50); From 9d81e60813be042cbf12a9a5e58bd59094728fc3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:26:12 +0800 Subject: [PATCH 12/63] Improve dispose logic --- Flow.Launcher/App.xaml.cs | 6 +-- Flow.Launcher/MainWindow.xaml.cs | 57 ++++++++++++++---------- Flow.Launcher/ViewModel/MainViewModel.cs | 1 + 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 016c2d06c..d78c6c47b 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -298,15 +298,12 @@ namespace Flow.Launcher _disposed = true; } - Stopwatch.Normal("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); if (disposing) { - API?.SaveAppAllSettings(); - await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); @@ -319,6 +316,7 @@ namespace Flow.Launcher public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e7be15081..adbb6f329 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -39,11 +39,13 @@ namespace Flow.Launcher private NotifyIcon _notifyIcon; // Window Context Menu - private readonly ContextMenu contextMenu = new(); + private readonly ContextMenu _contextMenu = new(); private readonly MainViewModel _viewModel; + // Window Event: Close Event + private bool _canClose = false; // Window Event: Key Event - private bool isArrowKeyPressed = false; + private bool _isArrowKeyPressed = false; // Window Sound Effects private MediaPlayer animationSoundWMP; @@ -60,7 +62,7 @@ namespace Flow.Launcher private bool _isClockPanelAnimating = false; // IDisposable - private bool _disposedValue = false; + private bool _disposed = false; #endregion @@ -231,10 +233,18 @@ namespace Flow.Launcher .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void OnClosing(object sender, CancelEventArgs e) + private async void OnClosing(object sender, CancelEventArgs e) { - _notifyIcon.Visible = false; - Notification.Uninstall(); + if (!_canClose) + { + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + _canClose = true; + Close(); + } } private void OnClosed(object sender, EventArgs e) @@ -292,12 +302,12 @@ namespace Flow.Launcher switch (e.Key) { case Key.Down: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectNextItemCommand.Execute(null); e.Handled = true; break; case Key.Up: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectPrevItemCommand.Execute(null); e.Handled = true; break; @@ -355,13 +365,13 @@ namespace Flow.Launcher { if (e.Key == Key.Up || e.Key == Key.Down) { - isArrowKeyPressed = false; + _isArrowKeyPressed = false; } } private void OnPreviewMouseMove(object sender, MouseEventArgs e) { - if (isArrowKeyPressed) + if (_isArrowKeyPressed) { e.Handled = true; // Ignore Mouse Hover when press Arrowkeys } @@ -531,11 +541,11 @@ namespace Flow.Launcher gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); - contextMenu.Items.Add(open); - contextMenu.Items.Add(gamemode); - contextMenu.Items.Add(positionreset); - contextMenu.Items.Add(settings); - contextMenu.Items.Add(exit); + _contextMenu.Items.Add(open); + _contextMenu.Items.Add(gamemode); + _contextMenu.Items.Add(positionreset); + _contextMenu.Items.Add(settings); + _contextMenu.Items.Add(exit); _notifyIcon.MouseClick += (o, e) => { @@ -546,14 +556,14 @@ namespace Flow.Launcher break; case MouseButtons.Right: - contextMenu.IsOpen = true; + _contextMenu.IsOpen = true; // Get context menu handle and bring it to the foreground - if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) + if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource) { Win32Helper.SetForegroundWindow(hwndSource.Handle); } - contextMenu.Focus(); + _contextMenu.Focus(); break; } }; @@ -561,7 +571,7 @@ namespace Flow.Launcher private void UpdateNotifyIconText() { - var menu = contextMenu; + var menu = _contextMenu; ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")"; ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); @@ -757,7 +767,7 @@ namespace Flow.Launcher if (_animating) return; - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _animating = true; UpdatePosition(false); @@ -835,7 +845,7 @@ namespace Flow.Launcher clocksb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; - isArrowKeyPressed = false; + _isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) { @@ -1009,14 +1019,15 @@ namespace Flow.Launcher protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (!_disposed) { if (disposing) { _hwndSource?.Dispose(); + _notifyIcon?.Dispose(); } - _disposedValue = true; + _disposed = true; } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 18e61914d..1668bac3a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1561,6 +1561,7 @@ namespace Flow.Launcher.ViewModel public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } From 48792b6a3bcb3d2b972b00eb39247f16ddaa535e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:29:47 +0800 Subject: [PATCH 13/63] Restore style --- Flow.Launcher/Helper/SingleInstance.cs | 265 +++++++++++++------------ 1 file changed, 133 insertions(+), 132 deletions(-) diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 76c109a39..de2579b62 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,137 +6,138 @@ using System.Windows; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper; - -public interface ISingleInstanceApp +namespace Flow.Launcher.Helper { - void OnSecondAppStarted(); -} - -/// -/// This class checks to make sure that only one instance of -/// this application is running at a time. -/// -/// -/// Note: this class should be used with some caution, because it does no -/// security checking. For example, if one instance of an app that uses this class -/// is running as Administrator, any other instance, even if it is not -/// running as Administrator, can activate it with command line arguments. -/// For most apps, this will not be much of an issue. -/// -public static class SingleInstance where TApplication: Application, ISingleInstanceApp -{ - #region Private Fields - - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; - - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; - private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; - - /// - /// Application mutex. - /// - internal static Mutex SingleInstanceMutex { get; set; } - - #endregion - - #region Public Methods - - /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. - /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance() - { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = InstanceMutexName + Environment.UserName; - - string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); - if (firstInstance) - { - _ = CreateRemoteServiceAsync(channelName); - return true; - } - else - { - _ = SignalFirstInstanceAsync(channelName); - return false; - } - } - - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() - { - SingleInstanceMutex?.ReleaseMutex(); - } - - #endregion - - #region Private Methods - - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteServiceAsync(string channelName) - { - using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); - while (true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - - // Do an asynchronous call to ActivateFirstInstance function - Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); - - // Disconect client - pipeServer.Disconnect(); - } - } - - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstanceAsync(string channelName) - { - // Create a client pipe connected to server - using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) - { - return; - } - - ((TApplication)Application.Current).OnSecondAppStarted(); - } - - #endregion + public interface ISingleInstanceApp + { + void OnSecondAppStarted(); + } + + /// + /// This class checks to make sure that only one instance of + /// this application is running at a time. + /// + /// + /// Note: this class should be used with some caution, because it does no + /// security checking. For example, if one instance of an app that uses this class + /// is running as Administrator, any other instance, even if it is not + /// running as Administrator, can activate it with command line arguments. + /// For most apps, this will not be much of an issue. + /// + public static class SingleInstance where TApplication : Application, ISingleInstanceApp + { + #region Private Fields + + /// + /// String delimiter used in channel names. + /// + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() + { + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; + + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) + { + _ = CreateRemoteServiceAsync(channelName); + return true; + } + else + { + _ = SignalFirstInstanceAsync(channelName); + return false; + } + } + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + SingleInstanceMutex?.ReleaseMutex(); + } + + #endregion + + #region Private Methods + + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) + { + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) + { + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); + } + } + + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); + + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).OnSecondAppStarted(); + } + + #endregion + } } From adbef0d99438a31f3ff141654a0a968c2091b89f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:34:03 +0800 Subject: [PATCH 14/63] Improve disposable interface for mainvm --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1668bac3a..ab67b21bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1554,6 +1554,8 @@ namespace Flow.Launcher.ViewModel if (disposing) { _updateSource?.Dispose(); + _resultsUpdateChannelWriter?.Complete(); + _resultsViewUpdateTask?.Dispose(); _disposed = true; } } From 93ccdee54add655af0384af0a436f3f01924922a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:40:33 +0800 Subject: [PATCH 15/63] Improve comments --- Flow.Launcher/MainWindow.xaml.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index adbb6f329..c2b35ed12 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -219,15 +219,15 @@ namespace Flow.Launcher } }; - // ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) + // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); - // ✅ ContextMenu.Visibility 변경 감지 + // Detecting ContextMenu.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(ContextMenu)) .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); - // ✅ History.Visibility 변경 감지 + // Detect History.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); @@ -242,6 +242,7 @@ namespace Flow.Launcher e.Cancel = true; await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); + // After plugins are all disposed, we can close the main window _canClose = true; Close(); } From 465108a9d487a0aee8d7bdb253a5e983bbfa715a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:58:53 +0800 Subject: [PATCH 16/63] Fix keyboard layout fetch issue --- Flow.Launcher.Infrastructure/Win32Helper.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 664f428ec..2248fadd6 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -371,9 +371,8 @@ namespace Flow.Launcher.Infrastructure if (enHKL == HKL.Null) return; // Get the current window thread ID - uint threadId = 0; - var result = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow(), &threadId); - if (result == 0 || threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + var threadId = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow()); + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); // Backup current keyboard layout if (backupPrevious) From 67be335600d96d1a8077b340c5848819208b0a9e Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 14:58:42 +0600 Subject: [PATCH 17/63] Rename methods to make their purpose more obvious; slight code style changes --- Flow.Launcher.Infrastructure/Win32Helper.cs | 14 ++++++-------- Flow.Launcher/ViewModel/MainViewModel.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 2248fadd6..3d8821fca 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -66,7 +66,7 @@ namespace Flow.Launcher.Infrastructure } /// - /// + /// /// /// /// DoNotRound, Round, RoundSmall, Default @@ -362,7 +362,7 @@ namespace Flow.Launcher.Infrastructure return HKL.Null; } - public static unsafe void SetEnglishKeyboardLayout(bool backupPrevious) + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) { // Find an installed English layout var enHKL = FindEnglishKeyboardLayout(); @@ -384,14 +384,12 @@ namespace Flow.Launcher.Infrastructure PInvoke.ActivateKeyboardLayout(enHKL, 0); } - public static void SetPreviousKeyboardLayout() + public static void RestorePreviousKeyboardLayout() { - if (_previousLayout != HKL.Null) - { - PInvoke.ActivateKeyboardLayout(_previousLayout, 0); + if (_previousLayout == HKL.Null) return; - _previousLayout = HKL.Null; - } + PInvoke.ActivateKeyboardLayout(_previousLayout, 0); + _previousLayout = HKL.Null; } #endregion diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0f355a80f..5ebed0369 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1376,7 +1376,7 @@ namespace Flow.Launcher.ViewModel if (StartWithEnglishMode) { - Win32Helper.SetEnglishKeyboardLayout(true); + Win32Helper.SwitchToEnglishKeyboardLayout(true); } }); } @@ -1385,6 +1385,11 @@ namespace Flow.Launcher.ViewModel public async void Hide() { + + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } lastHistoryIndex = 1; if (ExternalPreviewVisible) @@ -1447,11 +1452,6 @@ namespace Flow.Launcher.ViewModel Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - if (StartWithEnglishMode) - { - Win32Helper.SetPreviousKeyboardLayout(); - } - await Task.Delay(50); // Update WPF properties From c39079badc789a05bea0d23fbd37ab1c22641ecb Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 15:35:34 +0600 Subject: [PATCH 18/63] Revert accidental change --- Flow.Launcher/ViewModel/MainViewModel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 5ebed0369..59af95da5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1385,11 +1385,6 @@ namespace Flow.Launcher.ViewModel public async void Hide() { - - if (StartWithEnglishMode) - { - Win32Helper.RestorePreviousKeyboardLayout(); - } lastHistoryIndex = 1; if (ExternalPreviewVisible) @@ -1452,6 +1447,11 @@ namespace Flow.Launcher.ViewModel Win32Helper.DWMSetCloakForWindow(mainWindow, true); } + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + await Task.Delay(50); // Update WPF properties From 4146f4d3096cd44b5e81435d50d8df1711cd3a3d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 18:03:41 +0800 Subject: [PATCH 19/63] Use focus events to trigger --- Flow.Launcher/MainWindow.xaml | 2 ++ Flow.Launcher/MainWindow.xaml.cs | 16 ++++++++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 10 ---------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303ac..2440e89fa 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -242,6 +242,8 @@ InputMethod.PreferredImeState="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEStateConverter}}" PreviewDragOver="QueryTextBox_OnPreviewDragOver" PreviewKeyUp="QueryTextBox_KeyUp" + GotKeyboardFocus="QueryTextBox_OnGotKeyboardFocus" + PreviewLostKeyboardFocus="QueryTextBox_OnPreviewLostKeyboardFocus" Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Visibility="Visible" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95..bf560e621 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -989,6 +989,22 @@ namespace Flow.Launcher e.Handled = true; } + private void QueryTextBox_OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (_viewModel.StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } + } + + private void QueryTextBox_OnPreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (_viewModel.StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + } + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 59af95da5..97ed422c5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1373,11 +1373,6 @@ namespace Flow.Launcher.ViewModel MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); - - if (StartWithEnglishMode) - { - Win32Helper.SwitchToEnglishKeyboardLayout(true); - } }); } @@ -1447,11 +1442,6 @@ namespace Flow.Launcher.ViewModel Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - if (StartWithEnglishMode) - { - Win32Helper.RestorePreviousKeyboardLayout(); - } - await Task.Delay(50); // Update WPF properties From f83e8eddb6aad1ed6cbec06620db6f1b1e6dab74 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 16:23:34 +0600 Subject: [PATCH 20/63] Revert "Use focus events to trigger" This reverts commit 4146f4d3096cd44b5e81435d50d8df1711cd3a3d. --- Flow.Launcher/MainWindow.xaml | 2 -- Flow.Launcher/MainWindow.xaml.cs | 16 ---------------- Flow.Launcher/ViewModel/MainViewModel.cs | 10 ++++++++++ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 2440e89fa..5b63303ac 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -242,8 +242,6 @@ InputMethod.PreferredImeState="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEStateConverter}}" PreviewDragOver="QueryTextBox_OnPreviewDragOver" PreviewKeyUp="QueryTextBox_KeyUp" - GotKeyboardFocus="QueryTextBox_OnGotKeyboardFocus" - PreviewLostKeyboardFocus="QueryTextBox_OnPreviewLostKeyboardFocus" Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Visibility="Visible" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index bf560e621..2ce3d1e95 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -989,22 +989,6 @@ namespace Flow.Launcher e.Handled = true; } - private void QueryTextBox_OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) - { - if (_viewModel.StartWithEnglishMode) - { - Win32Helper.SwitchToEnglishKeyboardLayout(true); - } - } - - private void QueryTextBox_OnPreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) - { - if (_viewModel.StartWithEnglishMode) - { - Win32Helper.RestorePreviousKeyboardLayout(); - } - } - #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 97ed422c5..59af95da5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1373,6 +1373,11 @@ namespace Flow.Launcher.ViewModel MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + if (StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } }); } @@ -1442,6 +1447,11 @@ namespace Flow.Launcher.ViewModel Win32Helper.DWMSetCloakForWindow(mainWindow, true); } + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + await Task.Delay(50); // Update WPF properties From 6ad4b2355ed23a4df7693f3b12221908893acfad Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 17:08:09 +0600 Subject: [PATCH 21/63] Don't switch to English when IME can be disabled instead --- Flow.Launcher.Infrastructure/Win32Helper.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 3d8821fca..76d469414 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -330,6 +330,13 @@ namespace Flow.Launcher.Infrastructure 0x3009, 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, }; + private static readonly uint[] ImeLanguageIds = + { + 0x0004, 0x7804, 0x0804, 0x1004, 0x7C04, 0x0C04, 0x1404, 0x0404, 0x0011, 0x0411, 0x0012, 0x0412, + }; + + private const uint KeyboardLayoutLoWord = 0xFFFF; + // Store the previous keyboard layout private static HKL _previousLayout; @@ -350,7 +357,7 @@ namespace Flow.Launcher.Infrastructure foreach (var hkl in handles) { // The lower word contains the language identifier - var langId = (uint)hkl.Value & 0xFFFF; + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; // Check if it's an English layout if (EnglishLanguageIds.Contains(langId)) @@ -374,10 +381,18 @@ namespace Flow.Launcher.Infrastructure var threadId = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow()); if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + // If the current layout has an IME mode, disable it without switching to another layout + var currentLayout = PInvoke.GetKeyboardLayout(threadId); + var currentLayoutCode = (uint)currentLayout.Value & KeyboardLayoutLoWord; + if (ImeLanguageIds.Contains(currentLayoutCode)) + { + return; + } + // Backup current keyboard layout if (backupPrevious) { - _previousLayout = PInvoke.GetKeyboardLayout(threadId); + _previousLayout = currentLayout; } // Switch to English layout From ca04823dd77be4ba24d5a7dee69275049dd78eb8 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sat, 22 Mar 2025 19:40:26 +0600 Subject: [PATCH 22/63] Remove generic language code --- Flow.Launcher.Infrastructure/Win32Helper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 76d469414..f23f02447 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -326,8 +326,8 @@ namespace Flow.Launcher.Infrastructure // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f private static readonly uint[] EnglishLanguageIds = { - 0x0009, 0x0409, 0x0809, 0x0C09, 0x1000, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, - 0x3009, 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, + 0x0009, 0x0409, 0x0809, 0x0C09, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, 0x3009, + 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, }; private static readonly uint[] ImeLanguageIds = From 9167cba6367867b216c2440ef0f6c0393c1d1b8d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:08:47 +0800 Subject: [PATCH 23/63] Use selected item for binding preview properties & Code cleanup --- Flow.Launcher/MainWindow.xaml | 4 ++-- Flow.Launcher/ViewModel/MainViewModel.cs | 21 +++++++++++++---- Flow.Launcher/ViewModel/ResultViewModel.cs | 25 ++++++++++++++------- Flow.Launcher/ViewModel/ResultsViewModel.cs | 13 ++++++----- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303ac..d1f6e7812 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -441,7 +441,7 @@ diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..f06b8aa81 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -162,6 +162,7 @@ namespace Flow.Launcher.ViewModel switch (args.PropertyName) { case nameof(Results.SelectedItem): + SelectedItem = Results.SelectedItem; UpdatePreview(); break; } @@ -786,6 +787,18 @@ namespace Flow.Launcher.ViewModel #region Preview + private ResultViewModel _selectedItem; + + public ResultViewModel SelectedItem + { + get => _selectedItem; + set + { + _selectedItem = value; + OnPropertyChanged(); + } + } + public bool InternalPreviewVisible { get @@ -884,7 +897,7 @@ namespace Flow.Launcher.ViewModel private void ShowInternalPreview() { ResultAreaColumn = ResultAreaColumnPreviewShown; - Results.SelectedItem?.LoadPreviewImage(); + SelectedItem?.LoadPreviewImage(); } private void HideInternalPreview() @@ -939,14 +952,14 @@ namespace Flow.Launcher.ViewModel case false when InternalPreviewVisible: - Results.SelectedItem?.LoadPreviewImage(); + SelectedItem?.LoadPreviewImage(); break; } } private bool CanExternalPreviewSelectedResult(out string path) { - path = Results.SelectedItem?.Result?.Preview.FilePath; + path = SelectedItem?.Result?.Preview.FilePath; return !string.IsNullOrEmpty(path); } @@ -976,7 +989,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); ContextMenu.Clear(); - var selected = Results.SelectedItem?.Result; + var selected = SelectedItem?.Result; if (selected != null) // SelectedItem returns null if selection is empty. { diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 5130e7eba..172eca502 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Drawing.Text; +using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; @@ -6,16 +9,13 @@ using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using System.IO; -using System.Drawing.Text; -using System.Collections.Generic; namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - private static PrivateFontCollection fontCollection = new(); - private static Dictionary fonts = new(); + private static readonly PrivateFontCollection fontCollection = new(); + private static readonly Dictionary fonts = new(); public ResultViewModel(Result result, Settings settings) { @@ -39,11 +39,11 @@ namespace Flow.Launcher.ViewModel fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); } - if (fonts.ContainsKey(fontFamilyPath)) + if (fonts.TryGetValue(fontFamilyPath, out var value)) { Glyph = glyph with { - FontFamily = fonts[fontFamilyPath] + FontFamily = value }; } else @@ -171,7 +171,16 @@ namespace Flow.Launcher.ViewModel public ImageSource PreviewImage { - get => previewImage; + get + { + if (!PreviewImageLoaded) + { + PreviewImageLoaded = true; + _ = LoadPreviewImageAsync(); + } + + return previewImage; + } private set => previewImage = value; } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 7d2b5bc93..512f5c150 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,6 +1,4 @@ using System; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -10,6 +8,8 @@ using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; namespace Flow.Launcher.ViewModel { @@ -219,7 +219,6 @@ namespace Flow.Launcher.ViewModel if (newRawResults.Count == 0) return Results; - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); return Results.Where(r => r.Result.PluginID != resultId) @@ -241,6 +240,7 @@ namespace Flow.Launcher.ViewModel #endregion #region FormattedText Dependency Property + public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( "FormattedText", typeof(Inline), @@ -259,8 +259,7 @@ namespace Flow.Launcher.ViewModel private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBlock = d as TextBlock; - if (textBlock == null) return; + if (d is not TextBlock textBlock) return; var inline = (Inline)e.NewValue; @@ -269,6 +268,7 @@ namespace Flow.Launcher.ViewModel textBlock.Inlines.Add(inline); } + #endregion public class ResultCollection : List, INotifyCollectionChanged @@ -279,7 +279,6 @@ namespace Flow.Launcher.ViewModel public event NotifyCollectionChangedEventHandler CollectionChanged; - protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); @@ -297,6 +296,7 @@ namespace Flow.Launcher.ViewModel // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + private void AddAll(List Items) { for (int i = 0; i < Items.Count; i++) @@ -308,6 +308,7 @@ namespace Flow.Launcher.ViewModel OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } + public void RemoveAll(int Capacity = 512) { Clear(); From b93faffdc0d0fc2141cbd0738d1606b3eff5a56a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:12:20 +0800 Subject: [PATCH 24/63] Code cleanup --- Flow.Launcher/ViewModel/ResultViewModel.cs | 7 ++----- Flow.Launcher/ViewModel/ResultsViewModel.cs | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 172eca502..73b380394 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -21,10 +21,8 @@ namespace Flow.Launcher.ViewModel { Settings = settings; - if (result == null) - { - return; - } + if (result == null) return; + Result = result; if (Result.Glyph is { FontFamily: not null } glyph) @@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel Glyph = glyph; } } - } public Settings Settings { get; } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 512f5c150..61566b415 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel Results = new ResultCollection(); BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); } + public ResultsViewModel(Settings settings) : this() { _settings = settings; From 26ab2aecef33771913af596079959d419aab550b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:19:42 +0800 Subject: [PATCH 25/63] Revert to results selected item --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f06b8aa81..9b42ae613 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -989,7 +989,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); ContextMenu.Clear(); - var selected = SelectedItem?.Result; + var selected = Results.SelectedItem?.Result; if (selected != null) // SelectedItem returns null if selection is empty. { From f1bcfc1933e25901165a72c63279b9aa6ea241c9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:33:04 +0800 Subject: [PATCH 26/63] Support preview panel for history --- Flow.Launcher/MainWindow.xaml | 4 +- Flow.Launcher/ViewModel/MainViewModel.cs | 47 +++++++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index d1f6e7812..20e4b20d9 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -441,7 +441,7 @@ diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9b42ae613..6b307864e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -162,7 +162,20 @@ namespace Flow.Launcher.ViewModel switch (args.PropertyName) { case nameof(Results.SelectedItem): - SelectedItem = Results.SelectedItem; + _selectedItemFromQueryResults = true; + PreviewSelectedItem = Results.SelectedItem; + UpdatePreview(); + break; + } + }; + + History.PropertyChanged += (_, args) => + { + switch (args.PropertyName) + { + case nameof(History.SelectedItem): + _selectedItemFromQueryResults = false; + PreviewSelectedItem = History.SelectedItem; UpdatePreview(); break; } @@ -646,10 +659,12 @@ namespace Flow.Launcher.ViewModel private ResultsViewModel SelectedResults { - get { return _selectedResults; } + get => _selectedResults; set { + var isReturningFromQueryResults = SelectedIsFromQueryResults(); var isReturningFromContextMenu = ContextMenuSelected(); + var isReturningFromHistory = HistorySelected(); _selectedResults = value; if (SelectedIsFromQueryResults()) { @@ -670,12 +685,30 @@ namespace Flow.Launcher.ViewModel { ChangeQueryText(_queryTextBeforeLeaveResults); } + + // If we are returning from history and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value)) + { + PreviewSelectedItem = null; + } } else { Results.Visibility = Visibility.Collapsed; + History.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; + if(HistorySelected()) + { + // If we are returning from query results and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) + { + PreviewSelectedItem = null; + } + } + // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() @@ -787,9 +820,11 @@ namespace Flow.Launcher.ViewModel #region Preview + private bool? _selectedItemFromQueryResults; + private ResultViewModel _selectedItem; - public ResultViewModel SelectedItem + public ResultViewModel PreviewSelectedItem { get => _selectedItem; set @@ -897,7 +932,7 @@ namespace Flow.Launcher.ViewModel private void ShowInternalPreview() { ResultAreaColumn = ResultAreaColumnPreviewShown; - SelectedItem?.LoadPreviewImage(); + PreviewSelectedItem?.LoadPreviewImage(); } private void HideInternalPreview() @@ -952,14 +987,14 @@ namespace Flow.Launcher.ViewModel case false when InternalPreviewVisible: - SelectedItem?.LoadPreviewImage(); + PreviewSelectedItem?.LoadPreviewImage(); break; } } private bool CanExternalPreviewSelectedResult(out string path) { - path = SelectedItem?.Result?.Preview.FilePath; + path = PreviewSelectedItem == Results.SelectedItem ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty; return !string.IsNullOrEmpty(path); } From 3c7b8cf827dcabfad5c70b713c22d21ae25f7a08 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:37:24 +0800 Subject: [PATCH 27/63] Force preview panel height --- Flow.Launcher/MainWindow.xaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 20e4b20d9..b9e5af117 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -432,9 +432,13 @@ + + + From 00ac32708c043cf9b1e52a56894c12f726bef8d9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:41:20 +0800 Subject: [PATCH 28/63] Fix history visibility issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6b307864e..797e3c533 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -696,7 +696,6 @@ namespace Flow.Launcher.ViewModel else { Results.Visibility = Visibility.Collapsed; - History.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; if(HistorySelected()) From 655b017e9e5f7dbf5b50f607054c834a88b85788 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:42:14 +0800 Subject: [PATCH 29/63] Code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 797e3c533..905855d88 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -698,16 +698,6 @@ namespace Flow.Launcher.ViewModel Results.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; - if(HistorySelected()) - { - // If we are returning from query results and we have not set select item yet, - // we need to clear the preview selected item - if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) - { - PreviewSelectedItem = null; - } - } - // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() @@ -720,6 +710,16 @@ namespace Flow.Launcher.ViewModel { QueryText = string.Empty; } + + if (HistorySelected()) + { + // If we are returning from query results and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) + { + PreviewSelectedItem = null; + } + } } _selectedResults.Visibility = Visibility.Visible; From 3e64a341bc08ff75bea37817900b1cad80f1551b Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Sat, 22 Mar 2025 19:41:26 +0100 Subject: [PATCH 30/63] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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:

From e9f317f6a2531abaf54a73f9519ce55058ef42c7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 11:02:23 +0800 Subject: [PATCH 31/63] Change variable names --- Flow.Launcher/ViewModel/ResultViewModel.cs | 65 ++++++++++------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 73b380394..db124e078 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -14,8 +14,8 @@ namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - private static readonly PrivateFontCollection fontCollection = new(); - private static readonly Dictionary fonts = new(); + private static readonly PrivateFontCollection FontCollection = new(); + private static readonly Dictionary Fonts = new(); public ResultViewModel(Result result, Settings settings) { @@ -37,7 +37,7 @@ namespace Flow.Launcher.ViewModel fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); } - if (fonts.TryGetValue(fontFamilyPath, out var value)) + if (Fonts.TryGetValue(fontFamilyPath, out var value)) { Glyph = glyph with { @@ -46,11 +46,11 @@ namespace Flow.Launcher.ViewModel } else { - fontCollection.AddFontFile(fontFamilyPath); - fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}"; + FontCollection.AddFontFile(fontFamilyPath); + Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}"; Glyph = glyph with { - FontFamily = fonts[fontFamilyPath] + FontFamily = Fonts[fontFamilyPath] }; } } @@ -92,14 +92,10 @@ namespace Flow.Launcher.ViewModel get { if (PreviewImageAvailable) - { return Visibility.Visible; - } - else - { - // Fall back to icon - return ShowIcon; - } + + // Fall back to icon + return ShowIcon; } } @@ -108,9 +104,8 @@ namespace Flow.Launcher.ViewModel get { if (Result.RoundedIcon) - { return IconXY / 2; - } + return IconXY; } @@ -145,40 +140,40 @@ namespace Flow.Launcher.ViewModel ? Result.SubTitle : Result.SubTitleToolTip; - private volatile bool ImageLoaded; - private volatile bool PreviewImageLoaded; + private volatile bool _imageLoaded; + private volatile bool _previewImageLoaded; - private ImageSource image = ImageLoader.LoadingImage; - private ImageSource previewImage = ImageLoader.LoadingImage; + private ImageSource _image = ImageLoader.LoadingImage; + private ImageSource _previewImage = ImageLoader.LoadingImage; public ImageSource Image { get { - if (!ImageLoaded) + if (!_imageLoaded) { - ImageLoaded = true; + _imageLoaded = true; _ = LoadImageAsync(); } - return image; + return _image; } - private set => image = value; + private set => _image = value; } public ImageSource PreviewImage { get { - if (!PreviewImageLoaded) + if (!_previewImageLoaded) { - PreviewImageLoaded = true; + _previewImageLoaded = true; _ = LoadPreviewImageAsync(); } - return previewImage; + return _previewImage; } - private set => previewImage = value; + private set => _previewImage = value; } ///

@@ -194,8 +189,7 @@ namespace Flow.Launcher.ViewModel { try { - var image = icon(); - return image; + return icon(); } catch (Exception e) { @@ -214,7 +208,7 @@ namespace Flow.Launcher.ViewModel var iconDelegate = Result.Icon; if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img)) { - image = img; + _image = img; } else { @@ -229,7 +223,7 @@ namespace Flow.Launcher.ViewModel var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img)) { - previewImage = img; + _previewImage = img; } else { @@ -240,13 +234,10 @@ namespace Flow.Launcher.ViewModel public void LoadPreviewImage() { - if (ShowDefaultPreview == Visibility.Visible) + if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible) { - if (!PreviewImageLoaded && ShowPreviewImage == Visibility.Visible) - { - PreviewImageLoaded = true; - _ = LoadPreviewImageAsync(); - } + _previewImageLoaded = true; + _ = LoadPreviewImageAsync(); } } From 0ef4b0580881df69a2e396ce570d488ef9ba4108 Mon Sep 17 00:00:00 2001 From: DB p Date: Sun, 23 Mar 2025 17:21:03 +0900 Subject: [PATCH 32/63] Add Initial state for QueryTextBoxCursorMovedToEnd --- Flow.Launcher/MainWindow.xaml.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index d2e9dade9..ec8649efc 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -143,7 +143,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) From 747f9582c3673a72eaf707e765f3badfc5af6674 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 19:27:40 +0800 Subject: [PATCH 33/63] Fix keyboard restore issue when window is deactivated --- Flow.Launcher/MainWindow.xaml.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95..de15f30e8 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -250,11 +250,21 @@ namespace Flow.Launcher private async void OnDeactivated(object sender, EventArgs e) { + // When window is deactivated, FL cannot set keyboard correctly + // This is a workaround to restore the keyboard layout + if (_settings.HideWhenDeactivated && _viewModel.StartWithEnglishMode) + { + Activate(); + QueryTextBox.Focus(); + Win32Helper.RestorePreviousKeyboardLayout(); + } + _settings.WindowLeft = Left; _settings.WindowTop = Top; ClockPanel.Opacity = 0; SearchIcon.Opacity = 0; - //This condition stops extra hide call when animator is on, + + // This condition stops extra hide call when animator is on, // which causes the toggling to occasional hide instead of show. if (_viewModel.MainWindowVisibilityStatus) { @@ -262,8 +272,9 @@ namespace Flow.Launcher // This also stops the mainwindow from flickering occasionally after Settings window is opened // and always after Settings window is closed. if (_settings.UseAnimation) - + { await Task.Delay(100); + } if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) { From cd28c09c09b3a30a78571a9448db07fba2303e72 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 17:56:52 +0600 Subject: [PATCH 34/63] Fix the issue with not being able to switch back to the original keyboard layout in OnDeactivated --- Flow.Launcher.Infrastructure/NativeMethods.txt | 7 ++++++- Flow.Launcher.Infrastructure/Win32Helper.cs | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index d26478bbe..54ae099f8 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -51,4 +51,9 @@ WM_EXITSIZEMOVE GetKeyboardLayout GetWindowThreadProcessId ActivateKeyboardLayout -GetKeyboardLayoutList \ No newline at end of file +GetKeyboardLayoutList + +PostMessage +HWND_BROADCAST +WM_INPUTLANGCHANGEREQUEST +INPUTLANGCHANGE_FORWARD diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index f23f02447..197a5a556 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -403,7 +403,11 @@ namespace Flow.Launcher.Infrastructure { if (_previousLayout == HKL.Null) return; - PInvoke.ActivateKeyboardLayout(_previousLayout, 0); + PInvoke.PostMessage(HWND.HWND_BROADCAST, + PInvoke.WM_INPUTLANGCHANGEREQUEST, + PInvoke.INPUTLANGCHANGE_FORWARD, + _previousLayout.Value + ); _previousLayout = HKL.Null; } From bf011f11dbac1b28e7cbfc15e1e60157a1c0bc84 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 17:57:24 +0600 Subject: [PATCH 35/63] Revert "Fix keyboard restore issue when window is deactivated" This reverts commit 747f9582c3673a72eaf707e765f3badfc5af6674. --- Flow.Launcher/MainWindow.xaml.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index de15f30e8..2ce3d1e95 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -250,21 +250,11 @@ namespace Flow.Launcher private async void OnDeactivated(object sender, EventArgs e) { - // When window is deactivated, FL cannot set keyboard correctly - // This is a workaround to restore the keyboard layout - if (_settings.HideWhenDeactivated && _viewModel.StartWithEnglishMode) - { - Activate(); - QueryTextBox.Focus(); - Win32Helper.RestorePreviousKeyboardLayout(); - } - _settings.WindowLeft = Left; _settings.WindowTop = Top; ClockPanel.Opacity = 0; SearchIcon.Opacity = 0; - - // This condition stops extra hide call when animator is on, + //This condition stops extra hide call when animator is on, // which causes the toggling to occasional hide instead of show. if (_viewModel.MainWindowVisibilityStatus) { @@ -272,9 +262,8 @@ namespace Flow.Launcher // This also stops the mainwindow from flickering occasionally after Settings window is opened // and always after Settings window is closed. if (_settings.UseAnimation) - { + await Task.Delay(100); - } if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) { From 382d0c2bfe85416185d9ffcebfc0d9ab68065f31 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 18:26:11 +0600 Subject: [PATCH 36/63] Don't broadcast language change --- Flow.Launcher.Infrastructure/NativeMethods.txt | 1 - Flow.Launcher.Infrastructure/Win32Helper.cs | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 54ae099f8..833c6682b 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -54,6 +54,5 @@ ActivateKeyboardLayout GetKeyboardLayoutList PostMessage -HWND_BROADCAST WM_INPUTLANGCHANGEREQUEST INPUTLANGCHANGE_FORWARD diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 197a5a556..7bb9df2e7 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -403,7 +403,9 @@ namespace Flow.Launcher.Infrastructure { if (_previousLayout == HKL.Null) return; - PInvoke.PostMessage(HWND.HWND_BROADCAST, + var hwnd = PInvoke.GetForegroundWindow(); + PInvoke.PostMessage( + hwnd, PInvoke.WM_INPUTLANGCHANGEREQUEST, PInvoke.INPUTLANGCHANGE_FORWARD, _previousLayout.Value From 48aff32f1b9a699204766c1284bc1373a37fb218 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 18:28:19 +0600 Subject: [PATCH 37/63] Clarify why not switch keyboard layout for languages that have IME mode --- Flow.Launcher.Infrastructure/Win32Helper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 7bb9df2e7..f874c7006 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -381,7 +381,9 @@ namespace Flow.Launcher.Infrastructure var threadId = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow()); if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); - // If the current layout has an IME mode, disable it without switching to another layout + // 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 currentLayoutCode = (uint)currentLayout.Value & KeyboardLayoutLoWord; if (ImeLanguageIds.Contains(currentLayoutCode)) From 4df42a0f638ea0dc2a4be6999606cc1ea1141e63 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 18:40:41 +0600 Subject: [PATCH 38/63] Add doc comments and additional error handling in keyboard layout switch logic --- Flow.Launcher.Infrastructure/Win32Helper.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index f874c7006..7cebedfdc 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -350,7 +350,11 @@ namespace Flow.Launcher.Infrastructure var handles = new HKL[count]; fixed (HKL* h = handles) { - _ = PInvoke.GetKeyboardLayoutList(count, h); + var result = PInvoke.GetKeyboardLayoutList(count, h); + if (result != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } } // Look for any English keyboard layout @@ -369,6 +373,11 @@ namespace Flow.Launcher.Infrastructure return HKL.Null; } + /// + /// 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 @@ -401,11 +410,17 @@ namespace Flow.Launcher.Infrastructure 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, From 1bf573344ae676adb2d22198e0d10e7b9a3cb5ff Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 18:53:01 +0600 Subject: [PATCH 39/63] Fix incorrect error handling logic in keyboard layout change --- Flow.Launcher.Infrastructure/Win32Helper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 7cebedfdc..108f51fd4 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -351,7 +351,7 @@ namespace Flow.Launcher.Infrastructure fixed (HKL* h = handles) { var result = PInvoke.GetKeyboardLayoutList(count, h); - if (result != 0) + if (result == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } From 5be88dd38613bc1f110b8d0437611ff396502c9c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 20:57:12 +0800 Subject: [PATCH 40/63] Remove blank line --- Flow.Launcher.Infrastructure/NativeMethods.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 833c6682b..de74eb7f9 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -52,7 +52,6 @@ GetKeyboardLayout GetWindowThreadProcessId ActivateKeyboardLayout GetKeyboardLayoutList - PostMessage WM_INPUTLANGCHANGEREQUEST INPUTLANGCHANGE_FORWARD From c63debe296ec40bda08a8083bd667aec7a549f32 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 20:57:25 +0800 Subject: [PATCH 41/63] Add foreground window check --- Flow.Launcher.Infrastructure/Win32Helper.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 108f51fd4..226d7b76c 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -386,8 +386,12 @@ namespace Flow.Launcher.Infrastructure // No installed English layout found if (enHKL == HKL.Null) return; - // Get the current window thread ID - var threadId = PInvoke.GetWindowThreadProcessId(PInvoke.GetForegroundWindow()); + // 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. From 4fc7f70d18b4b284f1814f44addceda457f87f52 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 21:05:14 +0800 Subject: [PATCH 42/63] Adjust formats --- Flow.Launcher.Infrastructure/Win32Helper.cs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 226d7b76c..8c32899ec 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -351,10 +351,7 @@ namespace Flow.Launcher.Infrastructure fixed (HKL* h = handles) { var result = PInvoke.GetKeyboardLayoutList(count, h); - if (result == 0) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); } // Look for any English keyboard layout @@ -364,10 +361,7 @@ namespace Flow.Launcher.Infrastructure var langId = (uint)hkl.Value & KeyboardLayoutLoWord; // Check if it's an English layout - if (EnglishLanguageIds.Contains(langId)) - { - return hkl; - } + if (EnglishLanguageIds.Contains(langId)) return hkl; } return HKL.Null; @@ -399,16 +393,10 @@ namespace Flow.Launcher.Infrastructure // the IME mode instead of switching to another layout. var currentLayout = PInvoke.GetKeyboardLayout(threadId); var currentLayoutCode = (uint)currentLayout.Value & KeyboardLayoutLoWord; - if (ImeLanguageIds.Contains(currentLayoutCode)) - { - return; - } + if (ImeLanguageIds.Contains(currentLayoutCode)) return; // Backup current keyboard layout - if (backupPrevious) - { - _previousLayout = currentLayout; - } + if (backupPrevious) _previousLayout = currentLayout; // Switch to English layout PInvoke.ActivateKeyboardLayout(enHKL, 0); @@ -431,6 +419,7 @@ namespace Flow.Launcher.Infrastructure PInvoke.INPUTLANGCHANGE_FORWARD, _previousLayout.Value ); + _previousLayout = HKL.Null; } From d827d0ac9f42f88556476731c700c6ad8806e8fc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 22:22:08 +0800 Subject: [PATCH 43/63] Use language tag instead of language id --- .../NativeMethods.txt | 4 + Flow.Launcher.Infrastructure/Win32Helper.cs | 143 +++++++++++++----- 2 files changed, 108 insertions(+), 39 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index de74eb7f9..363ecb9d0 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -55,3 +55,7 @@ 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 8c32899ec..c84c2e3a8 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,16 +1,17 @@ using System; using System.ComponentModel; -using System.Linq; +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 @@ -323,16 +324,16 @@ namespace Flow.Launcher.Infrastructure #region Keyboard Layout - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f - private static readonly uint[] EnglishLanguageIds = - { - 0x0009, 0x0409, 0x0809, 0x0C09, 0x1009, 0x1409, 0x1809, 0x1C09, 0x2009, 0x2409, 0x2809, 0x2C09, 0x3009, - 0x3409, 0x3C09, 0x4009, 0x4409, 0x4809, 0x4C09, - }; + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; - private static readonly uint[] ImeLanguageIds = + // 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 = { - 0x0004, 0x7804, 0x0804, 0x1004, 0x7C04, 0x0C04, 0x1404, 0x0404, 0x0011, 0x0411, 0x0012, 0x0412, + "zh", // Chinese + "ja", // Japanese + "ko", // Korean }; private const uint KeyboardLayoutLoWord = 0xFFFF; @@ -340,33 +341,6 @@ namespace Flow.Launcher.Infrastructure // Store the previous keyboard layout private static HKL _previousLayout; - 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; - - // Check if it's an English layout - if (EnglishLanguageIds.Contains(langId)) return hkl; - } - - return HKL.Null; - } - /// /// Switches the keyboard layout to English if available. /// @@ -392,8 +366,14 @@ namespace Flow.Launcher.Infrastructure // 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 currentLayoutCode = (uint)currentLayout.Value & KeyboardLayoutLoWord; - if (ImeLanguageIds.Contains(currentLayoutCode)) return; + 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; @@ -423,6 +403,91 @@ namespace Flow.Launcher.Infrastructure _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 (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key is not null && key.GetValue("Languages") is string[] languages) + { + foreach (string language in languages) + { + using RegistryKey subKey = key.OpenSubKey(language); + if (subKey is not null + && subKey.GetValue("TransientLangId") is int transientLangId + && transientLangId == langId) + { + return language; + } + } + } + } + + return CultureInfo.GetCultureInfo((int)langId).Name; + } + #endregion } } From 4f2a951adfe2d1d5d96770f1470184f9dce6d091 Mon Sep 17 00:00:00 2001 From: Yusyuriv Date: Sun, 23 Mar 2025 20:41:28 +0600 Subject: [PATCH 44/63] Small code style changes in keyboard change logic --- Flow.Launcher.Infrastructure/Win32Helper.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index c84c2e3a8..7a3a0c36e 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -464,19 +464,18 @@ namespace Flow.Launcher.Infrastructure // 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 (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD1 - or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD2 - or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD3 - or (int)PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) { - using RegistryKey key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); - if (key is not null && key.GetValue("Languages") is string[] languages) + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key?.GetValue("Languages") is string[] languages) { foreach (string language in languages) { - using RegistryKey subKey = key.OpenSubKey(language); - if (subKey is not null - && subKey.GetValue("TransientLangId") is int transientLangId + using var subKey = key.OpenSubKey(language); + if (subKey?.GetValue("TransientLangId") is int transientLangId && transientLangId == langId) { return language; From dfad11cbed8880c17248a2a3a689616ecc7a02a4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 24 Mar 2025 13:22:15 +0800 Subject: [PATCH 45/63] Fix language change issue --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 - .../Resource/Internationalization.cs | 79 +++++++++++++------ Flow.Launcher/App.xaml.cs | 9 ++- 3 files changed, 58 insertions(+), 33 deletions(-) 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..baf602096 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,10 +136,33 @@ 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) + private async Task ChangeLanguageAsync(Language language) + { + // 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 for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); + } + + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); @@ -128,28 +177,6 @@ namespace Flow.Launcher.Core.Resource } } - private void ChangeLanguage(Language language, bool isSystem) - { - language = language.NonNull(); - - 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(); - }); - } - public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 377d36c48..b102e384e 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -132,13 +132,14 @@ namespace Flow.Launcher // Register ResultsUpdated event after all plugins are loaded Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); - // Change language after all plugins are initialized - // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future - Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); - 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; var window = new MainWindow(); From a3193cf525c3a487bbe690e67f9b07fe05624802 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 24 Mar 2025 13:24:05 +0800 Subject: [PATCH 46/63] Move function position --- .../Resource/Internationalization.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index baf602096..ffa17ab4d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -144,6 +144,21 @@ namespace Flow.Launcher.Core.Resource _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } + private Language GetLanguageByLanguageCode(string languageCode) + { + var lowercase = languageCode.ToLower(); + var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); + if (language == null) + { + Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>"); + return AvailableLanguages.English; + } + else + { + return language; + } + } + private async Task ChangeLanguageAsync(Language language) { // Remove old language files and load language @@ -162,21 +177,6 @@ namespace Flow.Launcher.Core.Resource await Task.Run(UpdatePluginMetadataTranslations); } - private static Language GetLanguageByLanguageCode(string languageCode) - { - var lowercase = languageCode.ToLower(); - var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); - if (language == null) - { - Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>"); - return AvailableLanguages.English; - } - else - { - return language; - } - } - public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); From ec22f596d432a0a4dc39e2a2643827989e3f6bb2 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 24 Mar 2025 16:26:07 +0900 Subject: [PATCH 47/63] Fix Non Resource situation when change theme --- Flow.Launcher.Core/Resource/Theme.cs | 41 ++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index fc5c2e4e9..25abda3a0 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Microsoft.Win32; using TextBox = System.Windows.Controls.TextBox; +using System.Diagnostics; namespace Flow.Launcher.Core.Resource { @@ -56,19 +57,24 @@ namespace Flow.Launcher.Core.Resource MakeSureThemeDirectoriesExist(); var dicts = Application.Current.Resources.MergedDictionaries; - _oldResource = dicts.First(d => + _oldResource = dicts.FirstOrDefault(d => { if (d.Source == null) return false; var p = d.Source.AbsolutePath; - var dir = Path.GetDirectoryName(p).NonNull(); - var info = new DirectoryInfo(dir); - var f = info.Name; - var e = Path.GetExtension(p); - var found = f == Folder && e == Extension; - return found; + return p.Contains(Folder) && Path.GetExtension(p) == Extension; }); + + if (_oldResource != null) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + else + { + Log.Error("현재 테마 리소스를 찾을 수 없습니다. 기본 테마로 초기화합니다."); + _oldTheme = Constant.DefaultTheme; + }; _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } @@ -98,11 +104,24 @@ namespace Flow.Launcher.Core.Resource private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - var dicts = Application.Current.Resources.MergedDictionaries; - - dicts.Remove(_oldResource); - dicts.Add(dictionaryToUpdate); + // 새 테마 리소스를 먼저 추가하고 + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + + // 그 다음 이전 테마 리소스 제거 + if (_oldResource != null && + _oldResource != dictionaryToUpdate && + Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) + { + Application.Current.Resources.MergedDictionaries.Remove(_oldResource); + } + _oldResource = dictionaryToUpdate; + + // 리소스 변경 후 문제가 없는지 검증 + Debug.WriteLine($"테마 변경 후 리소스 딕셔너리 수: {Application.Current.Resources.MergedDictionaries.Count}"); } private ResourceDictionary GetThemeResourceDictionary(string theme) From a078777608281192c3a2835178a895d822972d42 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 24 Mar 2025 18:10:12 +0900 Subject: [PATCH 48/63] - Fixed an issue where font settings were not applied when using the Blur theme - Removed bold style from highlight and delegated it to individual themes --- Flow.Launcher.Core/Resource/Theme.cs | 156 ++++++++++++++++-- .../ViewModels/SettingsPaneThemeViewModel.cs | 12 +- Flow.Launcher/Themes/Base.xaml | 5 - Flow.Launcher/Themes/BlurBlack Darker.xaml | 4 +- Flow.Launcher/Themes/BlurBlack.xaml | 4 +- Flow.Launcher/Themes/BlurWhite.xaml | 4 +- 6 files changed, 155 insertions(+), 30 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 25abda3a0..5e2e336aa 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -72,7 +72,7 @@ namespace Flow.Launcher.Core.Resource } else { - Log.Error("현재 테마 리소스를 찾을 수 없습니다. 기본 테마로 초기화합니다."); + Log.Error("Current theme resource not found. Initializing with default theme."); _oldTheme = Constant.DefaultTheme; }; _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); @@ -104,26 +104,149 @@ namespace Flow.Launcher.Core.Resource private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - // 새 테마 리소스를 먼저 추가하고 + // Add the new theme resource first if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) { Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); } - // 그 다음 이전 테마 리소스 제거 + // Then remove the old theme resource if (_oldResource != null && _oldResource != dictionaryToUpdate && Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) { Application.Current.Resources.MergedDictionaries.Remove(_oldResource); } - _oldResource = dictionaryToUpdate; - - // 리소스 변경 후 문제가 없는지 검증 - Debug.WriteLine($"테마 변경 후 리소스 딕셔너리 수: {Application.Current.Resources.MergedDictionaries.Count}"); } + /// + /// Updates only the font settings and refreshes the UI. + /// + public void UpdateFonts() + { + try + { + // Loads a ResourceDictionary for the specified theme. + var themeName = GetCurrentTheme(); + var dict = GetThemeResourceDictionary(themeName); + + // Applies font settings to the theme resource. + ApplyFontSettings(dict); + UpdateResourceDictionary(dict); + _ = RefreshFrameAsync(); + } + catch (Exception e) + { + Log.Exception("Error occurred while updating theme fonts", e); + } + } + + /// + /// Loads and applies font settings to the theme resource. + /// + private void ApplyFontSettings(ResourceDictionary dict) + { + if (dict["QueryBoxStyle"] is Style queryBoxStyle && + dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) + { + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); + + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); + SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch); + + SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultSubFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch); + + SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + } + + /// + /// Applies font properties to a Style. + /// + private void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox) + { + // Remove existing font-related setters + if (isTextBox) + { + // First, find the setters to remove and store them in a list + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBox.FontFamilyProperty || + setter.Property == TextBox.FontStyleProperty || + setter.Property == TextBox.FontWeightProperty || + setter.Property == TextBox.FontStretchProperty) + .ToList(); + + // Remove each found setter one by one + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + // Add New font setter + style.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + + // Set caret brush (retain existing logic) + var caretBrushPropertyValue = style.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) + style.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); + } + else + { + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBlock.FontFamilyProperty || + setter.Property == TextBlock.FontStyleProperty || + setter.Property == TextBlock.FontWeightProperty || + setter.Property == TextBlock.FontStretchProperty) + .ToList(); + + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch)); + } + } private ResourceDictionary GetThemeResourceDictionary(string theme) { var uri = GetThemePath(theme); @@ -286,9 +409,10 @@ namespace Flow.Launcher.Core.Resource if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - // reload all resources even if the theme itself hasn't changed in order to pickup changes - // to things like fonts - UpdateResourceDictionary(GetResourceDictionary(theme)); + // Retrieve theme resource – always use the resource with font settings applied. + var resourceDict = GetResourceDictionary(theme); + + UpdateResourceDictionary(resourceDict); _settings.Theme = theme; @@ -299,10 +423,11 @@ namespace Flow.Launcher.Core.Resource } BlurEnabled = IsBlurTheme(); - //if (_settings.UseDropShadowEffect) - // AddDropShadowEffectToCurrentTheme(); - //Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); - _ = SetBlurForWindowAsync(); + + // 블러 및 그림자 효과 적용을 위한 비동기 처리 + _ = RefreshFrameAsync(); + + return true; } catch (DirectoryNotFoundException) { @@ -324,7 +449,6 @@ namespace Flow.Launcher.Core.Resource } return false; } - return true; } #endregion @@ -500,7 +624,7 @@ namespace Flow.Launcher.Core.Resource private void SetBlurForWindow(string theme, BackdropTypes backdropType) { - var dict = GetThemeResourceDictionary(theme); + var dict = GetResourceDictionary(theme); // GetThemeResourceDictionary 대신 GetResourceDictionary 사용 if (dict == null) return; diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs index 61d365b64..48a0aa1d1 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -342,7 +342,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.QueryBoxFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -364,7 +364,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.QueryBoxFontStretch = value.Stretch.ToString(); Settings.QueryBoxFontWeight = value.Weight.ToString(); Settings.QueryBoxFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -386,7 +386,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -408,7 +408,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultFontStretch = value.Stretch.ToString(); Settings.ResultFontWeight = value.Weight.ToString(); Settings.ResultFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -432,7 +432,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultSubFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -453,7 +453,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultSubFontStretch = value.Stretch.ToString(); Settings.ResultSubFontWeight = value.Weight.ToString(); Settings.ResultSubFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index 907ded363..4772e7768 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -27,7 +27,6 @@ - + - + - + - - + - + +