From 3f5bd0b1816e8b9e4cfa99c89325523203ac323d Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Wed, 9 Feb 2022 21:18:11 -0600 Subject: [PATCH 001/243] Initial Implementation of quickswitch in Flow --- .../Flow.Launcher.Infrastructure.csproj | 2 + Flow.Launcher/App.xaml.cs | 2 + Flow.Launcher/Flow.Launcher.csproj | 10 ++ Flow.Launcher/QuickSwitch/NativeHelper.cs | 46 +++++++ Flow.Launcher/QuickSwitch/QuickSwitch.cs | 122 ++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 Flow.Launcher/QuickSwitch/NativeHelper.cs create mode 100644 Flow.Launcher/QuickSwitch/QuickSwitch.cs diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 40c2cb956ff..03a9cc4c832 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -53,6 +53,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 4ebff16a935..f867916e7c4 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -91,6 +91,8 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => // main windows needs initialized before theme change because of blur settigns ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.ChangeTheme(_settings.Theme); + + QuickSwitch.QuickSwitch.Initialize(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 35a6389ca11..1069df7ab23 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -12,6 +12,7 @@ false false en + true @@ -111,6 +112,15 @@ + + + QuickSwitch\Interop.SHDocVw.dll + + + QuickSwitch\Interop.Shell32.dll + + + diff --git a/Flow.Launcher/QuickSwitch/NativeHelper.cs b/Flow.Launcher/QuickSwitch/NativeHelper.cs new file mode 100644 index 00000000000..f0a1568887d --- /dev/null +++ b/Flow.Launcher/QuickSwitch/NativeHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Flow.Launcher.QuickSwitch +{ + public static class NativeHelper + { + + + public const uint WINEVENT_OUTOFCONTEXT = 0; + public const uint EVENT_SYSTEM_FOREGROUND = 3; + + [DllImport("user32.dll")] + public static extern unsafe IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, + delegate* unmanaged lpfnWinEventProc, + uint idProcess, uint idThread, uint dwFlags); + + [DllImport("user32")] + public static extern IntPtr GetForegroundWindow(); + + public enum WmType : uint + { + WM_KEYDOWN = 0x100, + WM_KEYUP = 0x101, + WM_CHAR = 0x102, + WM_SETTEXT = 0x000C, + WM_GETTEXT = 0x000D, + WM_USER = 0x0400, + CBEM_GETEDITCONTROL = 0x407, + } + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, StringBuilder lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, ref nint lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, nint lParam); + + } +} diff --git a/Flow.Launcher/QuickSwitch/QuickSwitch.cs b/Flow.Launcher/QuickSwitch/QuickSwitch.cs new file mode 100644 index 00000000000..6e8303f7f58 --- /dev/null +++ b/Flow.Launcher/QuickSwitch/QuickSwitch.cs @@ -0,0 +1,122 @@ +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; +using Interop.UIAutomationClient; +using SHDocVw; +using Shell32; +using System; +using System.IO; +using System.Runtime.InteropServices; +using WindowsInput; +using WindowsInput.Native; +using static Flow.Launcher.QuickSwitch.NativeHelper; + +namespace Flow.Launcher.QuickSwitch +{ + public static class QuickSwitch + { + private static CUIAutomation8 _automation = new CUIAutomation8Class(); + + private static InternetExplorer lastExplorerView; + + private static InputSimulator _inputSimulator = new(); + + private static IntPtr hookId; + + public static unsafe void Initialize() + { + hookId = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, + EVENT_SYSTEM_FOREGROUND, + IntPtr.Zero, + &WindowSwitch, + 0, + 0, + WINEVENT_OUTOFCONTEXT); + + HotKeyMapper.SetHotkey(new HotkeyModel("Alt+G"), (_, _) => + { + NavigateDialogPath(_automation.ElementFromHandle(GetForegroundWindow())); + }); + } + + private static void NavigateDialogPath(IUIAutomationElement window) + { + if (window is not { CurrentClassName: "#32770" } dialog) + { + return; + } + object? document; + try + { + document = lastExplorerView?.Document; + } + catch (COMException e) + { + return; + } + if (document is not IShellFolderViewDual2 folder) + return; + + var path = folder.Folder.Items().Item().Path; + if (!Path.IsPathRooted(path)) + return; + + + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); + + var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( + _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), + _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_AccessKeyPropertyId, "d"))); + + if (address == null) + { + Log.Error("Cannot Get specific Control"); + return; + } + + var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId); + edit.SetValue(path); + + SendMessage(address.CurrentNativeWindowHandle, NativeHelper.WmType.WM_KEYDOWN, (nuint)VirtualKeyCode.RETURN, IntPtr.Zero); + + + } + + [UnmanagedCallersOnly] + private static void WindowSwitch(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + { + IUIAutomationElement window; + try + { + window = _automation.ElementFromHandle(hwnd); + } + catch + { + return; + } + + if (window is { CurrentClassName: "#32770" }) + { + NavigateDialogPath(window); + return; + } + + ShellWindows shellWindows = new ShellWindowsClass(); + + foreach (var shellWindow in shellWindows) + { + if (shellWindow is not InternetExplorer explorer) + { + continue; + } + + if (explorer.HWND != (int)hwnd) + { + continue; + } + lastExplorerView = explorer; + + } + } + } +} \ No newline at end of file From 34c2e3f3778cf7c1653bedd7bdbd344e469a54dc Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Wed, 9 Feb 2022 21:18:39 -0600 Subject: [PATCH 002/243] Add required dll --- Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll | Bin 0 -> 154112 bytes Flow.Launcher/QuickSwitch/Interop.Shell32.dll | Bin 0 -> 38912 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll create mode 100644 Flow.Launcher/QuickSwitch/Interop.Shell32.dll diff --git a/Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll b/Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll new file mode 100644 index 0000000000000000000000000000000000000000..94bb6d5b7b0306bd874859ad394c269db17ad6de GIT binary patch literal 154112 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 CwsZyn literal 0 HcmV?d00001 diff --git a/Flow.Launcher/QuickSwitch/Interop.Shell32.dll b/Flow.Launcher/QuickSwitch/Interop.Shell32.dll new file mode 100644 index 0000000000000000000000000000000000000000..8f6785279a69a4862660a72a8169ff6a5cd5a928 GIT binary patch literal 38912 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 literal 0 HcmV?d00001 From 6404f232db9f618d79ada67dfa649547ac047d7d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Apr 2025 21:46:56 +0800 Subject: [PATCH 003/243] Transit package --- Flow.Launcher/Flow.Launcher.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 6ef14f51a39..29f2a81b0b7 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -90,7 +90,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - From b13cb7564faccdc2f8edf70d79b9d1abcd1b4fe9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Apr 2025 21:53:01 +0800 Subject: [PATCH 004/243] Code quality --- Flow.Launcher/QuickSwitch/NativeHelper.cs | 2 -- Flow.Launcher/QuickSwitch/QuickSwitch.cs | 20 ++++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher/QuickSwitch/NativeHelper.cs b/Flow.Launcher/QuickSwitch/NativeHelper.cs index f0a1568887d..8fb19607088 100644 --- a/Flow.Launcher/QuickSwitch/NativeHelper.cs +++ b/Flow.Launcher/QuickSwitch/NativeHelper.cs @@ -6,8 +6,6 @@ namespace Flow.Launcher.QuickSwitch { public static class NativeHelper { - - public const uint WINEVENT_OUTOFCONTEXT = 0; public const uint EVENT_SYSTEM_FOREGROUND = 3; diff --git a/Flow.Launcher/QuickSwitch/QuickSwitch.cs b/Flow.Launcher/QuickSwitch/QuickSwitch.cs index 6e8303f7f58..b367e12b592 100644 --- a/Flow.Launcher/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher/QuickSwitch/QuickSwitch.cs @@ -1,12 +1,12 @@ -using Flow.Launcher.Helper; +using System; +using System.IO; +using System.Runtime.InteropServices; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Logger; using Interop.UIAutomationClient; using SHDocVw; using Shell32; -using System; -using System.IO; -using System.Runtime.InteropServices; using WindowsInput; using WindowsInput.Native; using static Flow.Launcher.QuickSwitch.NativeHelper; @@ -45,12 +45,12 @@ private static void NavigateDialogPath(IUIAutomationElement window) { return; } - object? document; + object document; try { document = lastExplorerView?.Document; } - catch (COMException e) + catch (COMException) { return; } @@ -61,7 +61,6 @@ private static void NavigateDialogPath(IUIAutomationElement window) if (!Path.IsPathRooted(path)) return; - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( @@ -77,9 +76,7 @@ private static void NavigateDialogPath(IUIAutomationElement window) var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId); edit.SetValue(path); - SendMessage(address.CurrentNativeWindowHandle, NativeHelper.WmType.WM_KEYDOWN, (nuint)VirtualKeyCode.RETURN, IntPtr.Zero); - - + SendMessage(address.CurrentNativeWindowHandle, WmType.WM_KEYDOWN, (nuint)VirtualKeyCode.RETURN, IntPtr.Zero); } [UnmanagedCallersOnly] @@ -115,8 +112,7 @@ private static void WindowSwitch(IntPtr hWinEventHook, uint eventType, IntPtr hw continue; } lastExplorerView = explorer; - } } } -} \ No newline at end of file +} From bd5142f366f07a4e0de6799b3ae6471f0cd20213 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 12 Apr 2025 23:01:11 +0800 Subject: [PATCH 005/243] Use PInvoke instead & Improve code quality --- .../Flow.Launcher.Infrastructure.csproj | 11 +- .../NativeMethods.txt | 8 +- .../QuickSwitch/QuickSwitch.cs | 197 ++++++++++++++++++ Flow.Launcher/App.xaml.cs | 4 +- Flow.Launcher/Flow.Launcher.csproj | 11 - Flow.Launcher/Helper/HotKeyMapper.cs | 2 + Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll | Bin 154112 -> 0 bytes Flow.Launcher/QuickSwitch/Interop.Shell32.dll | Bin 38912 -> 0 bytes Flow.Launcher/QuickSwitch/NativeHelper.cs | 44 ---- Flow.Launcher/QuickSwitch/QuickSwitch.cs | 118 ----------- 10 files changed, 217 insertions(+), 178 deletions(-) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs delete mode 100644 Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll delete mode 100644 Flow.Launcher/QuickSwitch/Interop.Shell32.dll delete mode 100644 Flow.Launcher/QuickSwitch/NativeHelper.cs delete mode 100644 Flow.Launcher/QuickSwitch/QuickSwitch.cs diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index f526923f5c7..6148c138def 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net7.0-windows @@ -67,6 +67,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -76,5 +77,13 @@ + + + QuickSwitch\Interop.SHDocVw.dll + + + QuickSwitch\Interop.Shell32.dll + + \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 18b20602213..b0a78eb8b73 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -53,4 +53,10 @@ INPUTLANGCHANGE_FORWARD LOCALE_TRANSIENT_KEYBOARD1 LOCALE_TRANSIENT_KEYBOARD2 LOCALE_TRANSIENT_KEYBOARD3 -LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file +LOCALE_TRANSIENT_KEYBOARD4 + +SetWinEventHook +SendMessage +EVENT_SYSTEM_FOREGROUND +WINEVENT_OUTOFCONTEXT +WM_KEYDOWN \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs new file mode 100644 index 00000000000..cb23a073197 --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -0,0 +1,197 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; +using Interop.UIAutomationClient; +using NHotkey; +using SHDocVw; +using Shell32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Accessibility; +using WindowsInput; +using WindowsInput.Native; + +namespace Flow.Launcher.Infrastructure.QuickSwitch +{ + public static class QuickSwitch + { + private static CUIAutomation8 _automation = new CUIAutomation8Class(); + + private static InternetExplorer lastExplorerView = null; + + private static readonly InputSimulator _inputSimulator = new(); + + private static UnhookWinEventSafeHandle _hookWinEventSafeHandle = null; + + public static void Initialize(Action> setHotkeyAction) + { + try + { + // Inspired from: https://github.com/citelao/dotnet_win32/blob/c830132d84eeed3a77e3a6e7f9ed6109258c7947/window_events/Program.cs + // Here we use an UnhookWinEventSafeHandle as return value so the result is IDisposable and + // can be cleaned up automatically for us. + _hookWinEventSafeHandle = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.EVENT_SYSTEM_FOREGROUND, + null, + WindowSwitch, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + if (_hookWinEventSafeHandle.IsInvalid) + { + Log.Error("Failed to set window event hook"); + return; + } + + setHotkeyAction(new HotkeyModel("Alt+G"), (_, _) => + { + NavigateDialogPath(_automation.ElementFromHandle(Win32Helper.GetForegroundWindow())); + }); + } + catch (System.Exception e) + { + Log.Exception(nameof(QuickSwitch), "Failed to initialize QuickSwitch", e); + } + } + + private static void NavigateDialogPath(IUIAutomationElement window) + { + if (window is not { CurrentClassName: "#32770" } dialog) return; + + object document; + try + { + document = lastExplorerView?.Document; + } + catch (COMException) + { + return; + } + + if (document is not IShellFolderViewDual2 folder) return; + + var path = folder.Folder.Items().Item().Path; + if (!Path.IsPathRooted(path)) return; + + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); + + var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( + _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), + _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_AccessKeyPropertyId, "d"))); + + if (address == null) + { + Log.Error("Cannot Get specific Control"); + return; + } + + var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId); + edit.SetValue(path); + + PInvoke.SendMessage( + new(address.CurrentNativeWindowHandle), + PInvoke.WM_KEYDOWN, + (nuint)VirtualKeyCode.RETURN, + IntPtr.Zero); + } + + private static void WindowSwitch( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + IUIAutomationElement window = null; + try + { + window = _automation.ElementFromHandle(hwnd); + } + catch + { + return; + } + + if (window is { CurrentClassName: "#32770" }) + { + NavigateDialogPath(window); + return; + } + + ShellWindowsClass shellWindows = null; + try + { + shellWindows = new ShellWindowsClass(); + + foreach (var shellWindow in shellWindows) + { + if (shellWindow is not InternetExplorer explorer) + { + continue; + } + + // Fix for CA2020: Wrap the conversion in a 'checked' statement + if (explorer.HWND != checked((int)hwnd)) + { + continue; + } + + // Release previous reference if exists + if (lastExplorerView != null) + { + Marshal.ReleaseComObject(lastExplorerView); + lastExplorerView = null; + } + + lastExplorerView = explorer; + } + } + catch (System.Exception e) + { + Log.Exception(nameof(QuickSwitch), "Failed to get shell windows", e); + } + finally + { + if (window != null) + { + Marshal.ReleaseComObject(window); + window = null; + } + if (shellWindows != null) + { + Marshal.ReleaseComObject(shellWindows); + shellWindows = null; + } + } + } + + public static void Dispose() + { + // Dispose handle + if (_hookWinEventSafeHandle != null) + { + _hookWinEventSafeHandle.Dispose(); + _hookWinEventSafeHandle = null; + } + + // Release ComObjects + if (lastExplorerView != null) + { + Marshal.ReleaseComObject(lastExplorerView); + lastExplorerView = null; + } + if (_automation != null) + { + Marshal.ReleaseComObject(_automation); + _automation = null; + } + } + } +} diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 0e4198e1b29..01b7a339a7a 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -184,9 +184,6 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); - // initialize quick switch - QuickSwitch.QuickSwitch.Initialize(); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); @@ -337,6 +334,7 @@ protected virtual void Dispose(bool disposing) // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); + Infrastructure.QuickSwitch.QuickSwitch.Dispose(); } Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 29f2a81b0b7..46ec27c1e13 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -12,7 +12,6 @@ false false en - true @@ -97,7 +96,6 @@ - @@ -116,15 +114,6 @@ - - - QuickSwitch\Interop.SHDocVw.dll - - - QuickSwitch\Interop.Shell32.dll - - - PreserveNewest diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 0771a60749a..cf285263c88 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -22,6 +22,8 @@ internal static void Initialize() SetHotkey(_settings.Hotkey, OnToggleHotkey); LoadCustomPluginHotkey(); + + Infrastructure.QuickSwitch.QuickSwitch.Initialize(SetHotkey); } internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) diff --git a/Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll b/Flow.Launcher/QuickSwitch/Interop.SHDocVw.dll deleted file mode 100644 index 94bb6d5b7b0306bd874859ad394c269db17ad6de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154112 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 CwsZyn diff --git a/Flow.Launcher/QuickSwitch/Interop.Shell32.dll b/Flow.Launcher/QuickSwitch/Interop.Shell32.dll deleted file mode 100644 index 8f6785279a69a4862660a72a8169ff6a5cd5a928..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38912 zcmeIb3wT^rx&OcRp4oHDrAaU0(iWz)lwL@i`^AFIWHM=DFDXrEMc^T6I&H_MnK(&W zNzzDz2g2919khA1vP^FX2cLin+1 zUpAIJHmzfQsz2J7Nw3RvZH#tz_4cL*qN|h9%wTUc)f;VUYmaVBuSw1;D2U9+cD+2I z)Dpv|q8sk-az-0f=CnK`PpO;G1Z?eV_mD=B5k#zul9;yL

gDrIQBtshfYSep1ddEK{+sVgi!)2-`QZ_BY^R)JCxnHEMWb=3mu z3Y&P9%-PP#9PFG}Xy;^nJ14zsKUm7L!?t*0Buc=lou}c#d#e)m!l~9xSbD5KHFA8G{$I*Cm7AD(0NsG6Lwc^ED2~!37n}A43o+!3TabN&%8RL!g zcp3$QdTj!)g$?Dd%~l!7u9luarJ2vjd_w1jb*B^2JT1O9uu7c0VS|!#L+ttk860if z<@4;49_t^bP@5qvJ&ArIQB*2VW8l0`lXuI5=@KdoR5M0?NFOQ_7hFLFJ1_R?d8Bbv zB+BlrF4Mg%SUI+nQz>?*9@;8_m?nTCfvCjH`2?XoqVtzw_B zpOGuLna=>~{@fn2lPbd!C4mg`|IaoqW)FW_oP`kytKYk@hcvHfk0 zhLrM=3%pHL^UCH`mQ|K>04nYYdcdYjm74x;?w3e^$3@HZ_JK^QcU`~K_yjjHav?eW z!gh5RdAWd{zUac%Cep9LU*gJXdSg#ou2JQKZAGgVe#wUgO8w>RN=1J%`dLT~f^XFd zD$POWl4m}u0-43d;0sh_J@h@RT(8JI!AFswAy**Mk3Ey0m)L1QUEqJaKcFu2Zv@}3 z@eYmqH9n;AX zsX#!@3P{b0faK!=>7hmA(!hmi*6MsZAY*OT`71PT4}65u&uILrF8u+>ay&RA=m9KFpGA-(Piz~vJz9uF3Ogbm||HbTXu>m zmc192pxr6D?Blwu1}5!3r`y%2DZ1>zY}r|wJvoN`G|T4dvfpJ{wI0Fni)A%xA;HUj zq1kyVVFm(sL_V)p4(*G43H*}A2Q)sW@oCVh^Nqq+sq=k}ztX72iDrVvnHpoDQ~%lG zT4UwVQH{UXXp9$4fyUD`&eT}0ae>B!#uXYb)7Y=^Dvj4`yhY>f8uw}Zn#TXs_+yPP zYkW;(-UJzUvc}mO&)2wIW0%Grjhi)IukjX*AJurb#;ILtg zlmVZdv z*n!~eoU$jzuor^!bISgl%d9cBSYvFl#@J$wvBer=i#5g;Ym6<{7+b6{wpcl~DDhc! z*3NCJSw%wQ9IOA%vI&2q-J2>ada$54w7}^Fc6w-`J|F89E0_8jvU`=%ii5t=R*Uub}L9SA-^HzmJug*N$N~k70Ypu+N8< z=d}Aq=)xTKWawgjp=6KnEVjs3J(^k1hb~j{<(AEU9g46AW?4a3_+ovnW4DvJvl;Rc z%Vh5I9h=>5L-<{aZ=$knQ+TuD3nrUspK;BZWzvqzp)3=ho#HU-hVVGf#ZH-w?lBqN zW75lMPEBh=_)2xU%Vc!>Vo~h8F}#3l0H;ssMXt8IImq>u&29@{qoNM8J{`VJO>^13 z;rHvSH+!}t;hWVAx9q#&4=cVw<->T^PhhiL_Cok$YL3ISEu}6KTjse;W;EYrVoRCB zv@PW>6IWnzn5VS2XIxlC-~F2*h^m=M{g&Ue|A$i@1q)NVH~GR|WKZ;4Fs zn03*ZGV!il&FXe~r5CzPX4K#^nNiGPdPa>dlNrTbCf-fBOlGvmW#Zjdm&uG4J50}L znagBGZ4R?!MlRDca+#iy%L-m#wjT569mrv_V#}RA^$~D^%OsLlxJ-`zc9+S~-{CSj z`Y&{u9Q`X@CP)7&m&wuZF**7@CP%-=Z~ z6Thr=nfS$H;unvJUpyv$@tF9=TQ)Y-?QZ5s}ywhdk7rB45 zj{w=TdR-=d@tF7}?Ud;~tIuV!XJuR_Bj_7Xz0*pcgHD+~#ylo{ZgR?WpPOAKeQt4? z^eOj`+D_|=yl<*29aeB_-ev0DE|XPxkIU}JTdam$_UXI@>S~uApzIo#Jxtj)m;IQs z?JoNrW!JjQ%wMeD>$0N!i`8{5o1Fi64x3Ba^={b$%3Nk$1l!@1S?lw^r*3fB<@rBS z?{nD)@*`@e!}PiM-&`j1yU}Gbznfep^ZS6yWPUfhOy+ls%Vd5ZbeYWWLoSo~?Q)sS z&to#bTir65pUd?8KJ1j~`Q7F+ncu&=Oy+mH!wSBizfXT-;2o(SbIQW9wjPtY?{>>% z?!zvVx!>V3nfu3GCUf88GMW1)TqbkB(`7REPr6Lz?lGDBr`$4`yUPq&+r3ViA#1zO zWit0qyG-VOm&3v`_q!dYkCl5|CdbNWU3N788g;M3tl#Bdr}jInz%1COKIbrNe8H&t zyvybkJg>g!u!6>dh3YFVTUv0jI^Z%n`aLG2f7LCM(ZA*}J^I%jrbqt|hw0JpcbSa- zfW!3Y-*TCZ{-DdQEO;@8-Bj?h$EuKJD+rGK2@+l{g=z0FIcR;?=n^RqWXc$iV9!O zVY3Sts2{pz^@WSok6iYS!pGH9F3S{Nte$q5^&V}AI?t9_hd{LW>o#|4c)xa@{;3)L$wyLnth zz3Q@CVSjbm?s4PPYcBiD81~gM?BE#ooiXgWG3?kFW)|g+5E;Y9k71{eVYA1ud1F}J z7?v2rE+|^;%}9Ijb!UG1>ev42MDBXTgYqq!eRUxo^tntt7;>3-ke~gtHN}Gwmx%}S zTqYj$n0U}*;z5sz2R$Yp^q6?iW8y)Ni3dF<9`u-a&|~63kBJ98CLZ*dc+g|wL65z& zsL057=eMJ%#3*#xT}5Xa6I^zv=ze8?y)WKTjCVNsWr zQ}Zl`>GeC?WpV_}beLYhb6h6tH``^he)C)=>o?zJvVJ@em0e9)zjBw!`gu&&&ttNF z9+UO+n5>`2Wc@rQ>*q09Kaa`!c}&*NW3ql8llAkMte?kZ{X8b?=P_Bo3U_|8epN1$ z^{aQ8tlxPqll6P6%VhmxE|c|Za+$1Ov&&@tT3oig_*`SL!}Mw{b(yT@GKcBaY;&2c z<^?X3)m-T^S?OI#+a+2t}>%{4BQ)m-Z`S{?J|jiYg{H#u+3!>1>0RFQQ$F&0*^@)cubfyX2Yu65@pQE;8hBnsZ|GKqp4T_#cR0hdV>e9&bQ1-H6P zqTn`{NfdmE|Zuz;4+DcuenTO;(nJ&Onk#- z5)@taoM_eW`;W3E`k4a2C=GK&$_)nL8JBQi#GH#i^m+_dy%i~Tv zL$2$-?J|j%@3>6j<@+v^c=>_LBwl{#GKrTTxlH24V-hbOlX&r%#EZuyUOXo8;xUOA zk4e0EOyb335-%Q;c=4FTi^n8hJSOqtF^LzCNxVGe&QIdy8J9`C{KRDvFF$jc#LM$8 zlX&^1%OqZY?J|j%7hNXt@>`cly!_5(5-)#nnZ%37Bwjow@$x6Pro_viT_*A3Fgsox z_Mr9i30~QQ){7I)Gu*O*+KCI)U!7hGnkFt(ueod)?DZ_O`?P-;0>#r=1>sqRV9T5{K#0Pj#7$KG|j6 zlRAy5E)!eMbeY)VF|oyE+7^$AEm5bPwq=^j#Fps})3%)LGO=Zr%X%jzjXCD>sH|DD zl3&&5npb9-{Hi|J+?!?ctNL8?ds!yGs?RmmG@8*)epR1qo~xPti^E)VyJm(eE|Fe7 ze3o5PzGs+gmQA;re9th~Y?@&+`JQ2}`E-`a_Y8B*m(I4!s>=JhjeCf_s6HE%!1 zX7WA5T(fGH&E$KAxn|33o5}YKbIs8#lkXYkn!lSPWfIpB*p#_;%@S?rPqXYy%?xhP zUJKZ$qEhdTQ0laYPArvYqn;R}FQ(?7#%SzTqVEfz*uxuX{@(gx;eoJ?#RSg7I{$R| z#JoMC)OO6y4+%U%STyOmCy!0@WuSyKHSYubdi67cS(&~CNrg~q#jJ_-1TS3Rq z|95--f75@iS99|+m#GCZ-?IulOX>#po9{<_{s~4j)C2!b&;LlD z()MLNO0>|MS8Jh+yRJ}5Z^-IToQqfg&3Wd;8U3G)F0C#dmmM8DGvg$}R0b^N_9=UzToPj6KC;{^d@aL7s=>XfgA-z_KqR(aH+QBBWNu3NzhO;LQ?u=@;>!I z))HwoBYVD({88Q8?{i8|rtv&~G^b`mu~_o9;(vuXimxsy7Jsh!7oD$o;A6#dOnv!( zJ@CIC_+JmO`*8-CDCg+niL%F@3HsISi6`F=-+ay6vpq=v-6i-I>8)4<2+Me^m~0TlypG%f&dH7?TnE^x7WmquyzQLW!&gvpOn z4}klOeQJHlXN~<@^Mtyx$T<&l@3p`fD&>9RrKhAHj+0Rq#~)$&{S% zMKeQdX7SBQ2v6jJWvU3QQj@?sbvoFfduY-%TXfqcd}|Qm`NtVxhnfw(UCje8(fxF) zO7d$|9k^bd5B8`=uum-l2hArT=7MhKI zs})D{j!>LZ;sH&I)s1Fns9S4Ny6rYJ_l36k^opY?4eZf%_V^a4=R*5*KL^qH!UuJ$ z!@8d%I)4OxY51t_$LE((eEvMODjfDpKhymZQPa`f8J zt!Q2ebffugI7Pl7xJ~!A4b9}p4m4Y>JzBE|&DO|1G%eOatvQJ1JCVa^W&}pTL%t(u zUX2{lrAN_}29zcBeO8`&Tb|DnJHu#>1)?CUjQ*;;=~_P<%`1ThT|bV-@W(B&IZmB> z^IFjt1Utd4R<~}|Z7ong%1fc?4-RSlHd=86=~mmQb1ZKM`a`~9aJ#jK`cv}vk#Dg^ zz@64X>NMsbCco7h1$S9T(DdXV(e@lgvz>pvD942_kf-+N`vPK*FCe3Y(eJdP;9hHb zV1arne>!z~@@J#jWi^2NtvEGfAU?^D2in!lf>!jqtxoWO)s6nG1>L$%w=PYg-)jwl z2d!6nxS;sx>M&Sv_lo z!6&UK_`Ee6JmhQ8*b1^!X-t7zts!tmU0e4x4L1Go$ zZ5;(!--uWq26tIeaKANMYZ}0?pg(vR+-r@3 z1;L}>0ZZjiRwcnO$T0=te-QtGEmo_>PH;vbrEv%>4eS8vU*kS-w>1Lpv<`#x5AL## zf`@#nKuW`4ixma;TC>3!fd+8D)e3I6I>BRs6u8?O0$&O20QXwMpg*_|JQf%M3xbD1 z))y=Z9tBz7LOFiJAnU91v%&3F19-^S3Ua)F_#fox0JmB@z+-`7o!OE?PRe-QtG_#Y(xLHrNmfAD~{1H?bzL2Dm~e{}vZh=0IGt)t+v zfGU#c4TH_0DEO>38@xK)06uTEf;WUaLHq;W5*`Ba4|vG84`eUW`NJAVwdSZsRV*_N zgNJ-kou3W1SPkHIt5xee!JSr0>xaNy)()*726tQgw0;EKYaQ15QED}oZv}VdcY=>vDR6iG5cs6E1KgWG3_fe^19#<*fX`cp!QJ_z;IY6_aBsev zATtevyYizT*HPf^{05N7)cQ`4$kh5FkjT{fVX!8!PwPj(V}VhSXq+f*qaYComIgY( zn!pf9Jb*+3$bJq|f0CXZNPUf+;C5?B<1omPrEwJGXe|+pf(NVyjh*23&=AOe1+r6t z&sw7({hy-y2kBp9CrJMqhe7(+I11ALsk(oV{xx=j^bgWMNdF-HgB;PPN&gKRJ3-=8 z<1omcqj416WvSBzqu^evL1QO)z#7sx3~mpNfR9?E;FFe`ETcq0_9Bp72yC&2z!`yI zurx3NZns83t|6!B{=wZ=1Gv}f1lbS47Hb%s5f}kW1EV1O>QreJ1vwjn9K|5VFUU%1 z90C2oQ4mj_p;s2flOQV#;z@9)H4NfOaJMxI;>k0$e>66L{$MBA92(L%3|<``(KrgS z4@3o{;4R?>jh*1_;bD-o1K47Xf-?eYnv5O=*@eKJRwua28UlA)!{AJDi70Zi)>}DY6 zT#(%iEDa2U>}KFjYZPQRtCrbCL3T56ztst{n}G+dVUXPnWH$rZ&1z(JQSf=I0Zc?X z!DW#la8+a&?2L?nsmLgpiKtqcT@>Vw17!CA?~DwACBb3vp2!GzUt|;UP z2WRGWg7fo+z`DF)FqSt04&;r3>}K^c$0#@>&;YWVf$U}=yBWxC2C|!h>}GF~IYvRw z3*bSk6Xd)Aa$W#AFMymEz~?QsK;{?)mqi-DRgq4xGcp9GBEw)NG6J$kfv*JAc{0Z+ zcxR*mED3gk_e6%k`y#{ONMr;&92o_hL+X5)V-%d9*8tY#b%L?HA#fmX82nV;D9B9T zs%Hu^Q;?a0%oJp%ATtG-DacG0>Y0Mf6lA6#GXsth-w9S141wzlhQY1Y2v`st1^a_)k+h9!Yyh!b z;}D4bV0FO=xW2%rzG+%WP(5gd)kA#q@lCS;83!*$CLojGry!HbPgM__XHst(d^&PA z{2cgf^{6=)UW&}8UO8zcQcZa+<@J>Fy^?u8x`jv#eH=+3i_o_si>bF1X+thR+K~&< zuOj7Nv6+|B?lPo{Hr+@PS%*G_Y@p6YB#pca=|=|9ZzkPJy{pK-hqhNE+u+wC*C9L5 zzYp0-y&I7aAh#eNLT*KW8|g=>cL(kEAa|ns6tWNb4E%0nKl#s7=Zn<65BUo6RpjgF zd7{&N069qhTgV~gF#HkZaq>^l?z_}|68S#zL*yy+&rtqj~f`EDeMtfOoL zbvBYulYbY|Pn|(zGqMH!R^%$=J(O*u?zQBvBfkTAA9X%}+=6@v{jJDt$VVvKP2D@l z?;(FD@+s=vjqbC^=aDZWUqQZ#{_Dv7)O`RshGq`sb0O)O~?^zb5@H`rniPBle#CEu5oaDBRxX;Gt%dgqvT&8{Wa-tvEgOp_t^DEbbluOD|*BCkm>WWmy!mNu#aO6DL}>{ z#pos=laN!8)6h*urXpv;r;$!4J)86#(%Hyd@};Enk#h2tq}8Ogr1hi=NY6(Wl8=$b zNf%*5E3(-4u(=dn8)-ZGRpc*1e=+H$$YtodkZvT2tV5SVHXs|3G`e>o{m3AEGwBx6 zt)y3xz6ZIQ{5H~Sk?Y9sAblU{PSP7mKR|j5@*(oKlHNx8QEb?a+<{$t(A`P85B=Tb zKZ|}p>F1FzqPq|I3i4It>*(%B9zYHv-$Hi?IgC64e~k2T(kDp2OZp`8eeypfeF}Mo z{EtbWBRxX;Gt%ctk0LLS|2650q%R|XB>!jR74m;2eH}6U?4y3x773HjBP~G2kuOFj zAd}#yAg7U^hD=A!M$SQIBXcP$Mdp*QMe30S$oa@ZB!iC2+K~&9Exs## z*ZOw){L0{cL)^BiJRTGE2Tuhj1*d>#1f$@b;0*Bm;4JX}yfmLt3Y3puVvBhD&jhf9 zUq4Ud7ul)&dN_k$WY0xY&u@s$Xj^&L&Qdj5wew5tJ9wK;7r(@=E;&8j@9pF7q+KI{Lxy`AWv6E)9= zrPbX!|CPeOZ>%@dL$2ljNdA(EVr_Dw_+*2|6E!O)Nu760y4c|Tq$kZ6@!m3%XAHui z$?@$YpU2Z2MLcofM>C7vs)&6gfW81UIrf9($00@h7eX_K)SAZ!Ucp@+1Np zdHY!ryG}keOX)-QqC)hOK$G`nNl&MsEn+t+LNlK}LVB+%VqY4Mri?y9>`xOZp8}dZ z;W3H)WTOQA>7dCDBz;Wft(hS;1vJ@@PD691aXOkaK$G2RGFDZCMQWNc1$`7Wc{XG! z`Lm2O&`$?VRm%=&@@}nZ-~u%rJdZt)r*QC)$&(bb!I+v0#+j4Jb1w721a_IKg^^A6 z)N1f;^kk~V?6fBDl3D<^^IVL{(-sTC3+cn;X^S{`DXmT3xwHuEqP59el@^2D?94oy z$UCb{wSi|?OrFAM2YYF4@NCK|@_iaJ>LT*}>SFYR^k?#nhTtZ8HPvQ%HF;j68{EQs zm`vUsv<}?Ldyq_?;Mf3Or8d&`-Jr?)g3{m+Bbe;{{or-z#mP3l_k1GF|cS6oN_7FwG;5Ar_nHd>gRH*N$!LM@ZG*W3aQ zb51eU9lSfollO`|1|Fd&ledaI0sf3WO!d5a68t6aPBD4f<%i&} z=)>UMA5W2gQ9T2`#QRH3-sABc_%eN%{6&bLfq$em|8j%3jTq|Bps8M=51vBP_!@nf zybt5oXq53H`Pb=>Cq<3lk~cM4#>?ab#_z$P@kcPsQ%t7HH(mh?jK6|~#_O~ir*XVt z7(7>}af0D93^frn)g;3rU!w68BZ%fyjWc)}%2a0?dEf%006fna2VQIxgO~7z1)|rO z1YTvF0^VVq27a9PDX>PyRPZ6=Oz=fx8u*ei9XyS@WRqv2&H*QLr_A48;a=HPQL_}B zX3hs^n&sGYw#IX~i)JU(IG?*|gQujb(UhCDBywK-ou4_R;ZN zJ;@Vtv%nJeOFw&^JTrGX`=7koy9_*oJ_-iQ)hny^UYDP(EJHlWd0N!Z~h#d zX#N5$F@Fi3YW@m5-TVzW#e4}o!#oB?&EJ7%nSTHy=Cj~T^Dp2m^Hp$;`5JgGfjW)# zGQl#_4_24~u*#gNrl}fp7FcJ_0pDVt3!Z1r1K(D3*7tAVuY9+Izwvzxe91Qq9`k)1{GIO;;2(US z1pnmQ3;xCTY4BCwUEpiJdqCyC7c~8!1O5IlfC2xPz>xpTV8nj_%=do{EcE{eSmYl8 z$NRqlPV|2hEb%`Gp6Y)HJl+2=IK}@cc!vK!!KnY+;936ffHVBx184gG3!LTu0XWD1 zBVU<%i+`TKOr7U11K;Ye02};OV57gr?;m1Q;OfHr{8x$nrgKMQs1|=Vf8Nscn!%pr zTUBdwx@S!?voe+3+!j~u>yte_l@+Q)051+QJX~X;KrtOZ}QF5U75_RZtdNa-tgwSZc480 z8tfVPJ6km-*QI)!GF|J&Sah84aBEX@Jl)$rkm*YG4k+7ytpmx8IawLF|2UpQouaI& zLba`3+mq@|+I_VTbPWtTRVs6uRphRijA*Y{j)BLS(O;3%y1!Dj>Z(gpy&IYayLyhJ z+1{nJZS^}@&79snWwwzQH90+vRaTs|YGv*mBusNg@!B6>sjW|9t3TUgQ>wqOYoL4m zanu>mDR$=T@T%kLS0Be~>``+JvK3DI)?WUQU3X6L*aEw9dtb6U)zxE9MTbe39BQr2 zsh(tevMbZQzLBFsjiq*NsH7)7>f#WE1U6$Ous!2!c8y>RltYu5G zr|)kZgR&DW&!jh{*frF~{_b?9C$(DH0ifEq^h;bX8ceN;4e;k3Ru2v&v&wlZ273om z8{lII`jSght0nQ;h^Ja}`p}Kmc6BGmR*R=Mu1xi(R`=vI zl^TiyJyW-x*Y#44^Hg_N|3DL4(7G-RjHk6fy(*fXq$$0&rw23LNw=)uYt%$n9ZXPV z_mZW_jjNLxuiW;{ycNl{*_}_d%7&KAbYZ^ZqsMp# z)Dv$N&!ziypRI&buf5AIPp5hZpaX0{tea!Y$(Ljv^OVl|RVOV&m_Cn@RvPsMApGeyE6Uj**_M=RpRpQq&_xPd$M~llN#8vg7Z+0 z8?(JQrDLkKCoyDWUph08T+^JfjU;qDAB)u}*iHL6{5?GlbM}h9HTZ~*$DV<-kLC2D za?S$IV&hf>ZP)C9#h#$E|J zBqh?ovpCzMr?AhFYF%>RvSnQauVhws^$aGJrgm18vA(_@4jb84b*1H98Fm8I?=+y9!i_PYhG{bTfcN3-`3>8rKBF_YeG2eWW4T2gDb$bqKE z&7pm326Wf$sVnR`=z~EV$8n@D10=6DY0RBUb{g7h$70f~Rar}2E6K>VY+IOXU5+WP z3Gp|+m^0P$&PgU`t)YbIC4sE?8;<_jWz6jss4?r{{G~e6H0X7WBdIZ{cMsD zyzDa7*4%zs>#|t9qjhCMleT3`T9;+B&CPDUJ+WeCVnt&tzF604YF*KBQ9KrJN!U?w zGE*gTPdbOb)hAV-RO!W5;&QbqmSIy)Tt1Lwxz})#7GqVm7U$9q}np3PY!b4QnHbaZ7%R5lS!_DHd}=;RtX zJnXHiIo&--sOsazF4ft-O>0wX&1QRjmZ$p$`$WAo**mEEDA~k9Z?WB?SH)g6xqr%D zbB^iOuA*NOE?A`2mU7uU(*|s9t!w!tAQf}_}X43tlOnR`d(Oyd@ z&;C4+afs&@dzP}Owwr9;=S*KJwsXF<_YFI2a=Gp`S0O#git1gkPcN>!A-a0|Adva# zgTtmB^5={l19OeZ?cVL8y?N!zw#(XE+E&GuENN?eTO!`kt~_Pp?H!3_?X7Lga+UG6 zrOV@OOWIcCRBXSfy}5Ns!l|~ZbyW5PUQM9ig9AKbcr+)ijKO?SD3Au4w&RmlN`r@^N3%=~mmErjQ(U!+}h?XL(m|vPU085^ds#)_%4G?gYBIWV2V7cD6#U zB=r4d;&LtpIj74QiW3@_#QHL0lN{T6%;o&0I2n>ZP;D1&br2DWi%kXNiT(-u$dUfOW0rxHiJJXx(CCzZM z(@`7m=})au`dp!6Yu0q6S0uZo?@e8LL$~X2sj^OXtJ;qAx^+ECcDfA=;;egS zd74YC>wcRqy^y1Pve*)-(;QpbwxYFzLjuy?5nIu5;d05YNW>G%ISp_F6|;6%)k*|y>$ty{raTz9)H(Xpy+#bQyCZE0(3 zl58{9>Ee#W(&bBHSra&FmvS8Ec5bW2PRy-jTWeSGtdRa=%a=FBI%1O5lXu2l(iV>` z$tjF2>ByP4-P6f*V?3pu$z<%S4u*)gwJmP7r`yun?)V_vn`a3-PE*?|+u0YjGjrQK zy=L~#L|;oUTHYG(xNwEt9TS%lsmS(4lgsOBj&CI@f<>Q;Bx6SiSA1OLb0qaEd*{}1 z!=G8?vT~C|a6dQ4lo7-2-CVxt+y#Tl%ocrY>a&5J>_@Cw*Gx_TPB zx;Mxn-PN;9_Ir-CZVu#rdWd(~S2)@*he+2>UMih%Q6*Pc`Y7q!5ZFO$2KB=;gYNOe(uRL1F^w@_3RLG7nRi8Lpya^doRrNsEsGwDY`e$8{PZG9F<%p zX20Id(Vlqwms9B8^X2H)aN(BTkVwu-*s^B&#F2Ox-!%4fl92o7*Z}9hF1spMJjt&9 zB%7S`Wz%3^A15Gg$+BkaZ)MVb^YlrDZ?UuKyfvIg`S_yx%kM8#+-%%7`rW4M&zO15 zAA(1(R-yZ@xMbyNRiiunQNx%*Edwzv$oNw=PSF_EI78zsD{2~3PNQBSGQ}4n<#%rB zpwJ&x#nn=)xSFyMbbK9!rk@|l#&(t1<&2U!Zu23}*sUbc;}-l+}q+h6oke{e%MaQ#2(MjQR>0OXe4rI$=x+`vb*8 z@0UTFri7yY@lBz^kc=2AEDS|0Y~T}!`i#Ot>1#^XCT7T+4y5UqBT>ImRA?4WDWV=m zOTw>*CfZQm{8X$W6)bMmxKwbe z6_)7~cNLAll!qUTDMh9XJf#Rf8=)zF#R`(cV+<%Xghg~QbhTI%3ie1OfskgTWK$#ZVBtf?q3uZYDeN)wHVs%*>Z=0;jpl*O78 zwH5UhO}Zt&2TKPM26XTciOw8$GV<7!);!{59@Uu~^UD8Q^CT!3M54qJE7b}^o#|{& zZ}3xF>EAB5e_Z2aV}B3NUpqC_8BPs$#xwgfo!B6svket>s=mSK)RGxi#VVVc z%c{#tD=HhCaAQ+NX?&i;w*#$_H)>YC` zRaI4Wb!Bx`8K3N$iAngLgN?RiEGNV9ZEJ!~hF#Rwgq;izOWB${CsW{Lc$&#B8s}vA z?H^5;vGf&#o?>}3c2&g7N~_Bn%j`(0uPKW&#k#V1ZF5y3QCF?a;&*GiJD-zzm94~{ zZCPC+UYRKSZ#of=)>c)OSJWr8U#exellnz!Ha5l^n-VqDtZZ(6!#FMO>9)GEs#?}kR#U8w)y5Ll%{5h}@v5fUQly!PDU)0UuB@(V ztgEsOkR#4+l4y$8mDk2%rS%n!a+Eifmo}EwR%dOFS60SitblZ0(VSqd60y?ergCwK z%&xq-DqFKIk!Y+;D4rx!Q=IV5ITGaJ!v5Jz_Z6?NYA%a0rRK_-O1rOEWks#m7mJ^@ zFBYqfSC-Y6HZ~^WrB(G!)unY6i)|A!P*4HLtRhV{yL%bhna~zVWjg`lmIbvm6Vkj$j;(=h>7cZ}>ZLBH9e~D7| zhwq}x(;StH~tYTI1 zn!0+1u566e+pAsQM2u$-+47q5nhNb2pYqwASWYJ3WP(m6q%%4+YHQ0`z8Fzk9TV5b zOF8tKN*kLh>)4zbA(lO46U|L^RgD!*rB#irc~wPyU1?*Yq8cw(5Pn9$Kg;}*H~X#6K}2{mJ)TPb+t9MrOmbVRh*1#tJqSry#?q*npD)) zBx;(=8%t};%ZZZ8>gv+^>KaZpHI222`lgzycy(2_Nif@_yt1~ezJ|bOQN(E+RVP`L zQw%kJ%ubLK09-xP$LiQ#n`#@&?H*2Yg33Kr>Yn7zNS-9f{b}FcPhyqji5M1D)v>AQ z!{;RZ>FG8}e8tL3>p5EN1#e8$aWv`Bi`7*&#q2FoE=%>i=1xquFNmSemG)NO0~dudk@IcLpvj_;A_82}35%rDJWfesx)CZCP?H z-dj^%y1KTy3-5K6b=Q|w*Ho=dDxR%V!R*8}F?26B{6HRE&whR*`KZIjrlW2>?t(K- zJv~eL?c9FiA(?dUDb?#|t7ya6hX2yczRcxDXK1V&c)$?_Th0l3sn>$MeMG5K?U)x8 zd5(Jia&J4&ORwZS*ZnW@N8zM*XO-=Ap>)DWjvo7JLsJb+ zAb+P`b@4Z0n|b28hbORGd4_v!_V35e(5*Ts?Lym6c^6M_rzq{!?cbyB^yfGVn zh7x`hKS@s=x=tDMuBGoXo+Vu^KHv#cleTgC+sJ=OTJuXf`Y19Zr>swpzlGU$>3S6F zI_K&-j^$1C(9e_PVn^Q?JEgxePN9Zt+e+$YsO`byNPY3ffUeui*gZKG zcqM5@n}-M1Q?G~rDtTgd9?!;0Uh!vRt?+diI97c1A%W?;-*yn43R*U)Yw z?J_(GFP^dIy$uOcnngpr8N5 zlGTi}1>dL0_fb2ey|V_*THSghxdCeQQ9DZA6fGorGP>0{$9d@-{3M?$y*6XVKYoRd cUmxA@p9zN9V#OZkfcs~%?SF0gANRoj2ToW-%K!iX diff --git a/Flow.Launcher/QuickSwitch/NativeHelper.cs b/Flow.Launcher/QuickSwitch/NativeHelper.cs deleted file mode 100644 index 8fb19607088..00000000000 --- a/Flow.Launcher/QuickSwitch/NativeHelper.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace Flow.Launcher.QuickSwitch -{ - public static class NativeHelper - { - public const uint WINEVENT_OUTOFCONTEXT = 0; - public const uint EVENT_SYSTEM_FOREGROUND = 3; - - [DllImport("user32.dll")] - public static extern unsafe IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, - delegate* unmanaged lpfnWinEventProc, - uint idProcess, uint idThread, uint dwFlags); - - [DllImport("user32")] - public static extern IntPtr GetForegroundWindow(); - - public enum WmType : uint - { - WM_KEYDOWN = 0x100, - WM_KEYUP = 0x101, - WM_CHAR = 0x102, - WM_SETTEXT = 0x000C, - WM_GETTEXT = 0x000D, - WM_USER = 0x0400, - CBEM_GETEDITCONTROL = 0x407, - } - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, StringBuilder lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, ref nint lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, WmType Msg, nuint wParam, nint lParam); - - } -} diff --git a/Flow.Launcher/QuickSwitch/QuickSwitch.cs b/Flow.Launcher/QuickSwitch/QuickSwitch.cs deleted file mode 100644 index b367e12b592..00000000000 --- a/Flow.Launcher/QuickSwitch/QuickSwitch.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.Logger; -using Interop.UIAutomationClient; -using SHDocVw; -using Shell32; -using WindowsInput; -using WindowsInput.Native; -using static Flow.Launcher.QuickSwitch.NativeHelper; - -namespace Flow.Launcher.QuickSwitch -{ - public static class QuickSwitch - { - private static CUIAutomation8 _automation = new CUIAutomation8Class(); - - private static InternetExplorer lastExplorerView; - - private static InputSimulator _inputSimulator = new(); - - private static IntPtr hookId; - - public static unsafe void Initialize() - { - hookId = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, - EVENT_SYSTEM_FOREGROUND, - IntPtr.Zero, - &WindowSwitch, - 0, - 0, - WINEVENT_OUTOFCONTEXT); - - HotKeyMapper.SetHotkey(new HotkeyModel("Alt+G"), (_, _) => - { - NavigateDialogPath(_automation.ElementFromHandle(GetForegroundWindow())); - }); - } - - private static void NavigateDialogPath(IUIAutomationElement window) - { - if (window is not { CurrentClassName: "#32770" } dialog) - { - return; - } - object document; - try - { - document = lastExplorerView?.Document; - } - catch (COMException) - { - return; - } - if (document is not IShellFolderViewDual2 folder) - return; - - var path = folder.Folder.Items().Item().Path; - if (!Path.IsPathRooted(path)) - return; - - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); - - var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( - _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), - _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_AccessKeyPropertyId, "d"))); - - if (address == null) - { - Log.Error("Cannot Get specific Control"); - return; - } - - var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId); - edit.SetValue(path); - - SendMessage(address.CurrentNativeWindowHandle, WmType.WM_KEYDOWN, (nuint)VirtualKeyCode.RETURN, IntPtr.Zero); - } - - [UnmanagedCallersOnly] - private static void WindowSwitch(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) - { - IUIAutomationElement window; - try - { - window = _automation.ElementFromHandle(hwnd); - } - catch - { - return; - } - - if (window is { CurrentClassName: "#32770" }) - { - NavigateDialogPath(window); - return; - } - - ShellWindows shellWindows = new ShellWindowsClass(); - - foreach (var shellWindow in shellWindows) - { - if (shellWindow is not InternetExplorer explorer) - { - continue; - } - - if (explorer.HWND != (int)hwnd) - { - continue; - } - lastExplorerView = explorer; - } - } - } -} From 60412c2df5c4ec1250dc368e900b436cec59cc37 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 10:21:03 +0800 Subject: [PATCH 006/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 19 +++++++-------- .../UserSettings/Settings.cs | 1 + Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/Helper/HotKeyMapper.cs | 23 +++++++++++++------ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index cb23a073197..5404e7b215f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -25,13 +25,10 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _hookWinEventSafeHandle = null; - public static void Initialize(Action> setHotkeyAction) + public static bool Initialize() { try { - // Inspired from: https://github.com/citelao/dotnet_win32/blob/c830132d84eeed3a77e3a6e7f9ed6109258c7947/window_events/Program.cs - // Here we use an UnhookWinEventSafeHandle as return value so the result is IDisposable and - // can be cleaned up automatically for us. _hookWinEventSafeHandle = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, @@ -44,18 +41,22 @@ public static void Initialize(Action> if (_hookWinEventSafeHandle.IsInvalid) { Log.Error("Failed to set window event hook"); - return; + return false; } - setHotkeyAction(new HotkeyModel("Alt+G"), (_, _) => - { - NavigateDialogPath(_automation.ElementFromHandle(Win32Helper.GetForegroundWindow())); - }); + return true; } catch (System.Exception e) { Log.Exception(nameof(QuickSwitch), "Failed to initialize QuickSwitch", e); } + + return false; + } + + public static void OnToggleHotkey(object sender, HotkeyEventArgs args) + { + NavigateDialogPath(_automation.ElementFromHandle(Win32Helper.GetForegroundWindow())); } private static void NavigateDialogPath(IUIAutomationElement window) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index e304a1b5040..46c49479420 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -53,6 +53,7 @@ public void Save() public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; + public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; public string Language { diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 01b7a339a7a..94327e6664f 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -334,7 +334,7 @@ protected virtual void Dispose(bool disposing) // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); - Infrastructure.QuickSwitch.QuickSwitch.Dispose(); + HotKeyMapper.Dispose(); } Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index cf285263c88..225cd0f2633 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -1,12 +1,13 @@ -using Flow.Launcher.Infrastructure.Hotkey; +using System; +using ChefKeys; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; -using System; +using Flow.Launcher.ViewModel; using NHotkey; using NHotkey.Wpf; -using Flow.Launcher.ViewModel; -using ChefKeys; -using Flow.Launcher.Infrastructure.Logger; -using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Helper; @@ -23,7 +24,15 @@ internal static void Initialize() SetHotkey(_settings.Hotkey, OnToggleHotkey); LoadCustomPluginHotkey(); - Infrastructure.QuickSwitch.QuickSwitch.Initialize(SetHotkey); + if (QuickSwitch.Initialize()) + { + SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); + } + } + + internal static void Dispose() + { + QuickSwitch.Dispose(); } internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) From c5bbb64dc3501183946541e603901df98178d469 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 10:48:49 +0800 Subject: [PATCH 007/243] Debug codes --- .../QuickSwitch/QuickSwitch.cs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 5404e7b215f..ed7697e03ad 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,7 +1,7 @@ using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Logger; using Interop.UIAutomationClient; using NHotkey; @@ -61,7 +61,10 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) private static void NavigateDialogPath(IUIAutomationElement window) { - if (window is not { CurrentClassName: "#32770" } dialog) return; + if (window is not { CurrentClassName: "#32770" } dialog) + { + return; + } object document; try @@ -73,12 +76,26 @@ private static void NavigateDialogPath(IUIAutomationElement window) return; } - if (document is not IShellFolderViewDual2 folder) return; + if (document is not IShellFolderViewDual2 folder) + { + return; + } - var path = folder.Folder.Items().Item().Path; - if (!Path.IsPathRooted(path)) return; + string path; + try + { + path = folder.Folder.Items().Item().Path; + if (!Path.IsPathRooted(path)) + { + return; + } + } + catch + { + return; + } - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); + //_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), @@ -86,7 +103,8 @@ private static void NavigateDialogPath(IUIAutomationElement window) if (address == null) { - Log.Error("Cannot Get specific Control"); + // I found issue here + Debug.WriteLine("Failed to find address edit control"); return; } @@ -145,11 +163,11 @@ uint dwmsEventTime } // Release previous reference if exists - if (lastExplorerView != null) + /*if (lastExplorerView != null) { Marshal.ReleaseComObject(lastExplorerView); lastExplorerView = null; - } + }*/ lastExplorerView = explorer; } @@ -160,7 +178,7 @@ uint dwmsEventTime } finally { - if (window != null) + /*if (window != null) { Marshal.ReleaseComObject(window); window = null; @@ -169,7 +187,7 @@ uint dwmsEventTime { Marshal.ReleaseComObject(shellWindows); shellWindows = null; - } + }*/ } } From 3e24f05628c6479becdd4234c48fab6f74d32de1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 10:52:09 +0800 Subject: [PATCH 008/243] Fix & Improve --- .../QuickSwitch/QuickSwitch.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index ed7697e03ad..6debe806c9a 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -17,6 +17,8 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch { public static class QuickSwitch { + private static readonly string ClassName = nameof(QuickSwitch); + private static CUIAutomation8 _automation = new CUIAutomation8Class(); private static InternetExplorer lastExplorerView = null; @@ -48,7 +50,7 @@ public static bool Initialize() } catch (System.Exception e) { - Log.Exception(nameof(QuickSwitch), "Failed to initialize QuickSwitch", e); + Log.Exception(ClassName, "Failed to initialize QuickSwitch", e); } return false; @@ -128,7 +130,7 @@ private static void WindowSwitch( uint dwmsEventTime ) { - IUIAutomationElement window = null; + IUIAutomationElement window; try { window = _automation.ElementFromHandle(hwnd); @@ -144,7 +146,7 @@ uint dwmsEventTime return; } - ShellWindowsClass shellWindows = null; + ShellWindowsClass shellWindows; try { shellWindows = new ShellWindowsClass(); @@ -174,7 +176,7 @@ uint dwmsEventTime } catch (System.Exception e) { - Log.Exception(nameof(QuickSwitch), "Failed to get shell windows", e); + Log.Exception(ClassName, "Failed to get shell windows", e); } finally { From 6c629b76385aaa5450b3e142a8b7385da5514b50 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 10:54:12 +0800 Subject: [PATCH 009/243] Code quality --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6debe806c9a..fed8c72fab0 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -96,6 +96,7 @@ private static void NavigateDialogPath(IUIAutomationElement window) { return; } + Debug.WriteLine($"Path: {path}"); //_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); @@ -158,8 +159,7 @@ uint dwmsEventTime continue; } - // Fix for CA2020: Wrap the conversion in a 'checked' statement - if (explorer.HWND != checked((int)hwnd)) + if (explorer.HWND != hwnd.Value) { continue; } From 2c22993f986b802e1bf778ace712e95a7dd2b187 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 11:57:27 +0800 Subject: [PATCH 010/243] Remove unused codes --- .../QuickSwitch/QuickSwitch.cs | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index fed8c72fab0..2a81827bb0c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -98,7 +98,8 @@ private static void NavigateDialogPath(IUIAutomationElement window) } Debug.WriteLine($"Path: {path}"); - //_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); + // Use Alt + D to focus address bar + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), @@ -106,7 +107,7 @@ private static void NavigateDialogPath(IUIAutomationElement window) if (address == null) { - // I found issue here + // I found I cannot get address edit control here Debug.WriteLine("Failed to find address edit control"); return; } @@ -119,6 +120,7 @@ private static void NavigateDialogPath(IUIAutomationElement window) PInvoke.WM_KEYDOWN, (nuint)VirtualKeyCode.RETURN, IntPtr.Zero); + Debug.WriteLine("Send Enter key to address edit control"); } private static void WindowSwitch( @@ -164,13 +166,6 @@ uint dwmsEventTime continue; } - // Release previous reference if exists - /*if (lastExplorerView != null) - { - Marshal.ReleaseComObject(lastExplorerView); - lastExplorerView = null; - }*/ - lastExplorerView = explorer; } } @@ -178,19 +173,6 @@ uint dwmsEventTime { Log.Exception(ClassName, "Failed to get shell windows", e); } - finally - { - /*if (window != null) - { - Marshal.ReleaseComObject(window); - window = null; - } - if (shellWindows != null) - { - Marshal.ReleaseComObject(shellWindows); - shellWindows = null; - }*/ - } } public static void Dispose() From 959e89f940bfd1c85bf2288263cf11545b798cce Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 12:28:26 +0800 Subject: [PATCH 011/243] Use codes from https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump --- .../NativeMethods.txt | 3 +- .../QuickSwitch/QuickSwitch.cs | 69 ++++++++------- Flow.Launcher.Infrastructure/Win32Helper.cs | 84 +++++++++++++++++++ 3 files changed, 124 insertions(+), 32 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index b0a78eb8b73..6e2dc7a883d 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -59,4 +59,5 @@ SetWinEventHook SendMessage EVENT_SYSTEM_FOREGROUND WINEVENT_OUTOFCONTEXT -WM_KEYDOWN \ No newline at end of file +WM_KEYDOWN +WM_SETTEXT \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 2a81827bb0c..23eb5e6d1d1 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,7 +1,6 @@ -using System; -using System.Diagnostics; -using System.IO; +using System.IO; using System.Runtime.InteropServices; +using System.Threading; using Flow.Launcher.Infrastructure.Logger; using Interop.UIAutomationClient; using NHotkey; @@ -11,7 +10,6 @@ using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; using WindowsInput; -using WindowsInput.Native; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -58,16 +56,11 @@ public static bool Initialize() public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - NavigateDialogPath(_automation.ElementFromHandle(Win32Helper.GetForegroundWindow())); + NavigateDialogPath(); } - private static void NavigateDialogPath(IUIAutomationElement window) + private static void NavigateDialogPath() { - if (window is not { CurrentClassName: "#32770" } dialog) - { - return; - } - object document; try { @@ -96,31 +89,45 @@ private static void NavigateDialogPath(IUIAutomationElement window) { return; } - Debug.WriteLine($"Path: {path}"); - // Use Alt + D to focus address bar - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.VK_D); + JumpToPath(path); + } - var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition( - _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId), - _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_AccessKeyPropertyId, "d"))); + private static bool JumpToPath(string path) + { + var t = new Thread(() => + { + // Jump after flow launcher window vanished (after JumpAction returned true) + // and the dialog had been in the foreground. The class name of a dialog window is "#32770". + var timeOut = !SpinWait.SpinUntil(() => GetForegroundWindowClassName() == "#32770", 1000); + if (timeOut) + { + return; + }; - if (address == null) + // Assume that the dialog is in the foreground now + Win32Helper.DirJump(_inputSimulator, path, PInvoke.GetForegroundWindow()); + }); + t.Start(); + return true; + + static string GetForegroundWindowClassName() { - // I found I cannot get address edit control here - Debug.WriteLine("Failed to find address edit control"); - return; + var handle = PInvoke.GetForegroundWindow(); + return GetClassName(handle); } + } - var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId); - edit.SetValue(path); - - PInvoke.SendMessage( - new(address.CurrentNativeWindowHandle), - PInvoke.WM_KEYDOWN, - (nuint)VirtualKeyCode.RETURN, - IntPtr.Zero); - Debug.WriteLine("Send Enter key to address edit control"); + private static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) + { + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => null, + _ => new string(buf), + }; + } } private static void WindowSwitch( @@ -145,7 +152,7 @@ uint dwmsEventTime if (window is { CurrentClassName: "#32770" }) { - NavigateDialogPath(window); + NavigateDialogPath(); return; } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 54604a27118..22796f99d49 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; +using System.Threading; using System.Windows; using System.Windows.Interop; using System.Windows.Media; @@ -13,6 +14,8 @@ using Windows.Win32.Graphics.Dwm; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; +using WindowsInput; +using WindowsInput.Native; using Point = System.Windows.Point; namespace Flow.Launcher.Infrastructure @@ -605,5 +608,86 @@ public static void OpenImeSettings() } #endregion + + #region Quick Switch + + // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump + + internal static bool DirJump(InputSimulator inputSimulator, string path, HWND dialogHandle, bool altD = true) + { + // Alt-D or Ctrl-L to focus on the path input box + if (altD) + { + inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + } + else + { + inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + } + + // Get the handle of the path input box and then set the text. + // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. + var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "WorkerW", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ReBarWindow32", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Address Band Root", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "msctls_progress32", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBoxEx32", null); + if (controlHandle == HWND.Null) + { + return DirJumpOnLegacyDialog(inputSimulator, path, dialogHandle); + } + + var timeOut = !SpinWait.SpinUntil(() => + { + int style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + return false; + } + + var editHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); + editHandle = PInvoke.FindWindowEx(editHandle, HWND.Null, "Edit", null); + if (editHandle == HWND.Null) + { + return false; + } + + SetWindowText(editHandle, path); + inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + + return true; + } + + internal static bool DirJumpOnLegacyDialog(InputSimulator inputSimulator, string path, HWND dialogHandle) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); + controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Edit", null); + if (controlHandle == HWND.Null) + { + return false; + } + + SetWindowText(controlHandle, path); + // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, + // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. + inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + + return true; + } + + private static unsafe nint SetWindowText(HWND handle, string text) + { + fixed (char* textPtr = text + '\0') + { + return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; + } + } + + #endregion } } From fb874114c0ef8a1826997aa4f7be7f64f8af3db1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 13:26:47 +0800 Subject: [PATCH 012/243] Add auto switch support & Use const for dialog class name --- .../QuickSwitch/QuickSwitch.cs | 20 ++++++++++++++----- .../UserSettings/Settings.cs | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 23eb5e6d1d1..0551c4aa01a 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,7 +1,9 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; using Interop.UIAutomationClient; using NHotkey; using SHDocVw; @@ -17,6 +19,11 @@ public static class QuickSwitch { private static readonly string ClassName = nameof(QuickSwitch); + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + + // The class name of a dialog window is "#32770". + private const string DialogWindowClassName = "#32770"; + private static CUIAutomation8 _automation = new CUIAutomation8Class(); private static InternetExplorer lastExplorerView = null; @@ -98,8 +105,8 @@ private static bool JumpToPath(string path) var t = new Thread(() => { // Jump after flow launcher window vanished (after JumpAction returned true) - // and the dialog had been in the foreground. The class name of a dialog window is "#32770". - var timeOut = !SpinWait.SpinUntil(() => GetForegroundWindowClassName() == "#32770", 1000); + // and the dialog had been in the foreground. + var timeOut = !SpinWait.SpinUntil(() => GetForegroundWindowClassName() == DialogWindowClassName, 1000); if (timeOut) { return; @@ -150,10 +157,13 @@ uint dwmsEventTime return; } - if (window is { CurrentClassName: "#32770" }) + if (_settings.AutoQuickSwitch) { - NavigateDialogPath(); - return; + if (window is { CurrentClassName: DialogWindowClassName }) + { + NavigateDialogPath(); + return; + } } ShellWindowsClass shellWindows; diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 46c49479420..fdaf0381615 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -53,7 +53,9 @@ public void Save() public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; - public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; + public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Ctrl} + G"; + + public bool AutoQuickSwitch { get; set; } = false; public string Language { From 6ba6058f14566b73de6bfa63dbe8cc25532f204c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 14:19:27 +0800 Subject: [PATCH 013/243] Replace Interop.Shell32 with PInovke --- .../Flow.Launcher.Infrastructure.csproj | 3 --- .../NativeMethods.txt | 3 ++- .../QuickSwitch/QuickSwitch.cs | 23 ++++++++++++++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 6148c138def..5a92aa814fe 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -81,9 +81,6 @@ QuickSwitch\Interop.SHDocVw.dll - - QuickSwitch\Interop.Shell32.dll - \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 6e2dc7a883d..99185b78420 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -60,4 +60,5 @@ SendMessage EVENT_SYSTEM_FOREGROUND WINEVENT_OUTOFCONTEXT WM_KEYDOWN -WM_SETTEXT \ No newline at end of file +WM_SETTEXT +IShellFolderViewDual2 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0551c4aa01a..a1ee33ac245 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -7,10 +7,10 @@ using Interop.UIAutomationClient; using NHotkey; using SHDocVw; -using Shell32; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; +using Windows.Win32.UI.Shell; using WindowsInput; namespace Flow.Launcher.Infrastructure.QuickSwitch @@ -78,7 +78,7 @@ private static void NavigateDialogPath() return; } - if (document is not IShellFolderViewDual2 folder) + if (document is not IShellFolderViewDual2 folderView) { return; } @@ -86,7 +86,24 @@ private static void NavigateDialogPath() string path; try { - path = folder.Folder.Items().Item().Path; + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Check if the item is part of the file system + if (folderItem != null && folderItem.IsFileSystem) + { + path = folderItem.Path; + } + else + { + // Handle non-file system paths (e.g., virtual folders) + path = string.Empty; + } + if (!Path.IsPathRooted(path)) { return; From 6f13f89d953a002be6be97682548a327b84cbae7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 14:19:35 +0800 Subject: [PATCH 014/243] Change default hotkey --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index fdaf0381615..0d5b79e0cd0 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -53,7 +53,7 @@ public void Save() public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; - public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Ctrl} + G"; + public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; public bool AutoQuickSwitch { get; set; } = false; From d229bfed40fa7c598422e41e1712f544796bd86e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 14:49:21 +0800 Subject: [PATCH 015/243] Replace Interop.SHDocVw with PInvoke --- .../Flow.Launcher.Infrastructure.csproj | 6 --- .../NativeMethods.txt | 6 ++- .../QuickSwitch/QuickSwitch.cs | 50 +++++++++++++------ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 5a92aa814fe..46ec7ac1042 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -76,11 +76,5 @@ - - - - QuickSwitch\Interop.SHDocVw.dll - - \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 99185b78420..4349ae07fa5 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -61,4 +61,8 @@ EVENT_SYSTEM_FOREGROUND WINEVENT_OUTOFCONTEXT WM_KEYDOWN WM_SETTEXT -IShellFolderViewDual2 \ No newline at end of file +IShellFolderViewDual2 +CoCreateInstance +CLSCTX +IShellWindows +IWebBrowser2 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index a1ee33ac245..d32d04bcd1c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Runtime.InteropServices; using System.Threading; using CommunityToolkit.Mvvm.DependencyInjection; @@ -6,9 +7,9 @@ using Flow.Launcher.Infrastructure.UserSettings; using Interop.UIAutomationClient; using NHotkey; -using SHDocVw; using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Shell; using WindowsInput; @@ -21,12 +22,12 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); - // The class name of a dialog window is "#32770". + // The class name of a dialog window private const string DialogWindowClassName = "#32770"; private static CUIAutomation8 _automation = new CUIAutomation8Class(); - private static InternetExplorer lastExplorerView = null; + private static IWebBrowser2 lastExplorerView = null; private static readonly InputSimulator _inputSimulator = new(); @@ -68,10 +69,16 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) private static void NavigateDialogPath() { - object document; + object document = null; try { - document = lastExplorerView?.Document; + if (lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = lastExplorerView; + document = explorerView.Document; + } } catch (COMException) { @@ -183,25 +190,22 @@ uint dwmsEventTime } } - ShellWindowsClass shellWindows; try { - shellWindows = new ShellWindowsClass(); - - foreach (var shellWindow in shellWindows) + EnumerateShellWindows((shellWindow) => { - if (shellWindow is not InternetExplorer explorer) + if (shellWindow is not IWebBrowser2 explorer) { - continue; + return; } if (explorer.HWND != hwnd.Value) { - continue; + return; } lastExplorerView = explorer; - } + }); } catch (System.Exception e) { @@ -209,6 +213,24 @@ uint dwmsEventTime } } + private static unsafe void EnumerateShellWindows(Action action) + { + // Create an instance of ShellWindows + var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass + var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows + + PInvoke.CoCreateInstance(&clsidShellWindows, null, CLSCTX.CLSCTX_ALL, &iidIShellWindows, out var shellWindowsObj); + + var shellWindows = (IShellWindows)shellWindowsObj; + + // Enumerate the shell windows + int count = shellWindows.Count; + for (var i = 0; i < count; i++) + { + action(shellWindows.Item(i)); + } + } + public static void Dispose() { // Dispose handle From 2d25fefd7f0ac2eb76a810c0dbebba35d5df9fb0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 14:58:44 +0800 Subject: [PATCH 016/243] Initialize QuickSwitch earlier --- .../QuickSwitch/QuickSwitch.cs | 24 ++++++++----------- Flow.Launcher/App.xaml.cs | 3 +++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index d32d04bcd1c..25b16cb8309 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -33,11 +33,13 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _hookWinEventSafeHandle = null; + private static bool _isInitialized = false; + public static bool Initialize() { - try - { - _hookWinEventSafeHandle = PInvoke.SetWinEventHook( + if (_isInitialized) return true; + + _hookWinEventSafeHandle = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, null, @@ -46,20 +48,14 @@ public static bool Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); - if (_hookWinEventSafeHandle.IsInvalid) - { - Log.Error("Failed to set window event hook"); - return false; - } - - return true; - } - catch (System.Exception e) + if (_hookWinEventSafeHandle.IsInvalid) { - Log.Exception(ClassName, "Failed to initialize QuickSwitch", e); + Log.Error(ClassName, "Failed to initialize QuickSwitch"); + return false; } - return false; + _isInitialized = true; + return true; } public static void OnToggleHotkey(object sender, HotkeyEventArgs args) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 94327e6664f..95df690f3c4 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -15,6 +15,7 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -184,6 +185,8 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); + QuickSwitch.Initialize(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); From ac255bac065aced6b2e04b7d84d808245614f01d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 15:11:25 +0800 Subject: [PATCH 017/243] Add support in HotKey page --- Flow.Launcher/HotkeyControl.xaml.cs | 7 ++++++- Flow.Launcher/Languages/en.xaml | 4 ++++ .../ViewModels/SettingsPaneHotkeyViewModel.cs | 7 +++++++ .../SettingPages/Views/SettingsPaneHotkey.xaml | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 8762a934bbb..293a76368b2 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -109,7 +109,8 @@ public enum HotkeyType SelectPrevItemHotkey, SelectPrevItemHotkey2, SelectNextItemHotkey, - SelectNextItemHotkey2 + SelectNextItemHotkey2, + QuickSwitchHotkey, } // We can initialize settings in static field because it has been constructed in App constuctor @@ -140,6 +141,7 @@ public string Hotkey HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, + HotkeyType.QuickSwitchHotkey => _settings.QuickSwitchHotkey, _ => throw new System.NotImplementedException("Hotkey type not set") }; } @@ -196,6 +198,9 @@ public string Hotkey case HotkeyType.SelectNextItemHotkey2: _settings.SelectNextItemHotkey2 = value; break; + case HotkeyType.QuickSwitchHotkey: + _settings.QuickSwitchHotkey = value; + break; default: throw new System.NotImplementedException("Hotkey type not set"); } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index d2549bd9152..093d64c80a6 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -298,6 +298,10 @@ Use Segoe Fluent Icons Use Segoe Fluent Icons for query results where supported Press Key + Quick Switch + Enter shortcut to quickly navigate dialog path as current Explorer. + Quick Switch Automatically + Quick switch automatically when a navigate dialog is opened. HTTP Proxy diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs index 7a7c19dd358..c2c428df389 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs @@ -4,6 +4,7 @@ using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -34,6 +35,12 @@ private void SetTogglingHotkey(HotkeyModel hotkey) HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey); } + [RelayCommand] + private void SetQuickSwitchHotkey(HotkeyModel hotkey) + { + HotKeyMapper.SetHotkey(hotkey, QuickSwitch.OnToggleHotkey); + } + [RelayCommand] private void CustomHotkeyDelete() { diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index b1d72ede5bf..36c9bfe2368 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -67,6 +67,24 @@ + + + + + + + + + + Date: Sun, 13 Apr 2025 15:20:16 +0800 Subject: [PATCH 018/243] Code quality --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 25b16cb8309..09bce4acb62 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -215,7 +215,14 @@ private static unsafe void EnumerateShellWindows(Action action) var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows - PInvoke.CoCreateInstance(&clsidShellWindows, null, CLSCTX.CLSCTX_ALL, &iidIShellWindows, out var shellWindowsObj); + var result = PInvoke.CoCreateInstance( + &clsidShellWindows, + null, + CLSCTX.CLSCTX_ALL, + &iidIShellWindows, + out var shellWindowsObj); + + if (result.Failed) return; var shellWindows = (IShellWindows)shellWindowsObj; From 974276118b6fa6183e31466df0903ac46abe2a0d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 18:29:37 +0800 Subject: [PATCH 019/243] Initialize explorer window during startup --- .../QuickSwitch/QuickSwitch.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 09bce4acb62..23c176433d5 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -39,6 +39,18 @@ public static bool Initialize() { if (_isInitialized) return true; + // Check all foreground windows and check if there are explorer windows + EnumerateShellWindows((shellWindow) => + { + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } + + lastExplorerView = explorer; + }); + + // Call WindowSwitch when the foreground window changes and check if there are explorer windows _hookWinEventSafeHandle = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, From a75779cf02d7f3ad6eae82ec79383c17931431e3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 13 Apr 2025 21:19:48 +0800 Subject: [PATCH 020/243] Make helper function public --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- Flow.Launcher.Infrastructure/Win32Helper.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 23c176433d5..2b45a4e4db6 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -145,7 +145,7 @@ private static bool JumpToPath(string path) }; // Assume that the dialog is in the foreground now - Win32Helper.DirJump(_inputSimulator, path, PInvoke.GetForegroundWindow()); + Win32Helper.DirJump(_inputSimulator, path, Win32Helper.GetForegroundWindow()); }); t.Start(); return true; diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 22796f99d49..819c3ef71ac 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -613,8 +613,11 @@ public static void OpenImeSettings() // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - internal static bool DirJump(InputSimulator inputSimulator, string path, HWND dialogHandle, bool altD = true) + public static bool DirJump(InputSimulator inputSimulator, string path, nint dialog, bool altD = true) { + // Get the handle of the dialog window + var dialogHandle = new HWND(dialog); + // Alt-D or Ctrl-L to focus on the path input box if (altD) { @@ -627,7 +630,7 @@ internal static bool DirJump(InputSimulator inputSimulator, string path, HWND di // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. - var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "WorkerW", null); + var controlHandle = PInvoke.FindWindowEx(new(dialogHandle), HWND.Null, "WorkerW", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ReBarWindow32", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Address Band Root", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "msctls_progress32", null); From 6ebb582951a9034874e14fb4bd5020416c6a8c34 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 13:04:43 +0800 Subject: [PATCH 021/243] Support quick switch main window --- .../NativeMethods.txt | 4 +- .../QuickSwitch/QuickSwitch.cs | 147 ++++++++++++++---- .../UserSettings/Settings.cs | 1 + Flow.Launcher.Infrastructure/Win32Helper.cs | 20 +++ Flow.Launcher/Helper/HotKeyMapper.cs | 1 - Flow.Launcher/MainWindow.xaml.cs | 80 +++++++++- Flow.Launcher/ViewModel/MainViewModel.cs | 29 +++- 7 files changed, 241 insertions(+), 41 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 4349ae07fa5..1a3120cfa09 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -65,4 +65,6 @@ IShellFolderViewDual2 CoCreateInstance CLSCTX IShellWindows -IWebBrowser2 \ No newline at end of file +IWebBrowser2 +EVENT_OBJECT_LOCATIONCHANGE +EVENT_OBJECT_DESTROY \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 2b45a4e4db6..faed09c5908 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -20,18 +20,30 @@ public static class QuickSwitch { private static readonly string ClassName = nameof(QuickSwitch); - private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + public static Action ShowQuickSwitchWindow { get; set; } = null; + + public static Action UpdateQuickSwitchWindow { get; set; } = null; + + public static Action DestoryQuickSwitchWindow { get; set; } = null; // The class name of a dialog window private const string DialogWindowClassName = "#32770"; + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + private static CUIAutomation8 _automation = new CUIAutomation8Class(); - private static IWebBrowser2 lastExplorerView = null; + private static IWebBrowser2 _lastExplorerView = null; private static readonly InputSimulator _inputSimulator = new(); - private static UnhookWinEventSafeHandle _hookWinEventSafeHandle = null; + private static UnhookWinEventSafeHandle _foregroundChangeHook = null; + + private static UnhookWinEventSafeHandle _locationChangeHook = null; + + private static UnhookWinEventSafeHandle _destroyChangeHook = null; + + private static HWND _dialogWindowHandle = HWND.Null; private static bool _isInitialized = false; @@ -47,20 +59,40 @@ public static bool Initialize() return; } - lastExplorerView = explorer; + _lastExplorerView = explorer; }); - // Call WindowSwitch when the foreground window changes and check if there are explorer windows - _hookWinEventSafeHandle = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_FOREGROUND, - PInvoke.EVENT_SYSTEM_FOREGROUND, - null, - WindowSwitch, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - - if (_hookWinEventSafeHandle.IsInvalid) + // Call ForegroundChange when the foreground window changes + _foregroundChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.EVENT_SYSTEM_FOREGROUND, + null, + ForegroundChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + // Call LocationChange when the location of the window changes + _locationChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + null, + LocationChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + // Call DestroyChange when the window is destroyed + _destroyChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.EVENT_OBJECT_DESTROY, + null, + DestroyChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + if (_foregroundChangeHook.IsInvalid || _locationChangeHook.IsInvalid || _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return false; @@ -80,11 +112,11 @@ private static void NavigateDialogPath() object document = null; try { - if (lastExplorerView != null) + if (_lastExplorerView != null) { // Use dynamic here because using IWebBrower2.Document can cause exception here: // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = lastExplorerView; + dynamic explorerView = _lastExplorerView; document = explorerView.Document; } } @@ -169,7 +201,7 @@ private static unsafe string GetClassName(HWND handle) } } - private static void WindowSwitch( + private static void ForegroundChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -189,15 +221,21 @@ uint dwmsEventTime return; } - if (_settings.AutoQuickSwitch) + // If window is dialog window, show quick switch window and navigate path if needed + if (window is { CurrentClassName: DialogWindowClassName }) { - if (window is { CurrentClassName: DialogWindowClassName }) + if (_settings.ShowQuickSwitchWindow) + { + _dialogWindowHandle = hwnd; + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + } + if (_settings.AutoQuickSwitch) { NavigateDialogPath(); - return; } } + // If window is explorer window, set _lastExplorerView to the explorer try { EnumerateShellWindows((shellWindow) => @@ -212,7 +250,7 @@ uint dwmsEventTime return; } - lastExplorerView = explorer; + _lastExplorerView = explorer; }); } catch (System.Exception e) @@ -221,6 +259,47 @@ uint dwmsEventTime } } + private static void LocationChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved, update the quick switch window position + if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd) + { + UpdateQuickSwitchWindow?.Invoke(); + } + } + + private static void DestroyChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the explorer window is destroyed, set _lastExplorerView to null + if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) + { + _lastExplorerView = null; + } + + // If the dialog window is destroyed, set _dialogWindowHandle to null + if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd) + { + _dialogWindowHandle = HWND.Null; + DestoryQuickSwitchWindow?.Invoke(); + } + } + private static unsafe void EnumerateShellWindows(Action action) { // Create an instance of ShellWindows @@ -239,7 +318,7 @@ private static unsafe void EnumerateShellWindows(Action action) var shellWindows = (IShellWindows)shellWindowsObj; // Enumerate the shell windows - int count = shellWindows.Count; + var count = shellWindows.Count; for (var i = 0; i < count; i++) { action(shellWindows.Item(i)); @@ -249,17 +328,27 @@ private static unsafe void EnumerateShellWindows(Action action) public static void Dispose() { // Dispose handle - if (_hookWinEventSafeHandle != null) + if (_foregroundChangeHook != null) + { + _foregroundChangeHook.Dispose(); + _foregroundChangeHook = null; + } + if (_locationChangeHook != null) + { + _locationChangeHook.Dispose(); + _locationChangeHook = null; + } + if (_destroyChangeHook != null) { - _hookWinEventSafeHandle.Dispose(); - _hookWinEventSafeHandle = null; + _destroyChangeHook.Dispose(); + _destroyChangeHook = null; } // Release ComObjects - if (lastExplorerView != null) + if (_lastExplorerView != null) { - Marshal.ReleaseComObject(lastExplorerView); - lastExplorerView = null; + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; } if (_automation != null) { diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0dda565a210..b07f1639a4f 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -56,6 +56,7 @@ public void Save() public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; public bool AutoQuickSwitch { get; set; } = false; + public bool ShowQuickSwitchWindow { get; set; } = true; public string Language { diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 819c3ef71ac..bb18bd30931 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -691,6 +691,26 @@ private static unsafe nint SetWindowText(HWND handle, string text) } } + public static unsafe bool GetWindowRect(nint handle, out Rect outRect) + { + var rect = new RECT(); + var result = PInvoke.GetWindowRect(new(handle), &rect); + if (!result) + { + outRect = new Rect(); + return false; + } + + // Convert RECT to Rect + outRect = new Rect( + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top + ); + return true; + } + #endregion } } diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 9dfd7673448..8b390b11de5 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -2,7 +2,6 @@ using ChefKeys; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.ViewModel; diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index bf7a45b1d25..80bf7f72091 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -20,6 +20,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.ViewModel; @@ -106,7 +107,7 @@ private void OnSourceInitialized(object sender, EventArgs e) Win32Helper.DisableControlBox(this); } - private async void OnLoaded(object sender, RoutedEventArgs _) + private async void OnLoaded(object sender, RoutedEventArgs e) { // Check first launch if (_settings.FirstLaunch) @@ -174,6 +175,9 @@ private async void OnLoaded(object sender, RoutedEventArgs _) // 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; + + // Register quick switch events + InitializeQuickSwitch(); // Initialize hotkey mapper after window is loaded HotKeyMapper.Initialize(); @@ -190,7 +194,7 @@ private async void OnLoaded(object sender, RoutedEventArgs _) if (_viewModel.MainWindowVisibilityStatus) { // Play sound effect before activing the window - if (_settings.UseSound) + if (_settings.UseSound && !_viewModel.QuickSwitch) { SoundPlay(); } @@ -213,7 +217,7 @@ private async void OnLoaded(object sender, RoutedEventArgs _) QueryTextBox.Focus(); // Play window animation - if (_settings.UseAnimation) + if (_settings.UseAnimation && !_viewModel.QuickSwitch) { WindowAnimation(); } @@ -320,6 +324,11 @@ private void OnClosed(object sender, EventArgs e) private void OnLocationChanged(object sender, EventArgs e) { + if (_viewModel.QuickSwitch) + { + return; + } + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { _settings.WindowLeft = Left; @@ -329,6 +338,11 @@ private void OnLocationChanged(object sender, EventArgs e) private async void OnDeactivated(object sender, EventArgs e) { + if (_viewModel.QuickSwitch) + { + return; + } + _settings.WindowLeft = Left; _settings.WindowTop = Top; @@ -467,6 +481,11 @@ private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { + if (_viewModel.QuickSwitch) + { + return IntPtr.Zero; + } + if (msg == Win32Helper.WM_ENTERSIZEMOVE) { _initialWidth = (int)Width; @@ -648,9 +667,16 @@ private void UpdateNotifyIconText() private void UpdatePosition() { - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); + if (_viewModel.QuickSwitch) + { + UpdateQuickSwitchPosition(); + } + else + { + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePosition(); + InitializePosition(); + } } private async Task PositionResetAsync() @@ -1130,6 +1156,48 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) #endregion + #region Quick Switch + + private void InitializeQuickSwitch() + { + QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitch; + QuickSwitch.UpdateQuickSwitchWindow = UpdateQuickSwitchPosition; + QuickSwitch.DestoryQuickSwitchWindow = _viewModel.ResetQuickSwitch; + } + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void UpdateQuickSwitchPosition() + { + await Task.Delay(300); // If don't give a time, Positioning will be weird. + + // Get dialog window rect + var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); + if (!result) return; + + // Move window below the bottom of the dialog and keep it center + Top = VerticalBottom(window); + Left = HorizonCenter(window); + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + private double HorizonCenter(Rect window) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0); + var left = (dip2.X - ActualWidth) / 2 + dip1.X; + return left; + } + + private double VerticalBottom(Rect window) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom); + return dip1.Y; + } + + #endregion + #region IDisposable protected virtual void Dispose(bool disposing) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 00675149b41..12f24bcda2e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1483,6 +1483,27 @@ public bool ShouldIgnoreHotkeys() #endregion + #region Quick Switch + + public bool QuickSwitch { get; private set; } + public nint DialogWindowHandle { get; private set; } = nint.Zero; + + public void SetupQuickSwitch(nint handle) + { + DialogWindowHandle = handle; + QuickSwitch = true; + Show(); + } + + public void ResetQuickSwitch() + { + DialogWindowHandle = nint.Zero; + QuickSwitch = false; + Hide(); + } + + #endregion + #region Public Methods #pragma warning disable VSTHRD100 // Avoid async void methods @@ -1499,7 +1520,7 @@ public void Show() Win32Helper.DWMSetCloakForWindow(mainWindow, false); // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !QuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1522,7 +1543,7 @@ public void Show() VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); // Switch keyboard layout - if (StartWithEnglishMode) + if (StartWithEnglishMode && !QuickSwitch) { Win32Helper.SwitchToEnglishKeyboardLayout(true); } @@ -1571,7 +1592,7 @@ public async void Hide() if (Application.Current?.MainWindow is MainWindow mainWindow) { // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !QuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1589,7 +1610,7 @@ public async void Hide() }, DispatcherPriority.Render); // Switch keyboard layout - if (StartWithEnglishMode) + if (StartWithEnglishMode && !QuickSwitch) { Win32Helper.RestorePreviousKeyboardLayout(); } From b62a4f96d4198fe0b79ba813fc13faeda5057052 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 14 Apr 2025 14:07:05 +0900 Subject: [PATCH 022/243] Add "Beta" string in Label --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index ed764581b8e..18d5d56e379 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -302,7 +302,7 @@ Show badges for query results where supported Show Result Badges for Global Query Only Show badges for global query results only - Quick Switch + Quick Switch (Beta) Enter shortcut to quickly navigate dialog path as current Explorer. Quick Switch Automatically Quick switch automatically when a navigate dialog is opened. From 0520ec3e75dbb8957c523d6632c1cf4f75b2d1ee Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 13:18:19 +0800 Subject: [PATCH 023/243] Fix possible COMException --- .../QuickSwitch/QuickSwitch.cs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index faed09c5908..7c6276d9eb6 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -240,17 +240,24 @@ uint dwmsEventTime { EnumerateShellWindows((shellWindow) => { - if (shellWindow is not IWebBrowser2 explorer) + try { - return; - } + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } + + if (explorer.HWND != hwnd.Value) + { + return; + } - if (explorer.HWND != hwnd.Value) + _lastExplorerView = explorer; + } + catch (COMException) { - return; + // Ignored } - - _lastExplorerView = explorer; }); } catch (System.Exception e) @@ -286,10 +293,17 @@ private static void DestroyChangeCallback( uint dwmsEventTime ) { - // If the explorer window is destroyed, set _lastExplorerView to null - if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) + try { - _lastExplorerView = null; + // If the explorer window is destroyed, set _lastExplorerView to null + if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) + { + _lastExplorerView = null; + } + } + catch (COMException) + { + // Ignored } // If the dialog window is destroyed, set _dialogWindowHandle to null From dd51be074f190704ac7b005c43d7f0688de9d652 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 13:20:20 +0800 Subject: [PATCH 024/243] Remove unnecessary Initialize --- .../QuickSwitch/QuickSwitch.cs | 13 ++++++++----- Flow.Launcher/Helper/HotKeyMapper.cs | 6 +----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 7c6276d9eb6..26f06808b9f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -47,9 +47,9 @@ public static class QuickSwitch private static bool _isInitialized = false; - public static bool Initialize() + public static void Initialize() { - if (_isInitialized) return true; + if (_isInitialized) return; // Check all foreground windows and check if there are explorer windows EnumerateShellWindows((shellWindow) => @@ -95,16 +95,19 @@ public static bool Initialize() if (_foregroundChangeHook.IsInvalid || _locationChangeHook.IsInvalid || _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); - return false; + return; } _isInitialized = true; - return true; + return; } public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - NavigateDialogPath(); + if (_isInitialized) + { + NavigateDialogPath(); + } } private static void NavigateDialogPath() diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 8b390b11de5..f586d384212 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -23,12 +23,8 @@ internal static void Initialize() _settings = Ioc.Default.GetService(); SetHotkey(_settings.Hotkey, OnToggleHotkey); + SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); LoadCustomPluginHotkey(); - - if (QuickSwitch.Initialize()) - { - SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); - } } internal static void Dispose() From 18f39a507d16a398b172b3c301d58a0db91250ff Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 13:29:34 +0800 Subject: [PATCH 025/243] Remove Interop.UIAutomationClient --- .../Flow.Launcher.Infrastructure.csproj | 1 - .../QuickSwitch/QuickSwitch.cs | 45 ++++++------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 46ec7ac1042..c7331ba4cb7 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -60,7 +60,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 26f06808b9f..bc1b9afd185 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; -using Interop.UIAutomationClient; using NHotkey; using Windows.Win32; using Windows.Win32.Foundation; @@ -31,8 +30,6 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); - private static CUIAutomation8 _automation = new CUIAutomation8Class(); - private static IWebBrowser2 _lastExplorerView = null; private static readonly InputSimulator _inputSimulator = new(); @@ -173,7 +170,7 @@ private static bool JumpToPath(string path) { // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. - var timeOut = !SpinWait.SpinUntil(() => GetForegroundWindowClassName() == DialogWindowClassName, 1000); + var timeOut = !SpinWait.SpinUntil(() => GetWindowClassName(PInvoke.GetForegroundWindow()) == DialogWindowClassName, 1000); if (timeOut) { return; @@ -184,23 +181,22 @@ private static bool JumpToPath(string path) }); t.Start(); return true; - - static string GetForegroundWindowClassName() - { - var handle = PInvoke.GetForegroundWindow(); - return GetClassName(handle); - } } - private static unsafe string GetClassName(HWND handle) + private static string GetWindowClassName(HWND handle) { - fixed (char* buf = new char[256]) + return GetClassName(handle); + + static unsafe string GetClassName(HWND handle) { - return PInvoke.GetClassName(handle, buf, 256) switch + fixed (char* buf = new char[256]) { - 0 => null, - _ => new string(buf), - }; + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => null, + _ => new string(buf), + }; + } } } @@ -214,18 +210,8 @@ private static void ForegroundChangeCallback( uint dwmsEventTime ) { - IUIAutomationElement window; - try - { - window = _automation.ElementFromHandle(hwnd); - } - catch - { - return; - } - // If window is dialog window, show quick switch window and navigate path if needed - if (window is { CurrentClassName: DialogWindowClassName }) + if (GetWindowClassName(hwnd) == DialogWindowClassName) { if (_settings.ShowQuickSwitchWindow) { @@ -367,11 +353,6 @@ public static void Dispose() Marshal.ReleaseComObject(_lastExplorerView); _lastExplorerView = null; } - if (_automation != null) - { - Marshal.ReleaseComObject(_automation); - _automation = null; - } } } } From d03301248cfde2e9c1de59b69b3ffae7f97a4333 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 13:45:47 +0800 Subject: [PATCH 026/243] Improve quick switch window moving & resizing --- .../NativeMethods.txt | 4 +- .../QuickSwitch/QuickSwitch.cs | 54 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 1a3120cfa09..272e5dcab7b 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -67,4 +67,6 @@ CLSCTX IShellWindows IWebBrowser2 EVENT_OBJECT_LOCATIONCHANGE -EVENT_OBJECT_DESTROY \ No newline at end of file +EVENT_OBJECT_DESTROY +EVENT_SYSTEM_MOVESIZESTART +EVENT_SYSTEM_MOVESIZEEND \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index bc1b9afd185..31a33799e71 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,6 +2,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -38,8 +39,12 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _locationChangeHook = null; + private static UnhookWinEventSafeHandle _moveSizeHook = null; + private static UnhookWinEventSafeHandle _destroyChangeHook = null; + private static DispatcherTimer _dragMoveTimer = null; + private static HWND _dialogWindowHandle = HWND.Null; private static bool _isInitialized = false; @@ -79,6 +84,16 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); + // Call MoveSizeCallBack when the window is moved or resized + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + null, + MoveSizeCallBack, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_DESTROY, @@ -89,12 +104,19 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); - if (_foregroundChangeHook.IsInvalid || _locationChangeHook.IsInvalid || _destroyChangeHook.IsInvalid) + if (_foregroundChangeHook.IsInvalid || + _locationChangeHook.IsInvalid || + _moveSizeHook.IsInvalid || + _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return; } + // Initialize timer + _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(30) }; + _dragMoveTimer.Tick += (s, e) => UpdateQuickSwitchWindow(); + _isInitialized = true; return; } @@ -272,6 +294,31 @@ uint dwmsEventTime } } + private static void MoveSizeCallBack( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved or resized, update the quick switch window position + if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) + { + switch (eventType) + { + case PInvoke.EVENT_SYSTEM_MOVESIZESTART: + _dragMoveTimer.Start(); // Start dragging position + break; + case PInvoke.EVENT_SYSTEM_MOVESIZEEND: + _dragMoveTimer.Stop(); // Stop dragging + break; + } + } + } + private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, @@ -341,6 +388,11 @@ public static void Dispose() _locationChangeHook.Dispose(); _locationChangeHook = null; } + if (_moveSizeHook != null) + { + _moveSizeHook.Dispose(); + _moveSizeHook = null; + } if (_destroyChangeHook != null) { _destroyChangeHook.Dispose(); From a0d447eff8b111755d281aa36589dd986eb0c954 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 15:30:34 +0800 Subject: [PATCH 027/243] Expose check path function --- .../QuickSwitch/QuickSwitch.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 31a33799e71..e33729579e2 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -129,6 +129,17 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) } } + public static bool CheckPath(string path) + { + // Is non-null + if (string.IsNullOrEmpty(path)) return false; + // Is absolute? + if (!Path.IsPathRooted(path)) return false; + // Is folder? + if (!Directory.Exists(path)) return false; + return true; + } + private static void NavigateDialogPath() { object document = null; @@ -173,7 +184,7 @@ private static void NavigateDialogPath() path = string.Empty; } - if (!Path.IsPathRooted(path)) + if (!CheckPath(path)) { return; } From 224c1094dc7227bf9ee463fa264d2005286b72a1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 15:37:33 +0800 Subject: [PATCH 028/243] Add quick switch property --- Flow.Launcher.Plugin/Result.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index f561fcb1dcf..1f7e9241e2a 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -257,6 +257,17 @@ public string PluginDirectory /// public bool ShowBadge { get; set; } = false; + /// + /// Determines if the result can be shown in quick switch window. + /// + public bool AllowQuickSwitch { get; set; } = false; + + /// + /// This holds the path which can be provided by plugin to be navigated to the + /// file dialog when records in quick switch window is clicked on a result. + /// + public string QuickSwitchPath { get; set; } + /// /// Run this result, asynchronously /// @@ -308,6 +319,8 @@ public Result Clone() AddSelectedCount = AddSelectedCount, RecordKey = RecordKey, ShowBadge = ShowBadge, + AllowQuickSwitch = AllowQuickSwitch, + QuickSwitchPath = QuickSwitchPath, }; } From b72ef915cd37bca9df0e0860c1cb30aec897e07a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 15:38:20 +0800 Subject: [PATCH 029/243] Support quick switch results in main view model --- Flow.Launcher/MainWindow.xaml.cs | 12 +++++------ Flow.Launcher/ViewModel/MainViewModel.cs | 27 +++++++++++++++--------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 80bf7f72091..63c8a097610 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -194,7 +194,7 @@ private async void OnLoaded(object sender, RoutedEventArgs e) if (_viewModel.MainWindowVisibilityStatus) { // Play sound effect before activing the window - if (_settings.UseSound && !_viewModel.QuickSwitch) + if (_settings.UseSound && !_viewModel.IsQuickSwitch) { SoundPlay(); } @@ -217,7 +217,7 @@ private async void OnLoaded(object sender, RoutedEventArgs e) QueryTextBox.Focus(); // Play window animation - if (_settings.UseAnimation && !_viewModel.QuickSwitch) + if (_settings.UseAnimation && !_viewModel.IsQuickSwitch) { WindowAnimation(); } @@ -324,7 +324,7 @@ private void OnClosed(object sender, EventArgs e) private void OnLocationChanged(object sender, EventArgs e) { - if (_viewModel.QuickSwitch) + if (_viewModel.IsQuickSwitch) { return; } @@ -338,7 +338,7 @@ private void OnLocationChanged(object sender, EventArgs e) private async void OnDeactivated(object sender, EventArgs e) { - if (_viewModel.QuickSwitch) + if (_viewModel.IsQuickSwitch) { return; } @@ -481,7 +481,7 @@ private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (_viewModel.QuickSwitch) + if (_viewModel.IsQuickSwitch) { return IntPtr.Zero; } @@ -667,7 +667,7 @@ private void UpdateNotifyIconText() private void UpdatePosition() { - if (_viewModel.QuickSwitch) + if (_viewModel.IsQuickSwitch) { UpdateQuickSwitchPosition(); } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 12f24bcda2e..0c61731b092 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -244,7 +244,7 @@ public void RegisterResultsUpdatedEvent() var token = e.Token == default ? _updateToken : e.Token; // make a clone to avoid possible issue that plugin will also change the list and items when updating view model - var resultsCopy = DeepCloneResults(e.Results, token); + var resultsCopy = CheckQuickSwitchAndDeepClone(e.Results, token); foreach (var result in resultsCopy) { @@ -447,9 +447,10 @@ private async Task OpenResultAsync(string index) } } - private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) + private IReadOnlyList CheckQuickSwitchAndDeepClone(IReadOnlyList results, CancellationToken token = default) { var resultsCopy = new List(); + foreach (var result in results.ToList()) { if (token.IsCancellationRequested) @@ -457,9 +458,15 @@ private static IReadOnlyList DeepCloneResults(IReadOnlyList resu break; } + if (IsQuickSwitch && !result.AllowQuickSwitch) + { + continue; + } + var resultCopy = result.Clone(); resultsCopy.Add(resultCopy); } + return resultsCopy; } @@ -1290,7 +1297,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, token); + resultsCopy = CheckQuickSwitchAndDeepClone(results, token); } foreach (var result in resultsCopy) @@ -1485,20 +1492,20 @@ public bool ShouldIgnoreHotkeys() #region Quick Switch - public bool QuickSwitch { get; private set; } + public bool IsQuickSwitch { get; private set; } public nint DialogWindowHandle { get; private set; } = nint.Zero; public void SetupQuickSwitch(nint handle) { DialogWindowHandle = handle; - QuickSwitch = true; + IsQuickSwitch = true; Show(); } public void ResetQuickSwitch() { DialogWindowHandle = nint.Zero; - QuickSwitch = false; + IsQuickSwitch = false; Hide(); } @@ -1520,7 +1527,7 @@ public void Show() Win32Helper.DWMSetCloakForWindow(mainWindow, false); // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !QuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !IsQuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1543,7 +1550,7 @@ public void Show() VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); // Switch keyboard layout - if (StartWithEnglishMode && !QuickSwitch) + if (StartWithEnglishMode && !IsQuickSwitch) { Win32Helper.SwitchToEnglishKeyboardLayout(true); } @@ -1592,7 +1599,7 @@ public async void Hide() if (Application.Current?.MainWindow is MainWindow mainWindow) { // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !QuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !IsQuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1610,7 +1617,7 @@ public async void Hide() }, DispatcherPriority.Render); // Switch keyboard layout - if (StartWithEnglishMode && !QuickSwitch) + if (StartWithEnglishMode && !IsQuickSwitch) { Win32Helper.RestorePreviousKeyboardLayout(); } From cc0dc8c189e0222ff97ff35c640eadfced7d1978 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 15:46:43 +0800 Subject: [PATCH 030/243] Support quick switch commands --- .../QuickSwitch/QuickSwitch.cs | 36 +++++++++---------- Flow.Launcher.Infrastructure/Win32Helper.cs | 18 +++++----- Flow.Launcher/ViewModel/MainViewModel.cs | 29 +++++++++------ 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index e33729579e2..01d7b43791f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -12,7 +12,6 @@ using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Shell; -using WindowsInput; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -33,8 +32,6 @@ public static class QuickSwitch private static IWebBrowser2 _lastExplorerView = null; - private static readonly InputSimulator _inputSimulator = new(); - private static UnhookWinEventSafeHandle _foregroundChangeHook = null; private static UnhookWinEventSafeHandle _locationChangeHook = null; @@ -129,16 +126,7 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) } } - public static bool CheckPath(string path) - { - // Is non-null - if (string.IsNullOrEmpty(path)) return false; - // Is absolute? - if (!Path.IsPathRooted(path)) return false; - // Is folder? - if (!Directory.Exists(path)) return false; - return true; - } + private static void NavigateDialogPath() { @@ -183,11 +171,6 @@ private static void NavigateDialogPath() // Handle non-file system paths (e.g., virtual folders) path = string.Empty; } - - if (!CheckPath(path)) - { - return; - } } catch { @@ -197,8 +180,10 @@ private static void NavigateDialogPath() JumpToPath(path); } - private static bool JumpToPath(string path) + public static bool JumpToPath(string path) { + if (!CheckPath(path)) return false; + var t = new Thread(() => { // Jump after flow launcher window vanished (after JumpAction returned true) @@ -210,10 +195,21 @@ private static bool JumpToPath(string path) }; // Assume that the dialog is in the foreground now - Win32Helper.DirJump(_inputSimulator, path, Win32Helper.GetForegroundWindow()); + Win32Helper.DirJump(path, Win32Helper.GetForegroundWindow()); }); t.Start(); return true; + + static bool CheckPath(string path) + { + // Is non-null + if (string.IsNullOrEmpty(path)) return false; + // Is absolute? + if (!Path.IsPathRooted(path)) return false; + // Is folder? + if (!Directory.Exists(path)) return false; + return true; + } } private static string GetWindowClassName(HWND handle) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index bb18bd30931..6fa4682f919 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -613,7 +613,9 @@ public static void OpenImeSettings() // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - public static bool DirJump(InputSimulator inputSimulator, string path, nint dialog, bool altD = true) + private static readonly InputSimulator _inputSimulator = new(); + + public static bool DirJump(string path, nint dialog, bool altD = true) { // Get the handle of the dialog window var dialogHandle = new HWND(dialog); @@ -621,11 +623,11 @@ public static bool DirJump(InputSimulator inputSimulator, string path, nint dial // Alt-D or Ctrl-L to focus on the path input box if (altD) { - inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); } else { - inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); } // Get the handle of the path input box and then set the text. @@ -637,7 +639,7 @@ public static bool DirJump(InputSimulator inputSimulator, string path, nint dial controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBoxEx32", null); if (controlHandle == HWND.Null) { - return DirJumpOnLegacyDialog(inputSimulator, path, dialogHandle); + return DirJumpOnLegacyDialog(path, dialogHandle); } var timeOut = !SpinWait.SpinUntil(() => @@ -658,12 +660,12 @@ public static bool DirJump(InputSimulator inputSimulator, string path, nint dial } SetWindowText(editHandle, path); - inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); return true; } - internal static bool DirJumpOnLegacyDialog(InputSimulator inputSimulator, string path, HWND dialogHandle) + internal static bool DirJumpOnLegacyDialog(string path, HWND dialogHandle) { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); @@ -677,8 +679,8 @@ internal static bool DirJumpOnLegacyDialog(InputSimulator inputSimulator, string SetWindowText(controlHandle, path); // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. - inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); - inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); return true; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0c61731b092..cbbf027f9a7 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -422,12 +423,25 @@ private async Task OpenResultAsync(string index) return; } - var hideWindow = await result.ExecuteAsync(new ActionContext + if (IsQuickSwitch) { - // not null means pressing modifier key + number, should ignore the modifier key - SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() - }) - .ConfigureAwait(false); + Win32Helper.SetForegroundWindow(DialogWindowHandle); + QuickSwitch.JumpToPath(result.QuickSwitchPath); + } + else + { + var hideWindow = await result.ExecuteAsync(new ActionContext + { + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() + }) + .ConfigureAwait(false); + + if (hideWindow) + { + Hide(); + } + } if (QueryResultsSelected()) { @@ -440,11 +454,6 @@ private async Task OpenResultAsync(string index) } lastHistoryIndex = 1; } - - if (hideWindow) - { - Hide(); - } } private IReadOnlyList CheckQuickSwitchAndDeepClone(IReadOnlyList results, CancellationToken token = default) From f1e674b3965d7819e99419da564bec2a95fc2b94 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:00:18 +0800 Subject: [PATCH 031/243] Support quick switch for explorer --- .../Search/ResultManager.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 5c4accdc05f..afb23505c54 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -164,7 +164,9 @@ internal static Result CreateFolderResult(string title, string subtitle, string Score = score, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"), SubTitleToolTip = path, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }, + AllowQuickSwitch = true, + QuickSwitchPath = path }; } @@ -204,7 +206,9 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK }, TitleToolTip = path, SubTitleToolTip = path, - ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed }, + AllowQuickSwitch = true, + QuickSwitchPath = path }; } @@ -260,22 +264,24 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK OpenFolder(folderPath); return true; }, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed }, + AllowQuickSwitch = true, + QuickSwitchPath = folderPath }; } internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - bool isMedia = IsMedia(Path.GetExtension(filePath)); + var isMedia = IsMedia(Path.GetExtension(filePath)); var title = Path.GetFileName(filePath); - + var directory = Path.GetDirectoryName(filePath); /* Preview Detail */ var result = new Result { Title = title, - SubTitle = Path.GetDirectoryName(filePath), + SubTitle = directory, IcoPath = filePath, Preview = new Result.PreviewInfo { @@ -299,7 +305,7 @@ internal static Result CreateFileResult(string filePath, Query query, int score { if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true); + OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty, true); } else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control) { @@ -307,7 +313,7 @@ internal static Result CreateFileResult(string filePath, Query query, int score } else { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty); + OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty); } } catch (Exception ex) @@ -319,7 +325,9 @@ internal static Result CreateFileResult(string filePath, Query query, int score }, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"), SubTitleToolTip = filePath, - ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }, + AllowQuickSwitch = true, + QuickSwitchPath = directory }; return result; } From f86d1319e75a726fa09ac5905b56852cd4bfb9d6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:00:59 +0800 Subject: [PATCH 032/243] Use right click to quick switch --- Flow.Launcher.Plugin/Result.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 1f7e9241e2a..c247208ad00 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -264,7 +264,7 @@ public string PluginDirectory /// /// This holds the path which can be provided by plugin to be navigated to the - /// file dialog when records in quick switch window is clicked on a result. + /// file dialog when records in quick switch window is right clicked on a result. /// public string QuickSwitchPath { get; set; } From dbbee2c85838b2cc64168cb19029b2180596cfad Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:10:27 +0800 Subject: [PATCH 033/243] Use right click for quick switch --- Flow.Launcher/ViewModel/MainViewModel.cs | 52 +++++++++++++++--------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index cbbf027f9a7..1018e75e20f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -348,16 +348,32 @@ public void ForwardHistory() [RelayCommand] private void LoadContextMenu() { - if (QueryResultsSelected()) + // For quick switch mode, we need to navigate to the path + if (IsQuickSwitch) { - // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing - // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing if (SelectedResults.SelectedItem != null) - SelectedResults = ContextMenu; + { + var result = SelectedResults.SelectedItem.Result; + Win32Helper.SetForegroundWindow(DialogWindowHandle); + QuickSwitch.JumpToPath(result.QuickSwitchPath); + } } + // For query mode, we load context menu else { - SelectedResults = Results; + if (QueryResultsSelected()) + { + // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing + // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing + if (SelectedResults.SelectedItem != null) + { + SelectedResults = ContextMenu; + } + } + else + { + SelectedResults = Results; + } } } @@ -423,24 +439,16 @@ private async Task OpenResultAsync(string index) return; } - if (IsQuickSwitch) - { - Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(result.QuickSwitchPath); - } - else + var hideWindow = await result.ExecuteAsync(new ActionContext { - var hideWindow = await result.ExecuteAsync(new ActionContext - { - // not null means pressing modifier key + number, should ignore the modifier key - SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() - }) + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() + }) .ConfigureAwait(false); - if (hideWindow) - { - Hide(); - } + if (hideWindow) + { + Hide(); } if (QueryResultsSelected()) @@ -1075,6 +1083,10 @@ private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { await QueryResultsAsync(searchDelay, isReQuery); } + else if (IsQuickSwitch) + { + return; + } else if (ContextMenuSelected()) { QueryContextMenu(); From 21df92d5b48269dcc79e8b234e4627e551c95196 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:13:23 +0800 Subject: [PATCH 034/243] Use HWND.Null to check dialog window handle --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 01d7b43791f..658149500c2 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -295,7 +295,7 @@ uint dwmsEventTime ) { // If the dialog window is moved, update the quick switch window position - if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd) + if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { UpdateQuickSwitchWindow?.Invoke(); } @@ -312,7 +312,7 @@ uint dwmsEventTime ) { // If the dialog window is moved or resized, update the quick switch window position - if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) + if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) { switch (eventType) { @@ -350,7 +350,7 @@ uint dwmsEventTime } // If the dialog window is destroyed, set _dialogWindowHandle to null - if (_dialogWindowHandle != null && _dialogWindowHandle == hwnd) + if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { _dialogWindowHandle = HWND.Null; DestoryQuickSwitchWindow?.Invoke(); From 75b7175079171de512c4ff9dee2b917793089633 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:14:22 +0800 Subject: [PATCH 035/243] Check dialog window handle --- Flow.Launcher/MainWindow.xaml.cs | 2 ++ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 63c8a097610..615d3106f5d 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1169,6 +1169,8 @@ private void InitializeQuickSwitch() private async void UpdateQuickSwitchPosition() { + if (_viewModel.DialogWindowHandle == nint.Zero) return; + await Task.Delay(300); // If don't give a time, Positioning will be weird. // Get dialog window rect diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1018e75e20f..a5de6dcd420 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -351,7 +351,7 @@ private void LoadContextMenu() // For quick switch mode, we need to navigate to the path if (IsQuickSwitch) { - if (SelectedResults.SelectedItem != null) + if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { var result = SelectedResults.SelectedItem.Result; Win32Helper.SetForegroundWindow(DialogWindowHandle); From 38e85556ea27134ffa8ef37b9866dfd2b4d732b0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:28:05 +0800 Subject: [PATCH 036/243] Add show quick switch window option --- Flow.Launcher/Languages/en.xaml | 6 ++++-- Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 18d5d56e379..38e14c3904e 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -303,9 +303,11 @@ Show Result Badges for Global Query Only Show badges for global query results only Quick Switch (Beta) - Enter shortcut to quickly navigate dialog path as current Explorer. + Enter shortcut to quickly navigate the path of a file dialog to the path of the current Explorer. Quick Switch Automatically - Quick switch automatically when a navigate dialog is opened. + Quick switch automatically navigate to the path of the current Explorer when a file dialog is opened. + Show Quick Switch Window + Show query window in the center-bottom of a file dialog to navigate its path. HTTP Proxy diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index 36c9bfe2368..a69e7d95e14 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -83,6 +83,13 @@ OffContent="{DynamicResource disable}" OnContent="{DynamicResource enable}" /> + + + + Date: Mon, 14 Apr 2025 16:31:49 +0800 Subject: [PATCH 037/243] Code quality --- Flow.Launcher.Infrastructure/PInvokeExtensions.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs index 1a72ab7a66a..23e4c0ce251 100644 --- a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -4,14 +4,16 @@ namespace Windows.Win32; -// Edited from: https://github.com/files-community/Files +/// +/// Edited from: https://github.com/files-community/Files +/// internal static partial class PInvoke { [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] - static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + private static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] - static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + private static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); // NOTE: // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. From 6e642b21dda16f0c1794933a2b4ec9bf1980d969 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:44:17 +0800 Subject: [PATCH 038/243] Make private --- 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 6fa4682f919..e38d5a45031 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -665,7 +665,7 @@ public static bool DirJump(string path, nint dialog, bool altD = true) return true; } - internal static bool DirJumpOnLegacyDialog(string path, HWND dialogHandle) + private static bool DirJumpOnLegacyDialog(string path, HWND dialogHandle) { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); From 556af5e22f683dba23b06afd6225e9e4712a8e11 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:45:11 +0800 Subject: [PATCH 039/243] Remove useless hooks --- .../NativeMethods.txt | 4 +- .../QuickSwitch/QuickSwitch.cs | 85 ------------------- 2 files changed, 1 insertion(+), 88 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 272e5dcab7b..908a3eb2088 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -66,7 +66,5 @@ CoCreateInstance CLSCTX IShellWindows IWebBrowser2 -EVENT_OBJECT_LOCATIONCHANGE EVENT_OBJECT_DESTROY -EVENT_SYSTEM_MOVESIZESTART -EVENT_SYSTEM_MOVESIZEEND \ No newline at end of file +SetParent \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 658149500c2..c42eed04e17 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,7 +2,6 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; -using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -33,15 +32,9 @@ public static class QuickSwitch private static IWebBrowser2 _lastExplorerView = null; private static UnhookWinEventSafeHandle _foregroundChangeHook = null; - - private static UnhookWinEventSafeHandle _locationChangeHook = null; - - private static UnhookWinEventSafeHandle _moveSizeHook = null; private static UnhookWinEventSafeHandle _destroyChangeHook = null; - private static DispatcherTimer _dragMoveTimer = null; - private static HWND _dialogWindowHandle = HWND.Null; private static bool _isInitialized = false; @@ -71,26 +64,6 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); - // Call LocationChange when the location of the window changes - _locationChangeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_OBJECT_LOCATIONCHANGE, - PInvoke.EVENT_OBJECT_LOCATIONCHANGE, - null, - LocationChangeCallback, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - - // Call MoveSizeCallBack when the window is moved or resized - _moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - null, - MoveSizeCallBack, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_DESTROY, @@ -102,18 +75,12 @@ public static void Initialize() PInvoke.WINEVENT_OUTOFCONTEXT); if (_foregroundChangeHook.IsInvalid || - _locationChangeHook.IsInvalid || - _moveSizeHook.IsInvalid || _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return; } - // Initialize timer - _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(30) }; - _dragMoveTimer.Tick += (s, e) => UpdateQuickSwitchWindow(); - _isInitialized = true; return; } @@ -284,48 +251,6 @@ uint dwmsEventTime } } - private static void LocationChangeCallback( - HWINEVENTHOOK hWinEventHook, - uint eventType, - HWND hwnd, - int idObject, - int idChild, - uint dwEventThread, - uint dwmsEventTime - ) - { - // If the dialog window is moved, update the quick switch window position - if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) - { - UpdateQuickSwitchWindow?.Invoke(); - } - } - - private static void MoveSizeCallBack( - HWINEVENTHOOK hWinEventHook, - uint eventType, - HWND hwnd, - int idObject, - int idChild, - uint dwEventThread, - uint dwmsEventTime - ) - { - // If the dialog window is moved or resized, update the quick switch window position - if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) - { - switch (eventType) - { - case PInvoke.EVENT_SYSTEM_MOVESIZESTART: - _dragMoveTimer.Start(); // Start dragging position - break; - case PInvoke.EVENT_SYSTEM_MOVESIZEEND: - _dragMoveTimer.Stop(); // Stop dragging - break; - } - } - } - private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, @@ -390,16 +315,6 @@ public static void Dispose() _foregroundChangeHook.Dispose(); _foregroundChangeHook = null; } - if (_locationChangeHook != null) - { - _locationChangeHook.Dispose(); - _locationChangeHook = null; - } - if (_moveSizeHook != null) - { - _moveSizeHook.Dispose(); - _moveSizeHook = null; - } if (_destroyChangeHook != null) { _destroyChangeHook.Dispose(); From b39887820ed7667cb0f6d1ba6e468c80413c5cf3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 16:58:55 +0800 Subject: [PATCH 040/243] Restore to original visibility status & Improve code quality --- .../QuickSwitch/QuickSwitch.cs | 13 ++++++------- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index c42eed04e17..6b21ff5a1c1 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -22,7 +22,7 @@ public static class QuickSwitch public static Action UpdateQuickSwitchWindow { get; set; } = null; - public static Action DestoryQuickSwitchWindow { get; set; } = null; + public static Action ResetQuickSwitchWindow { get; set; } = null; // The class name of a dialog window private const string DialogWindowClassName = "#32770"; @@ -209,13 +209,12 @@ uint dwmsEventTime // If window is dialog window, show quick switch window and navigate path if needed if (GetWindowClassName(hwnd) == DialogWindowClassName) { - if (_settings.ShowQuickSwitchWindow) - { - _dialogWindowHandle = hwnd; - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - } + _dialogWindowHandle = hwnd; + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); if (_settings.AutoQuickSwitch) { + // Showing quick switch window may bring focus + Win32Helper.SetForegroundWindow(hwnd); NavigateDialogPath(); } } @@ -278,7 +277,7 @@ uint dwmsEventTime if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { _dialogWindowHandle = HWND.Null; - DestoryQuickSwitchWindow?.Invoke(); + ResetQuickSwitchWindow?.Invoke(); } } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 615d3106f5d..413dd2e91fa 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1162,7 +1162,7 @@ private void InitializeQuickSwitch() { QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitch; QuickSwitch.UpdateQuickSwitchWindow = UpdateQuickSwitchPosition; - QuickSwitch.DestoryQuickSwitchWindow = _viewModel.ResetQuickSwitch; + QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; } #pragma warning disable VSTHRD100 // Avoid async void methods diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a5de6dcd420..11aa8f18730 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1515,9 +1515,15 @@ public bool ShouldIgnoreHotkeys() public bool IsQuickSwitch { get; private set; } public nint DialogWindowHandle { get; private set; } = nint.Zero; + + private bool PreviousMainWindowVisibilityStatus { get; set; } = true; public void SetupQuickSwitch(nint handle) { + if (!Settings.ShowQuickSwitchWindow) return; + + PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; + DialogWindowHandle = handle; IsQuickSwitch = true; Show(); @@ -1527,7 +1533,15 @@ public void ResetQuickSwitch() { DialogWindowHandle = nint.Zero; IsQuickSwitch = false; - Hide(); + + if (PreviousMainWindowVisibilityStatus) + { + Show(); + } + else + { + Hide(); + } } #endregion From 9ebacd42ebfa082df78f10a14085be6029be8c06 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 17:24:37 +0800 Subject: [PATCH 041/243] Use Proc to get size & position --- .../NativeMethods.txt | 5 ++- .../QuickSwitch/QuickSwitch.cs | 38 +++++++++++++++++-- Flow.Launcher.Infrastructure/Win32Helper.cs | 2 + Flow.Launcher/ViewModel/MainViewModel.cs | 2 - 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 908a3eb2088..026d31c0d41 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -67,4 +67,7 @@ CLSCTX IShellWindows IWebBrowser2 EVENT_OBJECT_DESTROY -SetParent \ No newline at end of file +SetParent +WM_SIZE +WM_MOVE +CallWindowProc \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6b21ff5a1c1..54c100acedf 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -11,6 +11,7 @@ using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -93,8 +94,6 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) } } - - private static void NavigateDialogPath() { object document = null; @@ -210,7 +209,11 @@ uint dwmsEventTime if (GetWindowClassName(hwnd) == DialogWindowClassName) { _dialogWindowHandle = hwnd; - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + if (_settings.ShowQuickSwitchWindow) + { + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + FixPositionToDialog(_dialogWindowHandle); + } if (_settings.AutoQuickSwitch) { // Showing quick switch window may bring focus @@ -306,6 +309,35 @@ private static unsafe void EnumerateShellWindows(Action action) } } + private static WNDPROC _oldWndProc; + private static WNDPROC _newWndProc; + + private static void FixPositionToDialog(HWND handle) + { + _newWndProc = new(NewWindowProc); + var pNewWndProc = Marshal.GetFunctionPointerForDelegate(_newWndProc); + var pOldWndProc = PInvoke.SetWindowLongPtr(handle, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, pNewWndProc); + _oldWndProc = pOldWndProc == nint.Zero ? null : Marshal.GetDelegateForFunctionPointer(pOldWndProc); + } + + private static LRESULT NewWindowProc(HWND param0, uint param1, WPARAM param2, LPARAM param3) + { + if (param1 == PInvoke.WM_SIZE || param1 == PInvoke.WM_MOVE) + { + UpdateQuickSwitchWindow?.Invoke(); + } + + if (_oldWndProc != null) + { + // Call the original window procedure + return PInvoke.CallWindowProc(_oldWndProc, param0, param1, param2, param3); + } + else + { + return new LRESULT(0); + } + } + public static void Dispose() { // Dispose handle diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index e38d5a45031..846415d2ebf 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -713,6 +713,8 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) return true; } + + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 11aa8f18730..a83ec655175 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1520,8 +1520,6 @@ public bool ShouldIgnoreHotkeys() public void SetupQuickSwitch(nint handle) { - if (!Settings.ShowQuickSwitchWindow) return; - PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; From 28e4b2fc820e628839ebadbb1d24646137a03515 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 18:02:58 +0800 Subject: [PATCH 042/243] Use windows hook --- .../NativeMethods.txt | 5 +- .../QuickSwitch/QuickSwitch.cs | 55 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 026d31c0d41..426f8637a8e 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -67,7 +67,8 @@ CLSCTX IShellWindows IWebBrowser2 EVENT_OBJECT_DESTROY -SetParent WM_SIZE WM_MOVE -CallWindowProc \ No newline at end of file +SetWindowsHookEx +HC_ACTION +CWPSTRUCT \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 54c100acedf..b7062b0ddc4 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -36,6 +37,8 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _destroyChangeHook = null; + private static UnhookWindowsHookExSafeHandle _callWndProcHook; + private static HWND _dialogWindowHandle = HWND.Null; private static bool _isInitialized = false; @@ -75,8 +78,16 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); + // Install hook for dialog window message + _callWndProcHook = PInvoke.SetWindowsHookEx( + WINDOWS_HOOK_ID.WH_CALLWNDPROC, + CallWndProc, + Process.GetCurrentProcess().SafeHandle, + 0); + if (_foregroundChangeHook.IsInvalid || - _destroyChangeHook.IsInvalid) + _destroyChangeHook.IsInvalid || + _callWndProcHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return; @@ -212,7 +223,6 @@ uint dwmsEventTime if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - FixPositionToDialog(_dialogWindowHandle); } if (_settings.AutoQuickSwitch) { @@ -309,33 +319,23 @@ private static unsafe void EnumerateShellWindows(Action action) } } - private static WNDPROC _oldWndProc; - private static WNDPROC _newWndProc; - - private static void FixPositionToDialog(HWND handle) + private static LRESULT CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) { - _newWndProc = new(NewWindowProc); - var pNewWndProc = Marshal.GetFunctionPointerForDelegate(_newWndProc); - var pOldWndProc = PInvoke.SetWindowLongPtr(handle, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, pNewWndProc); - _oldWndProc = pOldWndProc == nint.Zero ? null : Marshal.GetDelegateForFunctionPointer(pOldWndProc); - } - - private static LRESULT NewWindowProc(HWND param0, uint param1, WPARAM param2, LPARAM param3) - { - if (param1 == PInvoke.WM_SIZE || param1 == PInvoke.WM_MOVE) + if (nCode == PInvoke.HC_ACTION) { - UpdateQuickSwitchWindow?.Invoke(); + var msg = Marshal.PtrToStructure(lParam); + if (msg.hwnd == _dialogWindowHandle && + (msg.message == PInvoke.WM_MOVE || msg.message == PInvoke.WM_SIZE)) + { + UpdateQuickSwitchWindow?.Invoke(); + } } - if (_oldWndProc != null) - { - // Call the original window procedure - return PInvoke.CallWindowProc(_oldWndProc, param0, param1, param2, param3); - } - else - { - return new LRESULT(0); - } + return PInvoke.CallNextHookEx( + _callWndProcHook, + nCode, + wParam, + lParam); } public static void Dispose() @@ -351,6 +351,11 @@ public static void Dispose() _destroyChangeHook.Dispose(); _destroyChangeHook = null; } + if (_callWndProcHook != null) + { + _callWndProcHook.Dispose(); + _callWndProcHook = null; + } // Release ComObjects if (_lastExplorerView != null) From 25430cb19da87dacc74f2a6093c1958b96049dfd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 18:20:40 +0800 Subject: [PATCH 043/243] Fix hook issue --- Flow.Launcher.Infrastructure/NativeMethods.txt | 3 ++- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 426f8637a8e..191e648cc81 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -71,4 +71,5 @@ WM_SIZE WM_MOVE SetWindowsHookEx HC_ACTION -CWPSTRUCT \ No newline at end of file +CWPSTRUCT +GetCurrentThreadId \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index b7062b0ddc4..846dcd8f84e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -82,8 +81,8 @@ public static void Initialize() _callWndProcHook = PInvoke.SetWindowsHookEx( WINDOWS_HOOK_ID.WH_CALLWNDPROC, CallWndProc, - Process.GetCurrentProcess().SafeHandle, - 0); + null, + PInvoke.GetCurrentThreadId()); if (_foregroundChangeHook.IsInvalid || _destroyChangeHook.IsInvalid || From 8d07acd031a13ba6db39d7295ed876730a66c565 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 18:28:46 +0800 Subject: [PATCH 044/243] Use hook event back --- .../NativeMethods.txt | 9 +- .../QuickSwitch/QuickSwitch.cs | 116 +++++++++++++----- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 191e648cc81..4a31512ac15 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -67,9 +67,6 @@ CLSCTX IShellWindows IWebBrowser2 EVENT_OBJECT_DESTROY -WM_SIZE -WM_MOVE -SetWindowsHookEx -HC_ACTION -CWPSTRUCT -GetCurrentThreadId \ No newline at end of file +EVENT_OBJECT_LOCATIONCHANGE +EVENT_SYSTEM_MOVESIZESTART +EVENT_SYSTEM_MOVESIZEEND \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 846dcd8f84e..3bf55fd0d21 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,6 +2,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -11,7 +12,6 @@ using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Shell; -using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -34,9 +34,13 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _foregroundChangeHook = null; + private static UnhookWinEventSafeHandle _locationChangeHook = null; + + private static UnhookWinEventSafeHandle _moveSizeHook = null; + private static UnhookWinEventSafeHandle _destroyChangeHook = null; - private static UnhookWindowsHookExSafeHandle _callWndProcHook; + private static DispatcherTimer _dragMoveTimer = null; private static HWND _dialogWindowHandle = HWND.Null; @@ -67,6 +71,26 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); + // Call LocationChange when the location of the window changes + _locationChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + null, + LocationChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + // Call MoveSizeCallBack when the window is moved or resized + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + null, + MoveSizeCallBack, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_DESTROY, @@ -77,21 +101,19 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); - // Install hook for dialog window message - _callWndProcHook = PInvoke.SetWindowsHookEx( - WINDOWS_HOOK_ID.WH_CALLWNDPROC, - CallWndProc, - null, - PInvoke.GetCurrentThreadId()); - if (_foregroundChangeHook.IsInvalid || - _destroyChangeHook.IsInvalid || - _callWndProcHook.IsInvalid) + _locationChangeHook.IsInvalid || + _moveSizeHook.IsInvalid || + _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return; } + // Initialize timer + _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; + _dragMoveTimer.Tick += (s, e) => UpdateQuickSwitchWindow?.Invoke(); + _isInitialized = true; return; } @@ -262,6 +284,48 @@ uint dwmsEventTime } } + private static void LocationChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved, update the quick switch window position + if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) + { + UpdateQuickSwitchWindow?.Invoke(); + } + } + + private static void MoveSizeCallBack( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved or resized, update the quick switch window position + if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) + { + switch (eventType) + { + case PInvoke.EVENT_SYSTEM_MOVESIZESTART: + _dragMoveTimer.Start(); // Start dragging position + break; + case PInvoke.EVENT_SYSTEM_MOVESIZEEND: + _dragMoveTimer.Stop(); // Stop dragging + break; + } + } + } + private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, @@ -318,25 +382,6 @@ private static unsafe void EnumerateShellWindows(Action action) } } - private static LRESULT CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) - { - if (nCode == PInvoke.HC_ACTION) - { - var msg = Marshal.PtrToStructure(lParam); - if (msg.hwnd == _dialogWindowHandle && - (msg.message == PInvoke.WM_MOVE || msg.message == PInvoke.WM_SIZE)) - { - UpdateQuickSwitchWindow?.Invoke(); - } - } - - return PInvoke.CallNextHookEx( - _callWndProcHook, - nCode, - wParam, - lParam); - } - public static void Dispose() { // Dispose handle @@ -350,10 +395,15 @@ public static void Dispose() _destroyChangeHook.Dispose(); _destroyChangeHook = null; } - if (_callWndProcHook != null) + if (_locationChangeHook != null) + { + _locationChangeHook.Dispose(); + _locationChangeHook = null; + } + if (_moveSizeHook != null) { - _callWndProcHook.Dispose(); - _callWndProcHook = null; + _moveSizeHook.Dispose(); + _moveSizeHook = null; } // Release ComObjects From e74b0f6da1d3c5214bad31a1bb3ac231e9ad2420 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 20:05:14 +0800 Subject: [PATCH 045/243] Improve restoring to original visibility status & display interface --- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 46 ++++++++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 413dd2e91fa..18edd4f6a4b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -665,7 +665,7 @@ private void UpdateNotifyIconText() #region Window Position - private void UpdatePosition() + public void UpdatePosition() { if (_viewModel.IsQuickSwitch) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a83ec655175..0243e97c6f3 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1520,11 +1520,28 @@ public bool ShouldIgnoreHotkeys() public void SetupQuickSwitch(nint handle) { - PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; + if (handle != nint.Zero && DialogWindowHandle != handle) // Only set once for one file dialog + { + PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; + DialogWindowHandle = handle; + IsQuickSwitch = true; + } - DialogWindowHandle = handle; - IsQuickSwitch = true; - Show(); + if (MainWindowVisibilityStatus) + { + // Only update the position + if (PreviousMainWindowVisibilityStatus) + { + Application.Current?.Dispatcher.Invoke(() => + { + (Application.Current?.MainWindow as MainWindow).UpdatePosition(); + }); + } + } + else + { + Show(); + } } public void ResetQuickSwitch() @@ -1532,13 +1549,28 @@ public void ResetQuickSwitch() DialogWindowHandle = nint.Zero; IsQuickSwitch = false; - if (PreviousMainWindowVisibilityStatus) + if (PreviousMainWindowVisibilityStatus != MainWindowVisibilityStatus) { - Show(); + // Show or hide to change visibility + if (PreviousMainWindowVisibilityStatus) + { + Show(); + } + else + { + Hide(); + } } else { - Hide(); + // Only update the position + if (PreviousMainWindowVisibilityStatus) + { + Application.Current?.Dispatcher.Invoke(() => + { + (Application.Current?.MainWindow as MainWindow).UpdatePosition(); + }); + } } } From 00872abc2350a1e9e87303ce6e806fd63e42d20a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 20:14:55 +0800 Subject: [PATCH 046/243] Delay time for timer --- .../QuickSwitch/QuickSwitch.cs | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 3bf55fd0d21..0bd05c32046 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,6 +2,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; @@ -36,7 +37,7 @@ public static class QuickSwitch private static UnhookWinEventSafeHandle _locationChangeHook = null; - private static UnhookWinEventSafeHandle _moveSizeHook = null; + /*private static UnhookWinEventSafeHandle _moveSizeHook = null;*/ private static UnhookWinEventSafeHandle _destroyChangeHook = null; @@ -82,14 +83,14 @@ public static void Initialize() PInvoke.WINEVENT_OUTOFCONTEXT); // Call MoveSizeCallBack when the window is moved or resized - _moveSizeHook = PInvoke.SetWinEventHook( + /*_moveSizeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_MOVESIZESTART, PInvoke.EVENT_SYSTEM_MOVESIZEEND, null, MoveSizeCallBack, 0, 0, - PInvoke.WINEVENT_OUTOFCONTEXT); + PInvoke.WINEVENT_OUTOFCONTEXT);*/ // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( @@ -103,7 +104,7 @@ public static void Initialize() if (_foregroundChangeHook.IsInvalid || _locationChangeHook.IsInvalid || - _moveSizeHook.IsInvalid || + /*_moveSizeHook.IsInvalid ||*/ _destroyChangeHook.IsInvalid) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); @@ -227,7 +228,7 @@ static unsafe string GetClassName(HWND handle) } } - private static void ForegroundChangeCallback( + private static async void ForegroundChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -244,6 +245,10 @@ uint dwmsEventTime if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + // Here we delay 350ms because MainWindow.UpdateQuickSwitchPosition wait 300ms before position change + // and we use additional 50ms for waiting dialog initialization + await Task.Delay(350); + _dragMoveTimer?.Start(); } if (_settings.AutoQuickSwitch) { @@ -301,7 +306,10 @@ uint dwmsEventTime } } - private static void MoveSizeCallBack( + // TODO: Use a better way to detect dragging + // Here we do not start & stop the timer beacause the start time is not accurate (more than 1s delay) + // So we start & stop the timer when we find a file dialog window + /*private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -324,7 +332,7 @@ uint dwmsEventTime break; } } - } + }*/ private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -354,6 +362,7 @@ uint dwmsEventTime { _dialogWindowHandle = HWND.Null; ResetQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); } } @@ -390,20 +399,20 @@ public static void Dispose() _foregroundChangeHook.Dispose(); _foregroundChangeHook = null; } - if (_destroyChangeHook != null) - { - _destroyChangeHook.Dispose(); - _destroyChangeHook = null; - } if (_locationChangeHook != null) { _locationChangeHook.Dispose(); _locationChangeHook = null; } - if (_moveSizeHook != null) + /*if (_moveSizeHook != null) { _moveSizeHook.Dispose(); _moveSizeHook = null; + }*/ + if (_destroyChangeHook != null) + { + _destroyChangeHook.Dispose(); + _destroyChangeHook = null; } // Release ComObjects From 35560226040e778261263b6584a86a8f714a15a6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 20:30:56 +0800 Subject: [PATCH 047/243] Change delay location --- .../QuickSwitch/QuickSwitch.cs | 6 +----- Flow.Launcher/MainWindow.xaml.cs | 8 +------- Flow.Launcher/ViewModel/MainViewModel.cs | 10 +++++++++- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0bd05c32046..f8959544bcc 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,7 +2,6 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; @@ -228,7 +227,7 @@ static unsafe string GetClassName(HWND handle) } } - private static async void ForegroundChangeCallback( + private static void ForegroundChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -245,9 +244,6 @@ uint dwmsEventTime if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - // Here we delay 350ms because MainWindow.UpdateQuickSwitchPosition wait 300ms before position change - // and we use additional 50ms for waiting dialog initialization - await Task.Delay(350); _dragMoveTimer?.Start(); } if (_settings.AutoQuickSwitch) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 18edd4f6a4b..0c410c812cc 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1165,14 +1165,10 @@ private void InitializeQuickSwitch() QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; } -#pragma warning disable VSTHRD100 // Avoid async void methods - - private async void UpdateQuickSwitchPosition() + private void UpdateQuickSwitchPosition() { if (_viewModel.DialogWindowHandle == nint.Zero) return; - await Task.Delay(300); // If don't give a time, Positioning will be weird. - // Get dialog window rect var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); if (!result) return; @@ -1182,8 +1178,6 @@ private async void UpdateQuickSwitchPosition() Left = HorizonCenter(window); } -#pragma warning restore VSTHRD100 // Avoid async void methods - private double HorizonCenter(Rect window) { var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0); diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0243e97c6f3..61ad4e97c60 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1518,7 +1518,9 @@ public bool ShouldIgnoreHotkeys() private bool PreviousMainWindowVisibilityStatus { get; set; } = true; - public void SetupQuickSwitch(nint handle) +#pragma warning disable VSTHRD100 // Avoid async void methods + + public async void SetupQuickSwitch(nint handle) { if (handle != nint.Zero && DialogWindowHandle != handle) // Only set once for one file dialog { @@ -1527,6 +1529,10 @@ public void SetupQuickSwitch(nint handle) IsQuickSwitch = true; } + await Task.Delay(300); // If don't give a time, Positioning will be weird. + + if (handle == nint.Zero) return; // If handle is null, it means the dialog is closed, so return + if (MainWindowVisibilityStatus) { // Only update the position @@ -1544,6 +1550,8 @@ public void SetupQuickSwitch(nint handle) } } +#pragma warning restore VSTHRD100 // Avoid async void methods + public void ResetQuickSwitch() { DialogWindowHandle = nint.Zero; From 54350885dd53aa8e5fc719f3c2e7bd4a852b35a1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 20:51:12 +0800 Subject: [PATCH 048/243] Hide quick switch window when file dialog is unfocused --- .../QuickSwitch/QuickSwitch.cs | 71 +++++++++++++------ Flow.Launcher.Infrastructure/Win32Helper.cs | 15 ++-- Flow.Launcher/MainWindow.xaml.cs | 1 + Flow.Launcher/ViewModel/MainViewModel.cs | 11 +++ 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index f8959544bcc..eef7537fc77 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -25,6 +25,8 @@ public static class QuickSwitch public static Action ResetQuickSwitchWindow { get; set; } = null; + public static Action HideQuickSwitchWindow { get; set; } = null; + // The class name of a dialog window private const string DialogWindowClassName = "#32770"; @@ -42,6 +44,8 @@ public static class QuickSwitch private static DispatcherTimer _dragMoveTimer = null; + private static HWND _mainWindowHandle = HWND.Null; + private static HWND _dialogWindowHandle = HWND.Null; private static bool _isInitialized = false; @@ -110,6 +114,9 @@ public static void Initialize() return; } + // Initialize main window handle + _mainWindowHandle = Win32Helper.GetMainWindowHandle(); + // Initialize timer _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; _dragMoveTimer.Tick += (s, e) => UpdateQuickSwitchWindow?.Invoke(); @@ -237,15 +244,19 @@ private static void ForegroundChangeCallback( uint dwmsEventTime ) { - // If window is dialog window, show quick switch window and navigate path if needed + // File dialog window is foreground if (GetWindowClassName(hwnd) == DialogWindowClassName) { _dialogWindowHandle = hwnd; + + // Show quick switch window if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); _dragMoveTimer?.Start(); } + + // Navigate path if needed if (_settings.AutoQuickSwitch) { // Showing quick switch window may bring focus @@ -253,35 +264,49 @@ uint dwmsEventTime NavigateDialogPath(); } } - - // If window is explorer window, set _lastExplorerView to the explorer - try + // Quick switch window is foreground + else if (hwnd == _mainWindowHandle) + { + // Nothing to do + } + else { - EnumerateShellWindows((shellWindow) => + if (_dialogWindowHandle != HWND.Null) + { + // Neither quick switch window nor file dialog window is foreground + // Hide quick switch window until the file dialog window is brought to the foreground + HideQuickSwitchWindow?.Invoke(); + } + + // Check if explorer window is foreground + try { - try + EnumerateShellWindows((shellWindow) => { - if (shellWindow is not IWebBrowser2 explorer) + try { - return; - } + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } + + if (explorer.HWND != hwnd.Value) + { + return; + } - if (explorer.HWND != hwnd.Value) + _lastExplorerView = explorer; + } + catch (COMException) { - return; + // Ignored } - - _lastExplorerView = explorer; - } - catch (COMException) - { - // Ignored - } - }); - } - catch (System.Exception e) - { - Log.Exception(ClassName, "Failed to get shell windows", e); + }); + } + catch (System.Exception) + { + // Ignored + } } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 846415d2ebf..9928aa5d363 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -334,6 +334,16 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false) return new(windowHelper.Handle); } + internal static HWND GetMainWindowHandle() + { + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return HWND.Null; + + // Get the FL main window + var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + return hwnd; + } + #endregion #region Keyboard Layout @@ -368,11 +378,8 @@ public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) // No installed English layout found if (enHKL == HKL.Null) return; - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - // Get the FL main window - var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + var hwnd = GetMainWindowHandle(); if (hwnd == HWND.Null) return; // Check if the FL main window is the current foreground window diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0c410c812cc..7fe78752cae 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1163,6 +1163,7 @@ private void InitializeQuickSwitch() QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitch; QuickSwitch.UpdateQuickSwitchWindow = UpdateQuickSwitchPosition; QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; + QuickSwitch.HideQuickSwitchWindow = _viewModel.HideQuickSwitch; } private void UpdateQuickSwitchPosition() diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 61ad4e97c60..c8a96abb2c8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1582,6 +1582,17 @@ public void ResetQuickSwitch() } } + public void HideQuickSwitch() + { + if (DialogWindowHandle != nint.Zero) + { + if (MainWindowVisibilityStatus) + { + Hide(); + } + } + } + #endregion #region Public Methods From 61406eb5c0f8c0ca75061eeb6b8ffed23983b8ea Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Apr 2025 21:12:01 +0800 Subject: [PATCH 049/243] Add lock & Improve explorer view initialization --- .../QuickSwitch/QuickSwitch.cs | 79 +++++++++++++------ Flow.Launcher.Infrastructure/Win32Helper.cs | 5 ++ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index eef7537fc77..d8863d72016 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -32,6 +32,10 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + private static readonly object _lastExplorerViewLock = new(); + + private static readonly object _dialogWindowHandleLock = new(); + private static IWebBrowser2 _lastExplorerView = null; private static UnhookWinEventSafeHandle _foregroundChangeHook = null; @@ -55,15 +59,28 @@ public static void Initialize() if (_isInitialized) return; // Check all foreground windows and check if there are explorer windows - EnumerateShellWindows((shellWindow) => + lock (_lastExplorerViewLock) { - if (shellWindow is not IWebBrowser2 explorer) + var explorerInitialized = false; + EnumerateShellWindows((shellWindow) => { - return; - } + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } - _lastExplorerView = explorer; - }); + // Initialize one explorer window even if it is not foreground + if (!explorerInitialized) + { + _lastExplorerView = explorer; + } + // Force update explorer window if it is foreground + else if (Win32Helper.IsForegroundWindow(explorer.HWND.Value)) + { + _lastExplorerView = explorer; + } + }); + } // Call ForegroundChange when the foreground window changes _foregroundChangeHook = PInvoke.SetWinEventHook( @@ -247,7 +264,10 @@ uint dwmsEventTime // File dialog window is foreground if (GetWindowClassName(hwnd) == DialogWindowClassName) { - _dialogWindowHandle = hwnd; + lock (_dialogWindowHandleLock) + { + _dialogWindowHandle = hwnd; + } // Show quick switch window if (_settings.ShowQuickSwitchWindow) @@ -281,27 +301,30 @@ uint dwmsEventTime // Check if explorer window is foreground try { - EnumerateShellWindows((shellWindow) => + lock (_lastExplorerViewLock) { - try + EnumerateShellWindows((shellWindow) => { - if (shellWindow is not IWebBrowser2 explorer) + try { - return; - } + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } + + if (explorer.HWND != hwnd.Value) + { + return; + } - if (explorer.HWND != hwnd.Value) + _lastExplorerView = explorer; + } + catch (COMException) { - return; + // Ignored } - - _lastExplorerView = explorer; - } - catch (COMException) - { - // Ignored - } - }); + }); + } } catch (System.Exception) { @@ -368,9 +391,12 @@ uint dwmsEventTime try { // If the explorer window is destroyed, set _lastExplorerView to null - if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) + lock (_lastExplorerViewLock) { - _lastExplorerView = null; + if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) + { + _lastExplorerView = null; + } } } catch (COMException) @@ -381,7 +407,10 @@ uint dwmsEventTime // If the dialog window is destroyed, set _dialogWindowHandle to null if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { - _dialogWindowHandle = HWND.Null; + lock (_dialogWindowHandleLock) + { + _dialogWindowHandle = HWND.Null; + } ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 9928aa5d363..c1975687bcf 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -133,6 +133,11 @@ public static bool IsForegroundWindow(Window window) return IsForegroundWindow(GetWindowHandle(window)); } + public static bool IsForegroundWindow(nint handle) + { + return IsForegroundWindow(new HWND(handle)); + } + internal static bool IsForegroundWindow(HWND handle) { return handle.Equals(PInvoke.GetForegroundWindow()); From 76c0003e81b458c9d3b694aa82029ace0a2b4fde Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 12:07:59 +0800 Subject: [PATCH 050/243] New api interfaces --- Flow.Launcher.Core/Plugin/PluginManager.cs | 29 +++++ .../QuickSwitch/QuickSwitch.cs | 1 + .../Interfaces/IAsyncQuickSwitch.cs | 24 ++++ .../Interfaces/IQuickSwitch.cs | 29 +++++ Flow.Launcher.Plugin/QuickSwitchResult.cs | 95 ++++++++++++++ Flow.Launcher.Plugin/Result.cs | 15 +-- Flow.Launcher/ViewModel/MainViewModel.cs | 120 ++++++++++++++---- Plugins/Flow.Launcher.Plugin.Explorer/Main.cs | 19 ++- .../Search/ResultManager.cs | 16 +-- 9 files changed, 294 insertions(+), 54 deletions(-) create mode 100644 Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs create mode 100644 Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs create mode 100644 Flow.Launcher.Plugin/QuickSwitchResult.cs diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 72303c8b754..330c5f05cba 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -318,6 +318,35 @@ public static async Task> QueryForPluginAsync(PluginPair pair, Quer return results; } + public static async Task> QueryQuickSwitchForPluginAsync(PluginPair pair, Query query, CancellationToken token) + { + var results = new List(); + var metadata = pair.Metadata; + + try + { + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + async () => results = await ((IAsyncQuickSwitch)pair.Plugin).QueryQuickSwitchAsync(query, token).ConfigureAwait(false)); + + token.ThrowIfCancellationRequested(); + if (results == null) + return null; + UpdatePluginMetadata(results, metadata, query); + + token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return null; + } + catch (Exception) + { + return null; + } + return results; + } + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index d8863d72016..ce3ae4085f8 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -230,6 +230,7 @@ static bool CheckPath(string path) if (!Path.IsPathRooted(path)) return false; // Is folder? if (!Directory.Exists(path)) return false; + // Is file? return true; } } diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs new file mode 100644 index 00000000000..f044ae48367 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Quick Switch Model + /// + public interface IAsyncQuickSwitch + { + /// + /// Asynchronous querying for quick switch window + /// + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncQuickSwitch interface + /// + /// Query to search + /// Cancel when querying job is obsolete + /// + Task> QueryQuickSwitchAsync(Query query, CancellationToken token); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs new file mode 100644 index 00000000000..5e43a73acf2 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Synchronous Quick Switch Model + /// + /// If the Querying method requires high IO transmission + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncQuickSwitch interface + /// + /// + public interface IQuickSwitch : IAsyncQuickSwitch + { + /// + /// Querying for quick switch window + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// Query to search + /// + List QueryQuickSwitch(Query query); + + Task> IAsyncQuickSwitch.QueryQuickSwitchAsync(Query query, CancellationToken token) => Task.Run(() => QueryQuickSwitch(query)); + } +} diff --git a/Flow.Launcher.Plugin/QuickSwitchResult.cs b/Flow.Launcher.Plugin/QuickSwitchResult.cs new file mode 100644 index 00000000000..b5dfc1e8ea2 --- /dev/null +++ b/Flow.Launcher.Plugin/QuickSwitchResult.cs @@ -0,0 +1,95 @@ +namespace Flow.Launcher.Plugin +{ + /// + /// Describes a result of a executed by a plugin in quick switch window + /// + public class QuickSwitchResult : Result + { + /// + /// This holds the path which can be provided by plugin to be navigated to the + /// file dialog when records in quick switch window is right clicked on a result. + /// + /// + /// If this path is file path, Flow will use its directory path instead. + /// + public required string QuickSwitchPath { get; init; } + + /// + /// Clones the current quick switch result + /// + public new QuickSwitchResult Clone() + { + return new QuickSwitchResult + { + Title = Title, + SubTitle = SubTitle, + ActionKeywordAssigned = ActionKeywordAssigned, + CopyText = CopyText, + AutoCompleteText = AutoCompleteText, + IcoPath = IcoPath, + BadgeIcoPath = BadgeIcoPath, + RoundedIcon = RoundedIcon, + Icon = Icon, + BadgeIcon = BadgeIcon, + Glyph = Glyph, + Action = Action, + AsyncAction = AsyncAction, + Score = Score, + TitleHighlightData = TitleHighlightData, + OriginQuery = OriginQuery, + PluginDirectory = PluginDirectory, + ContextData = ContextData, + PluginID = PluginID, + TitleToolTip = TitleToolTip, + SubTitleToolTip = SubTitleToolTip, + PreviewPanel = PreviewPanel, + ProgressBar = ProgressBar, + ProgressBarColor = ProgressBarColor, + Preview = Preview, + AddSelectedCount = AddSelectedCount, + RecordKey = RecordKey, + ShowBadge = ShowBadge, + QuickSwitchPath = QuickSwitchPath + }; + } + + /// + /// Convert to . + /// + public static QuickSwitchResult From(Result result, string quickSwitchPath) + { + return new QuickSwitchResult + { + Title = result.Title, + SubTitle = result.SubTitle, + ActionKeywordAssigned = result.ActionKeywordAssigned, + CopyText = result.CopyText, + AutoCompleteText = result.AutoCompleteText, + IcoPath = result.IcoPath, + BadgeIcoPath = result.BadgeIcoPath, + RoundedIcon = result.RoundedIcon, + Icon = result.Icon, + BadgeIcon = result.BadgeIcon, + Glyph = result.Glyph, + Action = result.Action, + AsyncAction = result.AsyncAction, + Score = result.Score, + TitleHighlightData = result.TitleHighlightData, + OriginQuery = result.OriginQuery, + PluginDirectory = result.PluginDirectory, + ContextData = result.ContextData, + PluginID = result.PluginID, + TitleToolTip = result.TitleToolTip, + SubTitleToolTip = result.SubTitleToolTip, + PreviewPanel = result.PreviewPanel, + ProgressBar = result.ProgressBar, + ProgressBarColor = result.ProgressBarColor, + Preview = result.Preview, + AddSelectedCount = result.AddSelectedCount, + RecordKey = result.RecordKey, + ShowBadge = result.ShowBadge, + QuickSwitchPath = quickSwitchPath + }; + } + } +} diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index c247208ad00..ecaec1abb12 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -257,17 +257,6 @@ public string PluginDirectory /// public bool ShowBadge { get; set; } = false; - /// - /// Determines if the result can be shown in quick switch window. - /// - public bool AllowQuickSwitch { get; set; } = false; - - /// - /// This holds the path which can be provided by plugin to be navigated to the - /// file dialog when records in quick switch window is right clicked on a result. - /// - public string QuickSwitchPath { get; set; } - /// /// Run this result, asynchronously /// @@ -318,9 +307,7 @@ public Result Clone() Preview = Preview, AddSelectedCount = AddSelectedCount, RecordKey = RecordKey, - ShowBadge = ShowBadge, - AllowQuickSwitch = AllowQuickSwitch, - QuickSwitchPath = QuickSwitchPath, + ShowBadge = ShowBadge }; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c8a96abb2c8..c8f7f4e0a9b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; using System.Linq; @@ -51,6 +52,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private Task _resultsViewUpdateTask; private readonly IReadOnlyList _emptyResult = new List(); + private readonly IReadOnlyList _emptyQuickSwitchResult = new List(); #endregion @@ -244,8 +246,16 @@ public void RegisterResultsUpdatedEvent() var token = e.Token == default ? _updateToken : e.Token; - // make a clone to avoid possible issue that plugin will also change the list and items when updating view model - var resultsCopy = CheckQuickSwitchAndDeepClone(e.Results, token); + IReadOnlyList resultsCopy; + if (e.Results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a clone to avoid possible issue that plugin will also change the list and items when updating view model + resultsCopy = DeepClone(e.Results, token); + } foreach (var result in resultsCopy) { @@ -354,8 +364,11 @@ private void LoadContextMenu() if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { var result = SelectedResults.SelectedItem.Result; - Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(result.QuickSwitchPath); + if (result is QuickSwitchResult quickSwitchResult) + { + Win32Helper.SetForegroundWindow(DialogWindowHandle); + QuickSwitch.JumpToPath(quickSwitchResult.QuickSwitchPath); + } } } // For query mode, we load context menu @@ -464,7 +477,7 @@ private async Task OpenResultAsync(string index) } } - private IReadOnlyList CheckQuickSwitchAndDeepClone(IReadOnlyList results, CancellationToken token = default) + private static IReadOnlyList DeepClone(IReadOnlyList results, CancellationToken token = default) { var resultsCopy = new List(); @@ -475,9 +488,22 @@ private IReadOnlyList CheckQuickSwitchAndDeepClone(IReadOnlyList break; } - if (IsQuickSwitch && !result.AllowQuickSwitch) + var resultCopy = result.Clone(); + resultsCopy.Add(resultCopy); + } + + return resultsCopy; + } + + private static IReadOnlyList DeepClone(IReadOnlyList results, CancellationToken token = default) + { + var resultsCopy = new List(); + + foreach (var result in results.ToList()) + { + if (token.IsCancellationRequested) { - continue; + break; } var resultCopy = result.Clone(); @@ -1191,6 +1217,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); var plugins = PluginManager.ValidPluginsForQuery(query); + var quickSwitch = IsQuickSwitch; // save quick switch state + + if (quickSwitch) + { + // Select for IAsyncQuickSwitch + plugins = new Collection(plugins.Where(p => p.Plugin is IAsyncQuickSwitch).ToList()); + } if (query == null || plugins.Count == 0) // shortcut expanded { @@ -1305,34 +1338,69 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = await PluginManager.QueryForPluginAsync(plugin, query, token); + if (quickSwitch) + { + var results = await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token); - if (token.IsCancellationRequested) - return; + if (token.IsCancellationRequested) + return; - IReadOnlyList resultsCopy; - if (results == null) - { - resultsCopy = _emptyResult; + IReadOnlyList resultsCopy; + if (results == null) + { + resultsCopy = _emptyQuickSwitchResult; + } + else + { + // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. + resultsCopy = DeepClone(results, token); + } + + foreach (var result in resultsCopy) + { + if (string.IsNullOrEmpty(result.BadgeIcoPath)) + { + result.BadgeIcoPath = plugin.Metadata.IcoPath; + } + } + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, + token, reSelect))) + { + App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); + } } else { - // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = CheckQuickSwitchAndDeepClone(results, token); - } + var results = await PluginManager.QueryForPluginAsync(plugin, query, token); - foreach (var result in resultsCopy) - { - if (string.IsNullOrEmpty(result.BadgeIcoPath)) + if (token.IsCancellationRequested) + return; + + IReadOnlyList resultsCopy; + if (results == null) { - result.BadgeIcoPath = plugin.Metadata.IcoPath; + resultsCopy = _emptyResult; + } + else + { + // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. + resultsCopy = DeepClone(results, token); } - } - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - token, reSelect))) - { - App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); + foreach (var result in resultsCopy) + { + if (string.IsNullOrEmpty(result.BadgeIcoPath)) + { + result.BadgeIcoPath = plugin.Metadata.IcoPath; + } + } + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, + token, reSelect))) + { + App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); + } } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index e4056131d4b..1b81edec6cc 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -8,13 +8,13 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using System.Windows; using System.Windows.Controls; using Flow.Launcher.Plugin.Explorer.Exceptions; +using System.Linq; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncQuickSwitch { internal static PluginInitContext Context { get; set; } @@ -26,6 +26,8 @@ public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n private SearchManager searchManager; + private static readonly List _emptyQuickSwitchResultList = new(); + public Control CreateSettingPanel() { return new ExplorerSettings(viewModel); @@ -96,5 +98,18 @@ public string GetTranslatedPluginDescription() { return Context.API.GetTranslation("plugin_explorer_plugin_description"); } + + public async Task> QueryQuickSwitchAsync(Query query, CancellationToken token) + { + try + { + var results = await searchManager.SearchAsync(query, token); + return results.Select(r => QuickSwitchResult.From(r, r.CopyText)).ToList(); + } + catch (Exception e) when (e is SearchException or EngineNotAvailableException) + { + return _emptyQuickSwitchResultList; + } + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index afb23505c54..b7991d28ef2 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -164,9 +164,7 @@ internal static Result CreateFolderResult(string title, string subtitle, string Score = score, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"), SubTitleToolTip = path, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }, - AllowQuickSwitch = true, - QuickSwitchPath = path + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed } }; } @@ -206,9 +204,7 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK }, TitleToolTip = path, SubTitleToolTip = path, - ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed }, - AllowQuickSwitch = true, - QuickSwitchPath = path + ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed } }; } @@ -264,9 +260,7 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK OpenFolder(folderPath); return true; }, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed }, - AllowQuickSwitch = true, - QuickSwitchPath = folderPath + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed } }; } @@ -325,9 +319,7 @@ internal static Result CreateFileResult(string filePath, Query query, int score }, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"), SubTitleToolTip = filePath, - ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }, - AllowQuickSwitch = true, - QuickSwitchPath = directory + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed } }; return result; } From e6c4d0b73e1778278c0fe85599c76289056fa0a3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 12:10:42 +0800 Subject: [PATCH 051/243] Fix code comments --- Flow.Launcher.Plugin/QuickSwitchResult.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Flow.Launcher.Plugin/QuickSwitchResult.cs b/Flow.Launcher.Plugin/QuickSwitchResult.cs index b5dfc1e8ea2..0940bf85fb3 100644 --- a/Flow.Launcher.Plugin/QuickSwitchResult.cs +++ b/Flow.Launcher.Plugin/QuickSwitchResult.cs @@ -9,9 +9,6 @@ public class QuickSwitchResult : Result /// This holds the path which can be provided by plugin to be navigated to the /// file dialog when records in quick switch window is right clicked on a result. /// - /// - /// If this path is file path, Flow will use its directory path instead. - /// public required string QuickSwitchPath { get; init; } /// From e111d227fbc505a044b3779cb2f01e60d2ec9ec4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 17:31:43 +0800 Subject: [PATCH 052/243] Remove blank lines --- Flow.Launcher.Infrastructure/Win32Helper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 238b4321657..db235cb9ade 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -718,8 +718,6 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) return true; } - - #endregion } } From 14185990f2e259193c2effd2b99d55dacc9c8b7f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 18:33:17 +0800 Subject: [PATCH 053/243] Support file path open --- .../QuickSwitch/QuickSwitch.cs | 22 +++++++--- Flow.Launcher.Infrastructure/Win32Helper.cs | 40 ++++++++++++++++--- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index ce3ae4085f8..c2ad39b1d65 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -204,7 +204,7 @@ private static void NavigateDialogPath() public static bool JumpToPath(string path) { - if (!CheckPath(path)) return false; + if (!CheckPath(path, out var isFile)) return false; var t = new Thread(() => { @@ -217,21 +217,31 @@ public static bool JumpToPath(string path) }; // Assume that the dialog is in the foreground now - Win32Helper.DirJump(path, Win32Helper.GetForegroundWindow()); + if (isFile) + { + Win32Helper.FileJump(path, Win32Helper.GetForegroundWindow()); + } + else + { + Win32Helper.DirJump(path, Win32Helper.GetForegroundWindow()); + } }); t.Start(); return true; - static bool CheckPath(string path) + static bool CheckPath(string path, out bool file) { - // Is non-null + file = false; + // Is non-null? if (string.IsNullOrEmpty(path)) return false; // Is absolute? if (!Path.IsPathRooted(path)) return false; // Is folder? - if (!Directory.Exists(path)) return false; + var isFolder = Directory.Exists(path); // Is file? - return true; + var isFile = File.Exists(path); + file = isFile; + return isFolder || isFile; } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index db235cb9ade..d32682510eb 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows; @@ -620,7 +621,17 @@ public static void OpenImeSettings() private static readonly InputSimulator _inputSimulator = new(); - public static bool DirJump(string path, nint dialog, bool altD = true) + public static bool FileJump(string filePath, nint dialog, bool altD = true) + { + return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialog, altD); + } + + public static bool DirJump(string dirPath, nint dialog, bool altD = true) + { + return DirFileJump(dirPath, null, dialog, altD); + } + + private static bool DirFileJump(string dirPath, string filePath, nint dialog, bool altD = true, bool editFileName = false) { // Get the handle of the dialog window var dialogHandle = new HWND(dialog); @@ -635,6 +646,12 @@ public static bool DirJump(string path, nint dialog, bool altD = true) _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); } + // Directly edit file name + if (editFileName) + { + return DirFileJumpForFileName(filePath, dialogHandle); + } + // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. var controlHandle = PInvoke.FindWindowEx(new(dialogHandle), HWND.Null, "WorkerW", null); @@ -644,7 +661,9 @@ public static bool DirJump(string path, nint dialog, bool altD = true) controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBoxEx32", null); if (controlHandle == HWND.Null) { - return DirJumpOnLegacyDialog(path, dialogHandle); + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we edit file name text box directly. + return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle); } var timeOut = !SpinWait.SpinUntil(() => @@ -664,15 +683,23 @@ public static bool DirJump(string path, nint dialog, bool altD = true) return false; } - SetWindowText(editHandle, path); + SetWindowText(editHandle, dirPath); _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + if (!string.IsNullOrEmpty(filePath)) + { + // After navigating to the path, we then set the file name. + return DirFileJump(null, Path.GetFileName(filePath), dialog, altD, true); + } + return true; } - private static bool DirJumpOnLegacyDialog(string path, HWND dialogHandle) + /// + /// Edit file name text box in the file open dialog. + /// + private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle) { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Edit", null); @@ -681,7 +708,8 @@ private static bool DirJumpOnLegacyDialog(string path, HWND dialogHandle) return false; } - SetWindowText(controlHandle, path); + SetWindowText(controlHandle, fileName); + // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); From dbb33ebcd5cc8210c21c108539fc7f661c8751a7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 18:36:24 +0800 Subject: [PATCH 054/243] Remove useless keyboard input --- Flow.Launcher.Infrastructure/Win32Helper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index d32682510eb..5e615178a28 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -636,6 +636,12 @@ private static bool DirFileJump(string dirPath, string filePath, nint dialog, bo // Get the handle of the dialog window var dialogHandle = new HWND(dialog); + // Directly edit file name input box. + if (editFileName) + { + return DirFileJumpForFileName(filePath, dialogHandle); + } + // Alt-D or Ctrl-L to focus on the path input box if (altD) { @@ -646,12 +652,6 @@ private static bool DirFileJump(string dirPath, string filePath, nint dialog, bo _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); } - // Directly edit file name - if (editFileName) - { - return DirFileJumpForFileName(filePath, dialogHandle); - } - // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. var controlHandle = PInvoke.FindWindowEx(new(dialogHandle), HWND.Null, "WorkerW", null); From 3aec2a170eda510f80d3ccf256f3e838fddbb6e0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Apr 2025 20:54:49 +0800 Subject: [PATCH 055/243] Fix handle clear check --- 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 c8f7f4e0a9b..864b7559e20 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1599,7 +1599,7 @@ public async void SetupQuickSwitch(nint handle) await Task.Delay(300); // If don't give a time, Positioning will be weird. - if (handle == nint.Zero) return; // If handle is null, it means the dialog is closed, so return + if (DialogWindowHandle == nint.Zero) return; // If handle is cleared, which means the dialog is closed, do nothing if (MainWindowVisibilityStatus) { From 0e024163d74b82ebe834a29ebd2c187010b77742 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 08:11:07 +0800 Subject: [PATCH 056/243] Improve auto quick switch --- .../QuickSwitch/QuickSwitch.cs | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index c2ad39b1d65..09ccfb68b05 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -150,7 +150,7 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) } } - private static void NavigateDialogPath() + private static void NavigateDialogPath(Action action = null) { object document = null; try @@ -199,10 +199,10 @@ private static void NavigateDialogPath() return; } - JumpToPath(path); + JumpToPath(path, action); } - public static bool JumpToPath(string path) + public static bool JumpToPath(string path, Action action = null) { if (!CheckPath(path, out var isFile)) return false; @@ -225,6 +225,9 @@ public static bool JumpToPath(string path) { Win32Helper.DirJump(path, Win32Helper.GetForegroundWindow()); } + + // Invoke action if provided + action?.Invoke(); }); t.Start(); return true; @@ -272,7 +275,7 @@ private static void ForegroundChangeCallback( uint dwmsEventTime ) { - // File dialog window is foreground + // File dialog window if (GetWindowClassName(hwnd) == DialogWindowClassName) { lock (_dialogWindowHandleLock) @@ -280,22 +283,30 @@ uint dwmsEventTime _dialogWindowHandle = hwnd; } + // Navigate to path + if (_settings.AutoQuickSwitch) + { + Win32Helper.SetForegroundWindow(hwnd); + NavigateDialogPath(() => + { + // Show quick switch window after path is navigated + if (_settings.ShowQuickSwitchWindow) + { + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + _dragMoveTimer?.Start(); + } + }); + return; + } + // Show quick switch window if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); _dragMoveTimer?.Start(); } - - // Navigate path if needed - if (_settings.AutoQuickSwitch) - { - // Showing quick switch window may bring focus - Win32Helper.SetForegroundWindow(hwnd); - NavigateDialogPath(); - } } - // Quick switch window is foreground + // Quick switch window else if (hwnd == _mainWindowHandle) { // Nothing to do From 854cc696239bebb952a4c9e593d02c0a525e8031 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 09:56:46 +0800 Subject: [PATCH 057/243] Check handle invalid --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 864b7559e20..a31684f03c2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1590,7 +1590,9 @@ public bool ShouldIgnoreHotkeys() public async void SetupQuickSwitch(nint handle) { - if (handle != nint.Zero && DialogWindowHandle != handle) // Only set once for one file dialog + if (handle == nint.Zero) return; + + if (DialogWindowHandle != handle) // Only set once for one file dialog { PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; From f337d8849a8cd6ef04e61e08c3106232cc2a8872 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 10:02:31 +0800 Subject: [PATCH 058/243] Remove useless codes --- .../QuickSwitch/QuickSwitch.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 09ccfb68b05..31afe3ccfe3 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -410,22 +410,6 @@ private static void DestroyChangeCallback( uint dwmsEventTime ) { - try - { - // If the explorer window is destroyed, set _lastExplorerView to null - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null && _lastExplorerView.HWND == hwnd.Value) - { - _lastExplorerView = null; - } - } - } - catch (COMException) - { - // Ignored - } - // If the dialog window is destroyed, set _dialogWindowHandle to null if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { From 79da928fb4e8daddd49f3e618de25c40f0d03d50 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 10:04:28 +0800 Subject: [PATCH 059/243] Do not fill twice & Add lock for stability --- .../QuickSwitch/QuickSwitch.cs | 101 ++++++++++++++---- Flow.Launcher.Infrastructure/Win32Helper.cs | 24 +++-- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 31afe3ccfe3..22ec68e0bdd 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -48,6 +49,13 @@ public static class QuickSwitch private static DispatcherTimer _dragMoveTimer = null; + // A list of all file dialog windows that are auto switched already + private static readonly List _autoSwitchedDialogs = new(); + + private static readonly object _autoSwitchedDialogsLock = new(); + + private static readonly SemaphoreSlim _navigationLock = new(1, 1); + private static HWND _mainWindowHandle = HWND.Null; private static HWND _dialogWindowHandle = HWND.Null; @@ -199,14 +207,16 @@ private static void NavigateDialogPath(Action action = null) return; } + Log.Debug(ClassName, $"Path: {path}"); JumpToPath(path, action); } - public static bool JumpToPath(string path, Action action = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] + public static void JumpToPath(string path, Action action = null) { - if (!CheckPath(path, out var isFile)) return false; + if (!CheckPath(path, out var isFile)) return; - var t = new Thread(() => + var t = new Thread(async () => { // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. @@ -217,20 +227,47 @@ public static bool JumpToPath(string path, Action action = null) }; // Assume that the dialog is in the foreground now - if (isFile) + await _navigationLock.WaitAsync(); + try + { + var dialog = Win32Helper.GetForegroundWindowHWND(); + + bool result; + if (isFile) + { + result = Win32Helper.FileJump(path, dialog); + } + else + { + result = Win32Helper.DirJump(path, dialog); + } + + if (result) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Add(dialog); + } + } + else + { + Log.Error(ClassName, "Failed to jump to path"); + } + } + catch (System.Exception e) { - Win32Helper.FileJump(path, Win32Helper.GetForegroundWindow()); + Log.Exception(ClassName, "Failed to jump to path", e); } - else + finally { - Win32Helper.DirJump(path, Win32Helper.GetForegroundWindow()); + _navigationLock.Release(); } - + // Invoke action if provided action?.Invoke(); }); t.Start(); - return true; + return; static bool CheckPath(string path, out bool file) { @@ -278,6 +315,8 @@ uint dwmsEventTime // File dialog window if (GetWindowClassName(hwnd) == DialogWindowClassName) { + Log.Debug(ClassName, $"Hwnd: {hwnd}"); + lock (_dialogWindowHandleLock) { _dialogWindowHandle = hwnd; @@ -286,24 +325,43 @@ uint dwmsEventTime // Navigate to path if (_settings.AutoQuickSwitch) { - Win32Helper.SetForegroundWindow(hwnd); - NavigateDialogPath(() => + // Check if we have already switched for this dialog + bool alreadySwitched; + lock (_dialogWindowHandleLock) + { + alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); + } + + // Just show quick switch window + if (alreadySwitched) { - // Show quick switch window after path is navigated if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); _dragMoveTimer?.Start(); } - }); - return; + } + // Show quick switch window after navigating the path + else + { + NavigateDialogPath(() => + { + if (_settings.ShowQuickSwitchWindow) + { + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + _dragMoveTimer?.Start(); + } + }); + } } - - // Show quick switch window - if (_settings.ShowQuickSwitchWindow) + else { - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); + // Show quick switch window + if (_settings.ShowQuickSwitchWindow) + { + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + _dragMoveTimer?.Start(); + } } } // Quick switch window @@ -413,10 +471,15 @@ uint dwmsEventTime // If the dialog window is destroyed, set _dialogWindowHandle to null if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { + Log.Debug(ClassName, $"Dialog Hwnd: {hwnd}"); lock (_dialogWindowHandleLock) { _dialogWindowHandle = HWND.Null; } + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 5e615178a28..e75f2b7ad94 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -116,7 +116,12 @@ public static unsafe string GetWallpaperPath() public static nint GetForegroundWindow() { - return PInvoke.GetForegroundWindow().Value; + return GetForegroundWindowHWND().Value; + } + + internal static HWND GetForegroundWindowHWND() + { + return PInvoke.GetForegroundWindow(); } public static bool SetForegroundWindow(Window window) @@ -621,21 +626,18 @@ public static void OpenImeSettings() private static readonly InputSimulator _inputSimulator = new(); - public static bool FileJump(string filePath, nint dialog, bool altD = true) + internal static bool FileJump(string filePath, HWND dialogHandle, bool altD = true) { - return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialog, altD); + return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle, altD); } - public static bool DirJump(string dirPath, nint dialog, bool altD = true) + internal static bool DirJump(string dirPath, HWND dialogHandle, bool altD = true) { - return DirFileJump(dirPath, null, dialog, altD); + return DirFileJump(dirPath, null, dialogHandle, altD); } - private static bool DirFileJump(string dirPath, string filePath, nint dialog, bool altD = true, bool editFileName = false) + private static bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true, bool editFileName = false) { - // Get the handle of the dialog window - var dialogHandle = new HWND(dialog); - // Directly edit file name input box. if (editFileName) { @@ -654,7 +656,7 @@ private static bool DirFileJump(string dirPath, string filePath, nint dialog, bo // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. - var controlHandle = PInvoke.FindWindowEx(new(dialogHandle), HWND.Null, "WorkerW", null); + var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "WorkerW", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ReBarWindow32", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Address Band Root", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "msctls_progress32", null); @@ -689,7 +691,7 @@ private static bool DirFileJump(string dirPath, string filePath, nint dialog, bo if (!string.IsNullOrEmpty(filePath)) { // After navigating to the path, we then set the file name. - return DirFileJump(null, Path.GetFileName(filePath), dialog, altD, true); + return DirFileJump(null, Path.GetFileName(filePath), dialogHandle, altD, true); } return true; From d6e69b29451a3a0d7ad2ee1fca2104ea66247ffd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 10:08:45 +0800 Subject: [PATCH 060/243] Initialize window visibility --- Flow.Launcher/MainWindow.xaml.cs | 2 ++ Flow.Launcher/ViewModel/MainViewModel.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 7fe78752cae..1e041604f4b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -135,10 +135,12 @@ private async void OnLoaded(object sender, RoutedEventArgs e) if (_settings.HideOnStartup) { _viewModel.Hide(); + _viewModel.InitializeVisibilityStatus(false); } else { _viewModel.Show(); + _viewModel.InitializeVisibilityStatus(true); } // Show notify icon when flowlauncher is hidden diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a31684f03c2..10853b4b9e7 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1584,7 +1584,12 @@ public bool ShouldIgnoreHotkeys() public bool IsQuickSwitch { get; private set; } public nint DialogWindowHandle { get; private set; } = nint.Zero; - private bool PreviousMainWindowVisibilityStatus { get; set; } = true; + private bool PreviousMainWindowVisibilityStatus { get; set; } + + public void InitializeVisibilityStatus(bool visibilityStatus) + { + PreviousMainWindowVisibilityStatus = visibilityStatus; + } #pragma warning disable VSTHRD100 // Avoid async void methods From 6484ec05e3bcd53c1fa56db7bba7d8df5d59dd3c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 18:12:38 +0800 Subject: [PATCH 061/243] Resolve conflicts --- Flow.Launcher.Infrastructure/Win32Helper.cs | 135 ++++++++++---------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 8362376d609..cbedf0d98d2 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -624,6 +624,74 @@ public static void OpenImeSettings() #endregion + #region System Font + + private static readonly Dictionary _languageToNotoSans = new() + { + { "ko", "Noto Sans KR" }, + { "ja", "Noto Sans JP" }, + { "zh-CN", "Noto Sans SC" }, + { "zh-SG", "Noto Sans SC" }, + { "zh-Hans", "Noto Sans SC" }, + { "zh-TW", "Noto Sans TC" }, + { "zh-HK", "Noto Sans TC" }, + { "zh-MO", "Noto Sans TC" }, + { "zh-Hant", "Noto Sans TC" }, + { "th", "Noto Sans Thai" }, + { "ar", "Noto Sans Arabic" }, + { "he", "Noto Sans Hebrew" }, + { "hi", "Noto Sans Devanagari" }, + { "bn", "Noto Sans Bengali" }, + { "ta", "Noto Sans Tamil" }, + { "el", "Noto Sans Greek" }, + { "ru", "Noto Sans" }, + { "en", "Noto Sans" }, + { "fr", "Noto Sans" }, + { "de", "Noto Sans" }, + { "es", "Noto Sans" }, + { "pt", "Noto Sans" } + }; + + public static string GetSystemDefaultFont() + { + try + { + var culture = CultureInfo.CurrentCulture; + var language = culture.Name; // e.g., "zh-TW" + var langPrefix = language.Split('-')[0]; // e.g., "zh" + + // First, try to find by full name, and if not found, fallback to prefix + if (TryGetNotoFont(language, out var notoFont) || TryGetNotoFont(langPrefix, out notoFont)) + { + // If the font is installed, return it + if (Fonts.SystemFontFamilies.Any(f => f.Source.Equals(notoFont))) + { + return notoFont; + } + } + + // If Noto font is not found, fallback to the system default font + var font = SystemFonts.MessageFontFamily; + if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-US"), out var englishName)) + { + return englishName; + } + + return font.Source ?? "Segoe UI"; + } + catch + { + return "Segoe UI"; + } + } + + private static bool TryGetNotoFont(string langKey, out string notoFont) + { + return _languageToNotoSans.TryGetValue(langKey, out notoFont); + } + + #endregion + #region Quick Switch // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump @@ -750,73 +818,6 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) rect.bottom - rect.top ); return true; - - #endregion - - #region System Font - - private static readonly Dictionary _languageToNotoSans = new() - { - { "ko", "Noto Sans KR" }, - { "ja", "Noto Sans JP" }, - { "zh-CN", "Noto Sans SC" }, - { "zh-SG", "Noto Sans SC" }, - { "zh-Hans", "Noto Sans SC" }, - { "zh-TW", "Noto Sans TC" }, - { "zh-HK", "Noto Sans TC" }, - { "zh-MO", "Noto Sans TC" }, - { "zh-Hant", "Noto Sans TC" }, - { "th", "Noto Sans Thai" }, - { "ar", "Noto Sans Arabic" }, - { "he", "Noto Sans Hebrew" }, - { "hi", "Noto Sans Devanagari" }, - { "bn", "Noto Sans Bengali" }, - { "ta", "Noto Sans Tamil" }, - { "el", "Noto Sans Greek" }, - { "ru", "Noto Sans" }, - { "en", "Noto Sans" }, - { "fr", "Noto Sans" }, - { "de", "Noto Sans" }, - { "es", "Noto Sans" }, - { "pt", "Noto Sans" } - }; - - public static string GetSystemDefaultFont() - { - try - { - var culture = CultureInfo.CurrentCulture; - var language = culture.Name; // e.g., "zh-TW" - var langPrefix = language.Split('-')[0]; // e.g., "zh" - - // First, try to find by full name, and if not found, fallback to prefix - if (TryGetNotoFont(language, out var notoFont) || TryGetNotoFont(langPrefix, out notoFont)) - { - // If the font is installed, return it - if (Fonts.SystemFontFamilies.Any(f => f.Source.Equals(notoFont))) - { - return notoFont; - } - } - - // If Noto font is not found, fallback to the system default font - var font = SystemFonts.MessageFontFamily; - if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-US"), out var englishName)) - { - return englishName; - } - - return font.Source ?? "Segoe UI"; - } - catch - { - return "Segoe UI"; - } - } - - private static bool TryGetNotoFont(string langKey, out string notoFont) - { - return _languageToNotoSans.TryGetValue(langKey, out notoFont); } #endregion From b3ff92fd780e58c4b647496193d1de17872fe356 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 18:13:59 +0800 Subject: [PATCH 062/243] Ensure thread safety for _lastExplorerView in NavigateDialogPath method --- .../QuickSwitch/QuickSwitch.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 22ec68e0bdd..4a681362085 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -163,12 +163,15 @@ private static void NavigateDialogPath(Action action = null) object document = null; try { - if (_lastExplorerView != null) + lock (_lastExplorerViewLock) { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; + if (_lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _lastExplorerView; + document = explorerView.Document; + } } } catch (COMException) From e442f361ed773f8b802a31a0c309d9c8a0d6418d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 18:25:12 +0800 Subject: [PATCH 063/243] Improve navigation handle check --- .../QuickSwitch/QuickSwitch.cs | 23 ++++++++++--------- Flow.Launcher/ViewModel/MainViewModel.cs | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 4a681362085..0623a5c6e4c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -154,11 +154,11 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { if (_isInitialized) { - NavigateDialogPath(); + NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); } } - private static void NavigateDialogPath(Action action = null) + private static void NavigateDialogPath(HWND dialog, Action action = null) { object document = null; try @@ -210,12 +210,11 @@ private static void NavigateDialogPath(Action action = null) return; } - Log.Debug(ClassName, $"Path: {path}"); - JumpToPath(path, action); + JumpToPath(dialog.Value, path, action); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] - public static void JumpToPath(string path, Action action = null) + public static void JumpToPath(nint dialog, string path, Action action = null) { if (!CheckPath(path, out var isFile)) return; @@ -223,7 +222,7 @@ public static void JumpToPath(string path, Action action = null) { // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. - var timeOut = !SpinWait.SpinUntil(() => GetWindowClassName(PInvoke.GetForegroundWindow()) == DialogWindowClassName, 1000); + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindow() == dialog, 1000); if (timeOut) { return; @@ -233,23 +232,25 @@ public static void JumpToPath(string path, Action action = null) await _navigationLock.WaitAsync(); try { - var dialog = Win32Helper.GetForegroundWindowHWND(); + var dialogHandle = new HWND(dialog); bool result; if (isFile) { - result = Win32Helper.FileJump(path, dialog); + result = Win32Helper.FileJump(path, dialogHandle); + Log.Debug(ClassName, $"File Jump: {path}"); } else { - result = Win32Helper.DirJump(path, dialog); + result = Win32Helper.DirJump(path, dialogHandle); + Log.Debug(ClassName, $"Dir Jump: {path}"); } if (result) { lock (_autoSwitchedDialogsLock) { - _autoSwitchedDialogs.Add(dialog); + _autoSwitchedDialogs.Add(dialogHandle); } } else @@ -347,7 +348,7 @@ uint dwmsEventTime // Show quick switch window after navigating the path else { - NavigateDialogPath(() => + NavigateDialogPath(hwnd, () => { if (_settings.ShowQuickSwitchWindow) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 10853b4b9e7..24ebc5d793c 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -24,6 +24,7 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Microsoft.VisualStudio.Threading; +using Windows.Win32; namespace Flow.Launcher.ViewModel { @@ -367,7 +368,7 @@ private void LoadContextMenu() if (result is QuickSwitchResult quickSwitchResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(quickSwitchResult.QuickSwitchPath); + QuickSwitch.JumpToPath(Win32Helper.GetForegroundWindow(), quickSwitchResult.QuickSwitchPath); } } } From 5b5cac6d4230c29d8a9f51ebb860f1c4a42e0c9b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 18:27:18 +0800 Subject: [PATCH 064/243] Fix lock issue --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0623a5c6e4c..426f6639e9b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -331,7 +331,7 @@ uint dwmsEventTime { // Check if we have already switched for this dialog bool alreadySwitched; - lock (_dialogWindowHandleLock) + lock (_autoSwitchedDialogsLock) { alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); } From e57816d9ac6255864855264643bc53dfd6b01ebc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 18:29:01 +0800 Subject: [PATCH 065/243] Improve quick switch dispose --- .../QuickSwitch/QuickSwitch.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 426f6639e9b..e0b0609fc92 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -516,6 +516,9 @@ private static unsafe void EnumerateShellWindows(Action action) public static void Dispose() { + // Reset initialize flag + _isInitialized = false; + // Dispose handle if (_foregroundChangeHook != null) { @@ -544,6 +547,13 @@ public static void Dispose() Marshal.ReleaseComObject(_lastExplorerView); _lastExplorerView = null; } + + // Stop drag move timer + if (_dragMoveTimer != null) + { + _dragMoveTimer.Stop(); + _dragMoveTimer = null; + } } } } From 58621b709a59351058f02fbf484ec0f62fce8c9a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 22:11:28 +0800 Subject: [PATCH 066/243] Add more codes --- .../NativeMethods.txt | 9 ++- Flow.Launcher.Infrastructure/Win32Helper.cs | 70 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 4a31512ac15..2ce38254523 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -69,4 +69,11 @@ IWebBrowser2 EVENT_OBJECT_DESTROY EVENT_OBJECT_LOCATIONCHANGE EVENT_SYSTEM_MOVESIZESTART -EVENT_SYSTEM_MOVESIZEEND \ No newline at end of file +EVENT_SYSTEM_MOVESIZEEND +GetFocus +SetFocus +MapVirtualKey +WM_KEYUP +WM_KEYDOWN +GetCurrentThreadId +AttachThreadInput \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index cbedf0d98d2..c8df5191836 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -708,7 +708,7 @@ internal static bool DirJump(string dirPath, HWND dialogHandle, bool altD = true return DirFileJump(dirPath, null, dialogHandle, altD); } - private static bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true, bool editFileName = false) + private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true, bool editFileName = false) { // Directly edit file name input box. if (editFileName) @@ -719,12 +719,26 @@ private static bool DirFileJump(string dirPath, string filePath, HWND dialogHand // Alt-D or Ctrl-L to focus on the path input box if (altD) { - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); } else { _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); } + /*if (altD) + { + SendKey(dialogHandle, VIRTUAL_KEY.VK_LMENU, false); // Press Left Alt + SendKey(dialogHandle, VIRTUAL_KEY.VK_D, false); // Press D + SendKey(dialogHandle, VIRTUAL_KEY.VK_D, true); // Release D + SendKey(dialogHandle, VIRTUAL_KEY.VK_LMENU, true); // Release Left Alt + } + else + { + SendKey(dialogHandle, VIRTUAL_KEY.VK_LCONTROL, false); // Press Left Ctrl + SendKey(dialogHandle, VIRTUAL_KEY.VK_L, false); // Press L + SendKey(dialogHandle, VIRTUAL_KEY.VK_L, true); // Release L + SendKey(dialogHandle, VIRTUAL_KEY.VK_LCONTROL, true); // Release Left Ctrl + }*/ // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. @@ -742,7 +756,7 @@ private static bool DirFileJump(string dirPath, string filePath, HWND dialogHand var timeOut = !SpinWait.SpinUntil(() => { - int style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + var style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; }, 1000); if (timeOut) @@ -757,6 +771,19 @@ private static bool DirFileJump(string dirPath, string filePath, HWND dialogHand return false; } + /*var dwMyID = PInvoke.GetCurrentThreadId(); + var dwCurID = PInvoke.GetWindowThreadProcessId(dialogHandle); + + PInvoke.AttachThreadInput(dwMyID, dwCurID, true); + + var timeOut1 = !SpinWait.SpinUntil(() => PInvoke.GetFocus() == editHandle, 1000); + if (timeOut1) + { + return false; + } + + PInvoke.AttachThreadInput(dwMyID, dwCurID, false);*/ + SetWindowText(editHandle, dirPath); _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); @@ -820,6 +847,43 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) return true; } + private static void SendKey(HWND hWnd, VIRTUAL_KEY virtualKey, bool isKeyUp) + { + // Get virtual key value + var virtualKeyValue = (ushort)virtualKey; + + // Get scan code and extended flag + var scanCode = PInvoke.MapVirtualKey(virtualKeyValue, MAP_VIRTUAL_KEY_TYPE.MAPVK_VK_TO_VSC); + + // Check if the key is an extended key (e.g., right Alt/Ctrl) + var isExtended = virtualKey == VIRTUAL_KEY.VK_RMENU || virtualKey == VIRTUAL_KEY.VK_RCONTROL; + + // Create lParam + var lParam = CreateKeyLParam(scanCode, isExtended, isKeyUp, !isKeyUp); + + // Send message + var message = isKeyUp ? PInvoke.WM_KEYUP : PInvoke.WM_KEYDOWN; + PInvoke.PostMessage(hWnd, message, virtualKeyValue, new(lParam)); + } + + private static nint CreateKeyLParam(uint scanCode, bool isExtended, bool isKeyUp, bool wasKeyDown) + { + uint lParam = 0x00000001; // Repeat count (1 keystroke) + + lParam |= (scanCode << 16); // Scan code + + if (isExtended) + lParam |= 0x01000000; // Extended key flag + + if (wasKeyDown) + lParam |= 0x40000000; // Previous key state (1 if down before message) + + if (isKeyUp) + lParam |= 0x80000000; // Transition state (1 for release) + + return (nint)lParam; + } + #endregion } } From f536599cd56f4c60cc853ed04304ea1c13e72878 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Apr 2025 22:14:24 +0800 Subject: [PATCH 067/243] Remove Quick switch automatically --- Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index a69e7d95e14..746d677e948 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -77,7 +77,11 @@ WindowTitle="{DynamicResource quickSwitchHotkey}" /> - + + Date: Fri, 18 Apr 2025 22:25:20 +0800 Subject: [PATCH 068/243] Check dialog handle --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index e0b0609fc92..8b3d20b5f68 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -160,6 +160,8 @@ public static void OnToggleHotkey(object sender, HotkeyEventArgs args) private static void NavigateDialogPath(HWND dialog, Action action = null) { + if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; + object document = null; try { From 877c6ab1db4b9104bbbc2292349351990f73bf60 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 11:33:01 +0800 Subject: [PATCH 069/243] Remove duplicate WM_KEYDOWN entry --- 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 2ce38254523..e3a8f8feda4 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -59,7 +59,6 @@ SetWinEventHook SendMessage EVENT_SYSTEM_FOREGROUND WINEVENT_OUTOFCONTEXT -WM_KEYDOWN WM_SETTEXT IShellFolderViewDual2 CoCreateInstance From 3649f70a3c5b570f2363981b01b1f59214e7082b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 11:55:09 +0800 Subject: [PATCH 070/243] Add codes --- Flow.Launcher.Infrastructure/Win32Helper.cs | 37 +++++++++++++++------ 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index c8df5191836..6e36823eac8 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -725,6 +725,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia { _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); } + // Cannot work when activating with hotkey /*if (altD) { SendKey(dialogHandle, VIRTUAL_KEY.VK_LMENU, false); // Press Left Alt @@ -771,18 +772,11 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia return false; } - /*var dwMyID = PInvoke.GetCurrentThreadId(); - var dwCurID = PInvoke.GetWindowThreadProcessId(dialogHandle); - - PInvoke.AttachThreadInput(dwMyID, dwCurID, true); - - var timeOut1 = !SpinWait.SpinUntil(() => PInvoke.GetFocus() == editHandle, 1000); - if (timeOut1) + // Sometimes it is not focused + /*if (!CheckFocus(dialogHandle, editHandle)) { return false; - } - - PInvoke.AttachThreadInput(dwMyID, dwCurID, false);*/ + }*/ SetWindowText(editHandle, dirPath); _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); @@ -809,6 +803,12 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle) return false; } + // Sometimes it is not focused + /*if (!CheckFocus(dialogHandle, controlHandle)) + { + return false; + }*/ + SetWindowText(controlHandle, fileName); // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, @@ -819,6 +819,23 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle) return true; } + private static unsafe bool CheckFocus(HWND dialogHandle, HWND inputHandle) + { + var dwMyID = PInvoke.GetCurrentThreadId(); + var dwCurID = PInvoke.GetWindowThreadProcessId(dialogHandle); + + PInvoke.AttachThreadInput(dwMyID, dwCurID, true); + + var timeOut1 = !SpinWait.SpinUntil(() => PInvoke.GetFocus() == inputHandle, 1000); + if (timeOut1) + { + return false; + } + + PInvoke.AttachThreadInput(dwMyID, dwCurID, false); + return true; + } + private static unsafe nint SetWindowText(HWND handle, string text) { fixed (char* textPtr = text + '\0') From e92ccffaef79c01092dde1b078b17471bf9cb82d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 12:01:31 +0800 Subject: [PATCH 071/243] Comment related control --- Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index 746d677e948..e0f3612ec5f 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -77,16 +77,13 @@ WindowTitle="{DynamicResource quickSwitchHotkey}" /> - - + + Date: Sat, 19 Apr 2025 12:03:59 +0800 Subject: [PATCH 072/243] Remove beta label --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 0226ed1bfc5..76e6cee9a84 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -306,7 +306,7 @@ Show badges for query results where supported Show Result Badges for Global Query Only Show badges for global query results only - Quick Switch (Beta) + Quick Switch Enter shortcut to quickly navigate the path of a file dialog to the path of the current Explorer. Quick Switch Automatically Quick switch automatically navigate to the path of the current Explorer when a file dialog is opened. From 8c30b9d044b9726dcf34391f7957023bb89ed4dd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 12:11:22 +0800 Subject: [PATCH 073/243] Make sure window events unhooked --- .../NativeMethods.txt | 1 + .../QuickSwitch/QuickSwitch.cs | 58 +++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index e3a8f8feda4..989030d6f2e 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -56,6 +56,7 @@ LOCALE_TRANSIENT_KEYBOARD3 LOCALE_TRANSIENT_KEYBOARD4 SetWinEventHook +UnhookWinEvent SendMessage EVENT_SYSTEM_FOREGROUND WINEVENT_OUTOFCONTEXT diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 8b3d20b5f68..84478037bfd 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -39,13 +39,13 @@ public static class QuickSwitch private static IWebBrowser2 _lastExplorerView = null; - private static UnhookWinEventSafeHandle _foregroundChangeHook = null; + private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; - private static UnhookWinEventSafeHandle _locationChangeHook = null; + private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; - /*private static UnhookWinEventSafeHandle _moveSizeHook = null;*/ + private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; - private static UnhookWinEventSafeHandle _destroyChangeHook = null; + private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; private static DispatcherTimer _dragMoveTimer = null; @@ -94,7 +94,7 @@ public static void Initialize() _foregroundChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, - null, + PInvoke.GetModuleHandle((PCWSTR)null), ForegroundChangeCallback, 0, 0, @@ -104,36 +104,36 @@ public static void Initialize() _locationChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_LOCATIONCHANGE, PInvoke.EVENT_OBJECT_LOCATIONCHANGE, - null, + PInvoke.GetModuleHandle((PCWSTR)null), LocationChangeCallback, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); // Call MoveSizeCallBack when the window is moved or resized - /*_moveSizeHook = PInvoke.SetWinEventHook( + _moveSizeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_MOVESIZESTART, PInvoke.EVENT_SYSTEM_MOVESIZEEND, - null, + PInvoke.GetModuleHandle((PCWSTR)null), MoveSizeCallBack, 0, 0, - PInvoke.WINEVENT_OUTOFCONTEXT);*/ + PInvoke.WINEVENT_OUTOFCONTEXT); // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_DESTROY, PInvoke.EVENT_OBJECT_DESTROY, - null, + PInvoke.GetModuleHandle((PCWSTR)null), DestroyChangeCallback, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); - if (_foregroundChangeHook.IsInvalid || - _locationChangeHook.IsInvalid || - /*_moveSizeHook.IsInvalid ||*/ - _destroyChangeHook.IsInvalid) + if (_foregroundChangeHook.IsNull || + _locationChangeHook.IsNull || + _moveSizeHook.IsNull || + _destroyChangeHook.IsNull) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); return; @@ -439,7 +439,7 @@ uint dwmsEventTime // TODO: Use a better way to detect dragging // Here we do not start & stop the timer beacause the start time is not accurate (more than 1s delay) // So we start & stop the timer when we find a file dialog window - /*private static void MoveSizeCallBack( + private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -462,7 +462,7 @@ uint dwmsEventTime break; } } - }*/ + } private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -522,25 +522,25 @@ public static void Dispose() _isInitialized = false; // Dispose handle - if (_foregroundChangeHook != null) + if (!_foregroundChangeHook.IsNull) { - _foregroundChangeHook.Dispose(); - _foregroundChangeHook = null; + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; } - if (_locationChangeHook != null) + if (!_locationChangeHook.IsNull) { - _locationChangeHook.Dispose(); - _locationChangeHook = null; + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; } - /*if (_moveSizeHook != null) + if (!_moveSizeHook.IsNull) { - _moveSizeHook.Dispose(); - _moveSizeHook = null; - }*/ - if (_destroyChangeHook != null) + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) { - _destroyChangeHook.Dispose(); - _destroyChangeHook = null; + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; } // Release ComObjects From d8e9fe0ef5e7d7d4958534c9be4e98067692b7cc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 12:15:37 +0800 Subject: [PATCH 074/243] Remove useless codes --- .../QuickSwitch/QuickSwitch.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 84478037bfd..6efc76f04d9 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -43,7 +43,7 @@ public static class QuickSwitch private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; - private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;*/ private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; @@ -111,14 +111,14 @@ public static void Initialize() PInvoke.WINEVENT_OUTOFCONTEXT); // Call MoveSizeCallBack when the window is moved or resized - _moveSizeHook = PInvoke.SetWinEventHook( + /*_moveSizeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_MOVESIZESTART, PInvoke.EVENT_SYSTEM_MOVESIZEEND, PInvoke.GetModuleHandle((PCWSTR)null), MoveSizeCallBack, 0, 0, - PInvoke.WINEVENT_OUTOFCONTEXT); + PInvoke.WINEVENT_OUTOFCONTEXT);*/ // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( @@ -132,7 +132,7 @@ public static void Initialize() if (_foregroundChangeHook.IsNull || _locationChangeHook.IsNull || - _moveSizeHook.IsNull || + /*_moveSizeHook.IsNull ||*/ _destroyChangeHook.IsNull) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); @@ -439,7 +439,7 @@ uint dwmsEventTime // TODO: Use a better way to detect dragging // Here we do not start & stop the timer beacause the start time is not accurate (more than 1s delay) // So we start & stop the timer when we find a file dialog window - private static void MoveSizeCallBack( + /*private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -462,7 +462,7 @@ uint dwmsEventTime break; } } - } + }*/ private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -532,11 +532,11 @@ public static void Dispose() PInvoke.UnhookWinEvent(_locationChangeHook); _locationChangeHook = HWINEVENTHOOK.Null; } - if (!_moveSizeHook.IsNull) + /*if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - } + }*/ if (!_destroyChangeHook.IsNull) { PInvoke.UnhookWinEvent(_destroyChangeHook); From 1bfae2188f42f28c16189247ac2bb568ce270848 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 12:17:00 +0800 Subject: [PATCH 075/243] Stop timer when hiding window --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6efc76f04d9..116ae3e292c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -382,6 +382,7 @@ uint dwmsEventTime // Neither quick switch window nor file dialog window is foreground // Hide quick switch window until the file dialog window is brought to the foreground HideQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); } // Check if explorer window is foreground From 343c1220d1b24775770fb77aaff3e76ae0eb6617 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 12:20:36 +0800 Subject: [PATCH 076/243] Cleanup codes --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 1 - Flow.Launcher/ViewModel/MainViewModel.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 116ae3e292c..c0cc62e0f49 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -437,7 +437,6 @@ uint dwmsEventTime } } - // TODO: Use a better way to detect dragging // Here we do not start & stop the timer beacause the start time is not accurate (more than 1s delay) // So we start & stop the timer when we find a file dialog window /*private static void MoveSizeCallBack( diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 24ebc5d793c..e37817548bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -24,7 +24,6 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Microsoft.VisualStudio.Threading; -using Windows.Win32; namespace Flow.Launcher.ViewModel { From 45441a5a5e77092a214b6ee83408d4a90aede047 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 16:40:51 +0800 Subject: [PATCH 077/243] Add settings for quick switch --- .../UserSettings/Settings.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index b50faa4192a..fc83fa0c1df 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -53,9 +53,6 @@ public void Save() public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; - public bool AutoQuickSwitch { get; set; } = false; - public bool ShowQuickSwitchWindow { get; set; } = true; - private string _language = Constant.SystemLanguageCode; public string Language { @@ -80,7 +77,7 @@ public string Theme } } public bool UseDropShadowEffect { get; set; } = true; - public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None; + public BackdropTypes BackdropType { get; set; } = BackdropTypes.None; /* Appearance Settings. It should be separated from the setting later.*/ public double WindowHeightSize { get; set; } = 42; @@ -233,6 +230,14 @@ public CustomBrowserViewModel CustomBrowser } }; + public bool AutoQuickSwitch { get; set; } = false; // Unused due to many issues + + public bool ShowQuickSwitchWindow { get; set; } = true; + + public QuickSwitchResultMethods OpenQuickSwitchResultMethod { get; set; } = QuickSwitchResultMethods.LeftClick; + + public QuickSwitchFileResultMethods OpenQuickSwitchFileResultMethod { get; set; } = QuickSwitchFileResultMethods.FullPath; + [JsonConverter(typeof(JsonStringEnumConverter))] public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; @@ -489,4 +494,17 @@ public enum BackdropTypes Mica, MicaAlt } + + public enum QuickSwitchResultMethods + { + LeftClick, + RightClick + } + + public enum QuickSwitchFileResultMethods + { + FullPath, + Directory, + DirectoryAndFileName + } } From 34fbc7946db9cb8e08c584dd4b6093201d6ce485 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 16:46:18 +0800 Subject: [PATCH 078/243] Add more debug info --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index c0cc62e0f49..816d2b45d36 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -81,11 +81,15 @@ public static void Initialize() if (!explorerInitialized) { _lastExplorerView = explorer; + + Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); } // Force update explorer window if it is foreground else if (Win32Helper.IsForegroundWindow(explorer.HWND.Value)) { _lastExplorerView = explorer; + + Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); } }); } @@ -321,7 +325,7 @@ uint dwmsEventTime // File dialog window if (GetWindowClassName(hwnd) == DialogWindowClassName) { - Log.Debug(ClassName, $"Hwnd: {hwnd}"); + Log.Debug(ClassName, $"Dialog Window: {hwnd}"); lock (_dialogWindowHandleLock) { @@ -374,6 +378,7 @@ uint dwmsEventTime else if (hwnd == _mainWindowHandle) { // Nothing to do + Log.Debug(ClassName, $"Quick Switch Window: {hwnd}"); } else { @@ -405,6 +410,8 @@ uint dwmsEventTime } _lastExplorerView = explorer; + + Log.Debug(ClassName, $"Explorer Window: {hwnd}"); } catch (COMException) { From 4fa3ec7b38aa7476e14aae0a4ea407243649062c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:00:23 +0800 Subject: [PATCH 079/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 402 +++++++++++------- 1 file changed, 238 insertions(+), 164 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 816d2b45d36..3c3201576cf 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch { public static class QuickSwitch { - private static readonly string ClassName = nameof(QuickSwitch); + #region Public Properties public static Action ShowQuickSwitchWindow { get; set; } = null; @@ -28,9 +28,15 @@ public static class QuickSwitch public static Action HideQuickSwitchWindow { get; set; } = null; + #endregion + + #region Private Fields + // The class name of a dialog window private const string DialogWindowClassName = "#32770"; + private static readonly string ClassName = nameof(QuickSwitch); + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); private static readonly object _lastExplorerViewLock = new(); @@ -49,6 +55,11 @@ public static class QuickSwitch private static DispatcherTimer _dragMoveTimer = null; + // A list of all file dialog windows that are shown quick switch window already + private static readonly List _shownQuickSwitchWindowDialogs = new(); + + private static readonly object _shownQuickSwitchWindowDialogsLock = new(); + // A list of all file dialog windows that are auto switched already private static readonly List _autoSwitchedDialogs = new(); @@ -62,6 +73,10 @@ public static class QuickSwitch private static bool _isInitialized = false; + #endregion + + #region Initialization + public static void Initialize() { if (_isInitialized) return; @@ -148,169 +163,64 @@ public static void Initialize() // Initialize timer _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; - _dragMoveTimer.Tick += (s, e) => UpdateQuickSwitchWindow?.Invoke(); + _dragMoveTimer.Tick += (s, e) => InvokeUpdateQuickSwitchWindow(); _isInitialized = true; return; } - public static void OnToggleHotkey(object sender, HotkeyEventArgs args) - { - if (_isInitialized) - { - NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); - } - } - - private static void NavigateDialogPath(HWND dialog, Action action = null) - { - if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; - - object document = null; - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; - } - } - } - catch (COMException) - { - return; - } - - if (document is not IShellFolderViewDual2 folderView) - { - return; - } + #endregion - string path; - try - { - // CSWin32 Folder does not have Self, so we need to use dynamic type here - // Use dynamic to bypass static typing - dynamic folder = folderView.Folder; + #region Events - // Access the Self property via dynamic binding - dynamic folderItem = folder.Self; + #region Invoke Properties - // Check if the item is part of the file system - if (folderItem != null && folderItem.IsFileSystem) - { - path = folderItem.Path; - } - else - { - // Handle non-file system paths (e.g., virtual folders) - path = string.Empty; - } - } - catch + private static void InvokeShowQuickSwitchWindow(bool alreadyShown) + { + // Show quick switch window + if (_settings.ShowQuickSwitchWindow && !alreadyShown) { - return; + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + _dragMoveTimer?.Start(); } - - JumpToPath(dialog.Value, path, action); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] - public static void JumpToPath(nint dialog, string path, Action action = null) + private static void InvokeUpdateQuickSwitchWindow() { - if (!CheckPath(path, out var isFile)) return; + // Update quick switch window + UpdateQuickSwitchWindow?.Invoke(); + } - var t = new Thread(async () => - { - // Jump after flow launcher window vanished (after JumpAction returned true) - // and the dialog had been in the foreground. - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindow() == dialog, 1000); - if (timeOut) - { - return; - }; + private static void InvokeResetQuickSwitchWindow() + { + // Reset quick switch window + ResetQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); + } - // Assume that the dialog is in the foreground now - await _navigationLock.WaitAsync(); - try - { - var dialogHandle = new HWND(dialog); + private static void InvokeHideQuickSwitchWindow() + { + // Neither quick switch window nor file dialog window is foreground + // Hide quick switch window until the file dialog window is brought to the foreground + HideQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); + } - bool result; - if (isFile) - { - result = Win32Helper.FileJump(path, dialogHandle); - Log.Debug(ClassName, $"File Jump: {path}"); - } - else - { - result = Win32Helper.DirJump(path, dialogHandle); - Log.Debug(ClassName, $"Dir Jump: {path}"); - } + #endregion - if (result) - { - lock (_autoSwitchedDialogsLock) - { - _autoSwitchedDialogs.Add(dialogHandle); - } - } - else - { - Log.Error(ClassName, "Failed to jump to path"); - } - } - catch (System.Exception e) - { - Log.Exception(ClassName, "Failed to jump to path", e); - } - finally - { - _navigationLock.Release(); - } - - // Invoke action if provided - action?.Invoke(); - }); - t.Start(); - return; + #region Hotkey - static bool CheckPath(string path, out bool file) + public static void OnToggleHotkey(object sender, HotkeyEventArgs args) + { + if (_isInitialized) { - file = false; - // Is non-null? - if (string.IsNullOrEmpty(path)) return false; - // Is absolute? - if (!Path.IsPathRooted(path)) return false; - // Is folder? - var isFolder = Directory.Exists(path); - // Is file? - var isFile = File.Exists(path); - file = isFile; - return isFolder || isFile; + NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); } } - private static string GetWindowClassName(HWND handle) - { - return GetClassName(handle); + #endregion - static unsafe string GetClassName(HWND handle) - { - fixed (char* buf = new char[256]) - { - return PInvoke.GetClassName(handle, buf, 256) switch - { - 0 => null, - _ => new string(buf), - }; - } - } - } + #region Windows Events private static void ForegroundChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -332,6 +242,12 @@ uint dwmsEventTime _dialogWindowHandle = hwnd; } + bool alreadyShown; + lock (_shownQuickSwitchWindowDialogsLock) + { + alreadyShown = _shownQuickSwitchWindowDialogs.Contains(hwnd); + } + // Navigate to path if (_settings.AutoQuickSwitch) { @@ -345,33 +261,20 @@ uint dwmsEventTime // Just show quick switch window if (alreadySwitched) { - if (_settings.ShowQuickSwitchWindow) - { - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); - } + InvokeShowQuickSwitchWindow(alreadyShown); } // Show quick switch window after navigating the path else { NavigateDialogPath(hwnd, () => { - if (_settings.ShowQuickSwitchWindow) - { - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); - } + InvokeShowQuickSwitchWindow(alreadyShown); }); } } else { - // Show quick switch window - if (_settings.ShowQuickSwitchWindow) - { - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); - } + InvokeShowQuickSwitchWindow(alreadyShown); } } // Quick switch window @@ -384,10 +287,7 @@ uint dwmsEventTime { if (_dialogWindowHandle != HWND.Null) { - // Neither quick switch window nor file dialog window is foreground - // Hide quick switch window until the file dialog window is brought to the foreground - HideQuickSwitchWindow?.Invoke(); - _dragMoveTimer?.Stop(); + InvokeHideQuickSwitchWindow(); } // Check if explorer window is foreground @@ -440,7 +340,7 @@ uint dwmsEventTime // If the dialog window is moved, update the quick switch window position if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) { - UpdateQuickSwitchWindow?.Invoke(); + InvokeUpdateQuickSwitchWindow(); } } @@ -493,11 +393,177 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - ResetQuickSwitchWindow?.Invoke(); - _dragMoveTimer?.Stop(); + InvokeResetQuickSwitchWindow(); + } + } + + #endregion + + #endregion + + #region Helper Methods + + #region Navigate Path + + private static void NavigateDialogPath(HWND dialog, Action action = null) + { + if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; + + object document = null; + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _lastExplorerView; + document = explorerView.Document; + } + } + } + catch (COMException) + { + return; + } + + if (document is not IShellFolderViewDual2 folderView) + { + return; + } + + string path; + try + { + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Check if the item is part of the file system + if (folderItem != null && folderItem.IsFileSystem) + { + path = folderItem.Path; + } + else + { + // Handle non-file system paths (e.g., virtual folders) + path = string.Empty; + } + } + catch + { + return; } + + JumpToPath(dialog.Value, path, action); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] + public static void JumpToPath(nint dialog, string path, Action action = null) + { + if (!CheckPath(path, out var isFile)) return; + + var t = new Thread(async () => + { + // Jump after flow launcher window vanished (after JumpAction returned true) + // and the dialog had been in the foreground. + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindow() == dialog, 1000); + if (timeOut) + { + return; + } + ; + + // Assume that the dialog is in the foreground now + await _navigationLock.WaitAsync(); + try + { + var dialogHandle = new HWND(dialog); + + bool result; + if (isFile) + { + result = Win32Helper.FileJump(path, dialogHandle); + Log.Debug(ClassName, $"File Jump: {path}"); + } + else + { + result = Win32Helper.DirJump(path, dialogHandle); + Log.Debug(ClassName, $"Dir Jump: {path}"); + } + + if (result) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Add(dialogHandle); + } + } + else + { + Log.Error(ClassName, "Failed to jump to path"); + } + } + catch (System.Exception e) + { + Log.Exception(ClassName, "Failed to jump to path", e); + } + finally + { + _navigationLock.Release(); + } + + // Invoke action if provided + action?.Invoke(); + }); + t.Start(); + return; + + static bool CheckPath(string path, out bool file) + { + file = false; + // Is non-null? + if (string.IsNullOrEmpty(path)) return false; + // Is absolute? + if (!Path.IsPathRooted(path)) return false; + // Is folder? + var isFolder = Directory.Exists(path); + // Is file? + var isFile = File.Exists(path); + file = isFile; + return isFolder || isFile; + } + } + + #endregion + + #region Class Name + + private static string GetWindowClassName(HWND handle) + { + return GetClassName(handle); + + static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) + { + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => null, + _ => new string(buf), + }; + } + } + } + + #endregion + + #region Enumerate Windows + private static unsafe void EnumerateShellWindows(Action action) { // Create an instance of ShellWindows @@ -523,6 +589,12 @@ private static unsafe void EnumerateShellWindows(Action action) } } + #endregion + + #endregion + + #region Dispose + public static void Dispose() { // Reset initialize flag @@ -564,5 +636,7 @@ public static void Dispose() _dragMoveTimer = null; } } + + #endregion } } From 56d2c6edeb66b61beb201c4d07925571521a2be4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:06:15 +0800 Subject: [PATCH 080/243] Check window shown --- .../QuickSwitch/QuickSwitch.cs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 3c3201576cf..fe61e7b389a 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -175,8 +175,15 @@ public static void Initialize() #region Invoke Properties - private static void InvokeShowQuickSwitchWindow(bool alreadyShown) + private static void InvokeShowQuickSwitchWindow(HWND hwnd) { + // Check if the quick switch window is already shown for this dialog + bool alreadyShown; + lock (_shownQuickSwitchWindowDialogsLock) + { + alreadyShown = _shownQuickSwitchWindowDialogs.Contains(hwnd); + } + // Show quick switch window if (_settings.ShowQuickSwitchWindow && !alreadyShown) { @@ -191,15 +198,27 @@ private static void InvokeUpdateQuickSwitchWindow() UpdateQuickSwitchWindow?.Invoke(); } - private static void InvokeResetQuickSwitchWindow() + private static void InvokeResetQuickSwitchWindow(HWND hwnd) { + // Remove the dialog from the list of shown quick switch windows + lock (_shownQuickSwitchWindowDialogsLock) + { + _shownQuickSwitchWindowDialogs.Remove(hwnd); + } + // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); } - private static void InvokeHideQuickSwitchWindow() + private static void InvokeHideQuickSwitchWindow(HWND hwnd) { + // Remove the dialog from the list of shown quick switch windows + lock (_shownQuickSwitchWindowDialogsLock) + { + _shownQuickSwitchWindowDialogs.Remove(hwnd); + } + // Neither quick switch window nor file dialog window is foreground // Hide quick switch window until the file dialog window is brought to the foreground HideQuickSwitchWindow?.Invoke(); @@ -242,12 +261,6 @@ uint dwmsEventTime _dialogWindowHandle = hwnd; } - bool alreadyShown; - lock (_shownQuickSwitchWindowDialogsLock) - { - alreadyShown = _shownQuickSwitchWindowDialogs.Contains(hwnd); - } - // Navigate to path if (_settings.AutoQuickSwitch) { @@ -261,20 +274,20 @@ uint dwmsEventTime // Just show quick switch window if (alreadySwitched) { - InvokeShowQuickSwitchWindow(alreadyShown); + InvokeShowQuickSwitchWindow(hwnd); } // Show quick switch window after navigating the path else { NavigateDialogPath(hwnd, () => { - InvokeShowQuickSwitchWindow(alreadyShown); + InvokeShowQuickSwitchWindow(hwnd); }); } } else { - InvokeShowQuickSwitchWindow(alreadyShown); + InvokeShowQuickSwitchWindow(hwnd); } } // Quick switch window @@ -287,7 +300,7 @@ uint dwmsEventTime { if (_dialogWindowHandle != HWND.Null) { - InvokeHideQuickSwitchWindow(); + InvokeHideQuickSwitchWindow(_dialogWindowHandle); } // Check if explorer window is foreground @@ -393,7 +406,7 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - InvokeResetQuickSwitchWindow(); + InvokeResetQuickSwitchWindow(hwnd); } } From 0583dffc4a79d0ecf7e16683092ace506810ab5e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:12:51 +0800 Subject: [PATCH 081/243] Code quality --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index fe61e7b389a..b24fe8fcd56 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -173,7 +173,7 @@ public static void Initialize() #region Events - #region Invoke Properties + #region Invoke Property Events private static void InvokeShowQuickSwitchWindow(HWND hwnd) { From d189bac1e1bf6a42c1f62f4edd58e60fd0719b78 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:18:28 +0800 Subject: [PATCH 082/243] Remove window shown check --- .../QuickSwitch/QuickSwitch.cs | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index b24fe8fcd56..3464d491a12 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -55,11 +55,6 @@ public static class QuickSwitch private static DispatcherTimer _dragMoveTimer = null; - // A list of all file dialog windows that are shown quick switch window already - private static readonly List _shownQuickSwitchWindowDialogs = new(); - - private static readonly object _shownQuickSwitchWindowDialogsLock = new(); - // A list of all file dialog windows that are auto switched already private static readonly List _autoSwitchedDialogs = new(); @@ -175,17 +170,10 @@ public static void Initialize() #region Invoke Property Events - private static void InvokeShowQuickSwitchWindow(HWND hwnd) + private static void InvokeShowQuickSwitchWindow() { - // Check if the quick switch window is already shown for this dialog - bool alreadyShown; - lock (_shownQuickSwitchWindowDialogsLock) - { - alreadyShown = _shownQuickSwitchWindowDialogs.Contains(hwnd); - } - // Show quick switch window - if (_settings.ShowQuickSwitchWindow && !alreadyShown) + if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); _dragMoveTimer?.Start(); @@ -198,27 +186,15 @@ private static void InvokeUpdateQuickSwitchWindow() UpdateQuickSwitchWindow?.Invoke(); } - private static void InvokeResetQuickSwitchWindow(HWND hwnd) + private static void InvokeResetQuickSwitchWindow() { - // Remove the dialog from the list of shown quick switch windows - lock (_shownQuickSwitchWindowDialogsLock) - { - _shownQuickSwitchWindowDialogs.Remove(hwnd); - } - // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); } - private static void InvokeHideQuickSwitchWindow(HWND hwnd) + private static void InvokeHideQuickSwitchWindow() { - // Remove the dialog from the list of shown quick switch windows - lock (_shownQuickSwitchWindowDialogsLock) - { - _shownQuickSwitchWindowDialogs.Remove(hwnd); - } - // Neither quick switch window nor file dialog window is foreground // Hide quick switch window until the file dialog window is brought to the foreground HideQuickSwitchWindow?.Invoke(); @@ -274,20 +250,20 @@ uint dwmsEventTime // Just show quick switch window if (alreadySwitched) { - InvokeShowQuickSwitchWindow(hwnd); + InvokeShowQuickSwitchWindow(); } // Show quick switch window after navigating the path else { NavigateDialogPath(hwnd, () => { - InvokeShowQuickSwitchWindow(hwnd); + InvokeShowQuickSwitchWindow(); }); } } else { - InvokeShowQuickSwitchWindow(hwnd); + InvokeShowQuickSwitchWindow(); } } // Quick switch window @@ -300,7 +276,7 @@ uint dwmsEventTime { if (_dialogWindowHandle != HWND.Null) { - InvokeHideQuickSwitchWindow(_dialogWindowHandle); + InvokeHideQuickSwitchWindow(); } // Check if explorer window is foreground @@ -406,7 +382,7 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - InvokeResetQuickSwitchWindow(hwnd); + InvokeResetQuickSwitchWindow(); } } From 6c379d30266da2fd7f70167928f07d465f9c5e02 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:19:55 +0800 Subject: [PATCH 083/243] Check main window visibility status --- Flow.Launcher/MainWindow.xaml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 1e041604f4b..b8682d91d1c 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1170,7 +1170,8 @@ private void InitializeQuickSwitch() private void UpdateQuickSwitchPosition() { - if (_viewModel.DialogWindowHandle == nint.Zero) return; + if (_viewModel.DialogWindowHandle == nint.Zero || + !_viewModel.MainWindowVisibilityStatus) return; // Get dialog window rect var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); From 5ae1c4b2a223e3d3cb4b80d0332c7daebebd33ea Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:28:02 +0800 Subject: [PATCH 084/243] Fast show window from hidden window --- Flow.Launcher/ViewModel/MainViewModel.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index e37817548bb..26f1b2c1346 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1602,11 +1602,14 @@ public async void SetupQuickSwitch(nint handle) PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; IsQuickSwitch = true; - } - await Task.Delay(300); // If don't give a time, Positioning will be weird. + // Wait for a while to make sure the dialog is shown + // If don't give a time, Positioning will be weird + await Task.Delay(300); + } - if (DialogWindowHandle == nint.Zero) return; // If handle is cleared, which means the dialog is closed, do nothing + // If handle is cleared, which means the dialog is closed, do nothing + if (DialogWindowHandle == nint.Zero) return; if (MainWindowVisibilityStatus) { From f73e768ddbccf51a30a21ad5e86987fbd7795eac Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 17:52:02 +0800 Subject: [PATCH 085/243] Use hook to check timer --- .../QuickSwitch/QuickSwitch.cs | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 3464d491a12..6511f29373d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -39,32 +39,30 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + private static IWebBrowser2 _lastExplorerView = null; private static readonly object _lastExplorerViewLock = new(); - private static readonly object _dialogWindowHandleLock = new(); + private static HWND _mainWindowHandle = HWND.Null; - private static IWebBrowser2 _lastExplorerView = null; + private static HWND _dialogWindowHandle = HWND.Null; + private static readonly object _dialogWindowHandleLock = new(); private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; - private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; - - /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;*/ - private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; private static DispatcherTimer _dragMoveTimer = null; // A list of all file dialog windows that are auto switched already private static readonly List _autoSwitchedDialogs = new(); - private static readonly object _autoSwitchedDialogsLock = new(); private static readonly SemaphoreSlim _navigationLock = new(1, 1); - private static HWND _mainWindowHandle = HWND.Null; + private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; - private static HWND _dialogWindowHandle = HWND.Null; + private static HWND _hookedDialogWindowHandle = HWND.Null; + private static readonly object _hookedDialogWindowHandleLock = new(); private static bool _isInitialized = false; @@ -124,16 +122,6 @@ public static void Initialize() 0, PInvoke.WINEVENT_OUTOFCONTEXT); - // Call MoveSizeCallBack when the window is moved or resized - /*_moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - PInvoke.GetModuleHandle((PCWSTR)null), - MoveSizeCallBack, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT);*/ - // Call DestroyChange when the window is destroyed _destroyChangeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_OBJECT_DESTROY, @@ -146,7 +134,6 @@ public static void Initialize() if (_foregroundChangeHook.IsNull || _locationChangeHook.IsNull || - /*_moveSizeHook.IsNull ||*/ _destroyChangeHook.IsNull) { Log.Error(ClassName, "Failed to initialize QuickSwitch"); @@ -170,13 +157,41 @@ public static void Initialize() #region Invoke Property Events - private static void InvokeShowQuickSwitchWindow() + private static unsafe void InvokeShowQuickSwitchWindow() { // Show quick switch window if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); + + lock (_hookedDialogWindowHandleLock) + { + var needHook = _hookedDialogWindowHandle == HWND.Null || + _hookedDialogWindowHandle != _dialogWindowHandle; + + if (needHook) + { + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + + // Call MoveSizeCallBack when the window is moved or resized + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindowHandle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + MoveSizeCallBack, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); + } + + _hookedDialogWindowHandle = _dialogWindowHandle; + } } } @@ -190,7 +205,17 @@ private static void InvokeResetQuickSwitchWindow() { // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); - _dragMoveTimer?.Stop(); + + lock (_hookedDialogWindowHandleLock) + { + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + + _hookedDialogWindowHandle = HWND.Null; + } } private static void InvokeHideQuickSwitchWindow() @@ -198,7 +223,6 @@ private static void InvokeHideQuickSwitchWindow() // Neither quick switch window nor file dialog window is foreground // Hide quick switch window until the file dialog window is brought to the foreground HideQuickSwitchWindow?.Invoke(); - _dragMoveTimer?.Stop(); } #endregion @@ -255,10 +279,7 @@ uint dwmsEventTime // Show quick switch window after navigating the path else { - NavigateDialogPath(hwnd, () => - { - InvokeShowQuickSwitchWindow(); - }); + NavigateDialogPath(hwnd, InvokeShowQuickSwitchWindow); } } else @@ -333,9 +354,7 @@ uint dwmsEventTime } } - // Here we do not start & stop the timer beacause the start time is not accurate (more than 1s delay) - // So we start & stop the timer when we find a file dialog window - /*private static void MoveSizeCallBack( + private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -346,7 +365,7 @@ uint dwmsEventTime ) { // If the dialog window is moved or resized, update the quick switch window position - if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd && _dragMoveTimer != null) + if (_dragMoveTimer != null) { switch (eventType) { @@ -358,7 +377,7 @@ uint dwmsEventTime break; } } - }*/ + } private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -600,11 +619,11 @@ public static void Dispose() PInvoke.UnhookWinEvent(_locationChangeHook); _locationChangeHook = HWINEVENTHOOK.Null; } - /*if (!_moveSizeHook.IsNull) + if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - }*/ + } if (!_destroyChangeHook.IsNull) { PInvoke.UnhookWinEvent(_destroyChangeHook); From 3e896b52b91771c20dd0198888be29c27cd37096 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 20:50:40 +0800 Subject: [PATCH 086/243] Do not use hook & unhook to check window moving --- .../QuickSwitch/QuickSwitch.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6511f29373d..152434e2ae9 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -59,10 +59,13 @@ public static class QuickSwitch private static readonly SemaphoreSlim _navigationLock = new(1, 1); - private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; private static HWND _hookedDialogWindowHandle = HWND.Null; - private static readonly object _hookedDialogWindowHandleLock = new(); + private static readonly object _hookedDialogWindowHandleLock = new();*/ private static bool _isInitialized = false; @@ -163,8 +166,12 @@ private static unsafe void InvokeShowQuickSwitchWindow() if (_settings.ShowQuickSwitchWindow) { ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + _dragMoveTimer?.Start(); - lock (_hookedDialogWindowHandleLock) + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*lock (_hookedDialogWindowHandleLock) { var needHook = _hookedDialogWindowHandle == HWND.Null || _hookedDialogWindowHandle != _dialogWindowHandle; @@ -191,7 +198,7 @@ private static unsafe void InvokeShowQuickSwitchWindow() } _hookedDialogWindowHandle = _dialogWindowHandle; - } + }*/ } } @@ -205,8 +212,12 @@ private static void InvokeResetQuickSwitchWindow() { // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); - lock (_hookedDialogWindowHandleLock) + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*lock (_hookedDialogWindowHandleLock) { if (!_moveSizeHook.IsNull) { @@ -215,7 +226,7 @@ private static void InvokeResetQuickSwitchWindow() } _hookedDialogWindowHandle = HWND.Null; - } + }*/ } private static void InvokeHideQuickSwitchWindow() @@ -223,6 +234,7 @@ private static void InvokeHideQuickSwitchWindow() // Neither quick switch window nor file dialog window is foreground // Hide quick switch window until the file dialog window is brought to the foreground HideQuickSwitchWindow?.Invoke(); + _dragMoveTimer?.Stop(); } #endregion @@ -354,7 +366,10 @@ uint dwmsEventTime } } - private static void MoveSizeCallBack( + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -377,7 +392,7 @@ uint dwmsEventTime break; } } - } + }*/ private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -619,11 +634,11 @@ public static void Dispose() PInvoke.UnhookWinEvent(_locationChangeHook); _locationChangeHook = HWINEVENTHOOK.Null; } - if (!_moveSizeHook.IsNull) + /*if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - } + }*/ if (!_destroyChangeHook.IsNull) { PInvoke.UnhookWinEvent(_destroyChangeHook); From 34e868a60d689fec86657bf39aca70fa31cf4058 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 20:53:52 +0800 Subject: [PATCH 087/243] Add quick switch window position --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index fc83fa0c1df..45029a9f2f1 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -238,6 +238,8 @@ public CustomBrowserViewModel CustomBrowser public QuickSwitchFileResultMethods OpenQuickSwitchFileResultMethod { get; set; } = QuickSwitchFileResultMethods.FullPath; + public QuickSwitchWindowPositions QuickSwitchWindowPosition { get; set; } = QuickSwitchWindowPositions.UnderDialog; + [JsonConverter(typeof(JsonStringEnumConverter))] public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; @@ -507,4 +509,10 @@ public enum QuickSwitchFileResultMethods Directory, DirectoryAndFileName } + + public enum QuickSwitchWindowPositions + { + UnderDialog, + CenterScreen + } } From 9a241e2647323e1d3460cc70dbda5cbb82010da2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 20:55:50 +0800 Subject: [PATCH 088/243] Add enable quick switch --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 45029a9f2f1..0540af499b3 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -230,7 +230,10 @@ public CustomBrowserViewModel CustomBrowser } }; - public bool AutoQuickSwitch { get; set; } = false; // Unused due to many issues + public bool EnableQuickSwitch { get; set; } = true; + + // TODO: Due to many issues, this option is removed from FL + public bool AutoQuickSwitch { get; set; } = false; public bool ShowQuickSwitchWindow { get; set; } = true; From bb20878f42c5b6c1886ff2698b708a6826b25c61 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 21:25:00 +0800 Subject: [PATCH 089/243] Support enable & disable quick switch --- .../QuickSwitch/QuickSwitch.cs | 207 +++++++++++------- .../UserSettings/Settings.cs | 2 +- Flow.Launcher/App.xaml.cs | 3 +- Flow.Launcher/Helper/HotKeyMapper.cs | 5 +- Flow.Launcher/Languages/en.xaml | 2 + .../SettingsPaneGeneralViewModel.cs | 22 ++ .../ViewModels/SettingsPaneHotkeyViewModel.cs | 5 +- .../Views/SettingsPaneGeneral.xaml | 34 +++ .../Views/SettingsPaneHotkey.xaml | 38 +--- 9 files changed, 210 insertions(+), 108 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 152434e2ae9..729862896c0 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -67,91 +67,137 @@ public static class QuickSwitch private static HWND _hookedDialogWindowHandle = HWND.Null; private static readonly object _hookedDialogWindowHandleLock = new();*/ - private static bool _isInitialized = false; + private static bool _initialized = false; + private static bool _enabled = false; #endregion - #region Initialization + #region Initialize & Setup - public static void Initialize() + public static void InitializeQuickSwitch() { - if (_isInitialized) return; + if (_initialized) return; - // Check all foreground windows and check if there are explorer windows - lock (_lastExplorerViewLock) + // Initialize main window handle + _mainWindowHandle = Win32Helper.GetMainWindowHandle(); + + // Initialize timer + _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; + _dragMoveTimer.Tick += (s, e) => InvokeUpdateQuickSwitchWindow(); + + _initialized = true; + } + + public static void SetupQuickSwitch(bool enabled) + { + if (enabled == _enabled) return; + + if (enabled) { - var explorerInitialized = false; - EnumerateShellWindows((shellWindow) => + // Check all foreground windows and check if there are explorer windows + lock (_lastExplorerViewLock) { - if (shellWindow is not IWebBrowser2 explorer) + var explorerInitialized = false; + EnumerateShellWindows((shellWindow) => { - return; - } + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } - // Initialize one explorer window even if it is not foreground - if (!explorerInitialized) - { - _lastExplorerView = explorer; + // Initialize one explorer window even if it is not foreground + if (!explorerInitialized) + { + _lastExplorerView = explorer; - Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); - } - // Force update explorer window if it is foreground - else if (Win32Helper.IsForegroundWindow(explorer.HWND.Value)) - { - _lastExplorerView = explorer; + Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); + } - Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); - } - }); - } + // Force update explorer window if it is foreground + else if (Win32Helper.IsForegroundWindow(explorer.HWND.Value)) + { + _lastExplorerView = explorer; - // Call ForegroundChange when the foreground window changes - _foregroundChangeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_FOREGROUND, - PInvoke.EVENT_SYSTEM_FOREGROUND, - PInvoke.GetModuleHandle((PCWSTR)null), - ForegroundChangeCallback, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - - // Call LocationChange when the location of the window changes - _locationChangeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_OBJECT_LOCATIONCHANGE, - PInvoke.EVENT_OBJECT_LOCATIONCHANGE, - PInvoke.GetModuleHandle((PCWSTR)null), - LocationChangeCallback, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - - // Call DestroyChange when the window is destroyed - _destroyChangeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_OBJECT_DESTROY, - PInvoke.EVENT_OBJECT_DESTROY, - PInvoke.GetModuleHandle((PCWSTR)null), - DestroyChangeCallback, - 0, - 0, - PInvoke.WINEVENT_OUTOFCONTEXT); - - if (_foregroundChangeHook.IsNull || - _locationChangeHook.IsNull || - _destroyChangeHook.IsNull) - { - Log.Error(ClassName, "Failed to initialize QuickSwitch"); - return; - } + Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); + } + }); + } - // Initialize main window handle - _mainWindowHandle = Win32Helper.GetMainWindowHandle(); + // Unhook events + if (!_foregroundChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; + } + if (!_locationChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; + } - // Initialize timer - _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; - _dragMoveTimer.Tick += (s, e) => InvokeUpdateQuickSwitchWindow(); + // Hook events + _foregroundChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.GetModuleHandle((PCWSTR)null), + ForegroundChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _locationChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.GetModuleHandle((PCWSTR)null), + LocationChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _destroyChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.GetModuleHandle((PCWSTR)null), + DestroyChangeCallback, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + if (_foregroundChangeHook.IsNull || + _locationChangeHook.IsNull || + _destroyChangeHook.IsNull) + { + Log.Error(ClassName, "Failed to enable QuickSwitch"); + return; + } + } + else + { + // Unhook events + if (!_foregroundChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; + } + if (!_locationChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; + } - _isInitialized = true; - return; + // Stop drag move timer + _dragMoveTimer?.Stop(); + } + + _enabled = enabled; } #endregion @@ -243,10 +289,7 @@ private static void InvokeHideQuickSwitchWindow() public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - if (_isInitialized) - { - NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); - } + NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); } #endregion @@ -620,10 +663,11 @@ private static unsafe void EnumerateShellWindows(Action action) public static void Dispose() { - // Reset initialize flag - _isInitialized = false; + // Reset flags + _enabled = false; + _initialized = false; - // Dispose handle + // Unhook events if (!_foregroundChangeHook.IsNull) { PInvoke.UnhookWinEvent(_foregroundChangeHook); @@ -646,9 +690,16 @@ public static void Dispose() } // Release ComObjects - if (_lastExplorerView != null) + try + { + if (_lastExplorerView != null) + { + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; + } + } + catch (COMException) { - Marshal.ReleaseComObject(_lastExplorerView); _lastExplorerView = null; } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0540af499b3..e9fee1fa0c8 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -232,7 +232,7 @@ public CustomBrowserViewModel CustomBrowser public bool EnableQuickSwitch { get; set; } = true; - // TODO: Due to many issues, this option is removed from FL + // TODO: TODO: Due to many issues, this option is removed from FL (see https://github.com/Flow-Launcher/Flow.Launcher/pull/1018) public bool AutoQuickSwitch { get; set; } = false; public bool ShowQuickSwitchWindow { get; set; } = true; diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 63b8c899ec3..ecd540b497a 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -185,7 +185,8 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); - QuickSwitch.Initialize(); + QuickSwitch.InitializeQuickSwitch(); + QuickSwitch.SetupQuickSwitch(_settings.EnableQuickSwitch); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index f586d384212..6edd55fdaad 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -23,7 +23,10 @@ internal static void Initialize() _settings = Ioc.Default.GetService(); SetHotkey(_settings.Hotkey, OnToggleHotkey); - SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); + if (_settings.EnableQuickSwitch) + { + SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); + } LoadCustomPluginHotkey(); } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 8034b3e20a6..da2c927d0ca 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -308,6 +308,8 @@ Show badges for global query results only Quick Switch Enter shortcut to quickly navigate the path of a file dialog to the path of the current Explorer. + Quick Switch + Quickly navigate to the path of the current Explorer when a file dialog is opened. Quick Switch Automatically Quick switch automatically navigate to the path of the current Explorer when a file dialog is opened. Show Quick Switch Window diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 6b56caf5e74..6019e5ca925 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -8,6 +8,7 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -144,6 +145,27 @@ public bool PortableMode public List LastQueryModes { get; } = DropdownDataGeneric.GetValues("LastQuery"); + public bool EnableQuickSwitch + { + get => Settings.EnableQuickSwitch; + set + { + if (Settings.EnableQuickSwitch != value) + { + Settings.EnableQuickSwitch = value; + QuickSwitch.SetupQuickSwitch(value); + if (Settings.EnableQuickSwitch) + { + HotKeyMapper.SetHotkey(new(Settings.QuickSwitchHotkey), QuickSwitch.OnToggleHotkey); + } + else + { + HotKeyMapper.RemoveHotkey(Settings.QuickSwitchHotkey); + } + } + } + } + public int SearchDelayTimeValue { get => Settings.SearchDelayTime; diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs index c2c428df389..faa3969c9f0 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs @@ -38,7 +38,10 @@ private void SetTogglingHotkey(HotkeyModel hotkey) [RelayCommand] private void SetQuickSwitchHotkey(HotkeyModel hotkey) { - HotKeyMapper.SetHotkey(hotkey, QuickSwitch.OnToggleHotkey); + if (Settings.EnableQuickSwitch) + { + HotKeyMapper.SetHotkey(hotkey, QuickSwitch.OnToggleHotkey); + } } [RelayCommand] diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index d45d28d8bba..6b0a6b06002 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -202,6 +202,40 @@ + + + + + + + + + + + + + - + - - - - - - - - - - - - + + + Date: Sat, 19 Apr 2025 21:25:40 +0800 Subject: [PATCH 090/243] Improve code comments --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 3 ++- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index e9fee1fa0c8..fb1771ad941 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -232,7 +232,8 @@ public CustomBrowserViewModel CustomBrowser public bool EnableQuickSwitch { get; set; } = true; - // TODO: TODO: Due to many issues, this option is removed from FL (see https://github.com/Flow-Launcher/Flow.Launcher/pull/1018) + // TODO: TODO: Due to many issues, this option is removed from FL + // Please see https://github.com/Flow-Launcher/Flow.Launcher/pull/1018 public bool AutoQuickSwitch { get; set; } = false; public bool ShowQuickSwitchWindow { get; set; } = true; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 6b0a6b06002..d26bc9f6385 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -214,7 +214,8 @@ OnContent="{DynamicResource enable}" /> - + + HTTP Proxy diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 6019e5ca925..90d1a41be64 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -166,6 +166,19 @@ public bool EnableQuickSwitch } } + public class QuickSwitchWindowPositionData : DropdownDataGeneric { } + public class QuickSwitchResultBehaviourData : DropdownDataGeneric { } + public class QuickSwitchFileResultBehaviourData : DropdownDataGeneric { } + + public List QuickSwitchWindowPositions { get; } = + DropdownDataGeneric.GetValues("QuickSwitchWindowPosition"); + + public List QuickSwitchResultBehaviours { get; } = + DropdownDataGeneric.GetValues("QuickSwitchResultBehaviour"); + + public List QuickSwitchFileResultBehaviours { get; } = + DropdownDataGeneric.GetValues("QuickSwitchFileResultBehaviour"); + public int SearchDelayTimeValue { get => Settings.SearchDelayTime; @@ -187,6 +200,9 @@ private void UpdateEnumDropdownLocalizations() DropdownDataGeneric.UpdateLabels(SearchWindowAligns); DropdownDataGeneric.UpdateLabels(SearchPrecisionScores); DropdownDataGeneric.UpdateLabels(LastQueryModes); + DropdownDataGeneric.UpdateLabels(QuickSwitchWindowPositions); + DropdownDataGeneric.UpdateLabels(QuickSwitchResultBehaviours); + DropdownDataGeneric.UpdateLabels(QuickSwitchFileResultBehaviours); } public string Language diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index d26bc9f6385..cf07000c918 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -214,27 +214,67 @@ OnContent="{DynamicResource enable}" /> - - - + + + + - - - + + + + + + + + + + + + + + + + Date: Sat, 19 Apr 2025 22:38:03 +0800 Subject: [PATCH 093/243] Support result behaviours & Change keyboard for quick switch window & Add reset check --- Flow.Launcher/ViewModel/MainViewModel.cs | 72 ++++++++++++++---------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 26f1b2c1346..8afd533e785 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -358,8 +358,8 @@ public void ForwardHistory() [RelayCommand] private void LoadContextMenu() { - // For quick switch mode, we need to navigate to the path - if (IsQuickSwitch) + // For quick switch and right click mode, we need to navigate to the path + if (IsQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.RightClick) { if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { @@ -367,27 +367,26 @@ private void LoadContextMenu() if (result is QuickSwitchResult quickSwitchResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(Win32Helper.GetForegroundWindow(), quickSwitchResult.QuickSwitchPath); + QuickSwitch.JumpToPath(DialogWindowHandle, quickSwitchResult.QuickSwitchPath); } } + return; } + // For query mode, we load context menu - else + if (QueryResultsSelected()) { - if (QueryResultsSelected()) - { - // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing - // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem != null) - { - SelectedResults = ContextMenu; - } - } - else + // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing + // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing + if (SelectedResults.SelectedItem != null) { - SelectedResults = Results; + SelectedResults = ContextMenu; } } + else + { + SelectedResults = Results; + } } [RelayCommand] @@ -452,16 +451,31 @@ private async Task OpenResultAsync(string index) return; } - var hideWindow = await result.ExecuteAsync(new ActionContext + // For quick switch and left click mode, we need to navigate to the path + if (IsQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.LeftClick) { - // not null means pressing modifier key + number, should ignore the modifier key - SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() - }) - .ConfigureAwait(false); - - if (hideWindow) + if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) + { + if (result is QuickSwitchResult quickSwitchResult) + { + Win32Helper.SetForegroundWindow(DialogWindowHandle); + QuickSwitch.JumpToPath(DialogWindowHandle, quickSwitchResult.QuickSwitchPath); + } + } + } + // For query mode, we execute the result + else { - Hide(); + var hideWindow = await result.ExecuteAsync(new ActionContext + { + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() + }).ConfigureAwait(false); + + if (hideWindow) + { + Hide(); + } } if (QueryResultsSelected()) @@ -1109,10 +1123,6 @@ private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { await QueryResultsAsync(searchDelay, isReQuery); } - else if (IsQuickSwitch) - { - return; - } else if (ContextMenuSelected()) { QueryContextMenu(); @@ -1581,7 +1591,7 @@ public bool ShouldIgnoreHotkeys() #region Quick Switch - public bool IsQuickSwitch { get; private set; } + public bool IsQuickSwitch { get; private set; } = false; public nint DialogWindowHandle { get; private set; } = nint.Zero; private bool PreviousMainWindowVisibilityStatus { get; set; } @@ -1632,6 +1642,8 @@ public async void SetupQuickSwitch(nint handle) public void ResetQuickSwitch() { + if (DialogWindowHandle == nint.Zero) return; + DialogWindowHandle = nint.Zero; IsQuickSwitch = false; @@ -1712,7 +1724,7 @@ public void Show() VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); // Switch keyboard layout - if (StartWithEnglishMode && !IsQuickSwitch) + if (StartWithEnglishMode) { Win32Helper.SwitchToEnglishKeyboardLayout(true); } @@ -1779,7 +1791,7 @@ public async void Hide() }, DispatcherPriority.Render); // Switch keyboard layout - if (StartWithEnglishMode && !IsQuickSwitch) + if (StartWithEnglishMode) { Win32Helper.RestorePreviousKeyboardLayout(); } From 2c6c7d1bc233896399e44e56ddd00506e035acf5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 19 Apr 2025 22:41:48 +0800 Subject: [PATCH 094/243] Improve display --- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index cf07000c918..93768e9ee2c 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -243,7 +243,6 @@ Type="InsideFit"> Date: Sat, 19 Apr 2025 23:04:25 +0800 Subject: [PATCH 095/243] Support file result behaviours --- .../QuickSwitch/QuickSwitch.cs | 18 ++++++++-- Flow.Launcher.Infrastructure/Win32Helper.cs | 34 +++++++++++-------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index fc80ee017b8..108a6b83342 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -582,8 +582,22 @@ public static void JumpToPath(nint dialog, string path, Action action = null) bool result; if (isFile) { - result = Win32Helper.FileJump(path, dialogHandle); - Log.Debug(ClassName, $"File Jump: {path}"); + switch (_settings.QuickSwitchFileResultBehaviour) + { + case QuickSwitchFileResultBehaviours.FullPath: + default: + result = Win32Helper.FileJump(path, dialogHandle, forceFileName: true); + Log.Debug(ClassName, $"File Jump FullPath: {path}"); + break; + case QuickSwitchFileResultBehaviours.Directory: + result = Win32Helper.DirJump(Path.GetDirectoryName(path), dialogHandle); + Log.Debug(ClassName, $"File Jump Directory: {path}"); + break; + case QuickSwitchFileResultBehaviours.DirectoryAndFileName: + result = Win32Helper.FileJump(path, dialogHandle); + Log.Debug(ClassName, $"File Jump DirectoryAndFileName: {path}"); + break; + } } else { diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 6e36823eac8..036f24e441f 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -698,9 +698,16 @@ private static bool TryGetNotoFont(string langKey, out string notoFont) private static readonly InputSimulator _inputSimulator = new(); - internal static bool FileJump(string filePath, HWND dialogHandle, bool altD = true) + internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool altD = true) { - return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle, altD); + if (forceFileName) + { + return DirFileJumpForFileName(filePath, dialogHandle); + } + else + { + return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle, altD); + } } internal static bool DirJump(string dirPath, HWND dialogHandle, bool altD = true) @@ -708,14 +715,8 @@ internal static bool DirJump(string dirPath, HWND dialogHandle, bool altD = true return DirFileJump(dirPath, null, dialogHandle, altD); } - private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true, bool editFileName = false) + private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true) { - // Directly edit file name input box. - if (editFileName) - { - return DirFileJumpForFileName(filePath, dialogHandle); - } - // Alt-D or Ctrl-L to focus on the path input box if (altD) { @@ -752,7 +753,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 // The dialog is a legacy one, so we edit file name text box directly. - return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle); + return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle, true); } var timeOut = !SpinWait.SpinUntil(() => @@ -784,7 +785,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia if (!string.IsNullOrEmpty(filePath)) { // After navigating to the path, we then set the file name. - return DirFileJump(null, Path.GetFileName(filePath), dialogHandle, altD, true); + return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, true); } return true; @@ -793,7 +794,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia /// /// Edit file name text box in the file open dialog. /// - private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle) + private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openTwice = false) { var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); @@ -811,10 +812,13 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle) SetWindowText(controlHandle, fileName); - // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, - // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + if (openTwice) + { + // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, + // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + } return true; } From 9e818d2745e54ddd7fe618ed6e074a75f690fffb Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 07:45:25 +0800 Subject: [PATCH 096/243] Reset window when changing quick switch mode --- Flow.Launcher/ViewModel/MainViewModel.cs | 108 ++++++++++++++++------- 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8afd533e785..3cfdcc5e021 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1595,6 +1595,7 @@ public bool ShouldIgnoreHotkeys() public nint DialogWindowHandle { get; private set; } = nint.Zero; private bool PreviousMainWindowVisibilityStatus { get; set; } + private bool ResetWindowOnNextShow { get; set; } = false; public void InitializeVisibilityStatus(bool visibilityStatus) { @@ -1630,17 +1631,19 @@ public async void SetupQuickSwitch(nint handle) { (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); + + _ = ResetWindowAsync(); } } else { Show(); + + _ = ResetWindowAsync(); } } -#pragma warning restore VSTHRD100 // Avoid async void methods - - public void ResetQuickSwitch() + public async void ResetQuickSwitch() { if (DialogWindowHandle == nint.Zero) return; @@ -1653,25 +1656,37 @@ public void ResetQuickSwitch() if (PreviousMainWindowVisibilityStatus) { Show(); + + _ = ResetWindowAsync(); } else { - Hide(); + await ResetWindowAsync(); + + Hide(false); } } else { - // Only update the position if (PreviousMainWindowVisibilityStatus) { + // Only update the position Application.Current?.Dispatcher.Invoke(() => { (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); + + _ = ResetWindowAsync(); + } + else + { + ResetWindowOnNextShow = true; } } } +#pragma warning restore VSTHRD100 // Avoid async void methods + public void HideQuickSwitch() { if (DialogWindowHandle != nint.Zero) @@ -1683,6 +1698,24 @@ public void HideQuickSwitch() } } + // Reset index & preview & selected results & query text + private async Task ResetWindowAsync() + { + lastHistoryIndex = 1; + + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + } + + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } + + await ChangeQueryTextAsync(string.Empty); + } + #endregion #region Public Methods @@ -1728,42 +1761,51 @@ public void Show() { Win32Helper.SwitchToEnglishKeyboardLayout(true); } - } - public async void Hide() - { - lastHistoryIndex = 1; - - if (ExternalPreviewVisible) + if (ResetWindowOnNextShow) { - await CloseExternalPreviewAsync(); + ResetWindowOnNextShow = false; + _ = ResetWindowAsync(); } + } - if (!QueryResultsSelected()) + public async void Hide(bool reset = true) + { + if (reset) { - SelectedResults = Results; - } + lastHistoryIndex = 1; - switch (Settings.LastQueryMode) - { - case LastQueryMode.Empty: - await ChangeQueryTextAsync(string.Empty); - break; - case LastQueryMode.Preserved: - case LastQueryMode.Selected: - LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; - break; - case LastQueryMode.ActionKeywordPreserved: - case LastQueryMode.ActionKeywordSelected: - var newQuery = _lastQuery.ActionKeyword; + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + } - if (!string.IsNullOrEmpty(newQuery)) - newQuery += " "; - await ChangeQueryTextAsync(newQuery); + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } - if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) - LastQuerySelected = false; - break; + switch (Settings.LastQueryMode) + { + case LastQueryMode.Empty: + await ChangeQueryTextAsync(string.Empty); + break; + case LastQueryMode.Preserved: + case LastQueryMode.Selected: + LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; + break; + case LastQueryMode.ActionKeywordPreserved: + case LastQueryMode.ActionKeywordSelected: + var newQuery = _lastQuery.ActionKeyword; + + if (!string.IsNullOrEmpty(newQuery)) + newQuery += " "; + await ChangeQueryTextAsync(newQuery); + + if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) + LastQuerySelected = false; + break; + } } // When application is exiting, the Application.Current will be null From ca937b4c32d6c982278494cee3293cadf053e904 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 08:29:42 +0800 Subject: [PATCH 097/243] Use GetDlgItem instead --- .../NativeMethods.txt | 3 ++- Flow.Launcher.Infrastructure/Win32Helper.cs | 20 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 989030d6f2e..c79682d60ea 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -76,4 +76,5 @@ MapVirtualKey WM_KEYUP WM_KEYDOWN GetCurrentThreadId -AttachThreadInput \ No newline at end of file +AttachThreadInput +GetDlgItem \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 036f24e441f..8bd7d9f5c8a 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -744,11 +744,11 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. - var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "WorkerW", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ReBarWindow32", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Address Band Root", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "msctls_progress32", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBoxEx32", null); + var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA005); // ReBarWindow32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // Address Band Root + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x0000); // msctls_progress32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBoxEx32 if (controlHandle == HWND.Null) { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 @@ -766,8 +766,8 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia return false; } - var editHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); - editHandle = PInvoke.FindWindowEx(editHandle, HWND.Null, "Edit", null); + var editHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBox + editHandle = PInvoke.GetDlgItem(editHandle, 0xA205); // Edit if (editHandle == HWND.Null) { return false; @@ -796,9 +796,9 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia /// private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openTwice = false) { - var controlHandle = PInvoke.FindWindowEx(dialogHandle, HWND.Null, "ComboBoxEx32", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "ComboBox", null); - controlHandle = PInvoke.FindWindowEx(controlHandle, HWND.Null, "Edit", null); + var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // Edit if (controlHandle == HWND.Null) { return false; From cd66288144aedbd88c155b81eaa6c433e6bf7574 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 08:58:21 +0800 Subject: [PATCH 098/243] Use click event to open --- .../NativeMethods.txt | 4 +- Flow.Launcher.Infrastructure/Win32Helper.cs | 40 ++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index c79682d60ea..daf9b9bb22f 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -77,4 +77,6 @@ WM_KEYUP WM_KEYDOWN GetCurrentThreadId AttachThreadInput -GetDlgItem \ No newline at end of file +GetDlgItem +PostMessage +BM_CLICK \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 8bd7d9f5c8a..ccc8ddd72e9 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -742,6 +742,12 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia SendKey(dialogHandle, VIRTUAL_KEY.VK_LCONTROL, true); // Release Left Ctrl }*/ + // Sometimes it is not focused + /*if (!CheckFocus(dialogHandle, editHandle)) + { + return false; + }*/ + // Get the handle of the path input box and then set the text. // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW @@ -773,19 +779,13 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia return false; } - // Sometimes it is not focused - /*if (!CheckFocus(dialogHandle, editHandle)) - { - return false; - }*/ - SetWindowText(editHandle, dirPath); _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); if (!string.IsNullOrEmpty(filePath)) { // After navigating to the path, we then set the file name. - return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, true); + return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle); } return true; @@ -794,7 +794,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia /// /// Edit file name text box in the file open dialog. /// - private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openTwice = false) + private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool open = false) { var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox @@ -804,20 +804,17 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, b return false; } - // Sometimes it is not focused - /*if (!CheckFocus(dialogHandle, controlHandle)) - { - return false; - }*/ - SetWindowText(controlHandle, fileName); - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); - if (openTwice) + if (open) { - // Alt-O (equivalent to press the Open button) twice. In normal cases it suffices to press once, - // but when the focus is on an irrelevant folder, that press once will just open the irrelevant one. - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_O); + var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button + if (openHandle == HWND.Null) + { + return false; + } + + ClickButton(openHandle); } return true; @@ -848,6 +845,11 @@ private static unsafe nint SetWindowText(HWND handle, string text) } } + private static unsafe nint ClickButton(HWND handle) + { + return PInvoke.PostMessage(handle, PInvoke.BM_CLICK, 0, 0).Value; + } + public static unsafe bool GetWindowRect(nint handle, out Rect outRect) { var rect = new RECT(); From 2de02a642e0c43584b7d1051756ffb9306ce2fde Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 09:20:42 +0800 Subject: [PATCH 099/243] Remove useless codes & Add new file result behaviour & Improve general page --- .../NativeMethods.txt | 7 -- .../QuickSwitch/QuickSwitch.cs | 11 +- .../UserSettings/Settings.cs | 1 + Flow.Launcher.Infrastructure/Win32Helper.cs | 107 ++---------------- Flow.Launcher/Languages/en.xaml | 7 +- .../Views/SettingsPaneGeneral.xaml | 2 +- 6 files changed, 26 insertions(+), 109 deletions(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index daf9b9bb22f..de63c8bd7f3 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -70,13 +70,6 @@ EVENT_OBJECT_DESTROY EVENT_OBJECT_LOCATIONCHANGE EVENT_SYSTEM_MOVESIZESTART EVENT_SYSTEM_MOVESIZEEND -GetFocus -SetFocus -MapVirtualKey -WM_KEYUP -WM_KEYDOWN -GetCurrentThreadId -AttachThreadInput GetDlgItem PostMessage BM_CLICK \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 108a6b83342..de4c4473670 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -585,10 +585,13 @@ public static void JumpToPath(nint dialog, string path, Action action = null) switch (_settings.QuickSwitchFileResultBehaviour) { case QuickSwitchFileResultBehaviours.FullPath: - default: result = Win32Helper.FileJump(path, dialogHandle, forceFileName: true); Log.Debug(ClassName, $"File Jump FullPath: {path}"); break; + case QuickSwitchFileResultBehaviours.FullPathOpen: + result = Win32Helper.FileJump(path, dialogHandle, forceFileName: true, openFile: true); + Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); + break; case QuickSwitchFileResultBehaviours.Directory: result = Win32Helper.DirJump(Path.GetDirectoryName(path), dialogHandle); Log.Debug(ClassName, $"File Jump Directory: {path}"); @@ -597,6 +600,12 @@ public static void JumpToPath(nint dialog, string path, Action action = null) result = Win32Helper.FileJump(path, dialogHandle); Log.Debug(ClassName, $"File Jump DirectoryAndFileName: {path}"); break; + default: + throw new ArgumentOutOfRangeException( + nameof(_settings.QuickSwitchFileResultBehaviour), + _settings.QuickSwitchFileResultBehaviour, + "Invalid QuickSwitchFileResultBehaviour" + ); } } else diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 192600acd0c..4dc10c2e49b 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -519,6 +519,7 @@ public enum QuickSwitchResultBehaviours public enum QuickSwitchFileResultBehaviours { FullPath, + FullPathOpen, Directory, DirectoryAndFileName } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index ccc8ddd72e9..a349af99518 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -698,58 +698,26 @@ private static bool TryGetNotoFont(string langKey, out string notoFont) private static readonly InputSimulator _inputSimulator = new(); - internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool altD = true) + internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) { if (forceFileName) { - return DirFileJumpForFileName(filePath, dialogHandle); + return DirFileJumpForFileName(filePath, dialogHandle, openFile); } else { - return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle, altD); + return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle); } } - internal static bool DirJump(string dirPath, HWND dialogHandle, bool altD = true) + internal static bool DirJump(string dirPath, HWND dialogHandle) { - return DirFileJump(dirPath, null, dialogHandle, altD); + return DirFileJump(dirPath, null, dialogHandle); } - private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle, bool altD = true) + private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle) { - // Alt-D or Ctrl-L to focus on the path input box - if (altD) - { - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); - } - else - { - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); - } - // Cannot work when activating with hotkey - /*if (altD) - { - SendKey(dialogHandle, VIRTUAL_KEY.VK_LMENU, false); // Press Left Alt - SendKey(dialogHandle, VIRTUAL_KEY.VK_D, false); // Press D - SendKey(dialogHandle, VIRTUAL_KEY.VK_D, true); // Release D - SendKey(dialogHandle, VIRTUAL_KEY.VK_LMENU, true); // Release Left Alt - } - else - { - SendKey(dialogHandle, VIRTUAL_KEY.VK_LCONTROL, false); // Press Left Ctrl - SendKey(dialogHandle, VIRTUAL_KEY.VK_L, false); // Press L - SendKey(dialogHandle, VIRTUAL_KEY.VK_L, true); // Release L - SendKey(dialogHandle, VIRTUAL_KEY.VK_LCONTROL, true); // Release Left Ctrl - }*/ - - // Sometimes it is not focused - /*if (!CheckFocus(dialogHandle, editHandle)) - { - return false; - }*/ - // Get the handle of the path input box and then set the text. - // The window with class name "ComboBoxEx32" is not visible when the path input box is not with the keyboard focus. var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA005); // ReBarWindow32 controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // Address Band Root @@ -780,12 +748,11 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia } SetWindowText(editHandle, dirPath); - _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); if (!string.IsNullOrEmpty(filePath)) { - // After navigating to the path, we then set the file name. - return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle); + // Note: I don't know why even openFile is set to false, the dialog still opens the file. + return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, false); } return true; @@ -794,7 +761,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia /// /// Edit file name text box in the file open dialog. /// - private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool open = false) + private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openFile) { var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox @@ -806,7 +773,7 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, b SetWindowText(controlHandle, fileName); - if (open) + if (openFile) { var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button if (openHandle == HWND.Null) @@ -820,23 +787,6 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, b return true; } - private static unsafe bool CheckFocus(HWND dialogHandle, HWND inputHandle) - { - var dwMyID = PInvoke.GetCurrentThreadId(); - var dwCurID = PInvoke.GetWindowThreadProcessId(dialogHandle); - - PInvoke.AttachThreadInput(dwMyID, dwCurID, true); - - var timeOut1 = !SpinWait.SpinUntil(() => PInvoke.GetFocus() == inputHandle, 1000); - if (timeOut1) - { - return false; - } - - PInvoke.AttachThreadInput(dwMyID, dwCurID, false); - return true; - } - private static unsafe nint SetWindowText(HWND handle, string text) { fixed (char* textPtr = text + '\0') @@ -870,43 +820,6 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) return true; } - private static void SendKey(HWND hWnd, VIRTUAL_KEY virtualKey, bool isKeyUp) - { - // Get virtual key value - var virtualKeyValue = (ushort)virtualKey; - - // Get scan code and extended flag - var scanCode = PInvoke.MapVirtualKey(virtualKeyValue, MAP_VIRTUAL_KEY_TYPE.MAPVK_VK_TO_VSC); - - // Check if the key is an extended key (e.g., right Alt/Ctrl) - var isExtended = virtualKey == VIRTUAL_KEY.VK_RMENU || virtualKey == VIRTUAL_KEY.VK_RCONTROL; - - // Create lParam - var lParam = CreateKeyLParam(scanCode, isExtended, isKeyUp, !isKeyUp); - - // Send message - var message = isKeyUp ? PInvoke.WM_KEYUP : PInvoke.WM_KEYDOWN; - PInvoke.PostMessage(hWnd, message, virtualKeyValue, new(lParam)); - } - - private static nint CreateKeyLParam(uint scanCode, bool isExtended, bool isKeyUp, bool wasKeyDown) - { - uint lParam = 0x00000001; // Repeat count (1 keystroke) - - lParam |= (scanCode << 16); // Scan code - - if (isExtended) - lParam |= 0x01000000; // Extended key flag - - if (wasKeyDown) - lParam |= 0x40000000; // Previous key state (1 if down before message) - - if (isKeyUp) - lParam |= 0x80000000; // Transition state (1 for release) - - return (nint)lParam; - } - #endregion } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 98a15387b51..0b425cac1ac 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -324,9 +324,10 @@ Right click Quick Switch File Navigation Behaviour Behaviour to navigate file dialogs when paths of the results are files - Open full path in file name box - Open file directory in path box - Open file directory in path box and file name in file name box + Fill full path in file name box + Fill full path in file name box and open + Fill directory in path box + Fill directory in path box, file name in file name box and open HTTP Proxy diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 93768e9ee2c..e38bb6689c4 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -267,7 +267,7 @@ Type="InsideFit"> Date: Sun, 20 Apr 2025 09:33:13 +0800 Subject: [PATCH 100/243] Only reset window first time open dialog --- Flow.Launcher/ViewModel/MainViewModel.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 3cfdcc5e021..3ff16a7c897 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1608,12 +1608,16 @@ public async void SetupQuickSwitch(nint handle) { if (handle == nint.Zero) return; - if (DialogWindowHandle != handle) // Only set once for one file dialog + // Only set flag & reset window once for one file dialog + var needReset = false; + if (DialogWindowHandle != handle) { PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; IsQuickSwitch = true; + needReset = true; + // Wait for a while to make sure the dialog is shown // If don't give a time, Positioning will be weird await Task.Delay(300); @@ -1632,14 +1636,20 @@ public async void SetupQuickSwitch(nint handle) (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); - _ = ResetWindowAsync(); + if (needReset) + { + _ = ResetWindowAsync(); + } } } else { Show(); - _ = ResetWindowAsync(); + if (needReset) + { + _ = ResetWindowAsync(); + } } } From 866d6f85756833049aaed100be5b7fcd84af23d3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 09:39:58 +0800 Subject: [PATCH 101/243] Improve quick switch position description --- Flow.Launcher.Infrastructure/Win32Helper.cs | 2 -- Flow.Launcher/Languages/en.xaml | 4 ++-- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index a349af99518..a6e10a755ae 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -696,8 +696,6 @@ private static bool TryGetNotoFont(string langKey, out string notoFont) // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - private static readonly InputSimulator _inputSimulator = new(); - internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) { if (forceFileName) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 0b425cac1ac..8ab5fec3508 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -316,8 +316,8 @@ Show quick switch search window when file dialogs are open to navigate its path. Quick Switch Window Position Select position for quick switch window - Fixed in center-bottom of dialogs - Shown and hidden as search window + Fixed under dialogs. Displayed after dialogs are created and until it is closed + Floating as search window. Displayed when activated like search window Quick Switch Result Navigation Behaviour Behaviour to navigate file dialogs to paths of the results Left click diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index e38bb6689c4..37a8d170f92 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -243,6 +243,7 @@ Type="InsideFit"> Date: Sun, 20 Apr 2025 10:58:32 +0800 Subject: [PATCH 102/243] Hide window when using left click --- 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 3ff16a7c897..0923bbb8599 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -454,6 +454,8 @@ private async Task OpenResultAsync(string index) // For quick switch and left click mode, we need to navigate to the path if (IsQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.LeftClick) { + Hide(); + if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { if (result is QuickSwitchResult quickSwitchResult) From b72077bd33706316c593bd6e1aa2ef0646f0dbaa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 11:47:08 +0800 Subject: [PATCH 103/243] Support quick switch window position --- .../QuickSwitch/QuickSwitch.cs | 60 +++++++++++++------ Flow.Launcher/MainWindow.xaml.cs | 16 ++--- Flow.Launcher/ViewModel/MainViewModel.cs | 57 +++++++++++------- 3 files changed, 83 insertions(+), 50 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index de4c4473670..9fc6a834088 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -28,6 +28,8 @@ public static class QuickSwitch public static Action HideQuickSwitchWindow { get; set; } = null; + public static QuickSwitchWindowPositions QuickSwitchWindowPosition { get; private set; } + #endregion #region Private Fields @@ -62,10 +64,10 @@ public static class QuickSwitch // Note: Here we do not start & stop the timer beacause when there are many dialog windows // Unhooking and hooking will take too much time which can make window position weird // So we start & stop the timer when we find a file dialog window - /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;*/ - private static HWND _hookedDialogWindowHandle = HWND.Null; - private static readonly object _hookedDialogWindowHandleLock = new();*/ + private static HWND _currentDialogWindowHandle = HWND.Null; + private static readonly object _currentDialogWindowHandleLock = new(); private static bool _initialized = false; private static bool _enabled = false; @@ -85,6 +87,9 @@ public static void InitializeQuickSwitch() _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; _dragMoveTimer.Tick += (s, e) => InvokeUpdateQuickSwitchWindow(); + // Initialize quick switch window position + QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; + _initialized = true; } @@ -240,18 +245,35 @@ private static unsafe void InvokeShowQuickSwitchWindow() // Show quick switch window if (_settings.ShowQuickSwitchWindow) { + lock (_currentDialogWindowHandleLock) + { + var currentDialogWindowChanged = _currentDialogWindowHandle == HWND.Null || + _currentDialogWindowHandle != _dialogWindowHandle; + + if (currentDialogWindowChanged) + { + // Save quick switch window position for one file dialog + QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; + } + + _currentDialogWindowHandle = _dialogWindowHandle; + } + ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); - _dragMoveTimer?.Start(); + if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + { + _dragMoveTimer?.Start(); + } // Note: Here we do not start & stop the timer beacause when there are many dialog windows // Unhooking and hooking will take too much time which can make window position weird // So we start & stop the timer when we find a file dialog window - /*lock (_hookedDialogWindowHandleLock) + /*lock (_currentDialogWindowHandleLock) { - var needHook = _hookedDialogWindowHandle == HWND.Null || - _hookedDialogWindowHandle != _dialogWindowHandle; + var currentDialogWindowChanged = _currentDialogWindowHandle == HWND.Null || + _currentDialogWindowHandle != _dialogWindowHandle; - if (needHook) + if (currentDialogWindowChanged) { if (!_moveSizeHook.IsNull) { @@ -271,8 +293,6 @@ private static unsafe void InvokeShowQuickSwitchWindow() threadId, PInvoke.WINEVENT_OUTOFCONTEXT); } - - _hookedDialogWindowHandle = _dialogWindowHandle; }*/ } } @@ -289,19 +309,19 @@ private static void InvokeResetQuickSwitchWindow() ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*lock (_hookedDialogWindowHandleLock) + lock (_currentDialogWindowHandleLock) { - if (!_moveSizeHook.IsNull) + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - } + }*/ - _hookedDialogWindowHandle = HWND.Null; - }*/ + _currentDialogWindowHandle = HWND.Null; + } } private static void InvokeHideQuickSwitchWindow() @@ -375,7 +395,6 @@ uint dwmsEventTime else if (hwnd == _mainWindowHandle) { // Nothing to do - Log.Debug(ClassName, $"Quick Switch Window: {hwnd}"); } else { @@ -730,6 +749,9 @@ public static void Dispose() PInvoke.UnhookWinEvent(_locationChangeHook); _locationChangeHook = HWINEVENTHOOK.Null; } + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window /*if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index b8682d91d1c..a2a009c48a5 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -196,7 +196,7 @@ private async void OnLoaded(object sender, RoutedEventArgs e) if (_viewModel.MainWindowVisibilityStatus) { // Play sound effect before activing the window - if (_settings.UseSound && !_viewModel.IsQuickSwitch) + if (_settings.UseSound && !_viewModel.IsQuickSwitchWindowUnderDialog()) { SoundPlay(); } @@ -219,7 +219,7 @@ private async void OnLoaded(object sender, RoutedEventArgs e) QueryTextBox.Focus(); // Play window animation - if (_settings.UseAnimation && !_viewModel.IsQuickSwitch) + if (_settings.UseAnimation && !_viewModel.IsQuickSwitchWindowUnderDialog()) { WindowAnimation(); } @@ -326,7 +326,7 @@ private void OnClosed(object sender, EventArgs e) private void OnLocationChanged(object sender, EventArgs e) { - if (_viewModel.IsQuickSwitch) + if (_viewModel.IsQuickSwitchWindowUnderDialog()) { return; } @@ -340,7 +340,7 @@ private void OnLocationChanged(object sender, EventArgs e) private async void OnDeactivated(object sender, EventArgs e) { - if (_viewModel.IsQuickSwitch) + if (_viewModel.IsQuickSwitchWindowUnderDialog()) { return; } @@ -483,7 +483,7 @@ private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (_viewModel.IsQuickSwitch) + if (_viewModel.IsQuickSwitchWindowUnderDialog()) { return IntPtr.Zero; } @@ -669,7 +669,7 @@ private void UpdateNotifyIconText() public void UpdatePosition() { - if (_viewModel.IsQuickSwitch) + if (_viewModel.IsQuickSwitchWindowUnderDialog()) { UpdateQuickSwitchPosition(); } @@ -1170,8 +1170,8 @@ private void InitializeQuickSwitch() private void UpdateQuickSwitchPosition() { - if (_viewModel.DialogWindowHandle == nint.Zero || - !_viewModel.MainWindowVisibilityStatus) return; + if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return; + if (!_viewModel.IsQuickSwitchWindowUnderDialog()) return; // Get dialog window rect var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0923bbb8599..b4053628857 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1593,17 +1593,24 @@ public bool ShouldIgnoreHotkeys() #region Quick Switch - public bool IsQuickSwitch { get; private set; } = false; public nint DialogWindowHandle { get; private set; } = nint.Zero; - + + private bool IsQuickSwitch { get; set; } = false; + + private static QuickSwitchWindowPositions QuickSwitchWindowPosition => QuickSwitch.QuickSwitchWindowPosition; + private bool PreviousMainWindowVisibilityStatus { get; set; } - private bool ResetWindowOnNextShow { get; set; } = false; public void InitializeVisibilityStatus(bool visibilityStatus) { PreviousMainWindowVisibilityStatus = visibilityStatus; } + public bool IsQuickSwitchWindowUnderDialog() + { + return IsQuickSwitch && QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; + } + #pragma warning disable VSTHRD100 // Avoid async void methods public async void SetupQuickSwitch(nint handle) @@ -1611,14 +1618,14 @@ public async void SetupQuickSwitch(nint handle) if (handle == nint.Zero) return; // Only set flag & reset window once for one file dialog - var needReset = false; + var dialogWindowHandleChanged = false; if (DialogWindowHandle != handle) { PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; IsQuickSwitch = true; - needReset = true; + dialogWindowHandleChanged = true; // Wait for a while to make sure the dialog is shown // If don't give a time, Positioning will be weird @@ -1630,27 +1637,34 @@ public async void SetupQuickSwitch(nint handle) if (MainWindowVisibilityStatus) { - // Only update the position - if (PreviousMainWindowVisibilityStatus) + if (dialogWindowHandleChanged) { + // Only update the position Application.Current?.Dispatcher.Invoke(() => { (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); - if (needReset) - { - _ = ResetWindowAsync(); - } + _ = ResetWindowAsync(); } } else { - Show(); + if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + { + Show(); - if (needReset) + if (dialogWindowHandleChanged) + { + _ = ResetWindowAsync(); + } + } + else { - _ = ResetWindowAsync(); + if (dialogWindowHandleChanged) + { + _ = ResetWindowAsync(); + } } } } @@ -1692,7 +1706,7 @@ public async void ResetQuickSwitch() } else { - ResetWindowOnNextShow = true; + _ = ResetWindowAsync(); } } } @@ -1703,9 +1717,12 @@ public void HideQuickSwitch() { if (DialogWindowHandle != nint.Zero) { - if (MainWindowVisibilityStatus) + if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { - Hide(); + if (MainWindowVisibilityStatus) + { + Hide(); + } } } } @@ -1773,12 +1790,6 @@ public void Show() { Win32Helper.SwitchToEnglishKeyboardLayout(true); } - - if (ResetWindowOnNextShow) - { - ResetWindowOnNextShow = false; - _ = ResetWindowAsync(); - } } public async void Hide(bool reset = true) From 337b96e17b55851b62d5dda82e372dda6ae213e7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 11:48:42 +0800 Subject: [PATCH 104/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 115 +++++++++++++++++- Flow.Launcher.Infrastructure/Win32Helper.cs | 110 +---------------- 2 files changed, 111 insertions(+), 114 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 9fc6a834088..3ebbb6d8254 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -13,6 +13,7 @@ using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -604,19 +605,19 @@ public static void JumpToPath(nint dialog, string path, Action action = null) switch (_settings.QuickSwitchFileResultBehaviour) { case QuickSwitchFileResultBehaviours.FullPath: - result = Win32Helper.FileJump(path, dialogHandle, forceFileName: true); + result = FileJump(path, dialogHandle, forceFileName: true); Log.Debug(ClassName, $"File Jump FullPath: {path}"); break; case QuickSwitchFileResultBehaviours.FullPathOpen: - result = Win32Helper.FileJump(path, dialogHandle, forceFileName: true, openFile: true); + result = FileJump(path, dialogHandle, forceFileName: true, openFile: true); Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); break; case QuickSwitchFileResultBehaviours.Directory: - result = Win32Helper.DirJump(Path.GetDirectoryName(path), dialogHandle); + result = DirJump(Path.GetDirectoryName(path), dialogHandle); Log.Debug(ClassName, $"File Jump Directory: {path}"); break; case QuickSwitchFileResultBehaviours.DirectoryAndFileName: - result = Win32Helper.FileJump(path, dialogHandle); + result = FileJump(path, dialogHandle); Log.Debug(ClassName, $"File Jump DirectoryAndFileName: {path}"); break; default: @@ -629,7 +630,7 @@ public static void JumpToPath(nint dialog, string path, Action action = null) } else { - result = Win32Helper.DirJump(path, dialogHandle); + result = DirJump(path, dialogHandle); Log.Debug(ClassName, $"Dir Jump: {path}"); } @@ -676,6 +677,110 @@ static bool CheckPath(string path, out bool file) } } + // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump + + internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) + { + if (forceFileName) + { + return DirFileJumpForFileName(filePath, dialogHandle, openFile); + } + else + { + return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle); + } + } + + internal static bool DirJump(string dirPath, HWND dialogHandle) + { + return DirFileJump(dirPath, null, dialogHandle); + } + + private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle) + { + // Get the handle of the path input box and then set the text. + var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA005); // ReBarWindow32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // Address Band Root + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x0000); // msctls_progress32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBoxEx32 + if (controlHandle == HWND.Null) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we edit file name text box directly. + return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle, true); + } + + var timeOut = !SpinWait.SpinUntil(() => + { + var style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + return false; + } + + var editHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBox + editHandle = PInvoke.GetDlgItem(editHandle, 0xA205); // Edit + if (editHandle == HWND.Null) + { + return false; + } + + SetWindowText(editHandle, dirPath); + + if (!string.IsNullOrEmpty(filePath)) + { + // Note: I don't know why even openFile is set to false, the dialog still opens the file. + return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, false); + } + + return true; + } + + /// + /// Edit file name text box in the file open dialog. + /// + private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openFile) + { + var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox + controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // Edit + if (controlHandle == HWND.Null) + { + return false; + } + + SetWindowText(controlHandle, fileName); + + if (openFile) + { + var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button + if (openHandle == HWND.Null) + { + return false; + } + + ClickButton(openHandle); + } + + return true; + } + + private static unsafe nint SetWindowText(HWND handle, string text) + { + fixed (char* textPtr = text + '\0') + { + return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; + } + } + + private static unsafe nint ClickButton(HWND handle) + { + return PInvoke.PostMessage(handle, PInvoke.BM_CLICK, 0, 0).Value; + } + #endregion #region Class Name diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index a6e10a755ae..53a5779de36 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -3,10 +3,8 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Threading; using System.Windows; using System.Windows.Interop; using System.Windows.Markup; @@ -18,8 +16,6 @@ using Windows.Win32.Graphics.Dwm; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; -using WindowsInput; -using WindowsInput.Native; using Point = System.Windows.Point; using SystemFonts = System.Windows.SystemFonts; @@ -692,111 +688,7 @@ private static bool TryGetNotoFont(string langKey, out string notoFont) #endregion - #region Quick Switch - - // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - - internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) - { - if (forceFileName) - { - return DirFileJumpForFileName(filePath, dialogHandle, openFile); - } - else - { - return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle); - } - } - - internal static bool DirJump(string dirPath, HWND dialogHandle) - { - return DirFileJump(dirPath, null, dialogHandle); - } - - private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle) - { - // Get the handle of the path input box and then set the text. - var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA005); // ReBarWindow32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // Address Band Root - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x0000); // msctls_progress32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBoxEx32 - if (controlHandle == HWND.Null) - { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we edit file name text box directly. - return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle, true); - } - - var timeOut = !SpinWait.SpinUntil(() => - { - var style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; - }, 1000); - if (timeOut) - { - return false; - } - - var editHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBox - editHandle = PInvoke.GetDlgItem(editHandle, 0xA205); // Edit - if (editHandle == HWND.Null) - { - return false; - } - - SetWindowText(editHandle, dirPath); - - if (!string.IsNullOrEmpty(filePath)) - { - // Note: I don't know why even openFile is set to false, the dialog still opens the file. - return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, false); - } - - return true; - } - - /// - /// Edit file name text box in the file open dialog. - /// - private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openFile) - { - var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // Edit - if (controlHandle == HWND.Null) - { - return false; - } - - SetWindowText(controlHandle, fileName); - - if (openFile) - { - var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button - if (openHandle == HWND.Null) - { - return false; - } - - ClickButton(openHandle); - } - - return true; - } - - private static unsafe nint SetWindowText(HWND handle, string text) - { - fixed (char* textPtr = text + '\0') - { - return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; - } - } - - private static unsafe nint ClickButton(HWND handle) - { - return PInvoke.PostMessage(handle, PInvoke.BM_CLICK, 0, 0).Value; - } + #region Window Rect public static unsafe bool GetWindowRect(nint handle, out Rect outRect) { From a7916acf043bd9000a4040b9d751636707c8c8f9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 11:53:10 +0800 Subject: [PATCH 105/243] Improve error log message --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 3ebbb6d8254..4496c2ed69e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -641,10 +641,6 @@ public static void JumpToPath(nint dialog, string path, Action action = null) _autoSwitchedDialogs.Add(dialogHandle); } } - else - { - Log.Error(ClassName, "Failed to jump to path"); - } } catch (System.Exception e) { @@ -708,6 +704,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 // The dialog is a legacy one, so we edit file name text box directly. + Log.Error(ClassName, "Failed to find control handle"); return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle, true); } @@ -718,6 +715,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia }, 1000); if (timeOut) { + Log.Error(ClassName, "Failed to find visible control handle"); return false; } @@ -725,6 +723,7 @@ private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dia editHandle = PInvoke.GetDlgItem(editHandle, 0xA205); // Edit if (editHandle == HWND.Null) { + Log.Error(ClassName, "Failed to find edit handle"); return false; } @@ -749,6 +748,7 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, b controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // Edit if (controlHandle == HWND.Null) { + Log.Error(ClassName, "Failed to find control handle"); return false; } @@ -759,6 +759,7 @@ private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, b var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button if (openHandle == HWND.Null) { + Log.Error(ClassName, "Failed to find open handle"); return false; } From 2b05e4fc79239ca874d382e3cd132bd42a945d80 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 12:23:03 +0800 Subject: [PATCH 106/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 4496c2ed69e..3c3f764c985 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -520,62 +520,7 @@ uint dwmsEventTime #region Navigate Path - private static void NavigateDialogPath(HWND dialog, Action action = null) - { - if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; - - object document = null; - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; - } - } - } - catch (COMException) - { - return; - } - - if (document is not IShellFolderViewDual2 folderView) - { - return; - } - - string path; - try - { - // CSWin32 Folder does not have Self, so we need to use dynamic type here - // Use dynamic to bypass static typing - dynamic folder = folderView.Folder; - - // Access the Self property via dynamic binding - dynamic folderItem = folder.Self; - - // Check if the item is part of the file system - if (folderItem != null && folderItem.IsFileSystem) - { - path = folderItem.Path; - } - else - { - // Handle non-file system paths (e.g., virtual folders) - path = string.Empty; - } - } - catch - { - return; - } - - JumpToPath(dialog.Value, path, action); - } + // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] public static void JumpToPath(nint dialog, string path, Action action = null) @@ -673,9 +618,64 @@ static bool CheckPath(string path, out bool file) } } - // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump + private static void NavigateDialogPath(HWND dialog, Action action = null) + { + if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; + + object document = null; + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _lastExplorerView; + document = explorerView.Document; + } + } + } + catch (COMException) + { + return; + } + + if (document is not IShellFolderViewDual2 folderView) + { + return; + } + + string path; + try + { + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Check if the item is part of the file system + if (folderItem != null && folderItem.IsFileSystem) + { + path = folderItem.Path; + } + else + { + // Handle non-file system paths (e.g., virtual folders) + path = string.Empty; + } + } + catch + { + return; + } + + JumpToPath(dialog.Value, path, action); + } - internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) + private static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) { if (forceFileName) { @@ -687,7 +687,7 @@ internal static bool FileJump(string filePath, HWND dialogHandle, bool forceFile } } - internal static bool DirJump(string dirPath, HWND dialogHandle) + private static bool DirJump(string dirPath, HWND dialogHandle) { return DirFileJump(dirPath, null, dialogHandle); } From ce93746e5b2a32601f1878e429fd1f0cf4499e9f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 13:43:57 +0800 Subject: [PATCH 107/243] Add third party explorer support --- .../Interface/IQuickSwitchExplorer.cs | 21 ++ .../QuickSwitch/Models/WindowsExplorer.cs | 162 ++++++++++++++++ .../QuickSwitch/QuickSwitch.cs | 180 ++++-------------- 3 files changed, 225 insertions(+), 138 deletions(-) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs new file mode 100644 index 00000000000..863ae0e4dcc --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs @@ -0,0 +1,21 @@ +using System; +using Windows.Win32.Foundation; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface +{ + /// + /// Interface for handling Windows Explorer instances in QuickSwitch. + /// + /// + /// Add models in QuickSwitch/Models folder and implement this interface. + /// Then add the instance in QuickSwitch._quickSwitchExplorers. + /// + internal interface IQuickSwitchExplorer : IDisposable + { + internal bool CheckExplorerWindow(HWND foreground); + + internal void RemoveExplorerWindow(); + + internal string GetExplorerPath(); + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs new file mode 100644 index 00000000000..facf62e5abc --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -0,0 +1,162 @@ +using System; +using System.Runtime.InteropServices; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +{ + internal class WindowsExplorer : IQuickSwitchExplorer + { + private static readonly string ClassName = nameof(WindowsExplorer); + + private static IWebBrowser2 _lastExplorerView = null; + private static readonly object _lastExplorerViewLock = new(); + + public bool CheckExplorerWindow(HWND foreground) + { + var isExplorer = false; + lock (_lastExplorerViewLock) + { + EnumerateShellWindows((shellWindow) => + { + try + { + if (shellWindow is not IWebBrowser2 explorer) + { + return; + } + + if (foreground != HWND.Null && explorer.HWND != foreground.Value) + { + return; + } + + _lastExplorerView = explorer; + isExplorer = true; + + Log.Debug(ClassName, $"{explorer.HWND.Value}"); + } + catch (COMException) + { + // Ignored + } + }); + } + return isExplorer; + + static unsafe void EnumerateShellWindows(Action action) + { + // Create an instance of ShellWindows + var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass + var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows + + var result = PInvoke.CoCreateInstance( + &clsidShellWindows, + null, + CLSCTX.CLSCTX_ALL, + &iidIShellWindows, + out var shellWindowsObj); + + if (result.Failed) return; + + var shellWindows = (IShellWindows)shellWindowsObj; + + // Enumerate the shell windows + var count = shellWindows.Count; + for (var i = 0; i < count; i++) + { + action(shellWindows.Item(i)); + } + } + } + + public string GetExplorerPath() + { + if (_lastExplorerView == null) return null; + + object document = null; + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _lastExplorerView; + document = explorerView.Document; + } + } + } + catch (COMException) + { + return null; + } + + if (document is not IShellFolderViewDual2 folderView) + { + return null; + } + + string path; + try + { + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Check if the item is part of the file system + if (folderItem != null && folderItem.IsFileSystem) + { + path = folderItem.Path; + } + else + { + // Handle non-file system paths (e.g., virtual folders) + path = string.Empty; + } + } + catch + { + return null; + } + + return path; + } + + public void RemoveExplorerWindow() + { + lock (_lastExplorerViewLock) + { + _lastExplorerView = null; + } + } + + public void Dispose() + { + // Release ComObjects + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; + } + } + } + catch (COMException) + { + _lastExplorerView = null; + } + } + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 3c3f764c985..864fa5309aa 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -1,18 +1,17 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using System.Threading; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Flow.Launcher.Infrastructure.QuickSwitch.Models; using Flow.Launcher.Infrastructure.UserSettings; using NHotkey; using Windows.Win32; using Windows.Win32.Foundation; -using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; -using Windows.Win32.UI.Shell; using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch @@ -42,8 +41,13 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); - private static IWebBrowser2 _lastExplorerView = null; - private static readonly object _lastExplorerViewLock = new(); + private static IQuickSwitchExplorer _lastExplorer = null; + private static readonly object _lastExplorerLock = new(); + + private static readonly List _quickSwitchExplorers = new() + { + new WindowsExplorer() + }; private static HWND _mainWindowHandle = HWND.Null; @@ -100,33 +104,25 @@ public static void SetupQuickSwitch(bool enabled) if (enabled) { - // Check all foreground windows and check if there are explorer windows - lock (_lastExplorerViewLock) + // Check if there are explorer windows + try { - var explorerInitialized = false; - EnumerateShellWindows((shellWindow) => + lock (_lastExplorerLock) { - if (shellWindow is not IWebBrowser2 explorer) - { - return; - } - - // Initialize one explorer window even if it is not foreground - if (!explorerInitialized) - { - _lastExplorerView = explorer; - - Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); - } - - // Force update explorer window if it is foreground - else if (Win32Helper.IsForegroundWindow(explorer.HWND.Value)) + foreach (var explorer in _quickSwitchExplorers) { - _lastExplorerView = explorer; - - Log.Debug(ClassName, $"Explorer Window: {explorer.HWND.Value}"); + // Use HWND.Null here because we want to check all windows + if (explorer.CheckExplorerWindow(HWND.Null)) + { + // Set last explorer view if not set, this is beacuse default WindowsExplorer is the first element + _lastExplorer ??= explorer; + } } - }); + } + } + catch (System.Exception) + { + // Ignored } // Unhook events @@ -183,9 +179,9 @@ public static void SetupQuickSwitch(bool enabled) else { // Remove last explorer - lock (_lastExplorerViewLock) + foreach (var explorer in _quickSwitchExplorers) { - _lastExplorerView = null; + explorer.RemoveExplorerWindow(); } // Remove dialog window handle @@ -404,34 +400,19 @@ uint dwmsEventTime InvokeHideQuickSwitchWindow(); } - // Check if explorer window is foreground + // Check if there are foreground explorer windows try { - lock (_lastExplorerViewLock) + lock (_lastExplorerLock) { - EnumerateShellWindows((shellWindow) => + foreach (var explorer in _quickSwitchExplorers) { - try - { - if (shellWindow is not IWebBrowser2 explorer) - { - return; - } - - if (explorer.HWND != hwnd.Value) - { - return; - } - - _lastExplorerView = explorer; - - Log.Debug(ClassName, $"Explorer Window: {hwnd}"); - } - catch (COMException) + if (explorer.CheckExplorerWindow(hwnd)) { - // Ignored + _lastExplorer = explorer; + break; } - }); + } } } catch (System.Exception) @@ -621,57 +602,12 @@ static bool CheckPath(string path, out bool file) private static void NavigateDialogPath(HWND dialog, Action action = null) { if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; - - object document = null; - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; - } - } - } - catch (COMException) - { - return; - } - - if (document is not IShellFolderViewDual2 folderView) - { - return; - } - string path; - try - { - // CSWin32 Folder does not have Self, so we need to use dynamic type here - // Use dynamic to bypass static typing - dynamic folder = folderView.Folder; - - // Access the Self property via dynamic binding - dynamic folderItem = folder.Self; - - // Check if the item is part of the file system - if (folderItem != null && folderItem.IsFileSystem) - { - path = folderItem.Path; - } - else - { - // Handle non-file system paths (e.g., virtual folders) - path = string.Empty; - } - } - catch + lock (_dialogWindowHandleLock) { - return; + path = _lastExplorer?.GetExplorerPath(); } - + if (string.IsNullOrEmpty(path)) return; JumpToPath(dialog.Value, path, action); } @@ -805,35 +741,6 @@ static unsafe string GetClassName(HWND handle) #endregion - #region Enumerate Windows - - private static unsafe void EnumerateShellWindows(Action action) - { - // Create an instance of ShellWindows - var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass - var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows - - var result = PInvoke.CoCreateInstance( - &clsidShellWindows, - null, - CLSCTX.CLSCTX_ALL, - &iidIShellWindows, - out var shellWindowsObj); - - if (result.Failed) return; - - var shellWindows = (IShellWindows)shellWindowsObj; - - // Enumerate the shell windows - var count = shellWindows.Count; - for (var i = 0; i < count; i++) - { - action(shellWindows.Item(i)); - } - } - - #endregion - #endregion #region Dispose @@ -869,18 +776,15 @@ public static void Dispose() _destroyChangeHook = HWINEVENTHOOK.Null; } - // Release ComObjects - try + // Dispose explorers + foreach (var explorer in _quickSwitchExplorers) { - if (_lastExplorerView != null) - { - Marshal.ReleaseComObject(_lastExplorerView); - _lastExplorerView = null; - } + explorer.Dispose(); } - catch (COMException) + _quickSwitchExplorers.Clear(); + lock (_lastExplorerLock) { - _lastExplorerView = null; + _lastExplorer = null; } // Stop drag move timer From d2325d5c660b852465863246536bef70c876b0df Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 14:57:36 +0800 Subject: [PATCH 108/243] Initialize third party file dialog support --- .../Interface/IQuickSwitchDialog.cs | 19 ++ .../Interface/IQuickSwitchDialogTab.cs | 13 + .../Interface/IQuickSwitchDialogWindow.cs | 12 + .../QuickSwitch/Models/WindowsDialog.cs | 102 +++++++ .../QuickSwitch/QuickSwitch.cs | 269 +++++++++++------- 5 files changed, 315 insertions(+), 100 deletions(-) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs new file mode 100644 index 00000000000..92f681e38d3 --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs @@ -0,0 +1,19 @@ +using System; +using Windows.Win32.Foundation; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface +{ + /// + /// Interface for handling File Dialog instances in QuickSwitch. + /// + /// + /// Add models in QuickSwitch/Models folder and implement this interface. + /// Then add the instance in QuickSwitch._quickSwitchDialogs. + /// + internal interface IQuickSwitchDialog : IDisposable + { + internal IQuickSwitchDialogWindow DialogWindow { get; } + + internal bool CheckDialogWindow(HWND hwnd); + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs new file mode 100644 index 00000000000..cb6523be45d --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs @@ -0,0 +1,13 @@ +using System; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface +{ + internal interface IQuickSwitchDialogTab : IDisposable + { + internal string GetCurrentFolder(); + + internal string GetCurrentFile(); + + internal bool OpenFolder(string path); + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs new file mode 100644 index 00000000000..e1ae59a9fda --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs @@ -0,0 +1,12 @@ +using System; +using Windows.Win32.Foundation; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface +{ + internal interface IQuickSwitchDialogWindow : IDisposable + { + internal HWND Handle { get; } + + internal IQuickSwitchDialogTab GetCurrentTab(); + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs new file mode 100644 index 00000000000..36e82f87b07 --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -0,0 +1,102 @@ +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +{ + internal class WindowsDialog : IQuickSwitchDialog + { + // The class name of a dialog window + private const string WindowsDialogClassName = "#32770"; + + public IQuickSwitchDialogWindow DialogWindow { get; private set; } + + public bool CheckDialogWindow(HWND hwnd) + { + if (GetWindowClassName(hwnd) == WindowsDialogClassName) + { + if (DialogWindow == null || DialogWindow.Handle != hwnd) + { + DialogWindow = new WindowsDialogWindow(hwnd); + } + return true; + } + return false; + } + + public void Dispose() + { + DialogWindow?.Dispose(); + DialogWindow = null; + } + + public static string GetWindowClassName(HWND handle) + { + return GetClassName(handle); + + static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) + { + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => null, + _ => new string(buf), + }; + } + } + } + } + + internal class WindowsDialogWindow : IQuickSwitchDialogWindow + { + public HWND Handle { get; private set; } + + public WindowsDialogWindow(HWND handle) + { + Handle = handle; + } + + public IQuickSwitchDialogTab GetCurrentTab() + { + return new WindowsDialogTab(Handle); + } + + public void Dispose() + { + Handle = HWND.Null; + } + } + + internal class WindowsDialogTab : IQuickSwitchDialogTab + { + public HWND Handle { get; private set; } + + public WindowsDialogTab(HWND handle) + { + Handle = handle; + } + + public string GetCurrentFolder() + { + // TODO + return string.Empty; + } + + public string GetCurrentFile() + { + // TODO + return string.Empty; + } + + public bool OpenFolder(string path) + { + return false; + } + + public void Dispose() + { + Handle = HWND.Null; + } + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 864fa5309aa..11a443210f2 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -34,25 +34,30 @@ public static class QuickSwitch #region Private Fields - // The class name of a dialog window - private const string DialogWindowClassName = "#32770"; - private static readonly string ClassName = nameof(QuickSwitch); private static readonly Settings _settings = Ioc.Default.GetRequiredService(); - private static IQuickSwitchExplorer _lastExplorer = null; - private static readonly object _lastExplorerLock = new(); + private static HWND _mainWindowHandle = HWND.Null; private static readonly List _quickSwitchExplorers = new() { new WindowsExplorer() }; - private static HWND _mainWindowHandle = HWND.Null; + private static IQuickSwitchExplorer _lastExplorer = null; + private static readonly object _lastExplorerLock = new(); - private static HWND _dialogWindowHandle = HWND.Null; - private static readonly object _dialogWindowHandleLock = new(); + private static readonly List _quickSwitchDialogs = new() + { + new WindowsDialog() + }; + + private static IQuickSwitchDialogWindow _dialogWindow = null; + private static readonly object _dialogWindowLock = new(); + + private static IQuickSwitchDialogWindow _currentDialogWindow = null; + private static readonly object _currentDialogWindowLock = new(); private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; @@ -64,15 +69,12 @@ public static class QuickSwitch private static readonly List _autoSwitchedDialogs = new(); private static readonly object _autoSwitchedDialogsLock = new(); - private static readonly SemaphoreSlim _navigationLock = new(1, 1); - // Note: Here we do not start & stop the timer beacause when there are many dialog windows // Unhooking and hooking will take too much time which can make window position weird // So we start & stop the timer when we find a file dialog window /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;*/ - private static HWND _currentDialogWindowHandle = HWND.Null; - private static readonly object _currentDialogWindowHandleLock = new(); + private static readonly SemaphoreSlim _navigationLock = new(1, 1); private static bool _initialized = false; private static bool _enabled = false; @@ -186,11 +188,21 @@ public static void SetupQuickSwitch(bool enabled) // Remove dialog window handle var dialogWindowExists = false; - lock (_dialogWindowHandleLock) + lock (_dialogWindowLock) { - if (_dialogWindowHandle != HWND.Null) + if (_dialogWindow != null) { - _dialogWindowHandle = HWND.Null; + _dialogWindow.Dispose(); + _dialogWindow = null; + dialogWindowExists = true; + } + } + lock (_currentDialogWindowLock) + { + if (_currentDialogWindow != null) + { + _currentDialogWindow.Dispose(); + _currentDialogWindow = null; dialogWindowExists = true; } } @@ -242,55 +254,57 @@ private static unsafe void InvokeShowQuickSwitchWindow() // Show quick switch window if (_settings.ShowQuickSwitchWindow) { - lock (_currentDialogWindowHandleLock) + lock (_currentDialogWindowLock) { - var currentDialogWindowChanged = _currentDialogWindowHandle == HWND.Null || - _currentDialogWindowHandle != _dialogWindowHandle; - - if (currentDialogWindowChanged) + lock (_dialogWindowLock) { - // Save quick switch window position for one file dialog - QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; - } + var currentDialogWindowChanged = _currentDialogWindow == null || + _currentDialogWindow != _dialogWindow; + + if (currentDialogWindowChanged) + { + // Save quick switch window position for one file dialog + QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; + } - _currentDialogWindowHandle = _dialogWindowHandle; + _currentDialogWindow = _dialogWindow; + } } - ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value); + ShowQuickSwitchWindow?.Invoke(_dialogWindow.Handle); if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { _dragMoveTimer?.Start(); - } - - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*lock (_currentDialogWindowHandleLock) - { - var currentDialogWindowChanged = _currentDialogWindowHandle == HWND.Null || - _currentDialogWindowHandle != _dialogWindowHandle; - - if (currentDialogWindowChanged) + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*lock (_currentDialogWindowLock) { - if (!_moveSizeHook.IsNull) + var currentDialogWindowChanged = _currentDialogWindow == null || + _currentDialogWindow != _dialogWindow; + + if (currentDialogWindowChanged) { - PInvoke.UnhookWinEvent(_moveSizeHook); - _moveSizeHook = HWINEVENTHOOK.Null; - } + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } - // Call MoveSizeCallBack when the window is moved or resized - uint processId; - var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindowHandle, &processId); - _moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - PInvoke.GetModuleHandle((PCWSTR)null), - MoveSizeCallBack, - processId, - threadId, - PInvoke.WINEVENT_OUTOFCONTEXT); - } - }*/ + // Call MoveSizeCallBack when the window is moved or resized + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + MoveSizeCallBack, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); + } + }*/ + } } } @@ -305,19 +319,22 @@ private static void InvokeResetQuickSwitchWindow() // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); - - lock (_currentDialogWindowHandleLock) + // Note: Here we do not start & stop the timer beacause when there are many dialog windows + // Unhooking and hooking will take too much time which can make window position weird + // So we start & stop the timer when we find a file dialog window + /*if (!_moveSizeHook.IsNull) { - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*if (!_moveSizeHook.IsNull) - { - PInvoke.UnhookWinEvent(_moveSizeHook); - _moveSizeHook = HWINEVENTHOOK.Null; - }*/ + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + }*/ - _currentDialogWindowHandle = HWND.Null; + lock (_dialogWindowLock) + { + _dialogWindow = null; + } + lock (_currentDialogWindowLock) + { + _currentDialogWindow = null; } } @@ -353,15 +370,23 @@ uint dwmsEventTime ) { // File dialog window - if (GetWindowClassName(hwnd) == DialogWindowClassName) + var findDialogWindow = false; + foreach (var dialog in _quickSwitchDialogs) { - Log.Debug(ClassName, $"Dialog Window: {hwnd}"); - - lock (_dialogWindowHandleLock) + if (dialog.CheckDialogWindow(hwnd)) { - _dialogWindowHandle = hwnd; - } + lock (_dialogWindowLock) + { + _dialogWindow = dialog.DialogWindow; + } + findDialogWindow = true; + Log.Debug(ClassName, $"Dialog Window: {hwnd}"); + break; + } + } + if (findDialogWindow) + { // Navigate to path if (_settings.AutoQuickSwitch) { @@ -395,7 +420,7 @@ uint dwmsEventTime } else { - if (_dialogWindowHandle != HWND.Null) + if (_dialogWindow != null) { InvokeHideQuickSwitchWindow(); } @@ -433,7 +458,7 @@ uint dwmsEventTime ) { // If the dialog window is moved, update the quick switch window position - if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) { InvokeUpdateQuickSwitchWindow(); } @@ -478,12 +503,12 @@ uint dwmsEventTime ) { // If the dialog window is destroyed, set _dialogWindowHandle to null - if (_dialogWindowHandle != HWND.Null && _dialogWindowHandle == hwnd) + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) { Log.Debug(ClassName, $"Dialog Hwnd: {hwnd}"); - lock (_dialogWindowHandleLock) + lock (_dialogWindowLock) { - _dialogWindowHandle = HWND.Null; + _dialogWindow = null; } lock (_autoSwitchedDialogsLock) { @@ -599,16 +624,66 @@ static bool CheckPath(string path, out bool file) } } - private static void NavigateDialogPath(HWND dialog, Action action = null) + private static void NavigateDialogPath(HWND hwnd, Action action = null) { - if (dialog == HWND.Null || GetWindowClassName(dialog) != DialogWindowClassName) return; + if (hwnd == HWND.Null) return; + + IQuickSwitchDialogWindow dialogWindow = null; + // First check dialog window + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + dialogWindow = _dialogWindow; + } + } + if (dialogWindow == null) + { + // Then check current dialog window + lock (_currentDialogWindowLock) + { + if (_currentDialogWindow != null && _currentDialogWindow.Handle == hwnd) + { + dialogWindow = _currentDialogWindow; + } + } + } + if (dialogWindow == null) + { + // After that check all dialog windows + foreach (var dialog in _quickSwitchDialogs) + { + if (dialog.DialogWindow.Handle == hwnd) + { + dialogWindow = dialog.DialogWindow; + break; + } + } + } + if (dialogWindow == null) + { + // Finally search for the dialog window + foreach (var dialog in _quickSwitchDialogs) + { + if (dialog.CheckDialogWindow(hwnd)) + { + dialogWindow = dialog.DialogWindow; + break; + } + } + } + if (dialogWindow == null) return; + + // Get explorer path string path; - lock (_dialogWindowHandleLock) + lock (_dialogWindowLock) { path = _lastExplorer?.GetExplorerPath(); } if (string.IsNullOrEmpty(path)) return; - JumpToPath(dialog.Value, path, action); + + // Jump to path + JumpToPath(hwnd.Value, path, action); } private static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) @@ -720,27 +795,6 @@ private static unsafe nint ClickButton(HWND handle) #endregion - #region Class Name - - private static string GetWindowClassName(HWND handle) - { - return GetClassName(handle); - - static unsafe string GetClassName(HWND handle) - { - fixed (char* buf = new char[256]) - { - return PInvoke.GetClassName(handle, buf, 256) switch - { - 0 => null, - _ => new string(buf), - }; - } - } - } - - #endregion - #endregion #region Dispose @@ -787,6 +841,21 @@ public static void Dispose() _lastExplorer = null; } + // Dispose dialogs + foreach (var dialog in _quickSwitchDialogs) + { + dialog.Dispose(); + } + _quickSwitchDialogs.Clear(); + lock (_dialogWindowLock) + { + _dialogWindow = null; + } + lock (_currentDialogWindowLock) + { + _currentDialogWindow = null; + } + // Stop drag move timer if (_dragMoveTimer != null) { From 0da8eb3a21da08ea3f7b9c38fc74dbb6f161378b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 15:00:29 +0800 Subject: [PATCH 109/243] Fix code issue --- .../QuickSwitch/QuickSwitch.cs | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 11a443210f2..524c5042333 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -280,28 +280,31 @@ private static unsafe void InvokeShowQuickSwitchWindow() // So we start & stop the timer when we find a file dialog window /*lock (_currentDialogWindowLock) { - var currentDialogWindowChanged = _currentDialogWindow == null || + lock (_dialogWindowLock) + { + var currentDialogWindowChanged = _currentDialogWindow == null || _currentDialogWindow != _dialogWindow; - if (currentDialogWindowChanged) - { - if (!_moveSizeHook.IsNull) + if (currentDialogWindowChanged) { - PInvoke.UnhookWinEvent(_moveSizeHook); - _moveSizeHook = HWINEVENTHOOK.Null; + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + + // Call MoveSizeCallBack when the window is moved or resized + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + MoveSizeCallBack, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); } - - // Call MoveSizeCallBack when the window is moved or resized - uint processId; - var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); - _moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - PInvoke.GetModuleHandle((PCWSTR)null), - MoveSizeCallBack, - processId, - threadId, - PInvoke.WINEVENT_OUTOFCONTEXT); } }*/ } @@ -637,20 +640,9 @@ private static void NavigateDialogPath(HWND hwnd, Action action = null) dialogWindow = _dialogWindow; } } + // Then check all dialog windows if (dialogWindow == null) { - // Then check current dialog window - lock (_currentDialogWindowLock) - { - if (_currentDialogWindow != null && _currentDialogWindow.Handle == hwnd) - { - dialogWindow = _currentDialogWindow; - } - } - } - if (dialogWindow == null) - { - // After that check all dialog windows foreach (var dialog in _quickSwitchDialogs) { if (dialog.DialogWindow.Handle == hwnd) @@ -660,9 +652,9 @@ private static void NavigateDialogPath(HWND hwnd, Action action = null) } } } + // Finally search for the dialog window if (dialogWindow == null) { - // Finally search for the dialog window foreach (var dialog in _quickSwitchDialogs) { if (dialog.CheckDialogWindow(hwnd)) From 13656d1c5b83ec8ef882d2b4f3618aaa7efff1f3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 15:07:57 +0800 Subject: [PATCH 110/243] Remove current dialog --- .../QuickSwitch/QuickSwitch.cs | 90 ++++++------------- 1 file changed, 26 insertions(+), 64 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 524c5042333..00199f6e120 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -56,9 +56,6 @@ public static class QuickSwitch private static IQuickSwitchDialogWindow _dialogWindow = null; private static readonly object _dialogWindowLock = new(); - private static IQuickSwitchDialogWindow _currentDialogWindow = null; - private static readonly object _currentDialogWindowLock = new(); - private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; @@ -197,15 +194,6 @@ public static void SetupQuickSwitch(bool enabled) dialogWindowExists = true; } } - lock (_currentDialogWindowLock) - { - if (_currentDialogWindow != null) - { - _currentDialogWindow.Dispose(); - _currentDialogWindow = null; - dialogWindowExists = true; - } - } // Remove auto switched dialogs lock (_autoSwitchedDialogsLock) @@ -249,26 +237,15 @@ public static void SetupQuickSwitch(bool enabled) #region Invoke Property Events - private static unsafe void InvokeShowQuickSwitchWindow() + private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) { // Show quick switch window if (_settings.ShowQuickSwitchWindow) { - lock (_currentDialogWindowLock) + if (dialogWindowChanged) { - lock (_dialogWindowLock) - { - var currentDialogWindowChanged = _currentDialogWindow == null || - _currentDialogWindow != _dialogWindow; - - if (currentDialogWindowChanged) - { - // Save quick switch window position for one file dialog - QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; - } - - _currentDialogWindow = _dialogWindow; - } + // Save quick switch window position for one file dialog + QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; } ShowQuickSwitchWindow?.Invoke(_dialogWindow.Handle); @@ -278,34 +255,25 @@ private static unsafe void InvokeShowQuickSwitchWindow() // Note: Here we do not start & stop the timer beacause when there are many dialog windows // Unhooking and hooking will take too much time which can make window position weird // So we start & stop the timer when we find a file dialog window - /*lock (_currentDialogWindowLock) + /*if (dialogWindowChanged) { - lock (_dialogWindowLock) + if (!_moveSizeHook.IsNull) { - var currentDialogWindowChanged = _currentDialogWindow == null || - _currentDialogWindow != _dialogWindow; - - if (currentDialogWindowChanged) - { - if (!_moveSizeHook.IsNull) - { - PInvoke.UnhookWinEvent(_moveSizeHook); - _moveSizeHook = HWINEVENTHOOK.Null; - } - - // Call MoveSizeCallBack when the window is moved or resized - uint processId; - var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); - _moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - PInvoke.GetModuleHandle((PCWSTR)null), - MoveSizeCallBack, - processId, - threadId, - PInvoke.WINEVENT_OUTOFCONTEXT); - } + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; } + + // Call MoveSizeCallBack when the window is moved or resized + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + MoveSizeCallBack, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); }*/ } } @@ -335,10 +303,6 @@ private static void InvokeResetQuickSwitchWindow() { _dialogWindow = null; } - lock (_currentDialogWindowLock) - { - _currentDialogWindow = null; - } } private static void InvokeHideQuickSwitchWindow() @@ -374,12 +338,14 @@ uint dwmsEventTime { // File dialog window var findDialogWindow = false; + var dialogWindowChanged = false; foreach (var dialog in _quickSwitchDialogs) { if (dialog.CheckDialogWindow(hwnd)) { lock (_dialogWindowLock) { + dialogWindowChanged = _dialogWindow == null || _dialogWindow.Handle != hwnd; _dialogWindow = dialog.DialogWindow; } @@ -403,17 +369,17 @@ uint dwmsEventTime // Just show quick switch window if (alreadySwitched) { - InvokeShowQuickSwitchWindow(); + InvokeShowQuickSwitchWindow(dialogWindowChanged); } // Show quick switch window after navigating the path else { - NavigateDialogPath(hwnd, InvokeShowQuickSwitchWindow); + NavigateDialogPath(hwnd, () => InvokeShowQuickSwitchWindow(dialogWindowChanged)); } } else { - InvokeShowQuickSwitchWindow(); + InvokeShowQuickSwitchWindow(dialogWindowChanged); } } // Quick switch window @@ -668,7 +634,7 @@ private static void NavigateDialogPath(HWND hwnd, Action action = null) // Get explorer path string path; - lock (_dialogWindowLock) + lock (_lastExplorerLock) { path = _lastExplorer?.GetExplorerPath(); } @@ -843,10 +809,6 @@ public static void Dispose() { _dialogWindow = null; } - lock (_currentDialogWindowLock) - { - _currentDialogWindow = null; - } // Stop drag move timer if (_dragMoveTimer != null) From 277171d2291c3f095289dd9d59d5bd54f6f37189 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 15:12:59 +0800 Subject: [PATCH 111/243] Use empty string --- .../QuickSwitch/Models/WindowsDialog.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 36e82f87b07..99a13a5ba6f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -40,7 +40,7 @@ static unsafe string GetClassName(HWND handle) { return PInvoke.GetClassName(handle, buf, 256) switch { - 0 => null, + 0 => string.Empty, _ => new string(buf), }; } @@ -91,6 +91,7 @@ public string GetCurrentFile() public bool OpenFolder(string path) { + // TODO return false; } From 086f982a3ccb2a9818bc153bb0c701044c7ff645 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 15:35:34 +0800 Subject: [PATCH 112/243] Add auto switch option back --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 2 -- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 4dc10c2e49b..5c84d204665 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -232,8 +232,6 @@ public CustomBrowserViewModel CustomBrowser public bool EnableQuickSwitch { get; set; } = true; - // TODO: TODO: Due to many issues, this option is removed from FL - // Please see https://github.com/Flow-Launcher/Flow.Launcher/pull/1018 public bool AutoQuickSwitch { get; set; } = false; public bool ShowQuickSwitchWindow { get; set; } = true; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 37a8d170f92..502e92dbdc8 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -215,9 +215,7 @@ - - - + Date: Sun, 20 Apr 2025 15:37:20 +0800 Subject: [PATCH 113/243] Improve strings --- Flow.Launcher/Languages/en.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 8ab5fec3508..8784ed49919 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -311,9 +311,9 @@ Quick Switch Quickly navigate to the path of the current Explorer when a file dialog is opened. Quick Switch Automatically - Quick switch automatically navigate to the path of the current Explorer when a file dialog is opened. + Automatically navigate to the path of the current Explorer when a file dialog is opened. Show Quick Switch Window - Show quick switch search window when file dialogs are open to navigate its path. + Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). Quick Switch Window Position Select position for quick switch window Fixed under dialogs. Displayed after dialogs are created and until it is closed From 23b586e9212f21b57e842dd6db4a2547d317317d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 15:38:55 +0800 Subject: [PATCH 114/243] Improve log message --- .../QuickSwitch/Models/WindowsExplorer.cs | 3 --- .../QuickSwitch/QuickSwitch.cs | 23 ++++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index facf62e5abc..461afa1de06 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.QuickSwitch.Interface; using Windows.Win32; using Windows.Win32.Foundation; @@ -37,8 +36,6 @@ public bool CheckExplorerWindow(HWND foreground) _lastExplorerView = explorer; isExplorer = true; - - Log.Debug(ClassName, $"{explorer.HWND.Value}"); } catch (COMException) { diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 00199f6e120..f425a0a9f10 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -113,8 +113,14 @@ public static void SetupQuickSwitch(bool enabled) // Use HWND.Null here because we want to check all windows if (explorer.CheckExplorerWindow(HWND.Null)) { - // Set last explorer view if not set, this is beacuse default WindowsExplorer is the first element - _lastExplorer ??= explorer; + if (_lastExplorer == null) + { + Log.Debug(ClassName, $"Explorer window"); + // Set last explorer view if not set, + // this is beacuse default WindowsExplorer is the first element + _lastExplorer = explorer; + break; + } } } } @@ -403,6 +409,7 @@ uint dwmsEventTime { if (explorer.CheckExplorerWindow(hwnd)) { + Log.Debug(ClassName, $"Explorer window: {hwnd}"); _lastExplorer = explorer; break; } @@ -474,7 +481,7 @@ uint dwmsEventTime // If the dialog window is destroyed, set _dialogWindowHandle to null if (_dialogWindow != null && _dialogWindow.Handle == hwnd) { - Log.Debug(ClassName, $"Dialog Hwnd: {hwnd}"); + Log.Debug(ClassName, $"Destory dialog: {hwnd}"); lock (_dialogWindowLock) { _dialogWindow = null; @@ -525,20 +532,20 @@ public static void JumpToPath(nint dialog, string path, Action action = null) switch (_settings.QuickSwitchFileResultBehaviour) { case QuickSwitchFileResultBehaviours.FullPath: - result = FileJump(path, dialogHandle, forceFileName: true); Log.Debug(ClassName, $"File Jump FullPath: {path}"); + result = FileJump(path, dialogHandle, forceFileName: true); break; case QuickSwitchFileResultBehaviours.FullPathOpen: - result = FileJump(path, dialogHandle, forceFileName: true, openFile: true); Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); + result = FileJump(path, dialogHandle, forceFileName: true, openFile: true); break; case QuickSwitchFileResultBehaviours.Directory: - result = DirJump(Path.GetDirectoryName(path), dialogHandle); Log.Debug(ClassName, $"File Jump Directory: {path}"); + result = DirJump(Path.GetDirectoryName(path), dialogHandle); break; case QuickSwitchFileResultBehaviours.DirectoryAndFileName: - result = FileJump(path, dialogHandle); Log.Debug(ClassName, $"File Jump DirectoryAndFileName: {path}"); + result = FileJump(path, dialogHandle); break; default: throw new ArgumentOutOfRangeException( @@ -550,8 +557,8 @@ public static void JumpToPath(nint dialog, string path, Action action = null) } else { - result = DirJump(path, dialogHandle); Log.Debug(ClassName, $"Dir Jump: {path}"); + result = DirJump(path, dialogHandle); } if (result) From 7c50ca39efd04a1559b0c28ecb605d662858b377 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:24:06 +0800 Subject: [PATCH 115/243] Support third party file dialog --- .../NativeMethods.txt | 3 +- .../Interface/IQuickSwitchDialogTab.cs | 13 - .../Interface/IQuickSwitchDialogWindow.cs | 2 +- .../Interface/IQuickSwitchDialogWindowTab.cs | 20 ++ .../QuickSwitch/Models/WindowsDialog.cs | 146 ++++++++++- .../QuickSwitch/QuickSwitch.cs | 242 ++++++------------ 6 files changed, 243 insertions(+), 183 deletions(-) delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index de63c8bd7f3..7e7067ff064 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -72,4 +72,5 @@ EVENT_SYSTEM_MOVESIZESTART EVENT_SYSTEM_MOVESIZEEND GetDlgItem PostMessage -BM_CLICK \ No newline at end of file +BM_CLICK +WM_GETTEXT \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs deleted file mode 100644 index cb6523be45d..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogTab.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface -{ - internal interface IQuickSwitchDialogTab : IDisposable - { - internal string GetCurrentFolder(); - - internal string GetCurrentFile(); - - internal bool OpenFolder(string path); - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs index e1ae59a9fda..09bfa2ad9e6 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs @@ -7,6 +7,6 @@ internal interface IQuickSwitchDialogWindow : IDisposable { internal HWND Handle { get; } - internal IQuickSwitchDialogTab GetCurrentTab(); + internal IQuickSwitchDialogWindowTab GetCurrentTab(); } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs new file mode 100644 index 00000000000..0d6d173887a --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs @@ -0,0 +1,20 @@ +using System; +using Windows.Win32.Foundation; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface +{ + internal interface IQuickSwitchDialogWindowTab : IDisposable + { + internal HWND Handle { get; } + + internal string GetCurrentFolder(); + + internal string GetCurrentFile(); + + internal bool JumpFolder(string path, bool auto); + + internal bool JumpFile(string path); + + internal bool Open(); + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 99a13a5ba6f..d9ddae03a1a 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -1,6 +1,10 @@ -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using System; +using System.Threading; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch.Models { @@ -57,7 +61,7 @@ public WindowsDialogWindow(HWND handle) Handle = handle; } - public IQuickSwitchDialogTab GetCurrentTab() + public IQuickSwitchDialogWindowTab GetCurrentTab() { return new WindowsDialogTab(Handle); } @@ -68,31 +72,153 @@ public void Dispose() } } - internal class WindowsDialogTab : IQuickSwitchDialogTab + internal class WindowsDialogTab : IQuickSwitchDialogWindowTab { public HWND Handle { get; private set; } + private static readonly string ClassName = nameof(WindowsDialogTab); + + private readonly bool _legacy = false; + + private readonly HWND _pathControl; + private readonly HWND _pathEditor; + private readonly HWND _fileEditor; + private readonly HWND _openButton; + public WindowsDialogTab(HWND handle) { Handle = handle; + + // Get the handle of the path editor + // The window with class name "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus + _pathControl = PInvoke.GetDlgItem(handle, 0x0000); // WorkerW + _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA005); // ReBarWindow32 + _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // Address Band Root + _pathControl = PInvoke.GetDlgItem(_pathControl, 0x0000); // msctls_progress32 + _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBoxEx32 + if (_pathControl == HWND.Null) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we edit file name editor directly. + _legacy = true; + _pathEditor = HWND.Null; + Log.Info(ClassName, "Failed to find path control handle - Legacy dialog"); + } + else + { + _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox + _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit + if (_pathEditor == HWND.Null) + { + Log.Error(ClassName, "Failed to find path editor handle"); + } + } + + // Get the handle of the file name editor of Open file dialog + _fileEditor = PInvoke.GetDlgItem(handle, 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit + if (_fileEditor == HWND.Null) + { + // Get the handle of the file name editor of Save/SaveAs file dialog + _fileEditor = PInvoke.GetDlgItem(handle, 0x0000); // DUIViewWndClassName + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit + if (_fileEditor == HWND.Null) + { + Log.Error(ClassName, "Failed to find file name editor handle"); + } + } + + // Get the handle of the open button + _openButton = PInvoke.GetDlgItem(handle, 0x0001); // Open/Save/SaveAs Button + if (_openButton == HWND.Null) + { + Log.Error(ClassName, "Failed to find open button handle"); + } } public string GetCurrentFolder() { - // TODO - return string.Empty; + if (_pathEditor.IsNull) return string.Empty; + return GetWindowText(_pathEditor); } public string GetCurrentFile() { - // TODO - return string.Empty; + if (_fileEditor.IsNull) return string.Empty; + return GetWindowText(_fileEditor); } - public bool OpenFolder(string path) + public bool JumpFolder(string path, bool auto) { - // TODO - return false; + if (_legacy || auto) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we edit file name text box directly + if (_fileEditor.IsNull) return false; + SetWindowText(_fileEditor, path); + + if (_openButton.IsNull) return false; + PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + if (_pathControl.IsNull) return false; + + var timeOut = !SpinWait.SpinUntil(() => + { + var style = PInvoke.GetWindowLong(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + Log.Error(ClassName, "Path control is not visible"); + return false; + } + + if (_pathEditor.IsNull) return false; + + SetWindowText(_pathEditor, path); + return true; + } + + public bool JumpFile(string path) + { + if (_fileEditor.IsNull) return false; + SetWindowText(_fileEditor, path); + return true; + } + + public bool Open() + { + if (_openButton.IsNull) return false; + PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + return true; + } + + private static unsafe string GetWindowText(HWND handle) + { + int length; + Span buffer = stackalloc char[1000]; + fixed (char* pBuffer = buffer) + { + // If the control has no title bar or text, or if the control handle is invalid, the return value is zero. + length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer); + } + + return buffer[..length].ToString(); + } + + private static unsafe nint SetWindowText(HWND handle, string text) + { + fixed (char* textPtr = text + '\0') + { + return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; + } } public void Dispose() diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index f425a0a9f10..0a7c28699c3 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -12,7 +12,6 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; -using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.QuickSwitch { @@ -380,7 +379,7 @@ uint dwmsEventTime // Show quick switch window after navigating the path else { - NavigateDialogPath(hwnd, () => InvokeShowQuickSwitchWindow(dialogWindowChanged)); + NavigateDialogPath(hwnd, true, () => InvokeShowQuickSwitchWindow(dialogWindowChanged)); } } else @@ -498,14 +497,79 @@ uint dwmsEventTime #endregion - #region Helper Methods - - #region Navigate Path + #region Path Navigation // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump + public static void JumpToPath(nint hwnd, string path, Action action = null) + { + if (hwnd == nint.Zero) return; + + var dialogWindow = GetDialogWindow(new(hwnd)); + if (dialogWindow == null) return; + + var dialogWindowTab = dialogWindow.GetCurrentTab(); + if (dialogWindowTab == null) return; + + JumpToPath(dialogWindowTab, path, false, action); + } + + private static void NavigateDialogPath(HWND hwnd, bool auto = false, Action action = null) + { + if (hwnd == HWND.Null) return; + + var dialogWindow = GetDialogWindow(hwnd); + if (dialogWindow == null) return; + + var dialogWindowTab = dialogWindow.GetCurrentTab(); + if (dialogWindowTab == null) return; + + // Get explorer path + string path; + lock (_lastExplorerLock) + { + path = _lastExplorer?.GetExplorerPath(); + } + if (string.IsNullOrEmpty(path)) return; + + // Jump to path + JumpToPath(dialogWindowTab, path, auto, action); + } + + private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) + { + // First check dialog window + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + return _dialogWindow; + } + } + + // Then check all dialog windows + foreach (var dialog in _quickSwitchDialogs) + { + if (dialog.DialogWindow.Handle == hwnd) + { + return dialog.DialogWindow; + } + } + + // Finally search for the dialog window + foreach (var dialog in _quickSwitchDialogs) + { + if (dialog.CheckDialogWindow(hwnd)) + { + return dialog.DialogWindow; + } + } + + return null; + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] - public static void JumpToPath(nint dialog, string path, Action action = null) + private static void JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false, Action action = null) { if (!CheckPath(path, out var isFile)) return; @@ -513,19 +577,17 @@ public static void JumpToPath(nint dialog, string path, Action action = null) { // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindow() == dialog, 1000); + var dialogHandle = dialog.Handle; + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() == dialogHandle, 1000); if (timeOut) { return; } - ; // Assume that the dialog is in the foreground now await _navigationLock.WaitAsync(); try { - var dialogHandle = new HWND(dialog); - bool result; if (isFile) { @@ -533,19 +595,15 @@ public static void JumpToPath(nint dialog, string path, Action action = null) { case QuickSwitchFileResultBehaviours.FullPath: Log.Debug(ClassName, $"File Jump FullPath: {path}"); - result = FileJump(path, dialogHandle, forceFileName: true); + result = FileJump(path, dialog); break; case QuickSwitchFileResultBehaviours.FullPathOpen: Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); - result = FileJump(path, dialogHandle, forceFileName: true, openFile: true); + result = FileJump(path, dialog, openFile: true); break; case QuickSwitchFileResultBehaviours.Directory: Log.Debug(ClassName, $"File Jump Directory: {path}"); - result = DirJump(Path.GetDirectoryName(path), dialogHandle); - break; - case QuickSwitchFileResultBehaviours.DirectoryAndFileName: - Log.Debug(ClassName, $"File Jump DirectoryAndFileName: {path}"); - result = FileJump(path, dialogHandle); + result = DirJump(Path.GetDirectoryName(path), dialog, auto); break; default: throw new ArgumentOutOfRangeException( @@ -558,7 +616,7 @@ public static void JumpToPath(nint dialog, string path, Action action = null) else { Log.Debug(ClassName, $"Dir Jump: {path}"); - result = DirJump(path, dialogHandle); + result = DirJump(path, dialog, auto); } if (result) @@ -600,166 +658,34 @@ static bool CheckPath(string path, out bool file) } } - private static void NavigateDialogPath(HWND hwnd, Action action = null) - { - if (hwnd == HWND.Null) return; - - IQuickSwitchDialogWindow dialogWindow = null; - // First check dialog window - lock (_dialogWindowLock) - { - if (_dialogWindow != null && _dialogWindow.Handle == hwnd) - { - dialogWindow = _dialogWindow; - } - } - // Then check all dialog windows - if (dialogWindow == null) - { - foreach (var dialog in _quickSwitchDialogs) - { - if (dialog.DialogWindow.Handle == hwnd) - { - dialogWindow = dialog.DialogWindow; - break; - } - } - } - // Finally search for the dialog window - if (dialogWindow == null) - { - foreach (var dialog in _quickSwitchDialogs) - { - if (dialog.CheckDialogWindow(hwnd)) - { - dialogWindow = dialog.DialogWindow; - break; - } - } - } - if (dialogWindow == null) return; - - // Get explorer path - string path; - lock (_lastExplorerLock) - { - path = _lastExplorer?.GetExplorerPath(); - } - if (string.IsNullOrEmpty(path)) return; - - // Jump to path - JumpToPath(hwnd.Value, path, action); - } - - private static bool FileJump(string filePath, HWND dialogHandle, bool forceFileName = false, bool openFile = false) - { - if (forceFileName) - { - return DirFileJumpForFileName(filePath, dialogHandle, openFile); - } - else - { - return DirFileJump(Path.GetDirectoryName(filePath), filePath, dialogHandle); - } - } - - private static bool DirJump(string dirPath, HWND dialogHandle) + private static bool FileJump(string filePath, IQuickSwitchDialogWindowTab dialog, bool openFile = false) { - return DirFileJump(dirPath, null, dialogHandle); - } - - private static unsafe bool DirFileJump(string dirPath, string filePath, HWND dialogHandle) - { - // Get the handle of the path input box and then set the text. - var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x0000); // WorkerW - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA005); // ReBarWindow32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // Address Band Root - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x0000); // msctls_progress32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBoxEx32 - if (controlHandle == HWND.Null) - { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we edit file name text box directly. - Log.Error(ClassName, "Failed to find control handle"); - return DirFileJumpForFileName(string.IsNullOrEmpty(filePath) ? dirPath : filePath, dialogHandle, true); - } - - var timeOut = !SpinWait.SpinUntil(() => + if (!dialog.JumpFile(filePath)) { - var style = PInvoke.GetWindowLong(controlHandle, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; - }, 1000); - if (timeOut) - { - Log.Error(ClassName, "Failed to find visible control handle"); + Log.Error(ClassName, "Failed to jump file"); return false; } - var editHandle = PInvoke.GetDlgItem(controlHandle, 0xA205); // ComboBox - editHandle = PInvoke.GetDlgItem(editHandle, 0xA205); // Edit - if (editHandle == HWND.Null) + if (openFile && !dialog.Open()) { - Log.Error(ClassName, "Failed to find edit handle"); + Log.Error(ClassName, "Failed to open file"); return false; } - SetWindowText(editHandle, dirPath); - - if (!string.IsNullOrEmpty(filePath)) - { - // Note: I don't know why even openFile is set to false, the dialog still opens the file. - return DirFileJumpForFileName(Path.GetFileName(filePath), dialogHandle, false); - } - return true; } - /// - /// Edit file name text box in the file open dialog. - /// - private static bool DirFileJumpForFileName(string fileName, HWND dialogHandle, bool openFile) + private static bool DirJump(string dirPath, IQuickSwitchDialogWindowTab dialog, bool auto = false) { - var controlHandle = PInvoke.GetDlgItem(dialogHandle, 0x047C); // ComboBoxEx32 - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // ComboBox - controlHandle = PInvoke.GetDlgItem(controlHandle, 0x047C); // Edit - if (controlHandle == HWND.Null) + if (!dialog.JumpFolder(dirPath, auto)) { - Log.Error(ClassName, "Failed to find control handle"); + Log.Error(ClassName, "Failed to jump folder"); return false; } - SetWindowText(controlHandle, fileName); - - if (openFile) - { - var openHandle = PInvoke.GetDlgItem(dialogHandle, 0x0001); // "&Open" Button - if (openHandle == HWND.Null) - { - Log.Error(ClassName, "Failed to find open handle"); - return false; - } - - ClickButton(openHandle); - } - return true; } - private static unsafe nint SetWindowText(HWND handle, string text) - { - fixed (char* textPtr = text + '\0') - { - return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; - } - } - - private static unsafe nint ClickButton(HWND handle) - { - return PInvoke.PostMessage(handle, PInvoke.BM_CLICK, 0, 0).Value; - } - - #endregion - #endregion #region Dispose From 8fe6391aa443aaa8475e173ccc503dff3951abde Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:24:20 +0800 Subject: [PATCH 116/243] Fix string tooltip --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 8784ed49919..eb47ffbe0a7 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -313,7 +313,7 @@ Quick Switch Automatically Automatically navigate to the path of the current Explorer when a file dialog is opened. Show Quick Switch Window - Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). + Show quick switch search window when file dialogs are opened to navigate its path. Quick Switch Window Position Select position for quick switch window Fixed under dialogs. Displayed after dialogs are created and until it is closed From c5a36fdf4139254d21954db927b3872040c1070c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:24:50 +0800 Subject: [PATCH 117/243] Remove DirectoryAndFileName file behaviour --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 3 +-- Flow.Launcher/Languages/en.xaml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 5c84d204665..a17b3ef7e73 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -518,7 +518,6 @@ public enum QuickSwitchFileResultBehaviours { FullPath, FullPathOpen, - Directory, - DirectoryAndFileName + Directory } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index eb47ffbe0a7..3b3871418b2 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -327,7 +327,6 @@ Fill full path in file name box Fill full path in file name box and open Fill directory in path box - Fill directory in path box, file name in file name box and open HTTP Proxy From fc72b4b12ad1bc029f8b7c820b67502cf8dc62f0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:39:26 +0800 Subject: [PATCH 118/243] Revert "Fix string tooltip" This reverts commit 8fe6391aa443aaa8475e173ccc503dff3951abde. --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 3b3871418b2..252fa42aa55 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -313,7 +313,7 @@ Quick Switch Automatically Automatically navigate to the path of the current Explorer when a file dialog is opened. Show Quick Switch Window - Show quick switch search window when file dialogs are opened to navigate its path. + Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). Quick Switch Window Position Select position for quick switch window Fixed under dialogs. Displayed after dialogs are created and until it is closed From ab5d5ae27bb15dd19d13824f042cb496c3437ef6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:45:51 +0800 Subject: [PATCH 119/243] =?UTF-8?q?Hook=20delegates=20can=20be=20garbage?= =?UTF-8?q?=E2=80=91collected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickSwitch/QuickSwitch.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0a7c28699c3..26bcacc34ff 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -59,6 +59,10 @@ public static class QuickSwitch private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; + private static readonly WINEVENTPROC _fgProc = ForegroundChangeCallback; + private static readonly WINEVENTPROC _locProc = LocationChangeCallback; + private static readonly WINEVENTPROC _desProc = DestroyChangeCallback; + private static DispatcherTimer _dragMoveTimer = null; // A list of all file dialog windows that are auto switched already @@ -68,7 +72,8 @@ public static class QuickSwitch // Note: Here we do not start & stop the timer beacause when there are many dialog windows // Unhooking and hooking will take too much time which can make window position weird // So we start & stop the timer when we find a file dialog window - /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;*/ + /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _moveProc = MoveSizeCallBack;*/ private static readonly SemaphoreSlim _navigationLock = new(1, 1); @@ -151,7 +156,7 @@ public static void SetupQuickSwitch(bool enabled) PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.GetModuleHandle((PCWSTR)null), - ForegroundChangeCallback, + _fgProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); @@ -159,7 +164,7 @@ public static void SetupQuickSwitch(bool enabled) PInvoke.EVENT_OBJECT_LOCATIONCHANGE, PInvoke.EVENT_OBJECT_LOCATIONCHANGE, PInvoke.GetModuleHandle((PCWSTR)null), - LocationChangeCallback, + _locProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); @@ -167,7 +172,7 @@ public static void SetupQuickSwitch(bool enabled) PInvoke.EVENT_OBJECT_DESTROY, PInvoke.EVENT_OBJECT_DESTROY, PInvoke.GetModuleHandle((PCWSTR)null), - DestroyChangeCallback, + _desProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); @@ -268,14 +273,14 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) _moveSizeHook = HWINEVENTHOOK.Null; } - // Call MoveSizeCallBack when the window is moved or resized + // Call _moveProc when the window is moved or resized uint processId; var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); _moveSizeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_MOVESIZESTART, PInvoke.EVENT_SYSTEM_MOVESIZEEND, PInvoke.GetModuleHandle((PCWSTR)null), - MoveSizeCallBack, + _moveProc, processId, threadId, PInvoke.WINEVENT_OUTOFCONTEXT); From f78227771510c16962e56689e62cabb8bdcfa860 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 19:48:36 +0800 Subject: [PATCH 120/243] Remove useless modifiers --- .../QuickSwitch/Interface/IQuickSwitchDialog.cs | 4 ++-- .../Interface/IQuickSwitchDialogWindow.cs | 4 ++-- .../Interface/IQuickSwitchDialogWindowTab.cs | 12 ++++++------ .../QuickSwitch/Interface/IQuickSwitchExplorer.cs | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs index 92f681e38d3..40afdd62b2f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs @@ -12,8 +12,8 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// internal interface IQuickSwitchDialog : IDisposable { - internal IQuickSwitchDialogWindow DialogWindow { get; } + IQuickSwitchDialogWindow DialogWindow { get; } - internal bool CheckDialogWindow(HWND hwnd); + bool CheckDialogWindow(HWND hwnd); } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs index 09bfa2ad9e6..8834e27f78d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs @@ -5,8 +5,8 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface { internal interface IQuickSwitchDialogWindow : IDisposable { - internal HWND Handle { get; } + HWND Handle { get; } - internal IQuickSwitchDialogWindowTab GetCurrentTab(); + IQuickSwitchDialogWindowTab GetCurrentTab(); } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs index 0d6d173887a..d01059a7c76 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs @@ -5,16 +5,16 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface { internal interface IQuickSwitchDialogWindowTab : IDisposable { - internal HWND Handle { get; } + HWND Handle { get; } - internal string GetCurrentFolder(); + string GetCurrentFolder(); - internal string GetCurrentFile(); + string GetCurrentFile(); - internal bool JumpFolder(string path, bool auto); + bool JumpFolder(string path, bool auto); - internal bool JumpFile(string path); + bool JumpFile(string path); - internal bool Open(); + bool Open(); } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs index 863ae0e4dcc..0a40586690b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs @@ -12,10 +12,10 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// internal interface IQuickSwitchExplorer : IDisposable { - internal bool CheckExplorerWindow(HWND foreground); + bool CheckExplorerWindow(HWND foreground); - internal void RemoveExplorerWindow(); + void RemoveExplorerWindow(); - internal string GetExplorerPath(); + string GetExplorerPath(); } } From b690ca13ea22e92066aa9c3c4a5b98eeb7b6cc5a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 22:01:22 +0800 Subject: [PATCH 121/243] Improve code comments --- .../QuickSwitch/Interface/IQuickSwitchDialog.cs | 5 +++-- .../QuickSwitch/Interface/IQuickSwitchExplorer.cs | 5 +++-- .../QuickSwitch/Models/WindowsDialog.cs | 4 +++- .../QuickSwitch/Models/WindowsExplorer.cs | 3 +++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs index 40afdd62b2f..d2d08dbf510 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs @@ -7,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// Interface for handling File Dialog instances in QuickSwitch. /// /// - /// Add models in QuickSwitch/Models folder and implement this interface. - /// Then add the instance in QuickSwitch._quickSwitchDialogs. + /// Add models which implement IQuickSwitchDialog in folder QuickSwitch/Models. + /// E.g. Models.WindowsDialog. + /// Then add instances in QuickSwitch._quickSwitchDialogs. /// internal interface IQuickSwitchDialog : IDisposable { diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs index 0a40586690b..9bf3d95911f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs @@ -7,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// Interface for handling Windows Explorer instances in QuickSwitch. /// /// - /// Add models in QuickSwitch/Models folder and implement this interface. - /// Then add the instance in QuickSwitch._quickSwitchExplorers. + /// Add models which implement IQuickSwitchExplorer in folder QuickSwitch/Models. + /// E.g. Models.WindowsExplorer. + /// Then add instances in QuickSwitch._quickSwitchExplorers. /// internal interface IQuickSwitchExplorer : IDisposable { diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index d9ddae03a1a..a6440bda06b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -8,9 +8,11 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models { + /// + /// Class for handling Windows File Dialog instances in QuickSwitch. + /// internal class WindowsDialog : IQuickSwitchDialog { - // The class name of a dialog window private const string WindowsDialogClassName = "#32770"; public IQuickSwitchDialogWindow DialogWindow { get; private set; } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 461afa1de06..2e22987e743 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -8,6 +8,9 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models { + /// + /// Class for handling Windows Explorer instances in QuickSwitch. + /// internal class WindowsExplorer : IQuickSwitchExplorer { private static readonly string ClassName = nameof(WindowsExplorer); From 091f0ec1eb27ddad7fe9b877de7ab8742b6aed6b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Apr 2025 22:22:37 +0800 Subject: [PATCH 122/243] Fix possible handle issue --- .../QuickSwitch/Models/WindowsDialog.cs | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index a6440bda06b..b03f2a213fe 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -80,20 +80,26 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab private static readonly string ClassName = nameof(WindowsDialogTab); - private readonly bool _legacy = false; + private bool _legacy { get; set; } = false; - private readonly HWND _pathControl; - private readonly HWND _pathEditor; - private readonly HWND _fileEditor; - private readonly HWND _openButton; + private HWND _pathControl { get; set; } = HWND.Null; + private HWND _pathEditor { get; set; } = HWND.Null; + private HWND _fileEditor { get; set; } = HWND.Null; + private HWND _openButton { get; set; } = HWND.Null; public WindowsDialogTab(HWND handle) { Handle = handle; + GetPathControlEditor(); + GetFileEditor(); + GetOpenButton(); + } + private bool GetPathControlEditor() + { // Get the handle of the path editor // The window with class name "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus - _pathControl = PInvoke.GetDlgItem(handle, 0x0000); // WorkerW + _pathControl = PInvoke.GetDlgItem(Handle, 0x0000); // WorkerW _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA005); // ReBarWindow32 _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // Address Band Root _pathControl = PInvoke.GetDlgItem(_pathControl, 0x0000); // msctls_progress32 @@ -108,22 +114,29 @@ public WindowsDialogTab(HWND handle) } else { + _legacy = false; _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit if (_pathEditor == HWND.Null) { Log.Error(ClassName, "Failed to find path editor handle"); + return false; } } + return true; + } + + private bool GetFileEditor() + { // Get the handle of the file name editor of Open file dialog - _fileEditor = PInvoke.GetDlgItem(handle, 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit if (_fileEditor == HWND.Null) { // Get the handle of the file name editor of Save/SaveAs file dialog - _fileEditor = PInvoke.GetDlgItem(handle, 0x0000); // DUIViewWndClassName + _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox @@ -131,26 +144,35 @@ public WindowsDialogTab(HWND handle) if (_fileEditor == HWND.Null) { Log.Error(ClassName, "Failed to find file name editor handle"); + return false; } } + return true; + } + + private bool GetOpenButton() + { // Get the handle of the open button - _openButton = PInvoke.GetDlgItem(handle, 0x0001); // Open/Save/SaveAs Button + _openButton = PInvoke.GetDlgItem(Handle, 0x0001); // Open/Save/SaveAs Button if (_openButton == HWND.Null) { Log.Error(ClassName, "Failed to find open button handle"); + return false; } + + return true; } public string GetCurrentFolder() { - if (_pathEditor.IsNull) return string.Empty; + if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; return GetWindowText(_pathEditor); } public string GetCurrentFile() { - if (_fileEditor.IsNull) return string.Empty; + if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; return GetWindowText(_fileEditor); } @@ -160,16 +182,16 @@ public bool JumpFolder(string path, bool auto) { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 // The dialog is a legacy one, so we edit file name text box directly - if (_fileEditor.IsNull) return false; + if (_fileEditor.IsNull && !GetFileEditor()) return false; SetWindowText(_fileEditor, path); - if (_openButton.IsNull) return false; + if (_openButton.IsNull && !GetOpenButton()) return false; PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); return true; } - if (_pathControl.IsNull) return false; + if (_pathControl.IsNull && !GetPathControlEditor()) return false; var timeOut = !SpinWait.SpinUntil(() => { @@ -182,23 +204,25 @@ public bool JumpFolder(string path, bool auto) return false; } - if (_pathEditor.IsNull) return false; - + if (_pathEditor.IsNull && !GetPathControlEditor()) return false; SetWindowText(_pathEditor, path); + return true; } public bool JumpFile(string path) { - if (_fileEditor.IsNull) return false; + if (_fileEditor.IsNull && !GetPathControlEditor()) return false; SetWindowText(_fileEditor, path); + return true; } public bool Open() { - if (_openButton.IsNull) return false; + if (_openButton.IsNull && !GetOpenButton()) return false; PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + return true; } From 24829b2c7aa5da667d787c050e592cf4770e6207 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 14:50:34 +0800 Subject: [PATCH 123/243] Add dialog type & focus path editor when changing --- .../QuickSwitch/Models/WindowsDialog.cs | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index b03f2a213fe..0cd0d172f9d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -5,6 +5,8 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; +using WindowsInput; +using WindowsInput.Native; namespace Flow.Launcher.Infrastructure.QuickSwitch.Models { @@ -80,7 +82,10 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab private static readonly string ClassName = nameof(WindowsDialogTab); + private static readonly InputSimulator _inputSimulator = new(); + private bool _legacy { get; set; } = false; + private DialogType _type { get; set; } = DialogType.None; private HWND _pathControl { get; set; } = HWND.Null; private HWND _pathEditor { get; set; } = HWND.Null; @@ -90,15 +95,22 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab public WindowsDialogTab(HWND handle) { Handle = handle; - GetPathControlEditor(); + GetPathControlEditor(true); GetFileEditor(); GetOpenButton(); } - private bool GetPathControlEditor() + private bool GetPathControlEditor(bool focus) { + if (focus) + { + // Alt-D or Ctrl-L to focus on the path input box + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + } + // Get the handle of the path editor - // The window with class name "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus + // "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus _pathControl = PInvoke.GetDlgItem(Handle, 0x0000); // WorkerW _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA005); // ReBarWindow32 _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // Address Band Root @@ -106,15 +118,13 @@ private bool GetPathControlEditor() _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBoxEx32 if (_pathControl == HWND.Null) { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we edit file name editor directly. - _legacy = true; _pathEditor = HWND.Null; - Log.Info(ClassName, "Failed to find path control handle - Legacy dialog"); + _legacy = true; + Log.Info(ClassName, "Legacy dialog"); + return false; } else { - _legacy = false; _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit if (_pathEditor == HWND.Null) @@ -135,7 +145,7 @@ private bool GetFileEditor() _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit if (_fileEditor == HWND.Null) { - // Get the handle of the file name editor of Save/SaveAs file dialog + // Get the handle of the file name editor of Save / SaveAs file dialog _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink @@ -144,8 +154,19 @@ private bool GetFileEditor() if (_fileEditor == HWND.Null) { Log.Error(ClassName, "Failed to find file name editor handle"); + _type = DialogType.None; return false; } + else + { + Log.Debug(ClassName, "File dialog type: Save / Save As"); + _type = DialogType.SaveOrSaveAs; + } + } + else + { + Log.Debug(ClassName, "File dialog type: Open"); + _type = DialogType.Open; } return true; @@ -166,7 +187,7 @@ private bool GetOpenButton() public string GetCurrentFolder() { - if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; + if (_pathEditor.IsNull) return string.Empty; return GetWindowText(_pathEditor); } @@ -178,20 +199,23 @@ public string GetCurrentFile() public bool JumpFolder(string path, bool auto) { - if (_legacy || auto) + if (auto) { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we edit file name text box directly - if (_fileEditor.IsNull && !GetFileEditor()) return false; - SetWindowText(_fileEditor, path); - - if (_openButton.IsNull && !GetOpenButton()) return false; - PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + // Use legacy jump folder method for auto quick switch because file editor is default value. + // After setting path using file editor, we do not need to revert its value. + return JumpFolderWithFileEditor(path); + } - return true; + if (_legacy) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we can only edit file editor directly. + return JumpFolderWithFileEditor(path); } - if (_pathControl.IsNull && !GetPathControlEditor()) return false; + // Alt-D or Ctrl-L to focus on the path input box + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); var timeOut = !SpinWait.SpinUntil(() => { @@ -204,15 +228,28 @@ public bool JumpFolder(string path, bool auto) return false; } - if (_pathEditor.IsNull && !GetPathControlEditor()) return false; + if (_pathEditor.IsNull) return false; SetWindowText(_pathEditor, path); + _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + + return true; + } + + private bool JumpFolderWithFileEditor(string path) + { + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + return true; } public bool JumpFile(string path) { - if (_fileEditor.IsNull && !GetPathControlEditor()) return false; + if (_fileEditor.IsNull && !GetFileEditor()) return false; SetWindowText(_fileEditor, path); return true; @@ -251,5 +288,12 @@ public void Dispose() { Handle = HWND.Null; } + + private enum DialogType + { + None, + Open, + SaveOrSaveAs + } } } From 9b0cf6c89d73a18d9e0793cf46d0a5106213f421 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 15:22:03 +0800 Subject: [PATCH 124/243] Fix GetDlgItem issue & Improve code quality --- .../QuickSwitch/Models/WindowsDialog.cs | 215 ++++++++++-------- 1 file changed, 122 insertions(+), 93 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 0cd0d172f9d..73034d47283 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -78,8 +78,14 @@ public void Dispose() internal class WindowsDialogTab : IQuickSwitchDialogWindowTab { + #region Public Properties + public HWND Handle { get; private set; } + #endregion + + #region Private Fields + private static readonly string ClassName = nameof(WindowsDialogTab); private static readonly InputSimulator _inputSimulator = new(); @@ -92,36 +98,115 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab private HWND _fileEditor { get; set; } = HWND.Null; private HWND _openButton { get; set; } = HWND.Null; + #endregion + + #region Constructor + public WindowsDialogTab(HWND handle) { Handle = handle; - GetPathControlEditor(true); + GetPathControlEditor(); GetFileEditor(); GetOpenButton(); } - private bool GetPathControlEditor(bool focus) + #endregion + + #region Public Methods + + public string GetCurrentFolder() + { + if (_pathEditor.IsNull) return string.Empty; + return GetWindowText(_pathEditor); + } + + public string GetCurrentFile() { - if (focus) + if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; + return GetWindowText(_fileEditor); + } + + public bool JumpFolder(string path, bool auto) + { + if (auto) { - // Alt-D or Ctrl-L to focus on the path input box - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); - // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + // Use legacy jump folder method for auto quick switch because file editor is default value. + // After setting path using file editor, we do not need to revert its value. + return JumpFolderWithFileEditor(path); } - // Get the handle of the path editor + // Alt-D or Ctrl-L to focus on the path input box // "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus - _pathControl = PInvoke.GetDlgItem(Handle, 0x0000); // WorkerW - _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA005); // ReBarWindow32 - _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // Address Band Root - _pathControl = PInvoke.GetDlgItem(_pathControl, 0x0000); // msctls_progress32 - _pathControl = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBoxEx32 + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + + if (_pathControl.IsNull && !GetPathControlEditor()) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we can only edit file editor directly. + return JumpFolderWithFileEditor(path); + } + + var timeOut = !SpinWait.SpinUntil(() => + { + var style = PInvoke.GetWindowLong(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + Log.Error(ClassName, "Path control is not visible"); + return false; + } + + if (_pathEditor.IsNull) return false; + SetWindowText(_pathEditor, path); + + _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + + return true; + } + + public bool JumpFile(string path) + { + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + return true; + } + + public bool Open() + { + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + public void Dispose() + { + Handle = HWND.Null; + } + + #endregion + + #region Helper Methods + + #region Get Handles + + private bool GetPathControlEditor() + { + // Get the handle of the path editor + // (Must use PInvoke.FindWindowEx instead of PInvoke.GetDlgItem, or ReBarWindow32 will be null) + _pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205 if (_pathControl == HWND.Null) { _pathEditor = HWND.Null; _legacy = true; Log.Info(ClassName, "Legacy dialog"); - return false; } else { @@ -129,12 +214,12 @@ private bool GetPathControlEditor(bool focus) _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit if (_pathEditor == HWND.Null) { + _legacy = true; Log.Error(ClassName, "Failed to find path editor handle"); - return false; } } - return true; + return !_legacy; } private bool GetFileEditor() @@ -185,83 +270,9 @@ private bool GetOpenButton() return true; } - public string GetCurrentFolder() - { - if (_pathEditor.IsNull) return string.Empty; - return GetWindowText(_pathEditor); - } - - public string GetCurrentFile() - { - if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; - return GetWindowText(_fileEditor); - } - - public bool JumpFolder(string path, bool auto) - { - if (auto) - { - // Use legacy jump folder method for auto quick switch because file editor is default value. - // After setting path using file editor, we do not need to revert its value. - return JumpFolderWithFileEditor(path); - } - - if (_legacy) - { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we can only edit file editor directly. - return JumpFolderWithFileEditor(path); - } - - // Alt-D or Ctrl-L to focus on the path input box - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); - // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); - - var timeOut = !SpinWait.SpinUntil(() => - { - var style = PInvoke.GetWindowLong(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; - }, 1000); - if (timeOut) - { - Log.Error(ClassName, "Path control is not visible"); - return false; - } - - if (_pathEditor.IsNull) return false; - SetWindowText(_pathEditor, path); - - _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); - - return true; - } - - private bool JumpFolderWithFileEditor(string path) - { - if (_fileEditor.IsNull && !GetFileEditor()) return false; - SetWindowText(_fileEditor, path); - - if (_openButton.IsNull && !GetOpenButton()) return false; - PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); - - return true; - } - - public bool JumpFile(string path) - { - if (_fileEditor.IsNull && !GetFileEditor()) return false; - SetWindowText(_fileEditor, path); + #endregion - return true; - } - - public bool Open() - { - if (_openButton.IsNull && !GetOpenButton()) return false; - PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); - - return true; - } + #region Windows Text private static unsafe string GetWindowText(HWND handle) { @@ -284,16 +295,34 @@ private static unsafe nint SetWindowText(HWND handle, string text) } } - public void Dispose() + #endregion + + #region Legacy Jump Folder + + private bool JumpFolderWithFileEditor(string path) { - Handle = HWND.Null; + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; } + #endregion + + #endregion + + #region Classes + private enum DialogType { None, Open, SaveOrSaveAs } + + #endregion } } From ec966c1868f5f838ba94f921d400ebe60333f0e9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 15:33:50 +0800 Subject: [PATCH 125/243] Improve log message & legacy mode --- .../QuickSwitch/Models/WindowsDialog.cs | 13 ++++++++++--- .../QuickSwitch/QuickSwitch.cs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 73034d47283..f2e8ca925b8 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -144,6 +144,7 @@ public bool JumpFolder(string path, bool auto) { // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 // The dialog is a legacy one, so we can only edit file editor directly. + Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method"); return JumpFolderWithFileEditor(path); } @@ -154,11 +155,17 @@ public bool JumpFolder(string path, bool auto) }, 1000); if (timeOut) { - Log.Error(ClassName, "Path control is not visible"); - return false; + // Path control is not visible, so we can only edit file editor directly. + Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method"); + return JumpFolderWithFileEditor(path); } - if (_pathEditor.IsNull) return false; + if (_pathEditor.IsNull && !GetPathControlEditor()) + { + // Path editor cannot be found, so we can only edit file editor directly. + Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); + return JumpFolderWithFileEditor(path); + } SetWindowText(_pathEditor, path); _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 26bcacc34ff..76220b21f25 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -607,7 +607,7 @@ private static void JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, result = FileJump(path, dialog, openFile: true); break; case QuickSwitchFileResultBehaviours.Directory: - Log.Debug(ClassName, $"File Jump Directory: {path}"); + Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}"); result = DirJump(Path.GetDirectoryName(path), dialog, auto); break; default: From eb1fa187c4e79ea808f771bf6f0be2830b8be8f2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 15:54:37 +0800 Subject: [PATCH 126/243] Fix save / save as dialog possible issue --- .../QuickSwitch/Models/WindowsDialog.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index f2e8ca925b8..4ec819720e3 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -132,7 +132,7 @@ public bool JumpFolder(string path, bool auto) { // Use legacy jump folder method for auto quick switch because file editor is default value. // After setting path using file editor, we do not need to revert its value. - return JumpFolderWithFileEditor(path); + return JumpFolderWithFileEditor(path, false); } // Alt-D or Ctrl-L to focus on the path input box @@ -145,7 +145,7 @@ public bool JumpFolder(string path, bool auto) // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 // The dialog is a legacy one, so we can only edit file editor directly. Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method"); - return JumpFolderWithFileEditor(path); + return JumpFolderWithFileEditor(path, true); } var timeOut = !SpinWait.SpinUntil(() => @@ -157,14 +157,14 @@ public bool JumpFolder(string path, bool auto) { // Path control is not visible, so we can only edit file editor directly. Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method"); - return JumpFolderWithFileEditor(path); + return JumpFolderWithFileEditor(path, true); } if (_pathEditor.IsNull && !GetPathControlEditor()) { // Path editor cannot be found, so we can only edit file editor directly. Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); - return JumpFolderWithFileEditor(path); + return JumpFolderWithFileEditor(path, true); } SetWindowText(_pathEditor, path); @@ -306,8 +306,11 @@ private static unsafe nint SetWindowText(HWND handle, string text) #region Legacy Jump Folder - private bool JumpFolderWithFileEditor(string path) + private bool JumpFolderWithFileEditor(string path, bool resetFocus) { + // For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors. + if (resetFocus && _type == DialogType.SaveOrSaveAs) return false; + if (_fileEditor.IsNull && !GetFileEditor()) return false; SetWindowText(_fileEditor, path); From faa5cd1092bf95e6ce347673549fa824b3b97ff2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 16:15:19 +0800 Subject: [PATCH 127/243] Cache current tab to fix file editor handle issue --- .../QuickSwitch/Models/WindowsDialog.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 4ec819720e3..72a05915850 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -60,6 +60,10 @@ internal class WindowsDialogWindow : IQuickSwitchDialogWindow { public HWND Handle { get; private set; } + // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore + // So we need to cache the current tab and use the original handle + private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null; + public WindowsDialogWindow(HWND handle) { Handle = handle; @@ -67,7 +71,7 @@ public WindowsDialogWindow(HWND handle) public IQuickSwitchDialogWindowTab GetCurrentTab() { - return new WindowsDialogTab(Handle); + return _currentTab ??= new WindowsDialogTab(Handle); } public void Dispose() From 89283ef7c5108ec7f3455587fd4c8711a73e9e1a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 16:20:17 +0800 Subject: [PATCH 128/243] Code quality --- .../QuickSwitch/Models/WindowsDialog.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 72a05915850..ddd3f8ce287 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -15,10 +15,10 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// internal class WindowsDialog : IQuickSwitchDialog { - private const string WindowsDialogClassName = "#32770"; - public IQuickSwitchDialogWindow DialogWindow { get; private set; } + private const string WindowsDialogClassName = "#32770"; + public bool CheckDialogWindow(HWND hwnd) { if (GetWindowClassName(hwnd) == WindowsDialogClassName) @@ -38,7 +38,7 @@ public void Dispose() DialogWindow = null; } - public static string GetWindowClassName(HWND handle) + private static string GetWindowClassName(HWND handle) { return GetClassName(handle); From 1b3e03640fcab80e42fa50e7391f551790660a47 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 17:18:40 +0800 Subject: [PATCH 129/243] Fix quick switch window show issue --- .../QuickSwitch/QuickSwitch.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 76220b21f25..a3273eb7180 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -384,7 +384,10 @@ uint dwmsEventTime // Show quick switch window after navigating the path else { - NavigateDialogPath(hwnd, true, () => InvokeShowQuickSwitchWindow(dialogWindowChanged)); + if (!NavigateDialogPath(hwnd, true, () => InvokeShowQuickSwitchWindow(dialogWindowChanged))) + { + InvokeShowQuickSwitchWindow(dialogWindowChanged); + } } } else @@ -519,15 +522,15 @@ public static void JumpToPath(nint hwnd, string path, Action action = null) JumpToPath(dialogWindowTab, path, false, action); } - private static void NavigateDialogPath(HWND hwnd, bool auto = false, Action action = null) + private static bool NavigateDialogPath(HWND hwnd, bool auto = false, Action action = null) { - if (hwnd == HWND.Null) return; + if (hwnd == HWND.Null) return false; var dialogWindow = GetDialogWindow(hwnd); - if (dialogWindow == null) return; + if (dialogWindow == null) return false; var dialogWindowTab = dialogWindow.GetCurrentTab(); - if (dialogWindowTab == null) return; + if (dialogWindowTab == null) return false; // Get explorer path string path; @@ -535,10 +538,10 @@ private static void NavigateDialogPath(HWND hwnd, bool auto = false, Action acti { path = _lastExplorer?.GetExplorerPath(); } - if (string.IsNullOrEmpty(path)) return; + if (string.IsNullOrEmpty(path)) return false; // Jump to path - JumpToPath(dialogWindowTab, path, auto, action); + return JumpToPath(dialogWindowTab, path, auto, action); } private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) @@ -574,9 +577,9 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] - private static void JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false, Action action = null) + private static bool JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false, Action action = null) { - if (!CheckPath(path, out var isFile)) return; + if (!CheckPath(path, out var isFile)) return false; var t = new Thread(async () => { @@ -586,6 +589,7 @@ private static void JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() == dialogHandle, 1000); if (timeOut) { + action?.Invoke(); return; } @@ -645,7 +649,7 @@ private static void JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, action?.Invoke(); }); t.Start(); - return; + return true; static bool CheckPath(string path, out bool file) { From 136ccafe7b31d87611ff93046a881bdd9fef07ee Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 17:24:23 +0800 Subject: [PATCH 130/243] Fix multi-display alignment issue --- Flow.Launcher/MainWindow.xaml.cs | 71 ++++++++++++++------------------ 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index a2a009c48a5..6c7ce9400f5 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -669,13 +669,14 @@ private void UpdateNotifyIconText() public void UpdatePosition() { + // Intialize or update call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 if (_viewModel.IsQuickSwitchWindowUnderDialog()) { UpdateQuickSwitchPosition(); + UpdateQuickSwitchPosition(); } else { - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 InitializePosition(); InitializePosition(); } @@ -692,46 +693,38 @@ private async Task PositionResetAsync() private void InitializePosition() { - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePositionInner(); - InitializePositionInner(); - return; - - void InitializePositionInner() + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) - { - Top = _settings.WindowTop; - Left = _settings.WindowLeft; - } - else + Top = _settings.WindowTop; + Left = _settings.WindowLeft; + } + else + { + var screen = SelectedScreen(); + switch (_settings.SearchWindowAlign) { - var screen = SelectedScreen(); - switch (_settings.SearchWindowAlign) - { - case SearchWindowAligns.Center: - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - break; - case SearchWindowAligns.CenterTop: - Left = HorizonCenter(screen); - Top = 10; - break; - case SearchWindowAligns.LeftTop: - Left = HorizonLeft(screen); - Top = 10; - break; - case SearchWindowAligns.RightTop: - Left = HorizonRight(screen); - Top = 10; - break; - case SearchWindowAligns.Custom: - Left = Win32Helper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = Win32Helper.TransformPixelsToDIP(this, 0, - screen.WorkingArea.Y + _settings.CustomWindowTop).Y; - break; - } + case SearchWindowAligns.Center: + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + break; + case SearchWindowAligns.CenterTop: + Left = HorizonCenter(screen); + Top = 10; + break; + case SearchWindowAligns.LeftTop: + Left = HorizonLeft(screen); + Top = 10; + break; + case SearchWindowAligns.RightTop: + Left = HorizonRight(screen); + Top = 10; + break; + case SearchWindowAligns.Custom: + Left = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; + Top = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + break; } } } From c62a687230569c7605f96df2c35493736f2530ad Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 18:59:48 +0800 Subject: [PATCH 131/243] Code quality --- Flow.Launcher/MainWindow.xaml.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 6c7ce9400f5..5a0d735b414 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -669,11 +669,11 @@ private void UpdateNotifyIconText() public void UpdatePosition() { - // Intialize or update call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + // Intialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 if (_viewModel.IsQuickSwitchWindowUnderDialog()) { - UpdateQuickSwitchPosition(); - UpdateQuickSwitchPosition(); + InitializeQuickSwitchPosition(); + InitializeQuickSwitchPosition(); } else { @@ -1156,12 +1156,12 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) private void InitializeQuickSwitch() { QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitch; - QuickSwitch.UpdateQuickSwitchWindow = UpdateQuickSwitchPosition; + QuickSwitch.UpdateQuickSwitchWindow = InitializeQuickSwitchPosition; QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; QuickSwitch.HideQuickSwitchWindow = _viewModel.HideQuickSwitch; } - private void UpdateQuickSwitchPosition() + private void InitializeQuickSwitchPosition() { if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return; if (!_viewModel.IsQuickSwitchWindowUnderDialog()) return; From 56a645c2f0dcd2029415bc855fc2f4e4a1f8c3ef Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 19:01:35 +0800 Subject: [PATCH 132/243] Add exception information --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 330c5f05cba..982ef64bee9 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -340,8 +340,9 @@ public static async Task> QueryQuickSwitchForPluginAsync // null will be fine since the results will only be added into queue if the token hasn't been cancelled return null; } - catch (Exception) + catch (Exception e) { + API.LogException(ClassName, $"Failed to query quick switch for plugin: {metadata.Name}", e); return null; } return results; From 2332436d3d624f4ae18bd4d95b937acc99f6fbd6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 19:20:04 +0800 Subject: [PATCH 133/243] Fix possible null exception & Improve dialog window lock --- .../QuickSwitch/Models/WindowsDialog.cs | 4 +- .../QuickSwitch/QuickSwitch.cs | 68 +++++++++++++++---- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index ddd3f8ce287..68fa9cbf649 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -58,7 +58,7 @@ static unsafe string GetClassName(HWND handle) internal class WindowsDialogWindow : IQuickSwitchDialogWindow { - public HWND Handle { get; private set; } + public HWND Handle { get; private set; } = HWND.Null; // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore // So we need to cache the current tab and use the original handle @@ -84,7 +84,7 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab { #region Public Properties - public HWND Handle { get; private set; } + public HWND Handle { get; private set; } = HWND.Null; #endregion diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index a3273eb7180..0ea7659c1a4 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -258,7 +258,19 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; } - ShowQuickSwitchWindow?.Invoke(_dialogWindow.Handle); + IQuickSwitchDialogWindow dialogWindow = null; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + dialogWindow = _dialogWindow; + } + } + if (dialogWindow != null) + { + ShowQuickSwitchWindow?.Invoke(dialogWindow.Handle); + } + if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { _dragMoveTimer?.Start(); @@ -267,6 +279,17 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) // So we start & stop the timer when we find a file dialog window /*if (dialogWindowChanged) { + HWND dialogWindowHandle = HWND.Null; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + dialogWindowHandle = _dialogWindow.Handle; + } + } + + if (dialogWindowHandle == HWND.Null) return; + if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); @@ -275,7 +298,7 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) // Call _moveProc when the window is moved or resized uint processId; - var threadId = PInvoke.GetWindowThreadProcessId(_dialogWindow.Handle, &processId); + var threadId = PInvoke.GetWindowThreadProcessId(dialogWindowHandle, &processId); _moveSizeHook = PInvoke.SetWinEventHook( PInvoke.EVENT_SYSTEM_MOVESIZESTART, PInvoke.EVENT_SYSTEM_MOVESIZEEND, @@ -297,6 +320,11 @@ private static void InvokeUpdateQuickSwitchWindow() private static void InvokeResetQuickSwitchWindow() { + lock (_dialogWindowLock) + { + _dialogWindow = null; + } + // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); @@ -308,11 +336,6 @@ private static void InvokeResetQuickSwitchWindow() PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; }*/ - - lock (_dialogWindowLock) - { - _dialogWindow = null; - } } private static void InvokeHideQuickSwitchWindow() @@ -402,7 +425,15 @@ uint dwmsEventTime } else { - if (_dialogWindow != null) + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + dialogWindowExist = true; + } + } + if (dialogWindowExist) { InvokeHideQuickSwitchWindow(); } @@ -441,7 +472,15 @@ uint dwmsEventTime ) { // If the dialog window is moved, update the quick switch window position - if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + dialogWindowExist = true; + } + } + if (dialogWindowExist) { InvokeUpdateQuickSwitchWindow(); } @@ -486,13 +525,18 @@ uint dwmsEventTime ) { // If the dialog window is destroyed, set _dialogWindowHandle to null - if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + var dialogWindowExist = false; + lock (_dialogWindowLock) { - Log.Debug(ClassName, $"Destory dialog: {hwnd}"); - lock (_dialogWindowLock) + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) { + Log.Debug(ClassName, $"Destory dialog: {hwnd}"); _dialogWindow = null; + dialogWindowExist = true; } + } + if (dialogWindowExist) + { lock (_autoSwitchedDialogsLock) { _autoSwitchedDialogs.Remove(hwnd); From 3fe0a3e8c659a206edaacb83af5a9c1aa2e83a2e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 19:37:28 +0800 Subject: [PATCH 134/243] Remain focus on dialog when opening quick switch window --- Flow.Launcher/ViewModel/MainViewModel.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b4053628857..09eae8b3415 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -24,6 +24,7 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Microsoft.VisualStudio.Threading; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; namespace Flow.Launcher.ViewModel { @@ -1627,7 +1628,6 @@ public async void SetupQuickSwitch(nint handle) dialogWindowHandleChanged = true; - // Wait for a while to make sure the dialog is shown // If don't give a time, Positioning will be weird await Task.Delay(300); } @@ -1667,6 +1667,22 @@ public async void SetupQuickSwitch(nint handle) } } } + + if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + { + _ = Task.Run(() => + { + // Wait for a while to make sure the dialog is shown and quick switch window has gotten the focus + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() != DialogWindowHandle, 1000); + if (timeOut) + { + return; + } + + // Bring focus back to the the dialog + Win32Helper.SetForegroundWindow(DialogWindowHandle); + }); + } } public async void ResetQuickSwitch() From e85b4ed5ce97cbb2606337616832151ae5715270 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 19:45:04 +0800 Subject: [PATCH 135/243] Inline variable --- Flow.Launcher/ViewModel/MainViewModel.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 09eae8b3415..a7c10e1db00 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1598,8 +1598,6 @@ public bool ShouldIgnoreHotkeys() private bool IsQuickSwitch { get; set; } = false; - private static QuickSwitchWindowPositions QuickSwitchWindowPosition => QuickSwitch.QuickSwitchWindowPosition; - private bool PreviousMainWindowVisibilityStatus { get; set; } public void InitializeVisibilityStatus(bool visibilityStatus) @@ -1609,7 +1607,8 @@ public void InitializeVisibilityStatus(bool visibilityStatus) public bool IsQuickSwitchWindowUnderDialog() { - return IsQuickSwitch && QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; + return IsQuickSwitch && + QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; } #pragma warning disable VSTHRD100 // Avoid async void methods @@ -1650,7 +1649,7 @@ public async void SetupQuickSwitch(nint handle) } else { - if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { Show(); @@ -1668,7 +1667,7 @@ public async void SetupQuickSwitch(nint handle) } } - if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { _ = Task.Run(() => { @@ -1733,7 +1732,7 @@ public void HideQuickSwitch() { if (DialogWindowHandle != nint.Zero) { - if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { if (MainWindowVisibilityStatus) { From 907d68387bee63c7ebbcffc7b996b4a4fd1437de Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 19:54:36 +0800 Subject: [PATCH 136/243] Add warning for auto quick switch & Improve strings with Explorer --- Flow.Launcher/Languages/en.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index f394406ab1f..59f2df283f6 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -307,11 +307,11 @@ Show Result Badges for Global Query Only Show badges for global query results only Quick Switch - Enter shortcut to quickly navigate the path of a file dialog to the path of the current Explorer. + Enter shortcut to quickly navigate the path of a file dialog to the path of the current file manager. Quick Switch - Quickly navigate to the path of the current Explorer when a file dialog is opened. + Quickly navigate to the path of the current file manager when a file dialog is opened. Quick Switch Automatically - Automatically navigate to the path of the current Explorer when a file dialog is opened. + Automatically navigate to the path of the current file manager when a file dialog is opened. (It can possibly cause dialog apps force close) Show Quick Switch Window Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). Quick Switch Window Position From c343337328f30dd60820fc9a885cd79eed229082 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 21:46:44 +0800 Subject: [PATCH 137/243] Improve handle getter --- .../QuickSwitch/Models/WindowsDialog.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 68fa9cbf649..bdb89521f2e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -109,9 +109,6 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab public WindowsDialogTab(HWND handle) { Handle = handle; - GetPathControlEditor(); - GetFileEditor(); - GetOpenButton(); } #endregion @@ -120,7 +117,7 @@ public WindowsDialogTab(HWND handle) public string GetCurrentFolder() { - if (_pathEditor.IsNull) return string.Empty; + if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; return GetWindowText(_pathEditor); } @@ -164,7 +161,7 @@ public bool JumpFolder(string path, bool auto) return JumpFolderWithFileEditor(path, true); } - if (_pathEditor.IsNull && !GetPathControlEditor()) + if (_pathEditor.IsNull) { // Path editor cannot be found, so we can only edit file editor directly. Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); @@ -207,7 +204,7 @@ public void Dispose() private bool GetPathControlEditor() { // Get the handle of the path editor - // (Must use PInvoke.FindWindowEx instead of PInvoke.GetDlgItem, or ReBarWindow32 will be null) + // Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control _pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000 _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 From 9b58dd3ae480f6444fe41691ba9a0c8369e95dc8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 22:08:40 +0800 Subject: [PATCH 138/243] Improve dialog window check & Fix issue that dialogs are enabled on other dialog windows --- .../QuickSwitch/Models/WindowsDialog.cs | 116 ++++++++++-------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index bdb89521f2e..ed0d5a56265 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -21,13 +21,23 @@ internal class WindowsDialog : IQuickSwitchDialog public bool CheckDialogWindow(HWND hwnd) { - if (GetWindowClassName(hwnd) == WindowsDialogClassName) + // Has it been checked? + if (DialogWindow != null && DialogWindow.Handle == hwnd) { - if (DialogWindow == null || DialogWindow.Handle != hwnd) + return true; + } + + // Is it a Win32 dialog box? + if (GetClassName(hwnd) == WindowsDialogClassName) + { + // Is it a windows file dialog? + var dialogType = GetFileDialogType(hwnd); + if (dialogType != DialogType.Others) { - DialogWindow = new WindowsDialogWindow(hwnd); + DialogWindow = new WindowsDialogWindow(hwnd, dialogType); + + return true; } - return true; } return false; } @@ -38,22 +48,34 @@ public void Dispose() DialogWindow = null; } - private static string GetWindowClassName(HWND handle) - { - return GetClassName(handle); + #region Help Methods - static unsafe string GetClassName(HWND handle) + private static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) { - fixed (char* buf = new char[256]) + return PInvoke.GetClassName(handle, buf, 256) switch { - return PInvoke.GetClassName(handle, buf, 256) switch - { - 0 => string.Empty, - _ => new string(buf), - }; - } + 0 => string.Empty, + _ => new string(buf), + }; } } + + private static DialogType GetFileDialogType(HWND handle) + { + // Is it a Windows Open file dialog? + var fileEditor = PInvoke.GetDlgItem(handle, 0x047C); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open; + + // Is it a Windows Save or Save As file dialog? + fileEditor = PInvoke.GetDlgItem(handle, 0x0000); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs; + + return DialogType.Others; + } + + #endregion } internal class WindowsDialogWindow : IQuickSwitchDialogWindow @@ -64,14 +86,17 @@ internal class WindowsDialogWindow : IQuickSwitchDialogWindow // So we need to cache the current tab and use the original handle private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null; - public WindowsDialogWindow(HWND handle) + private readonly DialogType _dialogType; + + public WindowsDialogWindow(HWND handle, DialogType dialogType) { Handle = handle; + _dialogType = dialogType; } public IQuickSwitchDialogWindowTab GetCurrentTab() { - return _currentTab ??= new WindowsDialogTab(Handle); + return _currentTab ??= new WindowsDialogTab(Handle, _dialogType); } public void Dispose() @@ -94,9 +119,9 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab private static readonly InputSimulator _inputSimulator = new(); - private bool _legacy { get; set; } = false; - private DialogType _type { get; set; } = DialogType.None; + private readonly DialogType _dialogType; + private bool _legacy { get; set; } = false; private HWND _pathControl { get; set; } = HWND.Null; private HWND _pathEditor { get; set; } = HWND.Null; private HWND _fileEditor { get; set; } = HWND.Null; @@ -106,9 +131,11 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab #region Constructor - public WindowsDialogTab(HWND handle) + public WindowsDialogTab(HWND handle, DialogType dialogType) { Handle = handle; + _dialogType = dialogType; + Log.Debug(ClassName, $"File dialog type: {dialogType}"); } #endregion @@ -232,11 +259,14 @@ private bool GetPathControlEditor() private bool GetFileEditor() { - // Get the handle of the file name editor of Open file dialog - _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit - if (_fileEditor == HWND.Null) + if (_dialogType == DialogType.Open) + { + // Get the handle of the file name editor of Open file dialog + _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit + } + else { // Get the handle of the file name editor of Save / SaveAs file dialog _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName @@ -244,22 +274,12 @@ private bool GetFileEditor() _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit - if (_fileEditor == HWND.Null) - { - Log.Error(ClassName, "Failed to find file name editor handle"); - _type = DialogType.None; - return false; - } - else - { - Log.Debug(ClassName, "File dialog type: Save / Save As"); - _type = DialogType.SaveOrSaveAs; - } } - else + + if (_fileEditor == HWND.Null) { - Log.Debug(ClassName, "File dialog type: Open"); - _type = DialogType.Open; + Log.Error(ClassName, "Failed to find file name editor handle"); + return false; } return true; @@ -310,7 +330,7 @@ private static unsafe nint SetWindowText(HWND handle, string text) private bool JumpFolderWithFileEditor(string path, bool resetFocus) { // For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors. - if (resetFocus && _type == DialogType.SaveOrSaveAs) return false; + if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false; if (_fileEditor.IsNull && !GetFileEditor()) return false; SetWindowText(_fileEditor, path); @@ -324,16 +344,12 @@ private bool JumpFolderWithFileEditor(string path, bool resetFocus) #endregion #endregion + } - #region Classes - - private enum DialogType - { - None, - Open, - SaveOrSaveAs - } - - #endregion + internal enum DialogType + { + Others, + Open, + SaveOrSaveAs } } From ffaeab3033bf6aa239abdb4b6e66eaa57a6b274f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 22:17:48 +0800 Subject: [PATCH 139/243] Use event hook for dragging --- .../QuickSwitch/QuickSwitch.cs | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0ea7659c1a4..b873f4c267c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -69,11 +69,8 @@ public static class QuickSwitch private static readonly List _autoSwitchedDialogs = new(); private static readonly object _autoSwitchedDialogsLock = new(); - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; - private static HWINEVENTHOOK _moveProc = MoveSizeCallBack;*/ + private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack; private static readonly SemaphoreSlim _navigationLock = new(1, 1); @@ -273,11 +270,7 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { - _dragMoveTimer?.Start(); - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*if (dialogWindowChanged) + if (dialogWindowChanged) { HWND dialogWindowHandle = HWND.Null; lock (_dialogWindowLock) @@ -307,7 +300,7 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) processId, threadId, PInvoke.WINEVENT_OUTOFCONTEXT); - }*/ + } } } } @@ -328,14 +321,11 @@ private static void InvokeResetQuickSwitchWindow() // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); _dragMoveTimer?.Stop(); - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*if (!_moveSizeHook.IsNull) + if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - }*/ + } } private static void InvokeHideQuickSwitchWindow() @@ -486,10 +476,7 @@ uint dwmsEventTime } } - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*private static void MoveSizeCallBack( + private static void MoveSizeCallBack( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -512,7 +499,7 @@ uint dwmsEventTime break; } } - }*/ + } private static void DestroyChangeCallback( HWINEVENTHOOK hWinEventHook, @@ -760,14 +747,11 @@ public static void Dispose() PInvoke.UnhookWinEvent(_locationChangeHook); _locationChangeHook = HWINEVENTHOOK.Null; } - // Note: Here we do not start & stop the timer beacause when there are many dialog windows - // Unhooking and hooking will take too much time which can make window position weird - // So we start & stop the timer when we find a file dialog window - /*if (!_moveSizeHook.IsNull) + if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); _moveSizeHook = HWINEVENTHOOK.Null; - }*/ + } if (!_destroyChangeHook.IsNull) { PInvoke.UnhookWinEvent(_destroyChangeHook); From 8e64539dee1a962eefdcd3c5ea14c34ba0efe281 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 23:00:35 +0800 Subject: [PATCH 140/243] Use task instead of thread --- .../QuickSwitch/QuickSwitch.cs | 166 +++++++++--------- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 6 +- 3 files changed, 85 insertions(+), 89 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index b873f4c267c..f66628039b6 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; @@ -19,7 +20,7 @@ public static class QuickSwitch { #region Public Properties - public static Action ShowQuickSwitchWindow { get; set; } = null; + public static Func ShowQuickSwitchWindow { get; set; } = null; public static Action UpdateQuickSwitchWindow { get; set; } = null; @@ -244,7 +245,7 @@ public static void SetupQuickSwitch(bool enabled) #region Invoke Property Events - private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) + private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChanged) { // Show quick switch window if (_settings.ShowQuickSwitchWindow) @@ -263,9 +264,9 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) dialogWindow = _dialogWindow; } } - if (dialogWindow != null) + if (dialogWindow != null && ShowQuickSwitchWindow != null) { - ShowQuickSwitchWindow?.Invoke(dialogWindow.Handle); + await ShowQuickSwitchWindow.Invoke(dialogWindow.Handle); } if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) @@ -290,19 +291,24 @@ private static unsafe void InvokeShowQuickSwitchWindow(bool dialogWindowChanged) } // Call _moveProc when the window is moved or resized - uint processId; - var threadId = PInvoke.GetWindowThreadProcessId(dialogWindowHandle, &processId); - _moveSizeHook = PInvoke.SetWinEventHook( - PInvoke.EVENT_SYSTEM_MOVESIZESTART, - PInvoke.EVENT_SYSTEM_MOVESIZEEND, - PInvoke.GetModuleHandle((PCWSTR)null), - _moveProc, - processId, - threadId, - PInvoke.WINEVENT_OUTOFCONTEXT); + SetMoveProc(dialogWindowHandle); } } } + + unsafe void SetMoveProc(HWND handle) + { + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(handle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + _moveProc, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); + } } private static void InvokeUpdateQuickSwitchWindow() @@ -342,14 +348,15 @@ private static void InvokeHideQuickSwitchWindow() public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - NavigateDialogPath(Win32Helper.GetForegroundWindowHWND()); + _ = Task.Run(() => NavigateDialogPathAsync(Win32Helper.GetForegroundWindowHWND())); } #endregion #region Windows Events - private static void ForegroundChangeCallback( + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] + private static async void ForegroundChangeCallback( HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, @@ -392,20 +399,20 @@ uint dwmsEventTime // Just show quick switch window if (alreadySwitched) { - InvokeShowQuickSwitchWindow(dialogWindowChanged); + await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); } // Show quick switch window after navigating the path else { - if (!NavigateDialogPath(hwnd, true, () => InvokeShowQuickSwitchWindow(dialogWindowChanged))) + if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) { - InvokeShowQuickSwitchWindow(dialogWindowChanged); + await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); } } } else { - InvokeShowQuickSwitchWindow(dialogWindowChanged); + await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); } } // Quick switch window @@ -425,6 +432,7 @@ uint dwmsEventTime } if (dialogWindowExist) { + Log.Debug(ClassName, $"InvokeHideQuickSwitchWindow"); InvokeHideQuickSwitchWindow(); } @@ -540,7 +548,7 @@ uint dwmsEventTime // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - public static void JumpToPath(nint hwnd, string path, Action action = null) + public static async Task JumpToPathAsync(nint hwnd, string path) { if (hwnd == nint.Zero) return; @@ -550,10 +558,10 @@ public static void JumpToPath(nint hwnd, string path, Action action = null) var dialogWindowTab = dialogWindow.GetCurrentTab(); if (dialogWindowTab == null) return; - JumpToPath(dialogWindowTab, path, false, action); + await JumpToPathAsync(dialogWindowTab, path, false); } - private static bool NavigateDialogPath(HWND hwnd, bool auto = false, Action action = null) + private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = false) { if (hwnd == HWND.Null) return false; @@ -572,7 +580,7 @@ private static bool NavigateDialogPath(HWND hwnd, bool auto = false, Action acti if (string.IsNullOrEmpty(path)) return false; // Jump to path - return JumpToPath(dialogWindowTab, path, auto, action); + return await JumpToPathAsync(dialogWindowTab, path, auto); } private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) @@ -607,79 +615,67 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) return null; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD101:Avoid unsupported async delegates", Justification = "")] - private static bool JumpToPath(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false, Action action = null) + private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false) { if (!CheckPath(path, out var isFile)) return false; - var t = new Thread(async () => - { - // Jump after flow launcher window vanished (after JumpAction returned true) - // and the dialog had been in the foreground. - var dialogHandle = dialog.Handle; - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() == dialogHandle, 1000); - if (timeOut) - { - action?.Invoke(); - return; - } + // Jump after flow launcher window vanished (after JumpAction returned true) + // and the dialog had been in the foreground. + var dialogHandle = dialog.Handle; + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() == dialogHandle, 1000); + if (timeOut) return false; - // Assume that the dialog is in the foreground now - await _navigationLock.WaitAsync(); - try + // Assume that the dialog is in the foreground now + await _navigationLock.WaitAsync(); + try + { + bool result; + if (isFile) { - bool result; - if (isFile) + switch (_settings.QuickSwitchFileResultBehaviour) { - switch (_settings.QuickSwitchFileResultBehaviour) - { - case QuickSwitchFileResultBehaviours.FullPath: - Log.Debug(ClassName, $"File Jump FullPath: {path}"); - result = FileJump(path, dialog); - break; - case QuickSwitchFileResultBehaviours.FullPathOpen: - Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); - result = FileJump(path, dialog, openFile: true); - break; - case QuickSwitchFileResultBehaviours.Directory: - Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}"); - result = DirJump(Path.GetDirectoryName(path), dialog, auto); - break; - default: - throw new ArgumentOutOfRangeException( - nameof(_settings.QuickSwitchFileResultBehaviour), - _settings.QuickSwitchFileResultBehaviour, - "Invalid QuickSwitchFileResultBehaviour" - ); - } - } - else - { - Log.Debug(ClassName, $"Dir Jump: {path}"); - result = DirJump(path, dialog, auto); - } - - if (result) - { - lock (_autoSwitchedDialogsLock) - { - _autoSwitchedDialogs.Add(dialogHandle); - } + case QuickSwitchFileResultBehaviours.FullPath: + Log.Debug(ClassName, $"File Jump FullPath: {path}"); + result = FileJump(path, dialog); + break; + case QuickSwitchFileResultBehaviours.FullPathOpen: + Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); + result = FileJump(path, dialog, openFile: true); + break; + case QuickSwitchFileResultBehaviours.Directory: + Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}"); + result = DirJump(Path.GetDirectoryName(path), dialog, auto); + break; + default: + throw new ArgumentOutOfRangeException( + nameof(_settings.QuickSwitchFileResultBehaviour), + _settings.QuickSwitchFileResultBehaviour, + "Invalid QuickSwitchFileResultBehaviour" + ); } } - catch (System.Exception e) + else { - Log.Exception(ClassName, "Failed to jump to path", e); + Log.Debug(ClassName, $"Dir Jump: {path}"); + result = DirJump(path, dialog, auto); } - finally + + if (result) { - _navigationLock.Release(); + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Add(dialogHandle); + } } - - // Invoke action if provided - action?.Invoke(); - }); - t.Start(); + } + catch (System.Exception e) + { + Log.Exception(ClassName, "Failed to jump to path", e); + } + finally + { + _navigationLock.Release(); + } return true; static bool CheckPath(string path, out bool file) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 5a0d735b414..303cbf9463d 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1155,7 +1155,7 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) private void InitializeQuickSwitch() { - QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitch; + QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitchAsync; QuickSwitch.UpdateQuickSwitchWindow = InitializeQuickSwitchPosition; QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; QuickSwitch.HideQuickSwitchWindow = _viewModel.HideQuickSwitch; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a7c10e1db00..d5088ff2dfa 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -368,7 +368,7 @@ private void LoadContextMenu() if (result is QuickSwitchResult quickSwitchResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(DialogWindowHandle, quickSwitchResult.QuickSwitchPath); + _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)); } } return; @@ -462,7 +462,7 @@ private async Task OpenResultAsync(string index) if (result is QuickSwitchResult quickSwitchResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - QuickSwitch.JumpToPath(DialogWindowHandle, quickSwitchResult.QuickSwitchPath); + _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)); } } } @@ -1613,7 +1613,7 @@ public bool IsQuickSwitchWindowUnderDialog() #pragma warning disable VSTHRD100 // Avoid async void methods - public async void SetupQuickSwitch(nint handle) + public async Task SetupQuickSwitchAsync(nint handle) { if (handle == nint.Zero) return; From dc492b708a2ed915575d85e37761ff62b64944d0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 23:04:37 +0800 Subject: [PATCH 141/243] Make local function static --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index f66628039b6..b2700c74ffd 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -296,7 +296,7 @@ private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChan } } - unsafe void SetMoveProc(HWND handle) + static unsafe void SetMoveProc(HWND handle) { uint processId; var threadId = PInvoke.GetWindowThreadProcessId(handle, &processId); From 9729d251f9ec69a66f8927916bb32cb6acbb4e73 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Apr 2025 23:14:56 +0800 Subject: [PATCH 142/243] Improve log message --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index b2700c74ffd..70e46b3e1b2 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -418,7 +418,7 @@ uint dwmsEventTime // Quick switch window else if (hwnd == _mainWindowHandle) { - // Nothing to do + Log.Debug(ClassName, $"Main Window: {hwnd}"); } else { @@ -432,7 +432,6 @@ uint dwmsEventTime } if (dialogWindowExist) { - Log.Debug(ClassName, $"InvokeHideQuickSwitchWindow"); InvokeHideQuickSwitchWindow(); } From f9df58d8d7eeb3185209e821e2be25480b903f8f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 09:28:51 +0800 Subject: [PATCH 143/243] Fix result issue --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 70e46b3e1b2..2d0e9b6cb94 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -666,16 +666,18 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial _autoSwitchedDialogs.Add(dialogHandle); } } + + return result; } catch (System.Exception e) { Log.Exception(ClassName, "Failed to jump to path", e); + return false; } finally { _navigationLock.Release(); } - return true; static bool CheckPath(string path, out bool file) { From 33f958e0bce21cf7359bb37ce2c5414aa5f7331f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 09:31:42 +0800 Subject: [PATCH 144/243] Check auto startup only for Debug --- Flow.Launcher/App.xaml.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index ecd540b497a..575b39f17c6 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -202,6 +202,10 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => #pragma warning restore VSTHRD100 // Avoid async void methods + /// + /// check auto startup only for Debug + /// + [Conditional("RELEASE")] private void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed From 18f0daf9b8ca5fe92f81d0e9f95e369543912088 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 09:32:52 +0800 Subject: [PATCH 145/243] Add more logs --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 2d0e9b6cb94..8b0e52a68ab 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -422,6 +422,7 @@ uint dwmsEventTime } else { + Log.Debug(ClassName, $"Other Window: {hwnd}"); var dialogWindowExist = false; lock (_dialogWindowLock) { From 805d8413cbff690e7a400dda75d4a15b9927819a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 10:40:37 +0800 Subject: [PATCH 146/243] Code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d5088ff2dfa..19c994b1514 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -24,7 +24,6 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Microsoft.VisualStudio.Threading; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; namespace Flow.Launcher.ViewModel { @@ -1607,12 +1606,9 @@ public void InitializeVisibilityStatus(bool visibilityStatus) public bool IsQuickSwitchWindowUnderDialog() { - return IsQuickSwitch && - QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; + return IsQuickSwitch && QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; } -#pragma warning disable VSTHRD100 // Avoid async void methods - public async Task SetupQuickSwitchAsync(nint handle) { if (handle == nint.Zero) return; @@ -1684,6 +1680,8 @@ public async Task SetupQuickSwitchAsync(nint handle) } } +#pragma warning disable VSTHRD100 // Avoid async void methods + public async void ResetQuickSwitch() { if (DialogWindowHandle == nint.Zero) return; From e748076303ea7ecc8f3b05e58e5c3ae15122cb20 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 11:03:29 +0800 Subject: [PATCH 147/243] Wait 30ms before checking foreground & Update source & check --- Flow.Launcher/ViewModel/MainViewModel.cs | 63 ++++++++++++++---------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 19c994b1514..bb7efde6748 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -359,7 +359,7 @@ public void ForwardHistory() private void LoadContextMenu() { // For quick switch and right click mode, we need to navigate to the path - if (IsQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.RightClick) + if (_isQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.RightClick) { if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { @@ -452,7 +452,7 @@ private async Task OpenResultAsync(string index) } // For quick switch and left click mode, we need to navigate to the path - if (IsQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.LeftClick) + if (_isQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.LeftClick) { Hide(); @@ -1229,7 +1229,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); var plugins = PluginManager.ValidPluginsForQuery(query); - var quickSwitch = IsQuickSwitch; // save quick switch state + var quickSwitch = _isQuickSwitch; // save quick switch state if (quickSwitch) { @@ -1595,18 +1595,20 @@ public bool ShouldIgnoreHotkeys() public nint DialogWindowHandle { get; private set; } = nint.Zero; - private bool IsQuickSwitch { get; set; } = false; + private bool _isQuickSwitch = false; - private bool PreviousMainWindowVisibilityStatus { get; set; } + private bool _previousMainWindowVisibilityStatus; + + private CancellationTokenSource _quickSwitchSource; public void InitializeVisibilityStatus(bool visibilityStatus) { - PreviousMainWindowVisibilityStatus = visibilityStatus; + _previousMainWindowVisibilityStatus = visibilityStatus; } public bool IsQuickSwitchWindowUnderDialog() { - return IsQuickSwitch && QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; + return _isQuickSwitch && QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; } public async Task SetupQuickSwitchAsync(nint handle) @@ -1617,9 +1619,9 @@ public async Task SetupQuickSwitchAsync(nint handle) var dialogWindowHandleChanged = false; if (DialogWindowHandle != handle) { - PreviousMainWindowVisibilityStatus = MainWindowVisibilityStatus; DialogWindowHandle = handle; - IsQuickSwitch = true; + _previousMainWindowVisibilityStatus = MainWindowVisibilityStatus; + _isQuickSwitch = true; dialogWindowHandleChanged = true; @@ -1665,18 +1667,28 @@ public async Task SetupQuickSwitchAsync(nint handle) if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { - _ = Task.Run(() => - { - // Wait for a while to make sure the dialog is shown and quick switch window has gotten the focus - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() != DialogWindowHandle, 1000); - if (timeOut) + // Cancel the previous quick switch task + _quickSwitchSource?.Cancel(); + + // Create a new cancellation token source + _quickSwitchSource = new CancellationTokenSource(); + + // Wait 30ms for the dialog to be shown + _ = Task.Delay(30, _quickSwitchSource.Token).ContinueWith(_ => { - return; - } + // Check dialog handle + if (DialogWindowHandle == nint.Zero) return; + + // Wait for a while to make sure the dialog is shown and quick switch window has gotten the focus + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() != DialogWindowHandle, 1000); + if (timeOut) return; - // Bring focus back to the the dialog - Win32Helper.SetForegroundWindow(DialogWindowHandle); - }); + // Bring focus back to the the dialog + Win32Helper.SetForegroundWindow(DialogWindowHandle); + }, + _quickSwitchSource.Token, + TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); } } @@ -1687,12 +1699,12 @@ public async void ResetQuickSwitch() if (DialogWindowHandle == nint.Zero) return; DialogWindowHandle = nint.Zero; - IsQuickSwitch = false; + _isQuickSwitch = false; - if (PreviousMainWindowVisibilityStatus != MainWindowVisibilityStatus) + if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus) { // Show or hide to change visibility - if (PreviousMainWindowVisibilityStatus) + if (_previousMainWindowVisibilityStatus) { Show(); @@ -1707,7 +1719,7 @@ public async void ResetQuickSwitch() } else { - if (PreviousMainWindowVisibilityStatus) + if (_previousMainWindowVisibilityStatus) { // Only update the position Application.Current?.Dispatcher.Invoke(() => @@ -1776,7 +1788,7 @@ public void Show() Win32Helper.DWMSetCloakForWindow(mainWindow, false); // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !IsQuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isQuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1851,7 +1863,7 @@ public async void Hide(bool reset = true) if (Application.Current?.MainWindow is MainWindow mainWindow) { // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !IsQuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isQuickSwitch) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1977,6 +1989,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { _updateSource?.Dispose(); + _quickSwitchSource?.Dispose(); _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { From 11201dfe257a1799d4b161a392b69bd6ad4a94e5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 11:39:59 +0800 Subject: [PATCH 148/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 10 +++------- Flow.Launcher.Infrastructure/Win32Helper.cs | 7 +------ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 8b0e52a68ab..bb84ec2feae 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -348,7 +348,7 @@ private static void InvokeHideQuickSwitchWindow() public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - _ = Task.Run(() => NavigateDialogPathAsync(Win32Helper.GetForegroundWindowHWND())); + _ = Task.Run(() => NavigateDialogPathAsync(PInvoke.GetForegroundWindow())); } #endregion @@ -622,7 +622,7 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. var dialogHandle = dialog.Handle; - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() == dialogHandle, 1000); + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.IsForegroundWindow(dialogHandle), 1000); if (timeOut) return false; // Assume that the dialog is in the foreground now @@ -647,11 +647,7 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial result = DirJump(Path.GetDirectoryName(path), dialog, auto); break; default: - throw new ArgumentOutOfRangeException( - nameof(_settings.QuickSwitchFileResultBehaviour), - _settings.QuickSwitchFileResultBehaviour, - "Invalid QuickSwitchFileResultBehaviour" - ); + return false; } } else diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 53a5779de36..2a484da4074 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -116,12 +116,7 @@ public static unsafe string GetWallpaperPath() public static nint GetForegroundWindow() { - return GetForegroundWindowHWND().Value; - } - - internal static HWND GetForegroundWindowHWND() - { - return PInvoke.GetForegroundWindow(); + return PInvoke.GetForegroundWindow().Value; } public static bool SetForegroundWindow(Window window) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index bb7efde6748..243b534f0b3 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1680,7 +1680,7 @@ public async Task SetupQuickSwitchAsync(nint handle) if (DialogWindowHandle == nint.Zero) return; // Wait for a while to make sure the dialog is shown and quick switch window has gotten the focus - var timeOut = !SpinWait.SpinUntil(() => Win32Helper.GetForegroundWindowHWND() != DialogWindowHandle, 1000); + var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 1000); if (timeOut) return; // Bring focus back to the the dialog From 8eaefa3d05a6bda3645832d0f0d9f595d17ad15e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 11:43:26 +0800 Subject: [PATCH 149/243] Code quality --- .../QuickSwitch/QuickSwitch.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index bb84ec2feae..901998eff9b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -250,25 +250,24 @@ private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChan // Show quick switch window if (_settings.ShowQuickSwitchWindow) { + // Save quick switch window position for one file dialog if (dialogWindowChanged) { - // Save quick switch window position for one file dialog QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; } - IQuickSwitchDialogWindow dialogWindow = null; + // Call show quick switch window + IQuickSwitchDialogWindow dialogWindow; lock (_dialogWindowLock) { - if (_dialogWindow != null) - { dialogWindow = _dialogWindow; } - } if (dialogWindow != null && ShowQuickSwitchWindow != null) { await ShowQuickSwitchWindow.Invoke(dialogWindow.Handle); } + // Hook move size event if quick switch window is under dialog & dialog window changed if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { if (dialogWindowChanged) @@ -326,7 +325,11 @@ private static void InvokeResetQuickSwitchWindow() // Reset quick switch window ResetQuickSwitchWindow?.Invoke(); + + // Stop drag move timer _dragMoveTimer?.Stop(); + + // Unhook move size event if (!_moveSizeHook.IsNull) { PInvoke.UnhookWinEvent(_moveSizeHook); @@ -336,9 +339,10 @@ private static void InvokeResetQuickSwitchWindow() private static void InvokeHideQuickSwitchWindow() { - // Neither quick switch window nor file dialog window is foreground - // Hide quick switch window until the file dialog window is brought to the foreground + // Hide quick switch window HideQuickSwitchWindow?.Invoke(); + + // Stop drag move timer _dragMoveTimer?.Stop(); } @@ -366,8 +370,8 @@ private static async void ForegroundChangeCallback( uint dwmsEventTime ) { - // File dialog window - var findDialogWindow = false; + // Check if it is a file dialog window + var isDialogWindow = false; var dialogWindowChanged = false; foreach (var dialog in _quickSwitchDialogs) { @@ -379,13 +383,15 @@ uint dwmsEventTime _dialogWindow = dialog.DialogWindow; } - findDialogWindow = true; - Log.Debug(ClassName, $"Dialog Window: {hwnd}"); + isDialogWindow = true; break; } } - if (findDialogWindow) + + // Handle window based on its type + if (isDialogWindow) { + Log.Debug(ClassName, $"Dialog Window: {hwnd}"); // Navigate to path if (_settings.AutoQuickSwitch) { @@ -433,6 +439,7 @@ uint dwmsEventTime } if (dialogWindowExist) { + // Hide quick switch window until the file dialog window is brought to the foreground InvokeHideQuickSwitchWindow(); } From db694c4b1250360fc681ce7570224d92918f20f5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 14:01:41 +0800 Subject: [PATCH 150/243] Code quality --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 3 +-- Flow.Launcher/ViewModel/MainViewModel.cs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 901998eff9b..19ade907e04 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -312,7 +312,6 @@ static unsafe void SetMoveProc(HWND handle) private static void InvokeUpdateQuickSwitchWindow() { - // Update quick switch window UpdateQuickSwitchWindow?.Invoke(); } @@ -437,7 +436,7 @@ uint dwmsEventTime dialogWindowExist = true; } } - if (dialogWindowExist) + if (dialogWindowExist) // Neither quick switch window nor file dialog window is foreground { // Hide quick switch window until the file dialog window is brought to the foreground InvokeHideQuickSwitchWindow(); diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 243b534f0b3..d6dcf1c1a51 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -195,7 +195,8 @@ private void RegisterViewUpdate() var resultUpdateChannel = Channel.CreateUnbounded(); _resultsUpdateChannelWriter = resultUpdateChannel.Writer; _resultsViewUpdateTask = - Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + Task.Run(UpdateActionAsync).ContinueWith(continueAction, + CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); async Task UpdateActionAsync() { From a69d6ea56ff4c396a1809ae98188a2a58358e913 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 17:15:54 +0800 Subject: [PATCH 151/243] Add foreground changed lock & Improve code quality --- .../QuickSwitch/QuickSwitch.cs | 156 ++++++++++-------- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 36 ++-- 3 files changed, 109 insertions(+), 85 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 19ade907e04..15a311139f6 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -20,7 +20,7 @@ public static class QuickSwitch { #region Public Properties - public static Func ShowQuickSwitchWindow { get; set; } = null; + public static Func ShowQuickSwitchWindowAsync { get; set; } = null; public static Action UpdateQuickSwitchWindow { get; set; } = null; @@ -73,6 +73,7 @@ public static class QuickSwitch private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack; + private static readonly SemaphoreSlim _foregroundChangeLock = new(1, 1); private static readonly SemaphoreSlim _navigationLock = new(1, 1); private static bool _initialized = false; @@ -260,11 +261,11 @@ private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChan IQuickSwitchDialogWindow dialogWindow; lock (_dialogWindowLock) { - dialogWindow = _dialogWindow; - } - if (dialogWindow != null && ShowQuickSwitchWindow != null) + dialogWindow = _dialogWindow; + } + if (dialogWindow != null && ShowQuickSwitchWindowAsync != null) { - await ShowQuickSwitchWindow.Invoke(dialogWindow.Handle); + await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle); } // Hook move size event if quick switch window is under dialog & dialog window changed @@ -369,100 +370,109 @@ private static async void ForegroundChangeCallback( uint dwmsEventTime ) { - // Check if it is a file dialog window - var isDialogWindow = false; - var dialogWindowChanged = false; - foreach (var dialog in _quickSwitchDialogs) + await _foregroundChangeLock.WaitAsync(); + try { - if (dialog.CheckDialogWindow(hwnd)) + // Check if it is a file dialog window + var isDialogWindow = false; + var dialogWindowChanged = false; + foreach (var dialog in _quickSwitchDialogs) { - lock (_dialogWindowLock) + if (dialog.CheckDialogWindow(hwnd)) { - dialogWindowChanged = _dialogWindow == null || _dialogWindow.Handle != hwnd; - _dialogWindow = dialog.DialogWindow; - } + lock (_dialogWindowLock) + { + dialogWindowChanged = _dialogWindow == null || _dialogWindow.Handle != hwnd; + _dialogWindow = dialog.DialogWindow; + } - isDialogWindow = true; - break; + isDialogWindow = true; + break; + } } - } - // Handle window based on its type - if (isDialogWindow) - { - Log.Debug(ClassName, $"Dialog Window: {hwnd}"); - // Navigate to path - if (_settings.AutoQuickSwitch) + // Handle window based on its type + if (isDialogWindow) { - // Check if we have already switched for this dialog - bool alreadySwitched; - lock (_autoSwitchedDialogsLock) + Log.Debug(ClassName, $"Dialog Window: {hwnd}"); + // Navigate to path + if (_settings.AutoQuickSwitch) { - alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); - } + // Check if we have already switched for this dialog + bool alreadySwitched; + lock (_autoSwitchedDialogsLock) + { + alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); + } - // Just show quick switch window - if (alreadySwitched) - { - await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); - } - // Show quick switch window after navigating the path - else - { - if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) + // Just show quick switch window + if (alreadySwitched) { await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); } + // Show quick switch window after navigating the path + else + { + if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) + { + await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); + } + } + } + else + { + await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); } } - else + // Quick switch window + else if (hwnd == _mainWindowHandle) { - await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); + Log.Debug(ClassName, $"Main Window: {hwnd}"); } - } - // Quick switch window - else if (hwnd == _mainWindowHandle) - { - Log.Debug(ClassName, $"Main Window: {hwnd}"); - } - else - { - Log.Debug(ClassName, $"Other Window: {hwnd}"); - var dialogWindowExist = false; - lock (_dialogWindowLock) + // Other window + else { - if (_dialogWindow != null) + Log.Debug(ClassName, $"Other Window: {hwnd}"); + var dialogWindowExist = false; + lock (_dialogWindowLock) { - dialogWindowExist = true; + if (_dialogWindow != null) + { + dialogWindowExist = true; + } + } + if (dialogWindowExist) // Neither quick switch window nor file dialog window is foreground + { + // Hide quick switch window until the file dialog window is brought to the foreground + InvokeHideQuickSwitchWindow(); } - } - if (dialogWindowExist) // Neither quick switch window nor file dialog window is foreground - { - // Hide quick switch window until the file dialog window is brought to the foreground - InvokeHideQuickSwitchWindow(); - } - // Check if there are foreground explorer windows - try - { - lock (_lastExplorerLock) + // Check if there are foreground explorer windows + try { - foreach (var explorer in _quickSwitchExplorers) + lock (_lastExplorerLock) { - if (explorer.CheckExplorerWindow(hwnd)) + foreach (var explorer in _quickSwitchExplorers) { - Log.Debug(ClassName, $"Explorer window: {hwnd}"); - _lastExplorer = explorer; - break; + if (explorer.CheckExplorerWindow(hwnd)) + { + Log.Debug(ClassName, $"Explorer window: {hwnd}"); + _lastExplorer = explorer; + break; + } } } } - } - catch (System.Exception) - { - // Ignored + catch (System.Exception) + { + // Ignored + } } } + finally + { + _foregroundChangeLock.Release(); + } } private static void LocationChangeCallback( @@ -780,6 +790,10 @@ public static void Dispose() _dialogWindow = null; } + // Dispose locks + _foregroundChangeLock.Dispose(); + _navigationLock.Dispose(); + // Stop drag move timer if (_dragMoveTimer != null) { diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 303cbf9463d..f3d8ebaa9a9 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1155,7 +1155,7 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) private void InitializeQuickSwitch() { - QuickSwitch.ShowQuickSwitchWindow = _viewModel.SetupQuickSwitchAsync; + QuickSwitch.ShowQuickSwitchWindowAsync = _viewModel.SetupQuickSwitchAsync; QuickSwitch.UpdateQuickSwitchWindow = InitializeQuickSwitchPosition; QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; QuickSwitch.HideQuickSwitchWindow = _viewModel.HideQuickSwitch; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d6dcf1c1a51..6e0f96fa38d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1633,6 +1633,7 @@ public async Task SetupQuickSwitchAsync(nint handle) // If handle is cleared, which means the dialog is closed, do nothing if (DialogWindowHandle == nint.Zero) return; + // Initialize quick switch window if (MainWindowVisibilityStatus) { if (dialogWindowHandleChanged) @@ -1674,22 +1675,28 @@ public async Task SetupQuickSwitchAsync(nint handle) // Create a new cancellation token source _quickSwitchSource = new CancellationTokenSource(); - // Wait 30ms for the dialog to be shown - _ = Task.Delay(30, _quickSwitchSource.Token).ContinueWith(_ => + _ = Task.Run(() => { - // Check dialog handle - if (DialogWindowHandle == nint.Zero) return; + try + { + // Check task cancellation + if (_quickSwitchSource.Token.IsCancellationRequested) return; - // Wait for a while to make sure the dialog is shown and quick switch window has gotten the focus - var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 1000); - if (timeOut) return; + // Check dialog handle + if (DialogWindowHandle == nint.Zero) return; - // Bring focus back to the the dialog - Win32Helper.SetForegroundWindow(DialogWindowHandle); - }, - _quickSwitchSource.Token, - TaskContinuationOptions.NotOnCanceled, - TaskScheduler.Default); + // Wait 150ms to check if quick switch window gets the focus + var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); + if (timeOut) return; + + // Bring focus back to the the dialog + Win32Helper.SetForegroundWindow(DialogWindowHandle); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to focus on dialog window", e); + } + }); } } @@ -1745,6 +1752,9 @@ public void HideQuickSwitch() { if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { + // Warning: Main window is already in foreground + // This is because if you click popup menus in other applications to hide quick switch window, + // they can steal focus before showing main window if (MainWindowVisibilityStatus) { Hide(); From 3f932a8a8fe9521a05ad0232f1079840c7918a5e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 17:28:08 +0800 Subject: [PATCH 152/243] Check path first & Improve code quality --- .../QuickSwitch/QuickSwitch.cs | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 15a311139f6..5ae826bb400 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -564,29 +564,26 @@ uint dwmsEventTime // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump - public static async Task JumpToPathAsync(nint hwnd, string path) + public static async Task JumpToPathAsync(nint hwnd, string path) { - if (hwnd == nint.Zero) return; + // Check handle + if (hwnd == nint.Zero) return false; - var dialogWindow = GetDialogWindow(new(hwnd)); - if (dialogWindow == null) return; + // Check path + if (!CheckPath(path, out var isFile)) return false; - var dialogWindowTab = dialogWindow.GetCurrentTab(); - if (dialogWindowTab == null) return; + // Get dialog tab + var dialogWindowTab = GetDialogWindowTab(new(hwnd)); + if (dialogWindowTab == null) return false; - await JumpToPathAsync(dialogWindowTab, path, false); + return await JumpToPathAsync(dialogWindowTab, path, isFile, false); } private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = false) { + // Check handle if (hwnd == HWND.Null) return false; - var dialogWindow = GetDialogWindow(hwnd); - if (dialogWindow == null) return false; - - var dialogWindowTab = dialogWindow.GetCurrentTab(); - if (dialogWindowTab == null) return false; - // Get explorer path string path; lock (_lastExplorerLock) @@ -595,8 +592,38 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f } if (string.IsNullOrEmpty(path)) return false; + // Check path + if (!CheckPath(path, out var isFile)) return false; + + // Get dialog tab + var dialogWindowTab = GetDialogWindowTab(hwnd); + if (dialogWindowTab == null) return false; + // Jump to path - return await JumpToPathAsync(dialogWindowTab, path, auto); + return await JumpToPathAsync(dialogWindowTab, path, isFile, auto); + } + + private static bool CheckPath(string path, out bool file) + { + file = false; + // Is non-null? + if (string.IsNullOrEmpty(path)) return false; + // Is absolute? + if (!Path.IsPathRooted(path)) return false; + // Is folder? + var isFolder = Directory.Exists(path); + // Is file? + var isFile = File.Exists(path); + file = isFile; + return isFolder || isFile; + } + + private static IQuickSwitchDialogWindowTab GetDialogWindowTab(HWND hwnd) + { + var dialogWindow = GetDialogWindow(hwnd); + if (dialogWindow == null) return null; + var dialogWindowTab = dialogWindow.GetCurrentTab(); + return dialogWindowTab; } private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) @@ -631,10 +658,8 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) return null; } - private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dialog, string path, bool auto = false) + private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dialog, string path, bool isFile, bool auto = false) { - if (!CheckPath(path, out var isFile)) return false; - // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. var dialogHandle = dialog.Handle; @@ -691,21 +716,6 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial { _navigationLock.Release(); } - - static bool CheckPath(string path, out bool file) - { - file = false; - // Is non-null? - if (string.IsNullOrEmpty(path)) return false; - // Is absolute? - if (!Path.IsPathRooted(path)) return false; - // Is folder? - var isFolder = Directory.Exists(path); - // Is file? - var isFile = File.Exists(path); - file = isFile; - return isFolder || isFile; - } } private static bool FileJump(string filePath, IQuickSwitchDialogWindowTab dialog, bool openFile = false) From d443a2ed885d01acbcdef1a9c3b1115092bc06d2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 22 Apr 2025 17:37:08 +0800 Subject: [PATCH 153/243] Fix code comments --- Flow.Launcher/App.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 575b39f17c6..0e216343215 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -203,7 +203,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => #pragma warning restore VSTHRD100 // Avoid async void methods /// - /// check auto startup only for Debug + /// check startup only for Release /// [Conditional("RELEASE")] private void AutoStartup() From 4372b6a84a44c4cd45896fc8562c5bea36a61076 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 12:12:45 +0800 Subject: [PATCH 154/243] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 7 +++++-- Flow.Launcher/ViewModel/MainViewModel.cs | 8 +------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 982ef64bee9..f85fe75f85f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -260,13 +260,16 @@ public static async Task InitializePluginsAsync() } } - public static ICollection ValidPluginsForQuery(Query query) + public static ICollection ValidPluginsForQuery(Query query, bool quickSwitch) { if (query is null) return Array.Empty(); if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) - return GlobalPlugins; + return quickSwitch ? GlobalPlugins.Where(p => p.Plugin is IAsyncQuickSwitch).ToList() : GlobalPlugins; + + if (quickSwitch && plugin.Plugin is not IAsyncQuickSwitch) + return Array.Empty(); return new List { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index cd5f1b59820..4b63ea01c9e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1302,13 +1302,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (quickSwitch) - { - // Select for IAsyncQuickSwitch - plugins = new Collection(plugins.Where(p => p.Plugin is IAsyncQuickSwitch).ToList()); - } + var plugins = PluginManager.ValidPluginsForQuery(query, quickSwitch); if (plugins.Count == 1) { From 165ba172234dd8c0a29e542d5ada9f1a13aa8152 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 12:13:16 +0800 Subject: [PATCH 155/243] Add async quick switch as features --- Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs index f044ae48367..477b44975e7 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs @@ -7,7 +7,7 @@ namespace Flow.Launcher.Plugin /// /// Asynchronous Quick Switch Model /// - public interface IAsyncQuickSwitch + public interface IAsyncQuickSwitch : IFeatures { /// /// Asynchronous querying for quick switch window From e662a1b944d17bd97e3c02526fc746b568869621 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 09:15:40 +0800 Subject: [PATCH 156/243] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 071b6d609bf..20a404ddd6a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -255,7 +255,7 @@ public void RegisterResultsUpdatedEvent() else { // make a clone to avoid possible issue that plugin will also change the list and items when updating view model - resultsCopy = DeepClone(e.Results, token); + resultsCopy = DeepCloneResults(e.Results, token); } foreach (var result in resultsCopy) @@ -497,7 +497,7 @@ private async Task OpenResultAsync(string index) } } - private static IReadOnlyList DeepClone(IReadOnlyList results, CancellationToken token = default) + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) { var resultsCopy = new List(); @@ -515,7 +515,7 @@ private static IReadOnlyList DeepClone(IReadOnlyList results, Ca return resultsCopy; } - private static IReadOnlyList DeepClone(IReadOnlyList results, CancellationToken token = default) + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) { var resultsCopy = new List(); @@ -1428,7 +1428,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepClone(results, token); + resultsCopy = DeepCloneResults(results, token); } foreach (var result in resultsCopy) @@ -1453,8 +1453,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { var results = await PluginManager.QueryForPluginAsync(plugin, query, token); - if (token.IsCancellationRequested) - return; + if (token.IsCancellationRequested) return; IReadOnlyList resultsCopy; if (results == null) @@ -1464,7 +1463,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepClone(results, token); + resultsCopy = DeepCloneResults(results, token); } foreach (var result in resultsCopy) From 8d9b152a15ea6076f042f8ab1aefaf295c07b5ee Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 22:06:16 +0800 Subject: [PATCH 157/243] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 45 ++++++++++-------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 88f9fdfedb4..23fa8cc80e6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1313,19 +1313,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b if (query == null) // shortcut expanded { - App.API.LogDebug(ClassName, $"Clear query results"); - - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); - - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; + ClearResults(); return; } @@ -1336,19 +1324,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // Do not show home page for quick switch window if (quickSwitch && isHomeQuery) { - App.API.LogDebug(ClassName, $"Clear query results"); - - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); - - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; + ClearResults(); return; } @@ -1585,6 +1561,23 @@ void QueryHistoryTask() } } + private void ClearResults() + { + App.API.LogDebug(ClassName, $"Clear query results"); + + // Hide and clear results again because running query may show and add some results + Results.Visibility = Visibility.Collapsed; + Results.Clear(); + + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; + } + private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) { From 08406ef4e6474980c4275b1a0d6b5b322e3a4fd6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 13:24:06 +0800 Subject: [PATCH 158/243] Remove unused using --- 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 3420fab6db8..a3352951082 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; using System.Linq; From 397c5d1f7c8708ed0158e85e62b7fb7c077b595e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 17:59:41 +0800 Subject: [PATCH 159/243] Support Files explorer --- .../Flow.Launcher.Infrastructure.csproj | 2 + .../NativeMethods.txt | 2 + .../QuickSwitch/Models/FilesExplorer.cs | 199 ++++++++++++++++++ .../QuickSwitch/QuickSwitch.cs | 3 +- 4 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index dd2d71e28ec..dd4ed09d43d 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -55,6 +55,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 87d9fa522ad..b20d2950632 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -77,3 +77,5 @@ GetDlgItem PostMessage BM_CLICK WM_GETTEXT +OpenProcess +QueryFullProcessImageName diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs new file mode 100644 index 00000000000..fcd47aa52ca --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs @@ -0,0 +1,199 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using FlaUI.Core.AutomationElements; +using FlaUI.UIA3; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Microsoft.Win32.SafeHandles; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Threading; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +{ + /// + /// Class for handling Files instances in QuickSwitch. + /// + /// + /// Edited from: https://github.com/files-community/Listary.FileAppPlugin.Files + /// + internal class FilesExplorer : IQuickSwitchExplorer + { + private static readonly string ClassName = nameof(FilesExplorer); + + private static FilesWindow _lastExplorerView = null; + private static readonly object _lastExplorerViewLock = new(); + + public bool CheckExplorerWindow(HWND hWnd) + { + var isExplorer = false; + lock (_lastExplorerViewLock) + { + // Is it from Files? + var processName = Path.GetFileName(GetProcessPathFromHwnd(hWnd)); + if (processName == "Files.exe") + { + // Is it Files's file window? + try + { + var automation = new UIA3Automation(); + var Files = automation.FromHandle(hWnd); + if (Files.Name == "Files" || Files.Name.Contains("- Files")) + { + _lastExplorerView = new FilesWindow(hWnd, automation, Files); + isExplorer = true; + } + } + catch (TimeoutException e) + { + Log.Warn(ClassName, $"UIA timeout: {e}"); + } + catch (System.Exception e) + { + Log.Warn(ClassName, $"Failed to bind window: {e}"); + } + } + } + return isExplorer; + } + + private static unsafe string GetProcessPathFromHwnd(HWND hWnd) + { + uint pid; + var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid); + if (threadId == 0) return string.Empty; + + var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + if (process.Value != IntPtr.Zero) + { + using var safeHandle = new SafeProcessHandle(process.Value, true); + uint capacity = 2000; + Span buffer = new char[capacity]; + fixed (char* pBuffer = buffer) + { + if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity)) + { + return string.Empty; + } + + return buffer[..(int)capacity].ToString(); + } + } + + return string.Empty; + } + + public string GetExplorerPath() + { + if (_lastExplorerView == null) return null; + return _lastExplorerView.GetCurrentTab().GetCurrentFolder(); + } + + public void RemoveExplorerWindow() + { + lock (_lastExplorerViewLock) + { + _lastExplorerView = null; + } + } + + public void Dispose() + { + // Release ComObjects + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + _lastExplorerView.Dispose(); + _lastExplorerView = null; + } + } + } + catch (COMException) + { + _lastExplorerView = null; + } + } + + private class FilesWindow : IDisposable + { + private readonly UIA3Automation _automation; + private readonly AutomationElement _Files; + + public IntPtr Handle { get; } + + public FilesWindow(IntPtr hWnd, UIA3Automation automation, AutomationElement Files) + { + Handle = hWnd; + _automation = automation; + _Files = Files; + } + + public void Dispose() + { + _automation.Dispose(); + } + + public FilesTab GetCurrentTab() + { + return new FilesTab(_Files); + } + } + + private class FilesTab + { + private readonly TextBox _currentPathGet; + private readonly TextBox _currentPathSet; + + public FilesTab(AutomationElement Files) + { + // Find window content to reduce the scope + var _windowContent = Files.FindFirstChild(cf => cf.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge")); + + _currentPathGet = _windowContent.FindFirstChild(cf => cf.ByAutomationId("CurrentPathGet"))?.AsTextBox(); + if (_currentPathGet == null) + { + Log.Error(ClassName, "Failed to find CurrentPathGet"); + return; + } + + _currentPathSet = _windowContent.FindFirstChild(cf => cf.ByAutomationId("CurrentPathSet"))?.AsTextBox(); + if (_currentPathSet == null) + { + Log.Error(ClassName, "Failed to find CurrentPathSet"); + return; + } + } + + public string GetCurrentFolder() + { + try + { + return _currentPathGet.Text; + } + catch (System.Exception e) + { + Log.Error(ClassName, $"Failed to get current folder: {e}"); + return null; + } + } + + public bool OpenFolder(string path) + { + try + { + _currentPathSet.Text = path; + return true; + } + catch (System.Exception e) + { + Log.Error(ClassName, $"Failed to get current folder: {e}"); + return false; + } + } + } + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 5ae826bb400..e936eee93d9 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -42,7 +42,8 @@ public static class QuickSwitch private static readonly List _quickSwitchExplorers = new() { - new WindowsExplorer() + new WindowsExplorer(), + new FilesExplorer() }; private static IQuickSwitchExplorer _lastExplorer = null; From 2d7d311e72846e39ac39741d36c6d10507206c6c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 18:05:01 +0800 Subject: [PATCH 160/243] Remove null check --- .../QuickSwitch/Models/WindowsExplorer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 2e22987e743..101974dcdfa 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -32,7 +32,7 @@ public bool CheckExplorerWindow(HWND foreground) return; } - if (foreground != HWND.Null && explorer.HWND != foreground.Value) + if (explorer.HWND != foreground.Value) { return; } From 92249e9b75983c4dab8e9bd3fdea2ead64779436 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 18:06:50 +0800 Subject: [PATCH 161/243] Code quality --- .../QuickSwitch/Models/FilesExplorer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs index fcd47aa52ca..adef20d3309 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs @@ -25,23 +25,23 @@ internal class FilesExplorer : IQuickSwitchExplorer private static FilesWindow _lastExplorerView = null; private static readonly object _lastExplorerViewLock = new(); - public bool CheckExplorerWindow(HWND hWnd) + public bool CheckExplorerWindow(HWND foreground) { var isExplorer = false; lock (_lastExplorerViewLock) { // Is it from Files? - var processName = Path.GetFileName(GetProcessPathFromHwnd(hWnd)); + var processName = Path.GetFileName(GetProcessPathFromHwnd(foreground)); if (processName == "Files.exe") { // Is it Files's file window? try { var automation = new UIA3Automation(); - var Files = automation.FromHandle(hWnd); + var Files = automation.FromHandle(foreground); if (Files.Name == "Files" || Files.Name.Contains("- Files")) { - _lastExplorerView = new FilesWindow(hWnd, automation, Files); + _lastExplorerView = new FilesWindow(foreground, automation, Files); isExplorer = true; } } From c03926f8a013b25339493fe6a0aec4bd99e27376 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 21:19:52 +0800 Subject: [PATCH 162/243] Support Files for builtin shortcuts & Use topmost explorer window during initialization --- .../FileExplorerHelper.cs | 73 +------------------ .../QuickSwitch/QuickSwitch.cs | 56 +++++++++----- 2 files changed, 43 insertions(+), 86 deletions(-) diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index 1085cc83313..10d01d3d5cf 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -13,9 +9,10 @@ public static class FileExplorerHelper /// public static string GetActiveExplorerPath() { - var explorerWindow = GetActiveExplorer(); - string locationUrl = explorerWindow?.LocationURL; - return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null; + var explorerPath = QuickSwitch.QuickSwitch.GetActiveExplorerPath(); + if (string.IsNullOrEmpty(explorerPath)) return null; + + return GetDirectoryPath(new Uri(explorerPath).LocalPath); } /// @@ -30,67 +27,5 @@ private static string GetDirectoryPath(string path) return path; } - - /// - /// Gets the file explorer that is currently in the foreground - /// - private static dynamic GetActiveExplorer() - { - Type type = Type.GetTypeFromProgID("Shell.Application"); - if (type == null) return null; - dynamic shell = Activator.CreateInstance(type); - if (shell == null) - { - return null; - } - - var explorerWindows = new List(); - var openWindows = shell.Windows(); - for (int i = 0; i < openWindows.Count; i++) - { - var window = openWindows.Item(i); - if (window == null) continue; - - // find the desired window and make sure that it is indeed a file explorer - // we don't want the Internet Explorer or the classic control panel - // ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE" - if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe") - { - explorerWindows.Add(window); - } - } - - if (explorerWindows.Count == 0) return null; - - var zOrders = GetZOrder(explorerWindows); - - return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; - } - - /// - /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. - /// - private static IEnumerable GetZOrder(List hWnds) - { - var z = new int[hWnds.Count]; - for (var i = 0; i < hWnds.Count; i++) z[i] = -1; - - var index = 0; - var numRemaining = hWnds.Count; - PInvoke.EnumWindows((wnd, _) => - { - var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd); - if (searchIndex != -1) - { - z[searchIndex] = index; - numRemaining--; - if (numRemaining == 0) return false; - } - index++; - return true; - }, IntPtr.Zero); - - return z; - } } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index e936eee93d9..a288d7404de 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -107,26 +107,12 @@ public static void SetupQuickSwitch(bool enabled) if (enabled) { - // Check if there are explorer windows + // Check if there are explorer windows and get the topmost one try { - lock (_lastExplorerLock) + if (RefreshLastExplorer()) { - foreach (var explorer in _quickSwitchExplorers) - { - // Use HWND.Null here because we want to check all windows - if (explorer.CheckExplorerWindow(HWND.Null)) - { - if (_lastExplorer == null) - { - Log.Debug(ClassName, $"Explorer window"); - // Set last explorer view if not set, - // this is beacuse default WindowsExplorer is the first element - _lastExplorer = explorer; - break; - } - } - } + Log.Debug(ClassName, $"Explorer window found"); } } catch (System.Exception) @@ -241,6 +227,42 @@ public static void SetupQuickSwitch(bool enabled) _enabled = enabled; } + private static bool RefreshLastExplorer() + { + var found = false; + + lock (_lastExplorerLock) + { + // Enum windows from the top to the bottom + PInvoke.EnumWindows((hWnd, _) => + { + foreach (var explorer in _quickSwitchExplorers) + { + if (explorer.CheckExplorerWindow(hWnd)) + { + _lastExplorer = explorer; + found = true; + return false; + } + } + + // If we reach here, it means that the window is not a file explorer + return true; + }, IntPtr.Zero); + } + + return found; + } + + #endregion + + #region Active Explorer + + public static string GetActiveExplorerPath() + { + return RefreshLastExplorer() ? _lastExplorer.GetExplorerPath() : string.Empty; + } + #endregion #region Events From 231e6e80199340268209d0481f96cdb4a5a25bb5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 21:19:58 +0800 Subject: [PATCH 163/243] Improve Dispose --- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/Helper/HotKeyMapper.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 1a0c7a9d149..213e7a99764 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -357,7 +357,7 @@ protected virtual void Dispose(bool disposing) // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); - HotKeyMapper.Dispose(); + QuickSwitch.Dispose(); } API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 6edd55fdaad..aa598141f12 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -30,11 +30,6 @@ internal static void Initialize() LoadCustomPluginHotkey(); } - internal static void Dispose() - { - QuickSwitch.Dispose(); - } - internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) { if (!_mainViewModel.ShouldIgnoreHotkeys()) From 69076f38fdee0bc83d59a1f9367a0a284ea549dc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 21:40:48 +0800 Subject: [PATCH 164/243] Improve code quality --- Flow.Launcher.Infrastructure/FileExplorerHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index 10d01d3d5cf..be0c278ad66 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -10,9 +10,9 @@ public static class FileExplorerHelper public static string GetActiveExplorerPath() { var explorerPath = QuickSwitch.QuickSwitch.GetActiveExplorerPath(); - if (string.IsNullOrEmpty(explorerPath)) return null; - - return GetDirectoryPath(new Uri(explorerPath).LocalPath); + return !string.IsNullOrEmpty(explorerPath) ? + GetDirectoryPath(new Uri(explorerPath).LocalPath) : + null; } /// From 063d394bb4ec35ca8e1c4702b646ba63753efb4e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 21:51:12 +0800 Subject: [PATCH 165/243] Code quality --- .../QuickSwitch/Models/WindowsExplorer.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 101974dcdfa..d8252baa3bd 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -47,30 +47,30 @@ public bool CheckExplorerWindow(HWND foreground) }); } return isExplorer; + } - static unsafe void EnumerateShellWindows(Action action) - { - // Create an instance of ShellWindows - var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass - var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows + private static unsafe void EnumerateShellWindows(Action action) + { + // Create an instance of ShellWindows + var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass + var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows - var result = PInvoke.CoCreateInstance( - &clsidShellWindows, - null, - CLSCTX.CLSCTX_ALL, - &iidIShellWindows, - out var shellWindowsObj); + var result = PInvoke.CoCreateInstance( + &clsidShellWindows, + null, + CLSCTX.CLSCTX_ALL, + &iidIShellWindows, + out var shellWindowsObj); - if (result.Failed) return; + if (result.Failed) return; - var shellWindows = (IShellWindows)shellWindowsObj; + var shellWindows = (IShellWindows)shellWindowsObj; - // Enumerate the shell windows - var count = shellWindows.Count; - for (var i = 0; i < count; i++) - { - action(shellWindows.Item(i)); - } + // Enumerate the shell windows + var count = shellWindows.Count; + for (var i = 0; i < count; i++) + { + action(shellWindows.Item(i)); } } From 99d5a99cc2f5c57642063a4564c1405ba0e30a54 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 22:12:30 +0800 Subject: [PATCH 166/243] Ignore character case --- .../QuickSwitch/Models/FilesExplorer.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs index adef20d3309..aedbaaff84d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs @@ -28,37 +28,38 @@ internal class FilesExplorer : IQuickSwitchExplorer public bool CheckExplorerWindow(HWND foreground) { var isExplorer = false; - lock (_lastExplorerViewLock) + // Is it from Files? + var processName = Path.GetFileName(GetProcessPathFromHwnd(foreground)); + if (processName.ToLower() == "files.exe") { - // Is it from Files? - var processName = Path.GetFileName(GetProcessPathFromHwnd(foreground)); - if (processName == "Files.exe") + // Is it Files's file window? + try { - // Is it Files's file window? - try + var automation = new UIA3Automation(); + var Files = automation.FromHandle(foreground); + var lowerFilesName = Files.Name.ToLower(); + if (lowerFilesName == "files" || lowerFilesName.Contains("- files")) { - var automation = new UIA3Automation(); - var Files = automation.FromHandle(foreground); - if (Files.Name == "Files" || Files.Name.Contains("- Files")) + lock (_lastExplorerViewLock) { _lastExplorerView = new FilesWindow(foreground, automation, Files); - isExplorer = true; } + isExplorer = true; } - catch (TimeoutException e) - { - Log.Warn(ClassName, $"UIA timeout: {e}"); - } - catch (System.Exception e) - { - Log.Warn(ClassName, $"Failed to bind window: {e}"); - } + } + catch (TimeoutException e) + { + Log.Warn(ClassName, $"UIA timeout: {e}"); + } + catch (System.Exception e) + { + Log.Warn(ClassName, $"Failed to bind window: {e}"); } } return isExplorer; } - private static unsafe string GetProcessPathFromHwnd(HWND hWnd) + public static unsafe string GetProcessPathFromHwnd(HWND hWnd) { uint pid; var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid); From 08f9b68f960d5a5da26dce9bd70802c8db4c64fc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 22:19:30 +0800 Subject: [PATCH 167/243] Improve code quality --- .../QuickSwitch/Models/FilesExplorer.cs | 31 +-------------- Flow.Launcher.Infrastructure/Win32Helper.cs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs index aedbaaff84d..4339864b345 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs @@ -5,10 +5,7 @@ using FlaUI.UIA3; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.QuickSwitch.Interface; -using Microsoft.Win32.SafeHandles; -using Windows.Win32; using Windows.Win32.Foundation; -using Windows.Win32.System.Threading; namespace Flow.Launcher.Infrastructure.QuickSwitch.Models { @@ -29,7 +26,7 @@ public bool CheckExplorerWindow(HWND foreground) { var isExplorer = false; // Is it from Files? - var processName = Path.GetFileName(GetProcessPathFromHwnd(foreground)); + var processName = Win32Helper.GetProcessPathFromHwnd(foreground); if (processName.ToLower() == "files.exe") { // Is it Files's file window? @@ -59,32 +56,6 @@ public bool CheckExplorerWindow(HWND foreground) return isExplorer; } - public static unsafe string GetProcessPathFromHwnd(HWND hWnd) - { - uint pid; - var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid); - if (threadId == 0) return string.Empty; - - var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); - if (process.Value != IntPtr.Zero) - { - using var safeHandle = new SafeProcessHandle(process.Value, true); - uint capacity = 2000; - Span buffer = new char[capacity]; - fixed (char* pBuffer = buffer) - { - if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity)) - { - return string.Empty; - } - - return buffer[..(int)capacity].ToString(); - } - } - - return string.Empty; - } - public string GetExplorerPath() { if (_lastExplorerView == null) return null; diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 5251caab4cb..0f66fa9e094 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -13,9 +14,11 @@ using System.Windows.Media; using Flow.Launcher.Infrastructure.UserSettings; using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; +using Windows.Win32.System.Threading; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; using Point = System.Windows.Point; @@ -792,5 +795,40 @@ public static unsafe bool GetWindowRect(nint handle, out Rect outRect) } #endregion + + #region Window Process + + internal static unsafe string GetProcessNameFromHwnd(HWND hWnd) + { + return Path.GetFileName(GetProcessPathFromHwnd(hWnd)); + } + + internal static unsafe string GetProcessPathFromHwnd(HWND hWnd) + { + uint pid; + var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid); + if (threadId == 0) return string.Empty; + + var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + if (process.Value != IntPtr.Zero) + { + using var safeHandle = new SafeProcessHandle(process.Value, true); + uint capacity = 2000; + Span buffer = new char[capacity]; + fixed (char* pBuffer = buffer) + { + if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity)) + { + return string.Empty; + } + + return buffer[..(int)capacity].ToString(); + } + } + + return string.Empty; + } + + #endregion } } From 69d8b122da8380b2db5b2def37ddccd399b140bf Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 22:22:15 +0800 Subject: [PATCH 168/243] Add process name check for Windows Explorer --- .../QuickSwitch/Models/WindowsExplorer.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index d8252baa3bd..b85a95a3caa 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -21,35 +21,37 @@ internal class WindowsExplorer : IQuickSwitchExplorer public bool CheckExplorerWindow(HWND foreground) { var isExplorer = false; - lock (_lastExplorerViewLock) + // Is it from Explorer? + var processName = Win32Helper.GetProcessNameFromHwnd(foreground); + if (processName.ToLower() == "explorer.exe") { EnumerateShellWindows((shellWindow) => { try { - if (shellWindow is not IWebBrowser2 explorer) - { - return; - } + if (shellWindow is not IWebBrowser2 explorer) return true; + + if (explorer.HWND != foreground.Value) return true; - if (explorer.HWND != foreground.Value) + lock (_lastExplorerViewLock) { - return; + _lastExplorerView = explorer; } - - _lastExplorerView = explorer; isExplorer = true; + return false; } catch (COMException) { // Ignored } + + return true; }); } return isExplorer; } - private static unsafe void EnumerateShellWindows(Action action) + private static unsafe void EnumerateShellWindows(Func action) { // Create an instance of ShellWindows var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass @@ -70,7 +72,10 @@ private static unsafe void EnumerateShellWindows(Action action) var count = shellWindows.Count; for (var i = 0; i < count; i++) { - action(shellWindows.Item(i)); + if (!action(shellWindows.Item(i))) + { + return; + } } } From b232ff1ac732f636bf4e87dad23e3177484596f4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 10:38:07 +0800 Subject: [PATCH 169/243] Revert InitializePosition change --- Flow.Launcher/MainWindow.xaml.cs | 70 ++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 421efac8fef..43c42847f39 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -106,7 +106,7 @@ private void OnSourceInitialized(object sender, EventArgs e) Win32Helper.DisableControlBox(this); } - private async void OnLoaded(object sender, RoutedEventArgs e) + private void OnLoaded(object sender, RoutedEventArgs e) { // Check first launch if (_settings.FirstLaunch) @@ -715,38 +715,46 @@ private async Task PositionResetAsync() private void InitializePosition() { - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) - { - Top = _settings.WindowTop; - Left = _settings.WindowLeft; - } - else + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePositionInner(); + InitializePositionInner(); + return; + + void InitializePositionInner() { - var screen = SelectedScreen(); - switch (_settings.SearchWindowAlign) + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { - case SearchWindowAligns.Center: - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - break; - case SearchWindowAligns.CenterTop: - Left = HorizonCenter(screen); - Top = 10; - break; - case SearchWindowAligns.LeftTop: - Left = HorizonLeft(screen); - Top = 10; - break; - case SearchWindowAligns.RightTop: - Left = HorizonRight(screen); - Top = 10; - break; - case SearchWindowAligns.Custom: - Left = Win32Helper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = Win32Helper.TransformPixelsToDIP(this, 0, - screen.WorkingArea.Y + _settings.CustomWindowTop).Y; - break; + Top = _settings.WindowTop; + Left = _settings.WindowLeft; + } + else + { + var screen = SelectedScreen(); + switch (_settings.SearchWindowAlign) + { + case SearchWindowAligns.Center: + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + break; + case SearchWindowAligns.CenterTop: + Left = HorizonCenter(screen); + Top = 10; + break; + case SearchWindowAligns.LeftTop: + Left = HorizonLeft(screen); + Top = 10; + break; + case SearchWindowAligns.RightTop: + Left = HorizonRight(screen); + Top = 10; + break; + case SearchWindowAligns.Custom: + Left = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; + Top = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + break; + } } } } From 60bbf758c3a75be6e92ede882575b8f49b963756 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:25:36 +0800 Subject: [PATCH 170/243] Fix build issue --- 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 efd5f17b0ff..f68bb238f13 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1323,7 +1323,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var currentIsHomeQuery = query.RawQuery == string.Empty; // Do not show home page for quick switch window - if (quickSwitch && isHomeQuery) + if (quickSwitch && currentIsHomeQuery) { ClearResults(); return; From 5da96405fb4cbeea0fe5cb5f2010be7890f974cf Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:29:07 +0800 Subject: [PATCH 171/243] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f68bb238f13..4562cada141 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1310,8 +1310,6 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - var quickSwitch = _isQuickSwitch; // save quick switch state - if (query == null) // shortcut expanded { ClearResults(); @@ -1321,9 +1319,10 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); var currentIsHomeQuery = query.RawQuery == string.Empty; + var currentIsQuickSwitch = _isQuickSwitch; // Do not show home page for quick switch window - if (quickSwitch && currentIsHomeQuery) + if (currentIsQuickSwitch && currentIsHomeQuery) { ClearResults(); return; @@ -1361,7 +1360,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } else { - plugins = PluginManager.ValidPluginsForQuery(query, quickSwitch); + plugins = PluginManager.ValidPluginsForQuery(query, currentIsQuickSwitch); if (plugins.Count == 1) { @@ -1467,7 +1466,7 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - if (quickSwitch) + if (currentIsQuickSwitch) { var results = await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token); From fcf6194a182a6d54142ce8e420e7cc369b19a1a3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:29:39 +0800 Subject: [PATCH 172/243] Improve code quality --- 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 4562cada141..faec010d1d9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1322,7 +1322,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var currentIsQuickSwitch = _isQuickSwitch; // Do not show home page for quick switch window - if (currentIsQuickSwitch && currentIsHomeQuery) + if (currentIsHomeQuery && currentIsQuickSwitch) { ClearResults(); return; From 0e74643a46d0f4ff56569e955763ab9acf5394e6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:41:46 +0800 Subject: [PATCH 173/243] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 89 ++++++------------------ 1 file changed, 21 insertions(+), 68 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index faec010d1d9..0f7b957bf61 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -263,7 +263,7 @@ public void RegisterResultsUpdatedEvent() else { // make a clone to avoid possible issue that plugin will also change the list and items when updating view model - resultsCopy = DeepCloneResults(e.Results, token); + resultsCopy = DeepCloneResults(e.Results, false, token); } foreach (var result in resultsCopy) @@ -505,39 +505,31 @@ private async Task OpenResultAsync(string index) } } - private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, bool isQuickSwitch, CancellationToken token = default) { var resultsCopy = new List(); - foreach (var result in results.ToList()) + if (isQuickSwitch) { - if (token.IsCancellationRequested) + foreach (var result in results.ToList()) { - break; - } + if (token.IsCancellationRequested) break; - var resultCopy = result.Clone(); - resultsCopy.Add(resultCopy); + var resultCopy = ((QuickSwitchResult)result).Clone(); + resultsCopy.Add(resultCopy); + } } - - return resultsCopy; - } - - private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) - { - var resultsCopy = new List(); - - foreach (var result in results.ToList()) + else { - if (token.IsCancellationRequested) + foreach (var result in results.ToList()) { - break; - } + if (token.IsCancellationRequested) break; - var resultCopy = result.Clone(); - resultsCopy.Add(resultCopy); + var resultCopy = result.Clone(); + resultsCopy.Add(resultCopy); + } } - + return resultsCopy; } @@ -1466,51 +1458,12 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - if (currentIsQuickSwitch) - { - var results = await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token); - - if (token.IsCancellationRequested) return; - - IReadOnlyList resultsCopy; - if (results == null) - { - resultsCopy = _emptyQuickSwitchResult; - } - else - { - // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, token); - } - - foreach (var result in resultsCopy) - { - if (string.IsNullOrEmpty(result.BadgeIcoPath)) - { - result.BadgeIcoPath = plugin.Metadata.IcoPath; - } - } - - if (token.IsCancellationRequested) return; - - App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>"); - - // Indicate if to clear existing results so to show only ones from plugins with action keywords - var shouldClearExistingResults = ShouldClearExistingResults(query, currentIsHomeQuery); - _lastQuery = query; - _previousIsHomeQuery = currentIsHomeQuery; - - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - token, reSelect, shouldClearExistingResults))) - { - App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); - } - } - else { - var results = currentIsHomeQuery ? - await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : - await PluginManager.QueryForPluginAsync(plugin, query, token); + IReadOnlyList results = currentIsQuickSwitch ? + await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) : + currentIsHomeQuery ? + await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : + await PluginManager.QueryForPluginAsync(plugin, query, token); if (token.IsCancellationRequested) return; @@ -1522,7 +1475,7 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, token); + resultsCopy = DeepCloneResults(results, currentIsQuickSwitch, token); } foreach (var result in resultsCopy) From 2b4f777b4593ab59c1c9bc116e519b046ee8de8f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:48:00 +0800 Subject: [PATCH 174/243] Clear possible home page results --- Flow.Launcher/ViewModel/MainViewModel.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0f7b957bf61..089b58a0dd9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1813,7 +1813,7 @@ public async Task SetupQuickSwitchAsync(nint handle) (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); - _ = ResetWindowAsync(); + _ = ResetWindowAsync(true); } } else @@ -1824,14 +1824,14 @@ public async Task SetupQuickSwitchAsync(nint handle) if (dialogWindowHandleChanged) { - _ = ResetWindowAsync(); + _ = ResetWindowAsync(true); } } else { if (dialogWindowHandleChanged) { - _ = ResetWindowAsync(); + _ = ResetWindowAsync(true); } } } @@ -1933,7 +1933,7 @@ public void HideQuickSwitch() } // Reset index & preview & selected results & query text - private async Task ResetWindowAsync() + private async Task ResetWindowAsync(bool clearResults = false) { lastHistoryIndex = 1; @@ -1948,6 +1948,11 @@ private async Task ResetWindowAsync() } await ChangeQueryTextAsync(string.Empty); + + if (clearResults) + { + ClearResults(); + } } #endregion From 4676234b76faf4fecc3d46064167f6143c8f7580 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:53:06 +0800 Subject: [PATCH 175/243] Use requery instead of clear results --- Flow.Launcher/ViewModel/MainViewModel.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 089b58a0dd9..2aecf551b90 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1813,7 +1813,7 @@ public async Task SetupQuickSwitchAsync(nint handle) (Application.Current?.MainWindow as MainWindow).UpdatePosition(); }); - _ = ResetWindowAsync(true); + _ = ResetWindowAsync(); } } else @@ -1824,14 +1824,14 @@ public async Task SetupQuickSwitchAsync(nint handle) if (dialogWindowHandleChanged) { - _ = ResetWindowAsync(true); + _ = ResetWindowAsync(); } } else { if (dialogWindowHandleChanged) { - _ = ResetWindowAsync(true); + _ = ResetWindowAsync(); } } } @@ -1933,7 +1933,7 @@ public void HideQuickSwitch() } // Reset index & preview & selected results & query text - private async Task ResetWindowAsync(bool clearResults = false) + private async Task ResetWindowAsync() { lastHistoryIndex = 1; @@ -1947,12 +1947,7 @@ private async Task ResetWindowAsync(bool clearResults = false) SelectedResults = Results; } - await ChangeQueryTextAsync(string.Empty); - - if (clearResults) - { - ClearResults(); - } + await ChangeQueryTextAsync(string.Empty, true); } #endregion From cd49464ac8d7a024aa4fff4ece42d4c2ceac376a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:53:53 +0800 Subject: [PATCH 176/243] Make clear results local & Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 98 ++++++++++++------------ 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2aecf551b90..7780e93e856 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1441,6 +1441,23 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } // Local function + void ClearResults() + { + App.API.LogDebug(ClassName, $"Clear query results"); + + // Hide and clear results again because running query may show and add some results + Results.Visibility = Visibility.Collapsed; + Results.Clear(); + + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; + } + async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>"); @@ -1458,48 +1475,46 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - { - IReadOnlyList results = currentIsQuickSwitch ? - await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) : - currentIsHomeQuery ? - await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : - await PluginManager.QueryForPluginAsync(plugin, query, token); + IReadOnlyList results = currentIsQuickSwitch ? + await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) : + currentIsHomeQuery ? + await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : + await PluginManager.QueryForPluginAsync(plugin, query, token); - if (token.IsCancellationRequested) return; + if (token.IsCancellationRequested) return; - IReadOnlyList resultsCopy; - if (results == null) - { - resultsCopy = _emptyResult; - } - else - { - // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, currentIsQuickSwitch, token); - } + IReadOnlyList resultsCopy; + if (results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. + resultsCopy = DeepCloneResults(results, currentIsQuickSwitch, token); + } - foreach (var result in resultsCopy) + foreach (var result in resultsCopy) + { + if (string.IsNullOrEmpty(result.BadgeIcoPath)) { - if (string.IsNullOrEmpty(result.BadgeIcoPath)) - { - result.BadgeIcoPath = plugin.Metadata.IcoPath; - } + result.BadgeIcoPath = plugin.Metadata.IcoPath; } + } - if (token.IsCancellationRequested) return; + if (token.IsCancellationRequested) return; - App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>"); + App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>"); - // Indicate if to clear existing results so to show only ones from plugins with action keywords - var shouldClearExistingResults = ShouldClearExistingResults(query, currentIsHomeQuery); - _lastQuery = query; - _previousIsHomeQuery = currentIsHomeQuery; + // Indicate if to clear existing results so to show only ones from plugins with action keywords + var shouldClearExistingResults = ShouldClearExistingResults(query, currentIsHomeQuery); + _lastQuery = query; + _previousIsHomeQuery = currentIsHomeQuery; - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - token, reSelect, shouldClearExistingResults))) - { - App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); - } + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, + token, reSelect, shouldClearExistingResults))) + { + App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } } @@ -1522,23 +1537,6 @@ void QueryHistoryTask(CancellationToken token) } } - private void ClearResults() - { - App.API.LogDebug(ClassName, $"Clear query results"); - - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); - - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; - } - private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) { From a20befd369c740398b45d1c0038e31a285b54ec2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 10 May 2025 21:56:14 +0800 Subject: [PATCH 177/243] Remove blank lines --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7780e93e856..e8ce401c550 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1503,14 +1503,14 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : } if (token.IsCancellationRequested) return; - + App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>"); // Indicate if to clear existing results so to show only ones from plugins with action keywords var shouldClearExistingResults = ShouldClearExistingResults(query, currentIsHomeQuery); _lastQuery = query; _previousIsHomeQuery = currentIsHomeQuery; - + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, token, reSelect, shouldClearExistingResults))) { From 50f42a89b26b472aab2059300e3e8279c4bbcad3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 11 May 2025 09:13:07 +0800 Subject: [PATCH 178/243] Revert changes in FileExplorerHelper.cs --- .../FileExplorerHelper.cs | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index be0c278ad66..1085cc83313 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -9,10 +13,9 @@ public static class FileExplorerHelper /// public static string GetActiveExplorerPath() { - var explorerPath = QuickSwitch.QuickSwitch.GetActiveExplorerPath(); - return !string.IsNullOrEmpty(explorerPath) ? - GetDirectoryPath(new Uri(explorerPath).LocalPath) : - null; + var explorerWindow = GetActiveExplorer(); + string locationUrl = explorerWindow?.LocationURL; + return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null; } /// @@ -27,5 +30,67 @@ private static string GetDirectoryPath(string path) return path; } + + /// + /// Gets the file explorer that is currently in the foreground + /// + private static dynamic GetActiveExplorer() + { + Type type = Type.GetTypeFromProgID("Shell.Application"); + if (type == null) return null; + dynamic shell = Activator.CreateInstance(type); + if (shell == null) + { + return null; + } + + var explorerWindows = new List(); + var openWindows = shell.Windows(); + for (int i = 0; i < openWindows.Count; i++) + { + var window = openWindows.Item(i); + if (window == null) continue; + + // find the desired window and make sure that it is indeed a file explorer + // we don't want the Internet Explorer or the classic control panel + // ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE" + if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe") + { + explorerWindows.Add(window); + } + } + + if (explorerWindows.Count == 0) return null; + + var zOrders = GetZOrder(explorerWindows); + + return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; + } + + /// + /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. + /// + private static IEnumerable GetZOrder(List hWnds) + { + var z = new int[hWnds.Count]; + for (var i = 0; i < hWnds.Count; i++) z[i] = -1; + + var index = 0; + var numRemaining = hWnds.Count; + PInvoke.EnumWindows((wnd, _) => + { + var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd); + if (searchIndex != -1) + { + z[searchIndex] = index; + numRemaining--; + if (numRemaining == 0) return false; + } + index++; + return true; + }, IntPtr.Zero); + + return z; + } } } From d44f6e96d000ebb3b84748f7ff767a9730a91ef7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 11 May 2025 09:15:28 +0800 Subject: [PATCH 179/243] Remove Files support --- .../QuickSwitch/Models/FilesExplorer.cs | 171 ------------------ .../QuickSwitch/QuickSwitch.cs | 3 +- 2 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs deleted file mode 100644 index 4339864b345..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/FilesExplorer.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using FlaUI.Core.AutomationElements; -using FlaUI.UIA3; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; -using Windows.Win32.Foundation; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Models -{ - /// - /// Class for handling Files instances in QuickSwitch. - /// - /// - /// Edited from: https://github.com/files-community/Listary.FileAppPlugin.Files - /// - internal class FilesExplorer : IQuickSwitchExplorer - { - private static readonly string ClassName = nameof(FilesExplorer); - - private static FilesWindow _lastExplorerView = null; - private static readonly object _lastExplorerViewLock = new(); - - public bool CheckExplorerWindow(HWND foreground) - { - var isExplorer = false; - // Is it from Files? - var processName = Win32Helper.GetProcessPathFromHwnd(foreground); - if (processName.ToLower() == "files.exe") - { - // Is it Files's file window? - try - { - var automation = new UIA3Automation(); - var Files = automation.FromHandle(foreground); - var lowerFilesName = Files.Name.ToLower(); - if (lowerFilesName == "files" || lowerFilesName.Contains("- files")) - { - lock (_lastExplorerViewLock) - { - _lastExplorerView = new FilesWindow(foreground, automation, Files); - } - isExplorer = true; - } - } - catch (TimeoutException e) - { - Log.Warn(ClassName, $"UIA timeout: {e}"); - } - catch (System.Exception e) - { - Log.Warn(ClassName, $"Failed to bind window: {e}"); - } - } - return isExplorer; - } - - public string GetExplorerPath() - { - if (_lastExplorerView == null) return null; - return _lastExplorerView.GetCurrentTab().GetCurrentFolder(); - } - - public void RemoveExplorerWindow() - { - lock (_lastExplorerViewLock) - { - _lastExplorerView = null; - } - } - - public void Dispose() - { - // Release ComObjects - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - _lastExplorerView.Dispose(); - _lastExplorerView = null; - } - } - } - catch (COMException) - { - _lastExplorerView = null; - } - } - - private class FilesWindow : IDisposable - { - private readonly UIA3Automation _automation; - private readonly AutomationElement _Files; - - public IntPtr Handle { get; } - - public FilesWindow(IntPtr hWnd, UIA3Automation automation, AutomationElement Files) - { - Handle = hWnd; - _automation = automation; - _Files = Files; - } - - public void Dispose() - { - _automation.Dispose(); - } - - public FilesTab GetCurrentTab() - { - return new FilesTab(_Files); - } - } - - private class FilesTab - { - private readonly TextBox _currentPathGet; - private readonly TextBox _currentPathSet; - - public FilesTab(AutomationElement Files) - { - // Find window content to reduce the scope - var _windowContent = Files.FindFirstChild(cf => cf.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge")); - - _currentPathGet = _windowContent.FindFirstChild(cf => cf.ByAutomationId("CurrentPathGet"))?.AsTextBox(); - if (_currentPathGet == null) - { - Log.Error(ClassName, "Failed to find CurrentPathGet"); - return; - } - - _currentPathSet = _windowContent.FindFirstChild(cf => cf.ByAutomationId("CurrentPathSet"))?.AsTextBox(); - if (_currentPathSet == null) - { - Log.Error(ClassName, "Failed to find CurrentPathSet"); - return; - } - } - - public string GetCurrentFolder() - { - try - { - return _currentPathGet.Text; - } - catch (System.Exception e) - { - Log.Error(ClassName, $"Failed to get current folder: {e}"); - return null; - } - } - - public bool OpenFolder(string path) - { - try - { - _currentPathSet.Text = path; - return true; - } - catch (System.Exception e) - { - Log.Error(ClassName, $"Failed to get current folder: {e}"); - return false; - } - } - } - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index a288d7404de..5fc818a2a61 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -42,8 +42,7 @@ public static class QuickSwitch private static readonly List _quickSwitchExplorers = new() { - new WindowsExplorer(), - new FilesExplorer() + new WindowsExplorer() }; private static IQuickSwitchExplorer _lastExplorer = null; From 45ac0af102aa707a3e5c4ab6c6e26c18725c257d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 12 May 2025 19:22:03 +0800 Subject: [PATCH 180/243] Remove unused project --- .../Flow.Launcher.Infrastructure.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index dd4ed09d43d..dd2d71e28ec 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -55,8 +55,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive From 52a51cf5c913b206c06f9b89465935c55298bac8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 7 Jun 2025 17:02:49 +0800 Subject: [PATCH 181/243] Fix build issue --- .../QuickSwitch/Models/WindowsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index ed0d5a56265..c19f2729d6e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -178,7 +178,7 @@ public bool JumpFolder(string path, bool auto) var timeOut = !SpinWait.SpinUntil(() => { - var style = PInvoke.GetWindowLong(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; }, 1000); if (timeOut) From d59ad87fe76ee1d43ce1115d9621575acabb744a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 7 Jun 2025 17:10:56 +0800 Subject: [PATCH 182/243] Add null checks --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 16f71651577..c666630be05 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1859,7 +1859,7 @@ public async Task SetupQuickSwitchAsync(nint handle) // Only update the position Application.Current?.Dispatcher.Invoke(() => { - (Application.Current?.MainWindow as MainWindow).UpdatePosition(); + (Application.Current?.MainWindow as MainWindow)?.UpdatePosition(); }); _ = ResetWindowAsync(); @@ -1950,7 +1950,7 @@ public async void ResetQuickSwitch() // Only update the position Application.Current?.Dispatcher.Invoke(() => { - (Application.Current?.MainWindow as MainWindow).UpdatePosition(); + (Application.Current?.MainWindow as MainWindow)?.UpdatePosition(); }); _ = ResetWindowAsync(); From 38442fddee39b6780194530594ecefbe64afa42b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 9 Jun 2025 19:18:54 +0800 Subject: [PATCH 183/243] Fix build issue --- Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index bfdeb0dbf53..38391f77865 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -270,7 +270,8 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { var isMedia = IsMedia(Path.GetExtension(filePath)); - var title = Path.GetFileName(filePath); + var title = Path.GetFileName(filePath) ?? string.Empty; + var directory = Path.GetDirectoryName(filePath) ?? string.Empty; /* Preview Detail */ From 94a29cb66ad99dde53585dd7960a2b67f3def2a4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Jun 2025 14:36:57 +0800 Subject: [PATCH 184/243] Mark automatically quick switch feature as experimental --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index b3b4354ef72..d15674d94a2 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -319,7 +319,7 @@ Quick Switch Quickly navigate to the path of the current file manager when a file dialog is opened. Quick Switch Automatically - Automatically navigate to the path of the current file manager when a file dialog is opened. (It can possibly cause dialog apps force close) + Automatically navigate to the path of the current file manager when a file dialog is opened. (Experimental) Show Quick Switch Window Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). Quick Switch Window Position From 577ba1be1b063ce1cfd55a4007af41f269145ee9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Jun 2025 14:37:35 +0800 Subject: [PATCH 185/243] Do not show quick switch window by default --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index c158b117b95..0d81f29d8f2 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -296,7 +296,7 @@ public CustomBrowserViewModel CustomBrowser public bool AutoQuickSwitch { get; set; } = false; - public bool ShowQuickSwitchWindow { get; set; } = true; + public bool ShowQuickSwitchWindow { get; set; } = false; [JsonConverter(typeof(JsonStringEnumConverter))] public QuickSwitchWindowPositions QuickSwitchWindowPosition { get; set; } = QuickSwitchWindowPositions.UnderDialog; From 3dd98ad2731426ed147cd694339cae9f7c070890 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Jun 2025 14:39:47 +0800 Subject: [PATCH 186/243] Fix typos --- Flow.Launcher/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 57daa03ae3e..eb54b408b1e 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -814,7 +814,7 @@ private void InitializeContextMenu() public void UpdatePosition() { - // Intialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 if (_viewModel.IsQuickSwitchWindowUnderDialog()) { InitializeQuickSwitchPosition(); From cfbfb7b1b0fef3b787788124ffe84120371dce43 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Jun 2025 14:51:07 +0800 Subject: [PATCH 187/243] Remove files --- .../QuickSwitch/Models/WindowsDialog.cs | 355 ------------------ .../QuickSwitch/Models/WindowsExplorer.cs | 167 -------- 2 files changed, 522 deletions(-) delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs deleted file mode 100644 index c19f2729d6e..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Threading; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; -using WindowsInput; -using WindowsInput.Native; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Models -{ - /// - /// Class for handling Windows File Dialog instances in QuickSwitch. - /// - internal class WindowsDialog : IQuickSwitchDialog - { - public IQuickSwitchDialogWindow DialogWindow { get; private set; } - - private const string WindowsDialogClassName = "#32770"; - - public bool CheckDialogWindow(HWND hwnd) - { - // Has it been checked? - if (DialogWindow != null && DialogWindow.Handle == hwnd) - { - return true; - } - - // Is it a Win32 dialog box? - if (GetClassName(hwnd) == WindowsDialogClassName) - { - // Is it a windows file dialog? - var dialogType = GetFileDialogType(hwnd); - if (dialogType != DialogType.Others) - { - DialogWindow = new WindowsDialogWindow(hwnd, dialogType); - - return true; - } - } - return false; - } - - public void Dispose() - { - DialogWindow?.Dispose(); - DialogWindow = null; - } - - #region Help Methods - - private static unsafe string GetClassName(HWND handle) - { - fixed (char* buf = new char[256]) - { - return PInvoke.GetClassName(handle, buf, 256) switch - { - 0 => string.Empty, - _ => new string(buf), - }; - } - } - - private static DialogType GetFileDialogType(HWND handle) - { - // Is it a Windows Open file dialog? - var fileEditor = PInvoke.GetDlgItem(handle, 0x047C); - if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open; - - // Is it a Windows Save or Save As file dialog? - fileEditor = PInvoke.GetDlgItem(handle, 0x0000); - if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs; - - return DialogType.Others; - } - - #endregion - } - - internal class WindowsDialogWindow : IQuickSwitchDialogWindow - { - public HWND Handle { get; private set; } = HWND.Null; - - // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore - // So we need to cache the current tab and use the original handle - private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null; - - private readonly DialogType _dialogType; - - public WindowsDialogWindow(HWND handle, DialogType dialogType) - { - Handle = handle; - _dialogType = dialogType; - } - - public IQuickSwitchDialogWindowTab GetCurrentTab() - { - return _currentTab ??= new WindowsDialogTab(Handle, _dialogType); - } - - public void Dispose() - { - Handle = HWND.Null; - } - } - - internal class WindowsDialogTab : IQuickSwitchDialogWindowTab - { - #region Public Properties - - public HWND Handle { get; private set; } = HWND.Null; - - #endregion - - #region Private Fields - - private static readonly string ClassName = nameof(WindowsDialogTab); - - private static readonly InputSimulator _inputSimulator = new(); - - private readonly DialogType _dialogType; - - private bool _legacy { get; set; } = false; - private HWND _pathControl { get; set; } = HWND.Null; - private HWND _pathEditor { get; set; } = HWND.Null; - private HWND _fileEditor { get; set; } = HWND.Null; - private HWND _openButton { get; set; } = HWND.Null; - - #endregion - - #region Constructor - - public WindowsDialogTab(HWND handle, DialogType dialogType) - { - Handle = handle; - _dialogType = dialogType; - Log.Debug(ClassName, $"File dialog type: {dialogType}"); - } - - #endregion - - #region Public Methods - - public string GetCurrentFolder() - { - if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; - return GetWindowText(_pathEditor); - } - - public string GetCurrentFile() - { - if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; - return GetWindowText(_fileEditor); - } - - public bool JumpFolder(string path, bool auto) - { - if (auto) - { - // Use legacy jump folder method for auto quick switch because file editor is default value. - // After setting path using file editor, we do not need to revert its value. - return JumpFolderWithFileEditor(path, false); - } - - // Alt-D or Ctrl-L to focus on the path input box - // "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus - _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); - // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); - - if (_pathControl.IsNull && !GetPathControlEditor()) - { - // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 - // The dialog is a legacy one, so we can only edit file editor directly. - Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method"); - return JumpFolderWithFileEditor(path, true); - } - - var timeOut = !SpinWait.SpinUntil(() => - { - var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; - }, 1000); - if (timeOut) - { - // Path control is not visible, so we can only edit file editor directly. - Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method"); - return JumpFolderWithFileEditor(path, true); - } - - if (_pathEditor.IsNull) - { - // Path editor cannot be found, so we can only edit file editor directly. - Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); - return JumpFolderWithFileEditor(path, true); - } - SetWindowText(_pathEditor, path); - - _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); - - return true; - } - - public bool JumpFile(string path) - { - if (_fileEditor.IsNull && !GetFileEditor()) return false; - SetWindowText(_fileEditor, path); - - return true; - } - - public bool Open() - { - if (_openButton.IsNull && !GetOpenButton()) return false; - PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); - - return true; - } - - public void Dispose() - { - Handle = HWND.Null; - } - - #endregion - - #region Helper Methods - - #region Get Handles - - private bool GetPathControlEditor() - { - // Get the handle of the path editor - // Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control - _pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000 - _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 - _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 - _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000 - _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205 - if (_pathControl == HWND.Null) - { - _pathEditor = HWND.Null; - _legacy = true; - Log.Info(ClassName, "Legacy dialog"); - } - else - { - _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox - _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit - if (_pathEditor == HWND.Null) - { - _legacy = true; - Log.Error(ClassName, "Failed to find path editor handle"); - } - } - - return !_legacy; - } - - private bool GetFileEditor() - { - if (_dialogType == DialogType.Open) - { - // Get the handle of the file name editor of Open file dialog - _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit - } - else - { - // Get the handle of the file name editor of Save / SaveAs file dialog - _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox - _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit - } - - if (_fileEditor == HWND.Null) - { - Log.Error(ClassName, "Failed to find file name editor handle"); - return false; - } - - return true; - } - - private bool GetOpenButton() - { - // Get the handle of the open button - _openButton = PInvoke.GetDlgItem(Handle, 0x0001); // Open/Save/SaveAs Button - if (_openButton == HWND.Null) - { - Log.Error(ClassName, "Failed to find open button handle"); - return false; - } - - return true; - } - - #endregion - - #region Windows Text - - private static unsafe string GetWindowText(HWND handle) - { - int length; - Span buffer = stackalloc char[1000]; - fixed (char* pBuffer = buffer) - { - // If the control has no title bar or text, or if the control handle is invalid, the return value is zero. - length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer); - } - - return buffer[..length].ToString(); - } - - private static unsafe nint SetWindowText(HWND handle, string text) - { - fixed (char* textPtr = text + '\0') - { - return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; - } - } - - #endregion - - #region Legacy Jump Folder - - private bool JumpFolderWithFileEditor(string path, bool resetFocus) - { - // For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors. - if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false; - - if (_fileEditor.IsNull && !GetFileEditor()) return false; - SetWindowText(_fileEditor, path); - - if (_openButton.IsNull && !GetOpenButton()) return false; - PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); - - return true; - } - - #endregion - - #endregion - } - - internal enum DialogType - { - Others, - Open, - SaveOrSaveAs - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs deleted file mode 100644 index b85a95a3caa..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.System.Com; -using Windows.Win32.UI.Shell; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Models -{ - /// - /// Class for handling Windows Explorer instances in QuickSwitch. - /// - internal class WindowsExplorer : IQuickSwitchExplorer - { - private static readonly string ClassName = nameof(WindowsExplorer); - - private static IWebBrowser2 _lastExplorerView = null; - private static readonly object _lastExplorerViewLock = new(); - - public bool CheckExplorerWindow(HWND foreground) - { - var isExplorer = false; - // Is it from Explorer? - var processName = Win32Helper.GetProcessNameFromHwnd(foreground); - if (processName.ToLower() == "explorer.exe") - { - EnumerateShellWindows((shellWindow) => - { - try - { - if (shellWindow is not IWebBrowser2 explorer) return true; - - if (explorer.HWND != foreground.Value) return true; - - lock (_lastExplorerViewLock) - { - _lastExplorerView = explorer; - } - isExplorer = true; - return false; - } - catch (COMException) - { - // Ignored - } - - return true; - }); - } - return isExplorer; - } - - private static unsafe void EnumerateShellWindows(Func action) - { - // Create an instance of ShellWindows - var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass - var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows - - var result = PInvoke.CoCreateInstance( - &clsidShellWindows, - null, - CLSCTX.CLSCTX_ALL, - &iidIShellWindows, - out var shellWindowsObj); - - if (result.Failed) return; - - var shellWindows = (IShellWindows)shellWindowsObj; - - // Enumerate the shell windows - var count = shellWindows.Count; - for (var i = 0; i < count; i++) - { - if (!action(shellWindows.Item(i))) - { - return; - } - } - } - - public string GetExplorerPath() - { - if (_lastExplorerView == null) return null; - - object document = null; - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; - } - } - } - catch (COMException) - { - return null; - } - - if (document is not IShellFolderViewDual2 folderView) - { - return null; - } - - string path; - try - { - // CSWin32 Folder does not have Self, so we need to use dynamic type here - // Use dynamic to bypass static typing - dynamic folder = folderView.Folder; - - // Access the Self property via dynamic binding - dynamic folderItem = folder.Self; - - // Check if the item is part of the file system - if (folderItem != null && folderItem.IsFileSystem) - { - path = folderItem.Path; - } - else - { - // Handle non-file system paths (e.g., virtual folders) - path = string.Empty; - } - } - catch - { - return null; - } - - return path; - } - - public void RemoveExplorerWindow() - { - lock (_lastExplorerViewLock) - { - _lastExplorerView = null; - } - } - - public void Dispose() - { - // Release ComObjects - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - Marshal.ReleaseComObject(_lastExplorerView); - _lastExplorerView = null; - } - } - } - catch (COMException) - { - _lastExplorerView = null; - } - } - } -} From 6b7a70a87d658103b52c2e417e0d2d41837c9cac Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Jun 2025 14:52:18 +0800 Subject: [PATCH 188/243] Implement window dialogs & explorer Co-authored-by: idkidknow --- .../QuickSwitch/Models/WindowsDialog.cs | 355 ++++++++++++++++++ .../QuickSwitch/Models/WindowsExplorer.cs | 167 ++++++++ 2 files changed, 522 insertions(+) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs new file mode 100644 index 00000000000..c19f2729d6e --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -0,0 +1,355 @@ +using System; +using System.Threading; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WindowsInput; +using WindowsInput.Native; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +{ + /// + /// Class for handling Windows File Dialog instances in QuickSwitch. + /// + internal class WindowsDialog : IQuickSwitchDialog + { + public IQuickSwitchDialogWindow DialogWindow { get; private set; } + + private const string WindowsDialogClassName = "#32770"; + + public bool CheckDialogWindow(HWND hwnd) + { + // Has it been checked? + if (DialogWindow != null && DialogWindow.Handle == hwnd) + { + return true; + } + + // Is it a Win32 dialog box? + if (GetClassName(hwnd) == WindowsDialogClassName) + { + // Is it a windows file dialog? + var dialogType = GetFileDialogType(hwnd); + if (dialogType != DialogType.Others) + { + DialogWindow = new WindowsDialogWindow(hwnd, dialogType); + + return true; + } + } + return false; + } + + public void Dispose() + { + DialogWindow?.Dispose(); + DialogWindow = null; + } + + #region Help Methods + + private static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) + { + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => string.Empty, + _ => new string(buf), + }; + } + } + + private static DialogType GetFileDialogType(HWND handle) + { + // Is it a Windows Open file dialog? + var fileEditor = PInvoke.GetDlgItem(handle, 0x047C); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open; + + // Is it a Windows Save or Save As file dialog? + fileEditor = PInvoke.GetDlgItem(handle, 0x0000); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs; + + return DialogType.Others; + } + + #endregion + } + + internal class WindowsDialogWindow : IQuickSwitchDialogWindow + { + public HWND Handle { get; private set; } = HWND.Null; + + // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore + // So we need to cache the current tab and use the original handle + private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null; + + private readonly DialogType _dialogType; + + public WindowsDialogWindow(HWND handle, DialogType dialogType) + { + Handle = handle; + _dialogType = dialogType; + } + + public IQuickSwitchDialogWindowTab GetCurrentTab() + { + return _currentTab ??= new WindowsDialogTab(Handle, _dialogType); + } + + public void Dispose() + { + Handle = HWND.Null; + } + } + + internal class WindowsDialogTab : IQuickSwitchDialogWindowTab + { + #region Public Properties + + public HWND Handle { get; private set; } = HWND.Null; + + #endregion + + #region Private Fields + + private static readonly string ClassName = nameof(WindowsDialogTab); + + private static readonly InputSimulator _inputSimulator = new(); + + private readonly DialogType _dialogType; + + private bool _legacy { get; set; } = false; + private HWND _pathControl { get; set; } = HWND.Null; + private HWND _pathEditor { get; set; } = HWND.Null; + private HWND _fileEditor { get; set; } = HWND.Null; + private HWND _openButton { get; set; } = HWND.Null; + + #endregion + + #region Constructor + + public WindowsDialogTab(HWND handle, DialogType dialogType) + { + Handle = handle; + _dialogType = dialogType; + Log.Debug(ClassName, $"File dialog type: {dialogType}"); + } + + #endregion + + #region Public Methods + + public string GetCurrentFolder() + { + if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; + return GetWindowText(_pathEditor); + } + + public string GetCurrentFile() + { + if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; + return GetWindowText(_fileEditor); + } + + public bool JumpFolder(string path, bool auto) + { + if (auto) + { + // Use legacy jump folder method for auto quick switch because file editor is default value. + // After setting path using file editor, we do not need to revert its value. + return JumpFolderWithFileEditor(path, false); + } + + // Alt-D or Ctrl-L to focus on the path input box + // "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + + if (_pathControl.IsNull && !GetPathControlEditor()) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we can only edit file editor directly. + Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + + var timeOut = !SpinWait.SpinUntil(() => + { + var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + // Path control is not visible, so we can only edit file editor directly. + Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + + if (_pathEditor.IsNull) + { + // Path editor cannot be found, so we can only edit file editor directly. + Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + SetWindowText(_pathEditor, path); + + _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + + return true; + } + + public bool JumpFile(string path) + { + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + return true; + } + + public bool Open() + { + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + public void Dispose() + { + Handle = HWND.Null; + } + + #endregion + + #region Helper Methods + + #region Get Handles + + private bool GetPathControlEditor() + { + // Get the handle of the path editor + // Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control + _pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205 + if (_pathControl == HWND.Null) + { + _pathEditor = HWND.Null; + _legacy = true; + Log.Info(ClassName, "Legacy dialog"); + } + else + { + _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox + _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit + if (_pathEditor == HWND.Null) + { + _legacy = true; + Log.Error(ClassName, "Failed to find path editor handle"); + } + } + + return !_legacy; + } + + private bool GetFileEditor() + { + if (_dialogType == DialogType.Open) + { + // Get the handle of the file name editor of Open file dialog + _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit + } + else + { + // Get the handle of the file name editor of Save / SaveAs file dialog + _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit + } + + if (_fileEditor == HWND.Null) + { + Log.Error(ClassName, "Failed to find file name editor handle"); + return false; + } + + return true; + } + + private bool GetOpenButton() + { + // Get the handle of the open button + _openButton = PInvoke.GetDlgItem(Handle, 0x0001); // Open/Save/SaveAs Button + if (_openButton == HWND.Null) + { + Log.Error(ClassName, "Failed to find open button handle"); + return false; + } + + return true; + } + + #endregion + + #region Windows Text + + private static unsafe string GetWindowText(HWND handle) + { + int length; + Span buffer = stackalloc char[1000]; + fixed (char* pBuffer = buffer) + { + // If the control has no title bar or text, or if the control handle is invalid, the return value is zero. + length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer); + } + + return buffer[..length].ToString(); + } + + private static unsafe nint SetWindowText(HWND handle, string text) + { + fixed (char* textPtr = text + '\0') + { + return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; + } + } + + #endregion + + #region Legacy Jump Folder + + private bool JumpFolderWithFileEditor(string path, bool resetFocus) + { + // For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors. + if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false; + + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + #endregion + + #endregion + } + + internal enum DialogType + { + Others, + Open, + SaveOrSaveAs + } +} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs new file mode 100644 index 00000000000..b85a95a3caa --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -0,0 +1,167 @@ +using System; +using System.Runtime.InteropServices; +using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; + +namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +{ + /// + /// Class for handling Windows Explorer instances in QuickSwitch. + /// + internal class WindowsExplorer : IQuickSwitchExplorer + { + private static readonly string ClassName = nameof(WindowsExplorer); + + private static IWebBrowser2 _lastExplorerView = null; + private static readonly object _lastExplorerViewLock = new(); + + public bool CheckExplorerWindow(HWND foreground) + { + var isExplorer = false; + // Is it from Explorer? + var processName = Win32Helper.GetProcessNameFromHwnd(foreground); + if (processName.ToLower() == "explorer.exe") + { + EnumerateShellWindows((shellWindow) => + { + try + { + if (shellWindow is not IWebBrowser2 explorer) return true; + + if (explorer.HWND != foreground.Value) return true; + + lock (_lastExplorerViewLock) + { + _lastExplorerView = explorer; + } + isExplorer = true; + return false; + } + catch (COMException) + { + // Ignored + } + + return true; + }); + } + return isExplorer; + } + + private static unsafe void EnumerateShellWindows(Func action) + { + // Create an instance of ShellWindows + var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass + var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows + + var result = PInvoke.CoCreateInstance( + &clsidShellWindows, + null, + CLSCTX.CLSCTX_ALL, + &iidIShellWindows, + out var shellWindowsObj); + + if (result.Failed) return; + + var shellWindows = (IShellWindows)shellWindowsObj; + + // Enumerate the shell windows + var count = shellWindows.Count; + for (var i = 0; i < count; i++) + { + if (!action(shellWindows.Item(i))) + { + return; + } + } + } + + public string GetExplorerPath() + { + if (_lastExplorerView == null) return null; + + object document = null; + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _lastExplorerView; + document = explorerView.Document; + } + } + } + catch (COMException) + { + return null; + } + + if (document is not IShellFolderViewDual2 folderView) + { + return null; + } + + string path; + try + { + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Check if the item is part of the file system + if (folderItem != null && folderItem.IsFileSystem) + { + path = folderItem.Path; + } + else + { + // Handle non-file system paths (e.g., virtual folders) + path = string.Empty; + } + } + catch + { + return null; + } + + return path; + } + + public void RemoveExplorerWindow() + { + lock (_lastExplorerViewLock) + { + _lastExplorerView = null; + } + } + + public void Dispose() + { + // Release ComObjects + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; + } + } + } + catch (COMException) + { + _lastExplorerView = null; + } + } + } +} From 4a4e24bed3d888276a2bc9d5c55b63c379c805f4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 22 Jun 2025 11:57:17 +0800 Subject: [PATCH 189/243] Added quick switch information --- .../QuickSwitch/Interface/IQuickSwitchDialog.cs | 2 ++ .../QuickSwitch/Interface/IQuickSwitchExplorer.cs | 2 ++ .../QuickSwitch/Models/WindowsDialog.cs | 2 ++ .../QuickSwitch/Models/WindowsExplorer.cs | 2 ++ Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 8 ++++++++ Flow.Launcher/Languages/en.xaml | 2 ++ .../ViewModels/SettingsPaneGeneralViewModel.cs | 6 ++++++ .../SettingPages/Views/SettingsPaneGeneral.xaml | 9 +++++++++ 8 files changed, 33 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs index d2d08dbf510..5de7c8d752e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs @@ -13,6 +13,8 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// internal interface IQuickSwitchDialog : IDisposable { + string Name { get; } + IQuickSwitchDialogWindow DialogWindow { get; } bool CheckDialogWindow(HWND hwnd); diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs index 9bf3d95911f..5d99e8ef96f 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs @@ -13,6 +13,8 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface /// internal interface IQuickSwitchExplorer : IDisposable { + string Name { get; } + bool CheckExplorerWindow(HWND foreground); void RemoveExplorerWindow(); diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index c19f2729d6e..e01786bb974 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -19,6 +19,8 @@ internal class WindowsDialog : IQuickSwitchDialog private const string WindowsDialogClassName = "#32770"; + public string Name => "Windows"; + public bool CheckDialogWindow(HWND hwnd) { // Has it been checked? diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index b85a95a3caa..16132671435 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -18,6 +18,8 @@ internal class WindowsExplorer : IQuickSwitchExplorer private static IWebBrowser2 _lastExplorerView = null; private static readonly object _lastExplorerViewLock = new(); + public string Name => "Windows"; + public bool CheckExplorerWindow(HWND foreground) { var isExplorer = false; diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 5fc818a2a61..26a67e86100 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -81,6 +81,14 @@ public static class QuickSwitch #endregion + #region Supported Explorers & Dialogs Name + + public static string[] SupportedExplorerNames => _quickSwitchExplorers.ConvertAll(explorer => explorer.Name).ToArray(); + + public static string[] SupportedDialogNames => _quickSwitchDialogs.ConvertAll(dialog => dialog.Name).ToArray(); + + #endregion + #region Initialize & Setup public static void InitializeQuickSwitch() diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index d15674d94a2..d0132c34892 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -335,6 +335,8 @@ Fill full path in file name box Fill full path in file name box and open Fill directory in path box + Information for Quick Switch user + Currently Flow supports those file explorers ({0}) and those file dialogs ({1}) HTTP Proxy diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index fa906efa839..48a562cbb3e 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -180,6 +180,11 @@ public class QuickSwitchFileResultBehaviourData : DropdownDataGeneric QuickSwitchFileResultBehaviours { get; } = DropdownDataGeneric.GetValues("QuickSwitchFileResultBehaviour"); + public string QuickSwitchSupportedExplorerDialogMessage => + string.Format(App.API.GetTranslation("QuickSwitchSupportedExplorerDialogMessage"), + string.Join(", ", QuickSwitch.SupportedExplorerNames), + string.Join(", ", QuickSwitch.SupportedDialogNames)); + public int SearchDelayTimeValue { get => Settings.SearchDelayTime; @@ -217,6 +222,7 @@ private void UpdateEnumDropdownLocalizations() DropdownDataGeneric.UpdateLabels(QuickSwitchFileResultBehaviours); // Since we are using Binding instead of DynamicResource, we need to manually trigger the update OnPropertyChanged(nameof(AlwaysPreviewToolTip)); + OnPropertyChanged(nameof(QuickSwitchSupportedExplorerDialogMessage)); } public string Language diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 9ec0004ebff..28e64a616e8 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -288,6 +288,15 @@ + + Date: Sun, 22 Jun 2025 12:20:04 +0800 Subject: [PATCH 190/243] Added enter key tip --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index d0132c34892..f2e05e51393 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -328,7 +328,7 @@ Floating as search window. Displayed when activated like search window Quick Switch Result Navigation Behaviour Behaviour to navigate file dialogs to paths of the results - Left click + Left click or Enter key Right click Quick Switch File Navigation Behaviour Behaviour to navigate file dialogs when paths of the results are files From 4ff215ce1a53e65d8e75753c2e4e99141e58969f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 22 Jun 2025 19:09:32 +0800 Subject: [PATCH 191/243] Collapse auto quick switch --- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 28e64a616e8..95280870f66 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -231,7 +231,8 @@ + Type="InsideFit" + Visibility="Collapsed"> Date: Sat, 28 Jun 2025 10:26:25 +0800 Subject: [PATCH 192/243] Improve general setting page display --- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index b0179c78adb..eaa3235204f 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -237,7 +237,7 @@ OnContent="{DynamicResource enable}" /> - + - + Date: Sat, 28 Jun 2025 11:24:36 +0800 Subject: [PATCH 193/243] Use external plugin --- .../Interface/IQuickSwitchDialog.cs | 22 ----------- .../Interface/IQuickSwitchDialogWindow.cs | 12 ------ .../Interface/IQuickSwitchDialogWindowTab.cs | 20 ---------- .../Interface/IQuickSwitchExplorer.cs | 24 ------------ .../QuickSwitch/Models/WindowsDialog.cs | 24 ++++++------ .../QuickSwitch/Models/WindowsExplorer.cs | 13 +++---- .../QuickSwitch/QuickSwitch.cs | 14 ++----- .../Interfaces/IQuickSwitchDialog.cs | 39 +++++++++++++++++++ .../Interfaces/IQuickSwitchExplorer.cs | 32 +++++++++++++++ .../SettingsPaneGeneralViewModel.cs | 5 +-- 10 files changed, 92 insertions(+), 113 deletions(-) delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs delete mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs create mode 100644 Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs create mode 100644 Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs deleted file mode 100644 index 5de7c8d752e..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialog.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Windows.Win32.Foundation; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface -{ - /// - /// Interface for handling File Dialog instances in QuickSwitch. - /// - /// - /// Add models which implement IQuickSwitchDialog in folder QuickSwitch/Models. - /// E.g. Models.WindowsDialog. - /// Then add instances in QuickSwitch._quickSwitchDialogs. - /// - internal interface IQuickSwitchDialog : IDisposable - { - string Name { get; } - - IQuickSwitchDialogWindow DialogWindow { get; } - - bool CheckDialogWindow(HWND hwnd); - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs deleted file mode 100644 index 8834e27f78d..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindow.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Windows.Win32.Foundation; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface -{ - internal interface IQuickSwitchDialogWindow : IDisposable - { - HWND Handle { get; } - - IQuickSwitchDialogWindowTab GetCurrentTab(); - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs deleted file mode 100644 index d01059a7c76..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchDialogWindowTab.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Windows.Win32.Foundation; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface -{ - internal interface IQuickSwitchDialogWindowTab : IDisposable - { - HWND Handle { get; } - - string GetCurrentFolder(); - - string GetCurrentFile(); - - bool JumpFolder(string path, bool auto); - - bool JumpFile(string path); - - bool Open(); - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs deleted file mode 100644 index 5d99e8ef96f..00000000000 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Interface/IQuickSwitchExplorer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using Windows.Win32.Foundation; - -namespace Flow.Launcher.Infrastructure.QuickSwitch.Interface -{ - /// - /// Interface for handling Windows Explorer instances in QuickSwitch. - /// - /// - /// Add models which implement IQuickSwitchExplorer in folder QuickSwitch/Models. - /// E.g. Models.WindowsExplorer. - /// Then add instances in QuickSwitch._quickSwitchExplorers. - /// - internal interface IQuickSwitchExplorer : IDisposable - { - string Name { get; } - - bool CheckExplorerWindow(HWND foreground); - - void RemoveExplorerWindow(); - - string GetExplorerPath(); - } -} diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index e01786bb974..1bbe9161f4c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -1,7 +1,7 @@ using System; using System.Threading; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Flow.Launcher.Plugins; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -21,7 +21,7 @@ internal class WindowsDialog : IQuickSwitchDialog public string Name => "Windows"; - public bool CheckDialogWindow(HWND hwnd) + public bool CheckDialogWindow(IntPtr hwnd) { // Has it been checked? if (DialogWindow != null && DialogWindow.Handle == hwnd) @@ -30,10 +30,10 @@ public bool CheckDialogWindow(HWND hwnd) } // Is it a Win32 dialog box? - if (GetClassName(hwnd) == WindowsDialogClassName) + if (GetClassName(new(hwnd)) == WindowsDialogClassName) { // Is it a windows file dialog? - var dialogType = GetFileDialogType(hwnd); + var dialogType = GetFileDialogType(new(hwnd)); if (dialogType != DialogType.Others) { DialogWindow = new WindowsDialogWindow(hwnd, dialogType); @@ -82,7 +82,7 @@ private static DialogType GetFileDialogType(HWND handle) internal class WindowsDialogWindow : IQuickSwitchDialogWindow { - public HWND Handle { get; private set; } = HWND.Null; + public IntPtr Handle { get; private set; } = IntPtr.Zero; // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore // So we need to cache the current tab and use the original handle @@ -90,7 +90,7 @@ internal class WindowsDialogWindow : IQuickSwitchDialogWindow private readonly DialogType _dialogType; - public WindowsDialogWindow(HWND handle, DialogType dialogType) + public WindowsDialogWindow(IntPtr handle, DialogType dialogType) { Handle = handle; _dialogType = dialogType; @@ -111,7 +111,7 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab { #region Public Properties - public HWND Handle { get; private set; } = HWND.Null; + public IntPtr Handle { get; private set; } = IntPtr.Zero; #endregion @@ -133,7 +133,7 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab #region Constructor - public WindowsDialogTab(HWND handle, DialogType dialogType) + public WindowsDialogTab(IntPtr handle, DialogType dialogType) { Handle = handle; _dialogType = dialogType; @@ -234,7 +234,7 @@ private bool GetPathControlEditor() { // Get the handle of the path editor // Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control - _pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000 _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000 @@ -264,14 +264,14 @@ private bool GetFileEditor() if (_dialogType == DialogType.Open) { // Get the handle of the file name editor of Open file dialog - _fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32 _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit } else { // Get the handle of the file name editor of Save / SaveAs file dialog - _fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName + _fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox @@ -290,7 +290,7 @@ private bool GetFileEditor() private bool GetOpenButton() { // Get the handle of the open button - _openButton = PInvoke.GetDlgItem(Handle, 0x0001); // Open/Save/SaveAs Button + _openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button if (_openButton == HWND.Null) { Log.Error(ClassName, "Failed to find open button handle"); diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 16132671435..50236c34c9a 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -1,8 +1,7 @@ using System; using System.Runtime.InteropServices; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; +using Flow.Launcher.Plugins; using Windows.Win32; -using Windows.Win32.Foundation; using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; @@ -11,20 +10,18 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// /// Class for handling Windows Explorer instances in QuickSwitch. /// - internal class WindowsExplorer : IQuickSwitchExplorer + public class WindowsExplorer : IQuickSwitchExplorer { private static readonly string ClassName = nameof(WindowsExplorer); private static IWebBrowser2 _lastExplorerView = null; private static readonly object _lastExplorerViewLock = new(); - public string Name => "Windows"; - - public bool CheckExplorerWindow(HWND foreground) + public bool CheckExplorerWindow(IntPtr foreground) { var isExplorer = false; // Is it from Explorer? - var processName = Win32Helper.GetProcessNameFromHwnd(foreground); + var processName = Win32Helper.GetProcessNameFromHwnd(new(foreground)); if (processName.ToLower() == "explorer.exe") { EnumerateShellWindows((shellWindow) => @@ -33,7 +30,7 @@ public bool CheckExplorerWindow(HWND foreground) { if (shellWindow is not IWebBrowser2 explorer) return true; - if (explorer.HWND != foreground.Value) return true; + if (explorer.HWND != foreground) return true; lock (_lastExplorerViewLock) { diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 26a67e86100..c441651dcb5 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -6,9 +6,9 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch.Interface; using Flow.Launcher.Infrastructure.QuickSwitch.Models; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugins; using NHotkey; using Windows.Win32; using Windows.Win32.Foundation; @@ -81,14 +81,6 @@ public static class QuickSwitch #endregion - #region Supported Explorers & Dialogs Name - - public static string[] SupportedExplorerNames => _quickSwitchExplorers.ConvertAll(explorer => explorer.Name).ToArray(); - - public static string[] SupportedDialogNames => _quickSwitchDialogs.ConvertAll(dialog => dialog.Name).ToArray(); - - #endregion - #region Initialize & Setup public static void InitializeQuickSwitch() @@ -308,7 +300,7 @@ private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChan { if (_dialogWindow != null) { - dialogWindowHandle = _dialogWindow.Handle; + dialogWindowHandle = new(_dialogWindow.Handle); } } @@ -731,7 +723,7 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial { lock (_autoSwitchedDialogsLock) { - _autoSwitchedDialogs.Add(dialogHandle); + _autoSwitchedDialogs.Add(new(dialogHandle)); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs new file mode 100644 index 00000000000..8ed6bb38f79 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs @@ -0,0 +1,39 @@ +using System; + +namespace Flow.Launcher.Plugins +{ + /// + /// Interface for handling file dialog instances in QuickSwitch. + /// + public interface IQuickSwitchDialog : IDisposable + { + /// + /// + /// + IQuickSwitchDialogWindow DialogWindow { get; } + + bool CheckDialogWindow(IntPtr hwnd); + } + + public interface IQuickSwitchDialogWindow : IDisposable + { + IntPtr Handle { get; } + + IQuickSwitchDialogWindowTab GetCurrentTab(); + } + + public interface IQuickSwitchDialogWindowTab : IDisposable + { + IntPtr Handle { get; } + + string GetCurrentFolder(); + + string GetCurrentFile(); + + bool JumpFolder(string path, bool auto); + + bool JumpFile(string path); + + bool Open(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs new file mode 100644 index 00000000000..d2172abf313 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -0,0 +1,32 @@ +using System; + +namespace Flow.Launcher.Plugins +{ + /// + /// Interface for handling file explorer instances in QuickSwitch. + /// + public interface IQuickSwitchExplorer : IDisposable + { + /// + /// Check if the foreground window is a Windows Explorer instance. + /// + /// + /// The handle of the foreground window to check. + /// + /// + /// True if the foreground window is a Windows Explorer instance, otherwise false. + /// + bool CheckExplorerWindow(IntPtr foreground); + + /// + /// + /// + void RemoveExplorerWindow(); + + /// + /// + /// + /// + string GetExplorerPath(); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 48a562cbb3e..f93c58b5b5b 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -180,10 +180,7 @@ public class QuickSwitchFileResultBehaviourData : DropdownDataGeneric QuickSwitchFileResultBehaviours { get; } = DropdownDataGeneric.GetValues("QuickSwitchFileResultBehaviour"); - public string QuickSwitchSupportedExplorerDialogMessage => - string.Format(App.API.GetTranslation("QuickSwitchSupportedExplorerDialogMessage"), - string.Join(", ", QuickSwitch.SupportedExplorerNames), - string.Join(", ", QuickSwitch.SupportedDialogNames)); + public string QuickSwitchSupportedExplorerDialogMessage => string.Empty; public int SearchDelayTimeValue { From 2d34f1f78196f8e25dde608704482ec4a17229ca Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 11:43:39 +0800 Subject: [PATCH 194/243] Add IQuickSwitchExplorerWindow interface --- .../QuickSwitch/Models/WindowsDialog.cs | 2 - .../QuickSwitch/Models/WindowsExplorer.cs | 82 +++++++++++-------- .../QuickSwitch/QuickSwitch.cs | 4 +- .../Interfaces/IQuickSwitchExplorer.cs | 11 ++- 4 files changed, 56 insertions(+), 43 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 1bbe9161f4c..c847a531d1d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -19,8 +19,6 @@ internal class WindowsDialog : IQuickSwitchDialog private const string WindowsDialogClassName = "#32770"; - public string Name => "Windows"; - public bool CheckDialogWindow(IntPtr hwnd) { // Has it been checked? diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 50236c34c9a..dc07f409116 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -12,10 +12,9 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// public class WindowsExplorer : IQuickSwitchExplorer { - private static readonly string ClassName = nameof(WindowsExplorer); + public IQuickSwitchExplorerWindow ExplorerWindow { get; private set; } - private static IWebBrowser2 _lastExplorerView = null; - private static readonly object _lastExplorerViewLock = new(); + private static readonly string ClassName = nameof(WindowsExplorer); public bool CheckExplorerWindow(IntPtr foreground) { @@ -32,10 +31,7 @@ public bool CheckExplorerWindow(IntPtr foreground) if (explorer.HWND != foreground) return true; - lock (_lastExplorerViewLock) - { - _lastExplorerView = explorer; - } + ExplorerWindow = new WindowsExplorerWindow(foreground, explorer); isExplorer = true; return false; } @@ -78,6 +74,50 @@ private static unsafe void EnumerateShellWindows(Func action) } } + public void RemoveExplorerWindow() + { + ExplorerWindow = null; + } + + public void Dispose() + { + ExplorerWindow?.Dispose(); + } + } + + public class WindowsExplorerWindow : IQuickSwitchExplorerWindow + { + public IntPtr Handle { get; } + + private static readonly object _lastExplorerViewLock = new(); + private static IWebBrowser2 _lastExplorerView = null; + + internal WindowsExplorerWindow(IntPtr handle, IWebBrowser2 explorerView) + { + Handle = handle; + _lastExplorerView = explorerView; + } + + public void Dispose() + { + // Release ComObjects + try + { + lock (_lastExplorerViewLock) + { + if (_lastExplorerView != null) + { + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; + } + } + } + catch (COMException) + { + _lastExplorerView = null; + } + } + public string GetExplorerPath() { if (_lastExplorerView == null) return null; @@ -134,33 +174,5 @@ public string GetExplorerPath() return path; } - - public void RemoveExplorerWindow() - { - lock (_lastExplorerViewLock) - { - _lastExplorerView = null; - } - } - - public void Dispose() - { - // Release ComObjects - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - Marshal.ReleaseComObject(_lastExplorerView); - _lastExplorerView = null; - } - } - } - catch (COMException) - { - _lastExplorerView = null; - } - } } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index c441651dcb5..91ed221bc8d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -259,7 +259,7 @@ private static bool RefreshLastExplorer() public static string GetActiveExplorerPath() { - return RefreshLastExplorer() ? _lastExplorer.GetExplorerPath() : string.Empty; + return RefreshLastExplorer() ? _lastExplorer.ExplorerWindow?.GetExplorerPath() : string.Empty; } #endregion @@ -610,7 +610,7 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f string path; lock (_lastExplorerLock) { - path = _lastExplorer?.GetExplorerPath(); + path = _lastExplorer?.ExplorerWindow?.GetExplorerPath(); } if (string.IsNullOrEmpty(path)) return false; diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index d2172abf313..e98e100f792 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -7,6 +7,8 @@ namespace Flow.Launcher.Plugins /// public interface IQuickSwitchExplorer : IDisposable { + IQuickSwitchExplorerWindow ExplorerWindow { get; } + /// /// Check if the foreground window is a Windows Explorer instance. /// @@ -22,11 +24,12 @@ public interface IQuickSwitchExplorer : IDisposable /// /// void RemoveExplorerWindow(); + } + + public interface IQuickSwitchExplorerWindow : IDisposable + { + IntPtr Handle { get; } - /// - /// - /// - /// string GetExplorerPath(); } } From 29e74aee1eb86f39ea3160866b73bd53446f7839 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:15:23 +0800 Subject: [PATCH 195/243] Use new interface styles --- .../QuickSwitch/Models/WindowsExplorer.cs | 79 ++++++++----------- .../QuickSwitch/QuickSwitch.cs | 32 +++++--- .../Interfaces/IQuickSwitchDialog.cs | 2 + .../Interfaces/IQuickSwitchExplorer.cs | 13 +-- 4 files changed, 59 insertions(+), 67 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index dc07f409116..da225aeb22d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -12,13 +12,12 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// public class WindowsExplorer : IQuickSwitchExplorer { - public IQuickSwitchExplorerWindow ExplorerWindow { get; private set; } - - private static readonly string ClassName = nameof(WindowsExplorer); + private static IWebBrowser2 _lastExplorerView = null; - public bool CheckExplorerWindow(IntPtr foreground) + public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr foreground) { - var isExplorer = false; + IQuickSwitchExplorerWindow explorerWindow = null; + // Is it from Explorer? var processName = Win32Helper.GetProcessNameFromHwnd(new(foreground)); if (processName.ToLower() == "explorer.exe") @@ -31,8 +30,8 @@ public bool CheckExplorerWindow(IntPtr foreground) if (explorer.HWND != foreground) return true; - ExplorerWindow = new WindowsExplorerWindow(foreground, explorer); - isExplorer = true; + _lastExplorerView = explorer; + explorerWindow = new WindowsExplorerWindow(foreground, explorer); return false; } catch (COMException) @@ -43,7 +42,7 @@ public bool CheckExplorerWindow(IntPtr foreground) return true; }); } - return isExplorer; + return explorerWindow; } private static unsafe void EnumerateShellWindows(Func action) @@ -74,14 +73,21 @@ private static unsafe void EnumerateShellWindows(Func action) } } - public void RemoveExplorerWindow() - { - ExplorerWindow = null; - } - public void Dispose() { - ExplorerWindow?.Dispose(); + // Release ComObjects + try + { + if (_lastExplorerView != null) + { + Marshal.ReleaseComObject(_lastExplorerView); + _lastExplorerView = null; + } + } + catch (COMException) + { + _lastExplorerView = null; + } } } @@ -89,51 +95,27 @@ public class WindowsExplorerWindow : IQuickSwitchExplorerWindow { public IntPtr Handle { get; } - private static readonly object _lastExplorerViewLock = new(); - private static IWebBrowser2 _lastExplorerView = null; + private static IWebBrowser2 _explorerView = null; internal WindowsExplorerWindow(IntPtr handle, IWebBrowser2 explorerView) { Handle = handle; - _lastExplorerView = explorerView; - } - - public void Dispose() - { - // Release ComObjects - try - { - lock (_lastExplorerViewLock) - { - if (_lastExplorerView != null) - { - Marshal.ReleaseComObject(_lastExplorerView); - _lastExplorerView = null; - } - } - } - catch (COMException) - { - _lastExplorerView = null; - } + _explorerView = explorerView; } public string GetExplorerPath() { - if (_lastExplorerView == null) return null; + if (_explorerView == null) return null; object document = null; try { - lock (_lastExplorerViewLock) + if (_explorerView != null) { - if (_lastExplorerView != null) - { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _lastExplorerView; - document = explorerView.Document; - } + // Use dynamic here because using IWebBrower2.Document can cause exception here: + // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' + dynamic explorerView = _explorerView; + document = explorerView.Document; } } catch (COMException) @@ -174,5 +156,10 @@ public string GetExplorerPath() return path; } + + public void Dispose() + { + + } } } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 91ed221bc8d..18fbd4b149c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -40,9 +40,9 @@ public static class QuickSwitch private static HWND _mainWindowHandle = HWND.Null; - private static readonly List _quickSwitchExplorers = new() + private static readonly Dictionary _quickSwitchExplorers = new() { - new WindowsExplorer() + { new WindowsExplorer(), null } }; private static IQuickSwitchExplorer _lastExplorer = null; @@ -172,10 +172,11 @@ public static void SetupQuickSwitch(bool enabled) } else { - // Remove last explorer - foreach (var explorer in _quickSwitchExplorers) + // Remove explorer windows + foreach (var explorer in _quickSwitchExplorers.Keys) { - explorer.RemoveExplorerWindow(); + _quickSwitchExplorers[explorer]?.Dispose(); + _quickSwitchExplorers[explorer] = null; } // Remove dialog window handle @@ -235,10 +236,13 @@ private static bool RefreshLastExplorer() // Enum windows from the top to the bottom PInvoke.EnumWindows((hWnd, _) => { - foreach (var explorer in _quickSwitchExplorers) + foreach (var explorer in _quickSwitchExplorers.Keys) { - if (explorer.CheckExplorerWindow(hWnd)) + var explorerWindow = explorer.CheckExplorerWindow(hWnd); + if (explorerWindow != null) { + _quickSwitchExplorers[explorer]?.Dispose(); + _quickSwitchExplorers[explorer] = explorerWindow; _lastExplorer = explorer; found = true; return false; @@ -259,7 +263,7 @@ private static bool RefreshLastExplorer() public static string GetActiveExplorerPath() { - return RefreshLastExplorer() ? _lastExplorer.ExplorerWindow?.GetExplorerPath() : string.Empty; + return RefreshLastExplorer() ? _quickSwitchExplorers[_lastExplorer].GetExplorerPath() : string.Empty; } #endregion @@ -474,11 +478,14 @@ uint dwmsEventTime { lock (_lastExplorerLock) { - foreach (var explorer in _quickSwitchExplorers) + foreach (var explorer in _quickSwitchExplorers.Keys) { - if (explorer.CheckExplorerWindow(hwnd)) + var explorerWindow = explorer.CheckExplorerWindow(hwnd); + if (explorerWindow != null) { Log.Debug(ClassName, $"Explorer window: {hwnd}"); + _quickSwitchExplorers[explorer]?.Dispose(); + _quickSwitchExplorers[explorer] = explorerWindow; _lastExplorer = explorer; break; } @@ -610,7 +617,7 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f string path; lock (_lastExplorerLock) { - path = _lastExplorer?.ExplorerWindow?.GetExplorerPath(); + path = _quickSwitchExplorers[_lastExplorer]?.GetExplorerPath(); } if (string.IsNullOrEmpty(path)) return false; @@ -801,8 +808,9 @@ public static void Dispose() } // Dispose explorers - foreach (var explorer in _quickSwitchExplorers) + foreach (var explorer in _quickSwitchExplorers.Keys) { + _quickSwitchExplorers[explorer]?.Dispose(); explorer.Dispose(); } _quickSwitchExplorers.Clear(); diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs index 8ed6bb38f79..074dd6a8e0c 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace Flow.Launcher.Plugins { /// diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index e98e100f792..67548f2a160 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace Flow.Launcher.Plugins { /// @@ -7,8 +9,6 @@ namespace Flow.Launcher.Plugins /// public interface IQuickSwitchExplorer : IDisposable { - IQuickSwitchExplorerWindow ExplorerWindow { get; } - /// /// Check if the foreground window is a Windows Explorer instance. /// @@ -16,14 +16,9 @@ public interface IQuickSwitchExplorer : IDisposable /// The handle of the foreground window to check. /// /// - /// True if the foreground window is a Windows Explorer instance, otherwise false. + /// The explorer window if the foreground window is a Windows Explorer instance. Null if it is not. /// - bool CheckExplorerWindow(IntPtr foreground); - - /// - /// - /// - void RemoveExplorerWindow(); + IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr foreground); } public interface IQuickSwitchExplorerWindow : IDisposable From 6b303e2a48c8efe192501880141facee85d4fa35 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:18:42 +0800 Subject: [PATCH 196/243] Change modifies --- .../QuickSwitch/Models/WindowsDialog.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index c847a531d1d..64801fa7750 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -13,7 +13,7 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// /// Class for handling Windows File Dialog instances in QuickSwitch. /// - internal class WindowsDialog : IQuickSwitchDialog + public class WindowsDialog : IQuickSwitchDialog { public IQuickSwitchDialogWindow DialogWindow { get; private set; } @@ -78,7 +78,7 @@ private static DialogType GetFileDialogType(HWND handle) #endregion } - internal class WindowsDialogWindow : IQuickSwitchDialogWindow + public class WindowsDialogWindow : IQuickSwitchDialogWindow { public IntPtr Handle { get; private set; } = IntPtr.Zero; @@ -88,7 +88,7 @@ internal class WindowsDialogWindow : IQuickSwitchDialogWindow private readonly DialogType _dialogType; - public WindowsDialogWindow(IntPtr handle, DialogType dialogType) + internal WindowsDialogWindow(IntPtr handle, DialogType dialogType) { Handle = handle; _dialogType = dialogType; @@ -105,7 +105,7 @@ public void Dispose() } } - internal class WindowsDialogTab : IQuickSwitchDialogWindowTab + public class WindowsDialogTab : IQuickSwitchDialogWindowTab { #region Public Properties @@ -131,7 +131,7 @@ internal class WindowsDialogTab : IQuickSwitchDialogWindowTab #region Constructor - public WindowsDialogTab(IntPtr handle, DialogType dialogType) + internal WindowsDialogTab(IntPtr handle, DialogType dialogType) { Handle = handle; _dialogType = dialogType; From 2944f11deec0b0c29dc78f79ae4b982c3736a971 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:22:02 +0800 Subject: [PATCH 197/243] Change param name --- .../QuickSwitch/Models/WindowsExplorer.cs | 8 ++++---- Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index da225aeb22d..95c964cac8b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -14,12 +14,12 @@ public class WindowsExplorer : IQuickSwitchExplorer { private static IWebBrowser2 _lastExplorerView = null; - public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr foreground) + public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) { IQuickSwitchExplorerWindow explorerWindow = null; // Is it from Explorer? - var processName = Win32Helper.GetProcessNameFromHwnd(new(foreground)); + var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd)); if (processName.ToLower() == "explorer.exe") { EnumerateShellWindows((shellWindow) => @@ -28,10 +28,10 @@ public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr foreground) { if (shellWindow is not IWebBrowser2 explorer) return true; - if (explorer.HWND != foreground) return true; + if (explorer.HWND != hwnd) return true; _lastExplorerView = explorer; - explorerWindow = new WindowsExplorerWindow(foreground, explorer); + explorerWindow = new WindowsExplorerWindow(hwnd, explorer); return false; } catch (COMException) diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index 67548f2a160..72cacc1a3c4 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -12,13 +12,13 @@ public interface IQuickSwitchExplorer : IDisposable /// /// Check if the foreground window is a Windows Explorer instance. /// - /// + /// /// The handle of the foreground window to check. /// /// /// The explorer window if the foreground window is a Windows Explorer instance. Null if it is not. /// - IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr foreground); + IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr hwnd); } public interface IQuickSwitchExplorerWindow : IDisposable From aeb152b06ca0c43e8356c960ef8a3ca5d1d4f495 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:23:42 +0800 Subject: [PATCH 198/243] Improve docuements --- Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index 72cacc1a3c4..b03935cc8df 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -16,7 +16,7 @@ public interface IQuickSwitchExplorer : IDisposable /// The handle of the foreground window to check. /// /// - /// The explorer window if the foreground window is a Windows Explorer instance. Null if it is not. + /// The window if the foreground window is a file explorer instance. Null if it is not. /// IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr hwnd); } From a3d095f6a47ad596fefa637fd63d1205be2fe027 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:46:26 +0800 Subject: [PATCH 199/243] Change interface style --- .../QuickSwitch/Models/WindowsDialog.cs | 24 +++----- .../QuickSwitch/QuickSwitch.cs | 55 ++++++++++++++----- .../Interfaces/IQuickSwitchDialog.cs | 12 ++-- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 64801fa7750..406c6d363c5 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -15,18 +15,10 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// public class WindowsDialog : IQuickSwitchDialog { - public IQuickSwitchDialogWindow DialogWindow { get; private set; } - private const string WindowsDialogClassName = "#32770"; - public bool CheckDialogWindow(IntPtr hwnd) + public IQuickSwitchDialogWindow CheckDialogWindow(IntPtr hwnd) { - // Has it been checked? - if (DialogWindow != null && DialogWindow.Handle == hwnd) - { - return true; - } - // Is it a Win32 dialog box? if (GetClassName(new(hwnd)) == WindowsDialogClassName) { @@ -34,18 +26,16 @@ public bool CheckDialogWindow(IntPtr hwnd) var dialogType = GetFileDialogType(new(hwnd)); if (dialogType != DialogType.Others) { - DialogWindow = new WindowsDialogWindow(hwnd, dialogType); - - return true; + return new WindowsDialogWindow(hwnd, dialogType); } } - return false; + + return null; } public void Dispose() { - DialogWindow?.Dispose(); - DialogWindow = null; + } #region Help Methods @@ -101,7 +91,7 @@ public IQuickSwitchDialogWindowTab GetCurrentTab() public void Dispose() { - Handle = HWND.Null; + } } @@ -219,7 +209,7 @@ public bool Open() public void Dispose() { - Handle = HWND.Null; + } #endregion diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 18fbd4b149c..1f2075d3532 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -48,9 +48,9 @@ public static class QuickSwitch private static IQuickSwitchExplorer _lastExplorer = null; private static readonly object _lastExplorerLock = new(); - private static readonly List _quickSwitchDialogs = new() + private static readonly Dictionary _quickSwitchDialogs = new() { - new WindowsDialog() + { new WindowsDialog(), null } }; private static IQuickSwitchDialogWindow _dialogWindow = null; @@ -179,6 +179,12 @@ public static void SetupQuickSwitch(bool enabled) _quickSwitchExplorers[explorer] = null; } + // Remove dialog windows + foreach (var dialog in _quickSwitchDialogs.Keys) + { + _quickSwitchDialogs[dialog] = null; + } + // Remove dialog window handle var dialogWindowExists = false; lock (_dialogWindowLock) @@ -402,14 +408,27 @@ uint dwmsEventTime // Check if it is a file dialog window var isDialogWindow = false; var dialogWindowChanged = false; - foreach (var dialog in _quickSwitchDialogs) + foreach (var dialog in _quickSwitchDialogs.Keys) { - if (dialog.CheckDialogWindow(hwnd)) + IQuickSwitchDialogWindow dialogWindow; + var existingDialogWindow = _quickSwitchDialogs[dialog]; + if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) + { + // If the dialog window is already in the list, no need to check again + dialogWindow = existingDialogWindow; + } + else + { + dialogWindow = dialog.CheckDialogWindow(hwnd); + } + + // If the dialog window is found, set it + if (dialogWindow != null) { lock (_dialogWindowLock) { dialogWindowChanged = _dialogWindow == null || _dialogWindow.Handle != hwnd; - _dialogWindow = dialog.DialogWindow; + _dialogWindow = dialogWindow; } isDialogWindow = true; @@ -667,21 +686,30 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } // Then check all dialog windows - foreach (var dialog in _quickSwitchDialogs) + foreach (var dialogWindow in _quickSwitchDialogs.Values) { - if (dialog.DialogWindow.Handle == hwnd) + if (dialogWindow.Handle == hwnd) { - return dialog.DialogWindow; + return dialogWindow; } } - // Finally search for the dialog window - foreach (var dialog in _quickSwitchDialogs) + // Finally search for the dialog window again + foreach (var dialog in _quickSwitchDialogs.Keys) { - if (dialog.CheckDialogWindow(hwnd)) + IQuickSwitchDialogWindow dialogWindow; + var existingDialogWindow = _quickSwitchDialogs[dialog]; + if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) { - return dialog.DialogWindow; + // If the dialog window is already in the list, no need to check again + dialogWindow = existingDialogWindow; } + else + { + dialogWindow = dialog.CheckDialogWindow(hwnd); + } + + return dialogWindow; } return null; @@ -820,8 +848,9 @@ public static void Dispose() } // Dispose dialogs - foreach (var dialog in _quickSwitchDialogs) + foreach (var dialog in _quickSwitchDialogs.Keys) { + _quickSwitchDialogs[dialog]?.Dispose(); dialog.Dispose(); } _quickSwitchDialogs.Clear(); diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs index 074dd6a8e0c..2de5f950435 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs @@ -10,11 +10,15 @@ namespace Flow.Launcher.Plugins public interface IQuickSwitchDialog : IDisposable { /// - /// + /// Check if the foreground window is a file dialog instance. /// - IQuickSwitchDialogWindow DialogWindow { get; } - - bool CheckDialogWindow(IntPtr hwnd); + /// + /// The handle of the foreground window to check. + /// + /// + /// The window if the foreground window is a file dialog instance. Null if it is not. + /// + IQuickSwitchDialogWindow? CheckDialogWindow(IntPtr hwnd); } public interface IQuickSwitchDialogWindow : IDisposable From 53b3b7fe908de416d338210d65d8ca157941430c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:47:07 +0800 Subject: [PATCH 200/243] Only dispose in dispose function --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 1f2075d3532..5bd7778be65 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -175,7 +175,6 @@ public static void SetupQuickSwitch(bool enabled) // Remove explorer windows foreach (var explorer in _quickSwitchExplorers.Keys) { - _quickSwitchExplorers[explorer]?.Dispose(); _quickSwitchExplorers[explorer] = null; } @@ -191,7 +190,6 @@ public static void SetupQuickSwitch(bool enabled) { if (_dialogWindow != null) { - _dialogWindow.Dispose(); _dialogWindow = null; dialogWindowExists = true; } @@ -247,7 +245,6 @@ private static bool RefreshLastExplorer() var explorerWindow = explorer.CheckExplorerWindow(hWnd); if (explorerWindow != null) { - _quickSwitchExplorers[explorer]?.Dispose(); _quickSwitchExplorers[explorer] = explorerWindow; _lastExplorer = explorer; found = true; @@ -503,7 +500,6 @@ uint dwmsEventTime if (explorerWindow != null) { Log.Debug(ClassName, $"Explorer window: {hwnd}"); - _quickSwitchExplorers[explorer]?.Dispose(); _quickSwitchExplorers[explorer] = explorerWindow; _lastExplorer = explorer; break; From 9fc13f357a0b399e92e94c81b42046e681689dde Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:49:15 +0800 Subject: [PATCH 201/243] Improve dispose function --- .../QuickSwitch/Models/WindowsExplorer.cs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 95c964cac8b..eda6f2cb52b 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -12,8 +12,6 @@ namespace Flow.Launcher.Infrastructure.QuickSwitch.Models /// public class WindowsExplorer : IQuickSwitchExplorer { - private static IWebBrowser2 _lastExplorerView = null; - public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) { IQuickSwitchExplorerWindow explorerWindow = null; @@ -30,7 +28,6 @@ public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) if (explorer.HWND != hwnd) return true; - _lastExplorerView = explorer; explorerWindow = new WindowsExplorerWindow(hwnd, explorer); return false; } @@ -75,19 +72,7 @@ private static unsafe void EnumerateShellWindows(Func action) public void Dispose() { - // Release ComObjects - try - { - if (_lastExplorerView != null) - { - Marshal.ReleaseComObject(_lastExplorerView); - _lastExplorerView = null; - } - } - catch (COMException) - { - _lastExplorerView = null; - } + } } @@ -159,7 +144,19 @@ public string GetExplorerPath() public void Dispose() { - + // Release ComObjects + try + { + if (_explorerView != null) + { + Marshal.ReleaseComObject(_explorerView); + _explorerView = null; + } + } + catch (COMException) + { + _explorerView = null; + } } } } From e24df4e97258de86e62b0489a09852de853a8d67 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 12:54:06 +0800 Subject: [PATCH 202/243] Improve documents --- .../Interfaces/IQuickSwitchDialog.cs | 51 +++++++++++++++++++ .../Interfaces/IQuickSwitchExplorer.cs | 12 ++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs index 2de5f950435..ab4edec1d70 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs @@ -21,25 +21,76 @@ public interface IQuickSwitchDialog : IDisposable IQuickSwitchDialogWindow? CheckDialogWindow(IntPtr hwnd); } + /// + /// Interface for handling a specific file dialog window in QuickSwitch. + /// public interface IQuickSwitchDialogWindow : IDisposable { + /// + /// The handle of the dialog window. + /// IntPtr Handle { get; } + /// + /// Get the current tab of the dialog window. + /// + /// IQuickSwitchDialogWindowTab GetCurrentTab(); } + /// + /// Interface for handling a specific tab in a file dialog window in QuickSwitch. + /// public interface IQuickSwitchDialogWindowTab : IDisposable { + /// + /// The handle of the dialog tab. + /// IntPtr Handle { get; } + /// + /// Get the current folder path of the dialog tab. + /// + /// string GetCurrentFolder(); + /// + /// Get the current file of the dialog tab. + /// + /// string GetCurrentFile(); + /// + /// Jump to a folder in the dialog tab. + /// + /// + /// The path to the folder to jump to. + /// + /// + /// Whether folder jump is under automatical mode. + /// + /// + /// True if the jump was successful, false otherwise. + /// bool JumpFolder(string path, bool auto); + /// + /// Jump to a file in the dialog tab. + /// + /// + /// The path to the file to jump to. + /// + /// + /// True if the jump was successful, false otherwise. + /// bool JumpFile(string path); + /// + /// Open the file in the dialog tab. + /// + /// + /// True if the file was opened successfully, false otherwise. + /// bool Open(); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index b03935cc8df..8ad99e14eb6 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -21,10 +21,20 @@ public interface IQuickSwitchExplorer : IDisposable IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr hwnd); } + /// + /// Interface for handling a specific file explorer window in QuickSwitch. + /// public interface IQuickSwitchExplorerWindow : IDisposable { + /// + /// The handle of the explorer window. + /// IntPtr Handle { get; } - string GetExplorerPath(); + /// + /// Get the current folder path of the explorer window. + /// + /// + string? GetExplorerPath(); } } From 683d4fa9876e42f26dcdc512718a8bdb455e8e89 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:18:07 +0800 Subject: [PATCH 203/243] Fix namespaces --- .../QuickSwitch/Models/WindowsDialog.cs | 2 +- .../QuickSwitch/Models/WindowsExplorer.cs | 2 +- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs | 4 ++-- Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs index 406c6d363c5..66698abc47e 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs @@ -1,7 +1,7 @@ using System; using System.Threading; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Plugins; +using Flow.Launcher.Plugin; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index eda6f2cb52b..20d25c599ac 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -1,6 +1,6 @@ using System; using System.Runtime.InteropServices; -using Flow.Launcher.Plugins; +using Flow.Launcher.Plugin; using Windows.Win32; using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 5bd7778be65..8fed0d73113 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -8,7 +8,7 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.QuickSwitch.Models; using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugins; +using Flow.Launcher.Plugin; using NHotkey; using Windows.Win32; using Windows.Win32.Foundation; diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs index ab4edec1d70..2fb498bb541 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs @@ -2,12 +2,12 @@ #nullable enable -namespace Flow.Launcher.Plugins +namespace Flow.Launcher.Plugin { /// /// Interface for handling file dialog instances in QuickSwitch. /// - public interface IQuickSwitchDialog : IDisposable + public interface IQuickSwitchDialog : IFeatures, IDisposable { /// /// Check if the foreground window is a file dialog instance. diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs index 8ad99e14eb6..ea46e046a58 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs @@ -2,12 +2,12 @@ #nullable enable -namespace Flow.Launcher.Plugins +namespace Flow.Launcher.Plugin { /// /// Interface for handling file explorer instances in QuickSwitch. /// - public interface IQuickSwitchExplorer : IDisposable + public interface IQuickSwitchExplorer : IFeatures, IDisposable { /// /// Check if the foreground window is a Windows Explorer instance. From b6115faa19159082d1973e136fe154742aba8730 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:18:28 +0800 Subject: [PATCH 204/243] Add quick switch pair model --- .../QuickSwitch/QuickSwitchPair.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs new file mode 100644 index 00000000000..1467afe580d --- /dev/null +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs @@ -0,0 +1,63 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Infrastructure.QuickSwitch; + +public class QuickSwitchExplorerPair +{ + public IQuickSwitchExplorer Plugin { get; init; } + + public PluginMetadata Metadata { get; init; } + + public override string ToString() + { + return Metadata.Name; + } + + public override bool Equals(object obj) + { + if (obj is QuickSwitchExplorerPair r) + { + return string.Equals(r.Metadata.ID, Metadata.ID); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + var hashcode = Metadata.ID?.GetHashCode() ?? 0; + return hashcode; + } +} + +public class QuickSwitchDialogPair +{ + public IQuickSwitchDialog Plugin { get; init; } + + public PluginMetadata Metadata { get; init; } + + public override string ToString() + { + return Metadata.Name; + } + + public override bool Equals(object obj) + { + if (obj is QuickSwitchDialogPair r) + { + return string.Equals(r.Metadata.ID, Metadata.ID); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + var hashcode = Metadata.ID?.GetHashCode() ?? 0; + return hashcode; + } +} From 9977cc32a236e7ab50c071d0d054baa9c85550ca Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:22:53 +0800 Subject: [PATCH 205/243] Fix possible quick switch empty results issue --- 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 c666630be05..0e5db0eb7d8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1510,7 +1510,7 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : IReadOnlyList resultsCopy; if (results == null) { - resultsCopy = _emptyResult; + resultsCopy = currentIsQuickSwitch ? _emptyQuickSwitchResult : _emptyResult; } else { From 506bde8db7bf708628b2e026815a389ea72cb60e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:33:39 +0800 Subject: [PATCH 206/243] Use quick switch pair models --- Flow.Launcher.Core/Plugin/PluginManager.cs | 31 +++++++++ .../QuickSwitch/QuickSwitch.cs | 65 ++++++++++++++----- Flow.Launcher/App.xaml.cs | 2 +- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3bce05b47b1..9237a689fe7 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.QuickSwitch; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -27,6 +28,9 @@ public static class PluginManager private static IEnumerable _contextMenuPlugins; private static IEnumerable _homePlugins; + private static readonly List _quickSwitchExplorerPlugins = new(); + private static readonly List _quickSwitchDialogPlugins = new(); + public static List AllPlugins { get; private set; } public static readonly HashSet GlobalPlugins = new(); public static readonly Dictionary NonGlobalPlugins = new(); @@ -251,6 +255,23 @@ public static async Task InitializePluginsAsync() _contextMenuPlugins = GetPluginsForInterface(); _homePlugins = GetPluginsForInterface(); + foreach (var pair in GetPluginsForInterface()) + { + _quickSwitchExplorerPlugins.Add(new QuickSwitchExplorerPair + { + Plugin = (IQuickSwitchExplorer)pair.Plugin, + Metadata = pair.Metadata + }); + } + foreach (var pair in GetPluginsForInterface()) + { + _quickSwitchDialogPlugins.Add(new QuickSwitchDialogPair + { + Plugin = (IQuickSwitchDialog)pair.Plugin, + Metadata = pair.Metadata + }); + } + foreach (var plugin in AllPlugins) { // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin @@ -475,6 +496,16 @@ public static bool IsHomePlugin(string id) return _homePlugins.Any(p => p.Metadata.ID == id); } + public static IList GetQuickSwitchExplorers() + { + return _quickSwitchExplorerPlugins; + } + + public static IList GetQuickSwitchDialogs() + { + return _quickSwitchDialogPlugins; + } + public static bool ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 8fed0d73113..a6951248b9d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -30,6 +30,26 @@ public static class QuickSwitch public static QuickSwitchWindowPositions QuickSwitchWindowPosition { get; private set; } + public static QuickSwitchExplorerPair WindowsQuickSwitchExplorer { get; } = new() + { + Metadata = new() + { + ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the quick switch pairs + Disabled = false // Disabled is for enabling the Windows QuickSwitch explorers & dialogs + }, + Plugin = new WindowsExplorer() + }; + + public static QuickSwitchDialogPair WindowsQuickSwitchDialog { get; } = new() + { + Metadata = new() + { + ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the quick switch pairs + Disabled = false // Disabled is for enabling the Windows QuickSwitch explorers & dialogs + }, + Plugin = new WindowsDialog() + }; + #endregion #region Private Fields @@ -40,18 +60,12 @@ public static class QuickSwitch private static HWND _mainWindowHandle = HWND.Null; - private static readonly Dictionary _quickSwitchExplorers = new() - { - { new WindowsExplorer(), null } - }; + private static readonly Dictionary _quickSwitchExplorers = new(); - private static IQuickSwitchExplorer _lastExplorer = null; + private static QuickSwitchExplorerPair _lastExplorer = null; private static readonly object _lastExplorerLock = new(); - private static readonly Dictionary _quickSwitchDialogs = new() - { - { new WindowsDialog(), null } - }; + private static readonly Dictionary _quickSwitchDialogs = new(); private static IQuickSwitchDialogWindow _dialogWindow = null; private static readonly object _dialogWindowLock = new(); @@ -83,10 +97,23 @@ public static class QuickSwitch #region Initialize & Setup - public static void InitializeQuickSwitch() + public static void InitializeQuickSwitch(IList quickSwitchExplorers, + IList quickSwitchDialogs) { if (_initialized) return; + // Initialize quick switch explorers & dialogs + _quickSwitchExplorers.Add(WindowsQuickSwitchExplorer, null); + foreach (var explorer in quickSwitchExplorers) + { + _quickSwitchExplorers.Add(explorer, null); + } + _quickSwitchDialogs.Add(WindowsQuickSwitchDialog, null); + foreach (var dialog in quickSwitchDialogs) + { + _quickSwitchDialogs.Add(dialog, null); + } + // Initialize main window handle _mainWindowHandle = Win32Helper.GetMainWindowHandle(); @@ -242,7 +269,9 @@ private static bool RefreshLastExplorer() { foreach (var explorer in _quickSwitchExplorers.Keys) { - var explorerWindow = explorer.CheckExplorerWindow(hWnd); + if (explorer.Metadata.Disabled) continue; + + var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); if (explorerWindow != null) { _quickSwitchExplorers[explorer] = explorerWindow; @@ -407,6 +436,8 @@ uint dwmsEventTime var dialogWindowChanged = false; foreach (var dialog in _quickSwitchDialogs.Keys) { + if (dialog.Metadata.Disabled) continue; + IQuickSwitchDialogWindow dialogWindow; var existingDialogWindow = _quickSwitchDialogs[dialog]; if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) @@ -416,7 +447,7 @@ uint dwmsEventTime } else { - dialogWindow = dialog.CheckDialogWindow(hwnd); + dialogWindow = dialog.Plugin.CheckDialogWindow(hwnd); } // If the dialog window is found, set it @@ -496,7 +527,9 @@ uint dwmsEventTime { foreach (var explorer in _quickSwitchExplorers.Keys) { - var explorerWindow = explorer.CheckExplorerWindow(hwnd); + if (explorer.Metadata.Disabled) continue; + + var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd); if (explorerWindow != null) { Log.Debug(ClassName, $"Explorer window: {hwnd}"); @@ -693,6 +726,8 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) // Finally search for the dialog window again foreach (var dialog in _quickSwitchDialogs.Keys) { + if (dialog.Metadata.Disabled) continue; + IQuickSwitchDialogWindow dialogWindow; var existingDialogWindow = _quickSwitchDialogs[dialog]; if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) @@ -702,7 +737,7 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } else { - dialogWindow = dialog.CheckDialogWindow(hwnd); + dialogWindow = dialog.Plugin.CheckDialogWindow(hwnd); } return dialogWindow; @@ -835,7 +870,6 @@ public static void Dispose() foreach (var explorer in _quickSwitchExplorers.Keys) { _quickSwitchExplorers[explorer]?.Dispose(); - explorer.Dispose(); } _quickSwitchExplorers.Clear(); lock (_lastExplorerLock) @@ -847,7 +881,6 @@ public static void Dispose() foreach (var dialog in _quickSwitchDialogs.Keys) { _quickSwitchDialogs[dialog]?.Dispose(); - dialog.Dispose(); } _quickSwitchDialogs.Clear(); lock (_dialogWindowLock) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 79363262c91..04e494115d8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -228,7 +228,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); - QuickSwitch.InitializeQuickSwitch(); + QuickSwitch.InitializeQuickSwitch(PluginManager.GetQuickSwitchExplorers(), PluginManager.GetQuickSwitchDialogs()); QuickSwitch.SetupQuickSwitch(_settings.EnableQuickSwitch); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); From 92bc05c039bd323e01756995e23fdb6494f0564c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:40:09 +0800 Subject: [PATCH 207/243] Remove quick switch information --- Flow.Launcher/Languages/en.xaml | 2 -- .../ViewModels/SettingsPaneGeneralViewModel.cs | 2 -- .../SettingPages/Views/SettingsPaneGeneral.xaml | 9 --------- 3 files changed, 13 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index f2e05e51393..7537c05b25a 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -335,8 +335,6 @@ Fill full path in file name box Fill full path in file name box and open Fill directory in path box - Information for Quick Switch user - Currently Flow supports those file explorers ({0}) and those file dialogs ({1}) HTTP Proxy diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index f93c58b5b5b..7b72a05fe2e 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -180,8 +180,6 @@ public class QuickSwitchFileResultBehaviourData : DropdownDataGeneric QuickSwitchFileResultBehaviours { get; } = DropdownDataGeneric.GetValues("QuickSwitchFileResultBehaviour"); - public string QuickSwitchSupportedExplorerDialogMessage => string.Empty; - public int SearchDelayTimeValue { get => Settings.SearchDelayTime; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index eaa3235204f..6b874abba20 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -299,15 +299,6 @@ - - Date: Sat, 28 Jun 2025 13:40:41 +0800 Subject: [PATCH 208/243] Remove quick switch information --- .../SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 7b72a05fe2e..fa906efa839 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -217,7 +217,6 @@ private void UpdateEnumDropdownLocalizations() DropdownDataGeneric.UpdateLabels(QuickSwitchFileResultBehaviours); // Since we are using Binding instead of DynamicResource, we need to manually trigger the update OnPropertyChanged(nameof(AlwaysPreviewToolTip)); - OnPropertyChanged(nameof(QuickSwitchSupportedExplorerDialogMessage)); } public string Language From 38084188f50746661a0fab5f00075b58751465c3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:48:04 +0800 Subject: [PATCH 209/243] Update dialog window if found --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index a6951248b9d..047c4b88365 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -740,6 +740,13 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) dialogWindow = dialog.Plugin.CheckDialogWindow(hwnd); } + // Update dialog window if found + if (dialogWindow != null) + { + _quickSwitchDialogs[dialog] = dialogWindow; + return dialogWindow; + } + return dialogWindow; } From 8ef46842a9001d78d1bd3ae98a87e1e383d4cef4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 28 Jun 2025 13:57:48 +0800 Subject: [PATCH 210/243] Fix return issue --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 047c4b88365..09793d15e7c 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -746,8 +746,6 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) _quickSwitchDialogs[dialog] = dialogWindow; return dialogWindow; } - - return dialogWindow; } return null; From 47166057d331c965e32ccb7a66b14f2947a1d87c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 1 Jul 2025 21:50:37 +0800 Subject: [PATCH 211/243] Add quick switch hotkeys in model --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0d81f29d8f2..28aae808e0b 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -488,6 +488,8 @@ public List RegisteredHotkeys list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); + if (!string.IsNullOrEmpty(QuickSwitchHotkey)) + list.Add(new(QuickSwitchHotkey, "quickSwitchHotkey", () => QuickSwitchHotkey = "")); // Custom Query Hotkeys foreach (var customPluginHotkey in CustomPluginHotkeys) From b6be32182514c7a2ddd9a58037bd08af0b3d16f8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 7 Jul 2025 20:38:30 +0800 Subject: [PATCH 212/243] Check plugin modified state --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 37341e7745a..09b0742aae4 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -521,12 +521,12 @@ public static bool IsHomePlugin(string id) public static IList GetQuickSwitchExplorers() { - return _quickSwitchExplorerPlugins; + return _quickSwitchExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } public static IList GetQuickSwitchDialogs() { - return _quickSwitchDialogPlugins; + return _quickSwitchDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } public static bool ActionKeywordRegistered(string actionKeyword) From ed019d3386768bf09e7ecabd914c50dfac7242d7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 7 Jul 2025 20:48:28 +0800 Subject: [PATCH 213/243] Check plugin modified state & disabled state for quickSwitchDialogs --- .../QuickSwitch/QuickSwitch.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 09793d15e7c..59cd8b232cc 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -58,6 +58,10 @@ public static class QuickSwitch private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + private static HWND _mainWindowHandle = HWND.Null; private static readonly Dictionary _quickSwitchExplorers = new(); @@ -436,7 +440,8 @@ uint dwmsEventTime var dialogWindowChanged = false; foreach (var dialog in _quickSwitchDialogs.Keys) { - if (dialog.Metadata.Disabled) continue; + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled IQuickSwitchDialogWindow dialogWindow; var existingDialogWindow = _quickSwitchDialogs[dialog]; @@ -715,8 +720,12 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } // Then check all dialog windows - foreach (var dialogWindow in _quickSwitchDialogs.Values) + foreach (var dialog in _quickSwitchDialogs.Keys) { + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled + + var dialogWindow = _quickSwitchDialogs[dialog]; if (dialogWindow.Handle == hwnd) { return dialogWindow; @@ -726,7 +735,8 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) // Finally search for the dialog window again foreach (var dialog in _quickSwitchDialogs.Keys) { - if (dialog.Metadata.Disabled) continue; + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled IQuickSwitchDialogWindow dialogWindow; var existingDialogWindow = _quickSwitchDialogs[dialog]; From cf66077b13dcb6bb6da0582374b3c3bd9bf6a65b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 7 Jul 2025 20:53:58 +0800 Subject: [PATCH 214/243] Check plugin modified state & disabled state for quickSwitchExplorers --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 59cd8b232cc..99069fda818 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -273,7 +273,8 @@ private static bool RefreshLastExplorer() { foreach (var explorer in _quickSwitchExplorers.Keys) { - if (explorer.Metadata.Disabled) continue; + if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); if (explorerWindow != null) @@ -532,7 +533,8 @@ uint dwmsEventTime { foreach (var explorer in _quickSwitchExplorers.Keys) { - if (explorer.Metadata.Disabled) continue; + if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd); if (explorerWindow != null) From 6a3ca108360bc0e84b82c33ba6ba1e3f3d31dbd2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Jul 2025 13:17:24 +0800 Subject: [PATCH 215/243] Check dialog window nullability --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 99069fda818..0b78ac676db 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -728,7 +728,7 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) dialog.Metadata.Disabled) continue; // Plugin is disabled var dialogWindow = _quickSwitchDialogs[dialog]; - if (dialogWindow.Handle == hwnd) + if (dialogWindow != null && dialogWindow.Handle == hwnd) { return dialogWindow; } From f63c8ca8878a6bd38c3e6e8e79920c8abb7b196c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Jul 2025 15:06:57 +0800 Subject: [PATCH 216/243] Improve check path helper function --- .../QuickSwitch/QuickSwitch.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 0b78ac676db..6b513c53355 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -653,6 +653,9 @@ public static async Task JumpToPathAsync(nint hwnd, string path) // Check handle if (hwnd == nint.Zero) return false; + // Check path null or empty + if (string.IsNullOrEmpty(path)) return false; + // Check path if (!CheckPath(path, out var isFile)) return false; @@ -674,6 +677,8 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f { path = _quickSwitchExplorers[_lastExplorer]?.GetExplorerPath(); } + + // Check path null or empty if (string.IsNullOrEmpty(path)) return false; // Check path @@ -690,14 +695,19 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f private static bool CheckPath(string path, out bool file) { file = false; - // Is non-null? - if (string.IsNullOrEmpty(path)) return false; - // Is absolute? - if (!Path.IsPathRooted(path)) return false; + // shell: and shell::: paths + if (path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // file: URI paths + var localPath = path.StartsWith("file:", StringComparison.OrdinalIgnoreCase) + ? new Uri(path).LocalPath + : path; // Is folder? - var isFolder = Directory.Exists(path); + var isFolder = Directory.Exists(localPath); // Is file? - var isFile = File.Exists(path); + var isFile = File.Exists(localPath); file = isFile; return isFolder || isFile; } From 085d625c39334fe72ba5cf6bd9af4cc129b3702b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 17 Jul 2025 15:08:46 +0800 Subject: [PATCH 217/243] Support active explorer path --- .../QuickSwitch/Models/WindowsExplorer.cs | 194 +++++++++++++----- 1 file changed, 146 insertions(+), 48 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs index 20d25c599ac..4c068327b29 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs @@ -1,7 +1,9 @@ using System; using System.Runtime.InteropServices; +using System.Threading; using Flow.Launcher.Plugin; using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; @@ -28,10 +30,10 @@ public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) if (explorer.HWND != hwnd) return true; - explorerWindow = new WindowsExplorerWindow(hwnd, explorer); + explorerWindow = new WindowsExplorerWindow(hwnd); return false; } - catch (COMException) + catch { // Ignored } @@ -42,7 +44,7 @@ public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) return explorerWindow; } - private static unsafe void EnumerateShellWindows(Func action) + internal static unsafe void EnumerateShellWindows(Func action) { // Create an instance of ShellWindows var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass @@ -80,40 +82,119 @@ public class WindowsExplorerWindow : IQuickSwitchExplorerWindow { public IntPtr Handle { get; } - private static IWebBrowser2 _explorerView = null; + private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID; - internal WindowsExplorerWindow(IntPtr handle, IWebBrowser2 explorerView) + internal WindowsExplorerWindow(IntPtr handle) { Handle = handle; - _explorerView = explorerView; } public string GetExplorerPath() { - if (_explorerView == null) return null; + if (Handle == IntPtr.Zero) return null; - object document = null; - try + var activeTabHandle = GetActiveTabHandle(new(Handle)); + if (activeTabHandle.IsNull) return null; + + var window = GetExplorerByTabHandle(activeTabHandle); + if (window == null) return null; + + var path = GetLocation(window); + return path; + } + + public void Dispose() + { + + } + + #region Helper Methods + + // Inspired by: https://github.com/w4po/ExplorerTabUtility + + private static HWND GetActiveTabHandle(HWND windowHandle) + { + // Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass. + var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null); + return activeTab; + } + + private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle) + { + if (tabHandle.IsNull) return null; + + IWebBrowser2 window = null; + WindowsExplorer.EnumerateShellWindows((shellWindow) => { - if (_explorerView != null) + try { - // Use dynamic here because using IWebBrower2.Document can cause exception here: - // System.Runtime.InteropServices.InvalidOleVariantTypeException: 'Specified OLE variant is invalid.' - dynamic explorerView = _explorerView; - document = explorerView.Document; + return StartSTAThread(() => + { + if (shellWindow is not IWebBrowser2 explorer) return true; + + if (explorer is not IServiceProvider sp) return true; + + sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser); + if (shellBrowser == null) return true; + + try + { + shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd + + if (hWnd == tabHandle) + { + window = explorer; + return false; + } + } + catch + { + // Ignored + } + finally + { + Marshal.ReleaseComObject(shellBrowser); + } + + return true; + }) ?? true; + } + catch + { + // Ignored } - } - catch (COMException) - { - return null; - } - if (document is not IShellFolderViewDual2 folderView) + return true; + }); + + return window; + } + + private static bool? StartSTAThread(Func action) + { + bool? result = null; + var thread = new Thread(() => { - return null; - } + result = action(); + }) + { + IsBackground = true + }; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + return result; + } - string path; + private static string GetLocation(IWebBrowser2 window) + { + var path = window.LocationURL.ToString(); + if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path); + + // Recycle Bin, This PC, etc + if (window.Document is not IShellFolderViewDual folderView) return null; + + // Attempt to get the path from the folder view try { // CSWin32 Folder does not have Self, so we need to use dynamic type here @@ -123,40 +204,57 @@ public string GetExplorerPath() // Access the Self property via dynamic binding dynamic folderItem = folder.Self; - // Check if the item is part of the file system - if (folderItem != null && folderItem.IsFileSystem) - { - path = folderItem.Path; - } - else - { - // Handle non-file system paths (e.g., virtual folders) - path = string.Empty; - } + // Get path from the folder item + path = folderItem.Path; } catch { return null; } - return path; + return NormalizeLocation(path); } - public void Dispose() + private static string NormalizeLocation(string location) { - // Release ComObjects - try - { - if (_explorerView != null) - { - Marshal.ReleaseComObject(_explorerView); - _explorerView = null; - } - } - catch (COMException) - { - _explorerView = null; - } + if (location.IndexOf('%') > -1) + location = Environment.ExpandEnvironmentVariables(location); + + if (location.StartsWith("::", StringComparison.Ordinal)) + location = $"shell:{location}"; + + else if (location.StartsWith("{", StringComparison.Ordinal)) + location = $"shell:::{location}"; + + location = location.Trim(' ', '/', '\\', '\n', '\'', '"'); + + return location.Replace('/', '\\'); } + + #endregion } + + #region COM Interfaces + + // Inspired by: https://github.com/w4po/ExplorerTabUtility + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")] + [ComImport] + public interface IServiceProvider + { + [PreserveSig] + int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject); + } + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214E2-0000-0000-C000-000000000046")] + [ComImport] + public interface IShellBrowser + { + [PreserveSig] + int GetWindow(out nint handle); + } + + #endregion } From dfae92cd1044b71cbb5255b60105f18ba21d8b0c Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Thu, 17 Jul 2025 19:12:32 +0800 Subject: [PATCH 218/243] Improve translation Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 9303ad55821..27757721f00 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -380,7 +380,7 @@ Fixed under dialogs. Displayed after dialogs are created and until it is closed Floating as search window. Displayed when activated like search window Quick Switch Result Navigation Behaviour - Behaviour to navigate file dialogs to paths of the results + Behaviour to navigate file dialog to the result path Left click or Enter key Right click Quick Switch File Navigation Behaviour From 170c085aa3e09598c9f42df6423e1a9109f174c0 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Thu, 17 Jul 2025 19:12:46 +0800 Subject: [PATCH 219/243] Improve translation Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 27757721f00..2d8911673ea 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -384,7 +384,7 @@ Left click or Enter key Right click Quick Switch File Navigation Behaviour - Behaviour to navigate file dialogs when paths of the results are files + Behaviour to navigate file dialog when the results are file paths Fill full path in file name box Fill full path in file name box and open Fill directory in path box From 8af41b6014b1723ee9922b0df077b4ccb114b9e7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 12:55:19 +0800 Subject: [PATCH 220/243] Make sure quick switch state reset --- Flow.Launcher/ViewModel/MainViewModel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1d49fde88f3..7fdff107ca9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1936,11 +1936,16 @@ public async Task SetupQuickSwitchAsync(nint handle) public async void ResetQuickSwitch() { - if (DialogWindowHandle == nint.Zero) return; + // Cache original dialog window handle + var dialogWindowHandle = DialogWindowHandle; + // Reset the quick switch state DialogWindowHandle = nint.Zero; _isQuickSwitch = false; + // If dialog window handle is not set, we should not reset the main window visibility + if (dialogWindowHandle == nint.Zero) return; + if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus) { // Show or hide to change visibility From e50fd8049d13e8ddb64ecdf7ff274651ff2c88ca Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 13:37:26 +0800 Subject: [PATCH 221/243] Clear quick switch state if handle is cleare --- Flow.Launcher/ViewModel/MainViewModel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7fdff107ca9..a279fab9755 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1862,8 +1862,12 @@ public async Task SetupQuickSwitchAsync(nint handle) await Task.Delay(300); } - // If handle is cleared, which means the dialog is closed, do nothing - if (DialogWindowHandle == nint.Zero) return; + // If handle is cleared, which means the dialog is closed, clear quick switch state + if (DialogWindowHandle == nint.Zero) + { + _isQuickSwitch = false; + return; + } // Initialize quick switch window if (MainWindowVisibilityStatus) From b3c04c7c73c47dc90f4d12046a8570c6d51afbfa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 15:45:55 +0800 Subject: [PATCH 222/243] Add test logging --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6b513c53355..efaa7bfdb43 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -619,6 +619,8 @@ private static void DestroyChangeCallback( uint dwmsEventTime ) { + // TODO: Remove this logging + Log.Debug(ClassName, $"Destorying dialog: {hwnd}"); // If the dialog window is destroyed, set _dialogWindowHandle to null var dialogWindowExist = false; lock (_dialogWindowLock) From ebf0f60c4863d420325b0753bf47076ef6df2b2a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 16:41:23 +0800 Subject: [PATCH 223/243] Remove logging --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index efaa7bfdb43..6b513c53355 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -619,8 +619,6 @@ private static void DestroyChangeCallback( uint dwmsEventTime ) { - // TODO: Remove this logging - Log.Debug(ClassName, $"Destorying dialog: {hwnd}"); // If the dialog window is destroyed, set _dialogWindowHandle to null var dialogWindowExist = false; lock (_dialogWindowLock) From a797d5aec6541b504bbb52c5e5a7b2d6e1507b76 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 17:42:37 +0800 Subject: [PATCH 224/243] Add more event hooks --- .../NativeMethods.txt | 2 + .../QuickSwitch/QuickSwitch.cs | 116 +++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 757200d9053..965ab6caa8e 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -88,3 +88,5 @@ BM_CLICK WM_GETTEXT OpenProcess QueryFullProcessImageName +EVENT_OBJECT_HIDE +EVENT_SYSTEM_DIALOGEND diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index 6b513c53355..c06f5d39877 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -77,10 +77,14 @@ public static class QuickSwitch private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _hideChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _dialogEndChangeHook = HWINEVENTHOOK.Null; private static readonly WINEVENTPROC _fgProc = ForegroundChangeCallback; private static readonly WINEVENTPROC _locProc = LocationChangeCallback; private static readonly WINEVENTPROC _desProc = DestroyChangeCallback; + private static readonly WINEVENTPROC _hideProc = HideChangeCallback; + private static readonly WINEVENTPROC _dialogEndProc = DialogEndChangeCallback; private static DispatcherTimer _dragMoveTimer = null; @@ -166,6 +170,16 @@ public static void SetupQuickSwitch(bool enabled) PInvoke.UnhookWinEvent(_destroyChangeHook); _destroyChangeHook = HWINEVENTHOOK.Null; } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } // Hook events _foregroundChangeHook = PInvoke.SetWinEventHook( @@ -192,10 +206,28 @@ public static void SetupQuickSwitch(bool enabled) 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT); + _hideChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_HIDE, + PInvoke.EVENT_OBJECT_HIDE, + PInvoke.GetModuleHandle((PCWSTR)null), + _hideProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _dialogEndChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_DIALOGEND, + PInvoke.EVENT_SYSTEM_DIALOGEND, + PInvoke.GetModuleHandle((PCWSTR)null), + _dialogEndProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); if (_foregroundChangeHook.IsNull || _locationChangeHook.IsNull || - _destroyChangeHook.IsNull) + _destroyChangeHook.IsNull || + _hideChangeHook.IsNull || + _dialogEndChangeHook.IsNull) { Log.Error(ClassName, "Failed to enable QuickSwitch"); return; @@ -248,6 +280,16 @@ public static void SetupQuickSwitch(bool enabled) PInvoke.UnhookWinEvent(_destroyChangeHook); _destroyChangeHook = HWINEVENTHOOK.Null; } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } // Stop drag move timer _dragMoveTimer?.Stop(); @@ -640,6 +682,68 @@ uint dwmsEventTime } } + private static void HideChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is hidden, set _dialogWindowHandle to null + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + Log.Debug(ClassName, $"Hide dialog: {hwnd}"); + _dialogWindow = null; + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } + InvokeResetQuickSwitchWindow(); + } + } + + private static void DialogEndChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is ended, set _dialogWindowHandle to null + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + Log.Debug(ClassName, $"Dialog end: {hwnd}"); + _dialogWindow = null; + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } + InvokeResetQuickSwitchWindow(); + } + } + #endregion #endregion @@ -892,6 +996,16 @@ public static void Dispose() PInvoke.UnhookWinEvent(_destroyChangeHook); _destroyChangeHook = HWINEVENTHOOK.Null; } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } // Dispose explorers foreach (var explorer in _quickSwitchExplorers.Keys) From 24e46f52078bc8c515e771fd84a8c17521b87698 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 17:43:27 +0800 Subject: [PATCH 225/243] Improve logging --- Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index c06f5d39877..a434a973e81 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -729,7 +729,7 @@ uint dwmsEventTime { if (_dialogWindow != null && _dialogWindow.Handle == hwnd) { - Log.Debug(ClassName, $"Dialog end: {hwnd}"); + Log.Debug(ClassName, $"End dialog: {hwnd}"); _dialogWindow = null; dialogWindowExist = true; } From fd3c658206c40695de146a0d7385ea369b862f29 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 18:21:35 +0800 Subject: [PATCH 226/243] Improve quick switch window display effect --- Flow.Launcher/ViewModel/MainViewModel.cs | 50 ++++++++++++------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a279fab9755..4c1f804c262 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1887,12 +1887,14 @@ public async Task SetupQuickSwitchAsync(nint handle) { if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) { - Show(); - + // We wait for window to be reset before showing it because if window has results, + // showing it before resetting will cause flickering when results are clearing if (dialogWindowHandleChanged) { - _ = ResetWindowAsync(); + await ResetWindowAsync(); } + + Show(); } else { @@ -1912,27 +1914,27 @@ public async Task SetupQuickSwitchAsync(nint handle) _quickSwitchSource = new CancellationTokenSource(); _ = Task.Run(() => + { + try { - try - { - // Check task cancellation - if (_quickSwitchSource.Token.IsCancellationRequested) return; + // Check task cancellation + if (_quickSwitchSource.Token.IsCancellationRequested) return; - // Check dialog handle - if (DialogWindowHandle == nint.Zero) return; + // Check dialog handle + if (DialogWindowHandle == nint.Zero) return; - // Wait 150ms to check if quick switch window gets the focus - var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); - if (timeOut) return; + // Wait 150ms to check if quick switch window gets the focus + var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); + if (timeOut) return; - // Bring focus back to the the dialog - Win32Helper.SetForegroundWindow(DialogWindowHandle); - } - catch (Exception e) - { - App.API.LogException(ClassName, "Failed to focus on dialog window", e); - } - }); + // Bring focus back to the the dialog + Win32Helper.SetForegroundWindow(DialogWindowHandle); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to focus on dialog window", e); + } + }); } } @@ -1952,17 +1954,17 @@ public async void ResetQuickSwitch() if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus) { + // We wait for window to be reset before showing it because if window has results, + // showing it before resetting will cause flickering when results are clearing + await ResetWindowAsync(); + // Show or hide to change visibility if (_previousMainWindowVisibilityStatus) { Show(); - - _ = ResetWindowAsync(); } else { - await ResetWindowAsync(); - Hide(false); } } From 653097ec5885d08eb5914ad25f5d319c08f13226 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 18 Jul 2025 19:16:38 +0800 Subject: [PATCH 227/243] Add try-catch sentence --- .../QuickSwitch/QuickSwitch.cs | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs index a434a973e81..222466c42ef 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs @@ -799,21 +799,29 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f private static bool CheckPath(string path, out bool file) { file = false; - // shell: and shell::: paths - if (path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase)) + try + { + // shell: and shell::: paths + if (path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // file: URI paths + var localPath = path.StartsWith("file:", StringComparison.OrdinalIgnoreCase) + ? new Uri(path).LocalPath + : path; + // Is folder? + var isFolder = Directory.Exists(localPath); + // Is file? + var isFile = File.Exists(localPath); + file = isFile; + return isFolder || isFile; + } + catch (System.Exception e) { - return true; + Log.Exception(ClassName, "Failed to check path", e); + return false; } - // file: URI paths - var localPath = path.StartsWith("file:", StringComparison.OrdinalIgnoreCase) - ? new Uri(path).LocalPath - : path; - // Is folder? - var isFolder = Directory.Exists(localPath); - // Is file? - var isFile = File.Exists(localPath); - file = isFile; - return isFolder || isFile; } private static IQuickSwitchDialogWindowTab GetDialogWindowTab(HWND hwnd) From 978f971d8de7a68f9bef65676941ad380bfc789e Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:07:57 +0800 Subject: [PATCH 228/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 444ea2d60b4..2a54aa1477d 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -378,7 +378,7 @@ Quick Switch Window Position Select position for quick switch window Fixed under dialogs. Displayed after dialogs are created and until it is closed - Floating as search window. Displayed when activated like search window + Default search window position. Displayed when triggered by search window hotkey Quick Switch Result Navigation Behaviour Behaviour to navigate file dialog to the result path Left click or Enter key From c6e7a41663c1bc1ecba96cbb4b6bb8693345cc7b Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:08:06 +0800 Subject: [PATCH 229/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 2a54aa1477d..5abfeb6daeb 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -374,7 +374,7 @@ Quick Switch Automatically Automatically navigate to the path of the current file manager when a file dialog is opened. (Experimental) Show Quick Switch Window - Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog). + Display quick switch search window when the open/save dialog is shown to quickly navigate to file/folder locations. Quick Switch Window Position Select position for quick switch window Fixed under dialogs. Displayed after dialogs are created and until it is closed From deb3b15720142c42a3ce7aeaffd3c348b8323764 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:08:16 +0800 Subject: [PATCH 230/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 5abfeb6daeb..a55f8245455 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -380,7 +380,7 @@ Fixed under dialogs. Displayed after dialogs are created and until it is closed Default search window position. Displayed when triggered by search window hotkey Quick Switch Result Navigation Behaviour - Behaviour to navigate file dialog to the result path + Behaviour to navigate Open/Save As dialog to the selected result path Left click or Enter key Right click Quick Switch File Navigation Behaviour From 320cb955c4281ccd95c5723f774091bc34410983 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:08:33 +0800 Subject: [PATCH 231/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index a55f8245455..b37612b168b 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -370,7 +370,7 @@ Quick Switch Enter shortcut to quickly navigate the path of a file dialog to the path of the current file manager. Quick Switch - Quickly navigate to the path of the current file manager when a file dialog is opened. + When Open/Save As dialog opens, quickly navigate to the current path of the file manager. Quick Switch Automatically Automatically navigate to the path of the current file manager when a file dialog is opened. (Experimental) Show Quick Switch Window From f61991c90f7abed470a256f4f22411590d6c3dc8 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:08:43 +0800 Subject: [PATCH 232/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index b37612b168b..9ceeb69b490 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -384,7 +384,7 @@ Left click or Enter key Right click Quick Switch File Navigation Behaviour - Behaviour to navigate file dialog when the results are file paths + Behaviour to navigate Open/Save As dialog when the result is a file path Fill full path in file name box Fill full path in file name box and open Fill directory in path box From db9979f498a890fd1b7fac60b1dd3701d3ebe79d Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:08:53 +0800 Subject: [PATCH 233/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 9ceeb69b490..283271d688c 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -377,7 +377,7 @@ Display quick switch search window when the open/save dialog is shown to quickly navigate to file/folder locations. Quick Switch Window Position Select position for quick switch window - Fixed under dialogs. Displayed after dialogs are created and until it is closed + Fixed under the Open/Save As dialog. Displayed on dialog open and stays until dialogue is closed Default search window position. Displayed when triggered by search window hotkey Quick Switch Result Navigation Behaviour Behaviour to navigate Open/Save As dialog to the selected result path From 434df2e57e9bd692d23d242f0dbc7c47e6f37a0e Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:09:07 +0800 Subject: [PATCH 234/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 283271d688c..29e8ed0067c 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -368,7 +368,7 @@ Show Result Badges for Global Query Only Show badges for global query results only Quick Switch - Enter shortcut to quickly navigate the path of a file dialog to the path of the current file manager. + Enter shortcut to quickly navigate the Open/Save As dialogue to the path of the current file manager. Quick Switch When Open/Save As dialog opens, quickly navigate to the current path of the file manager. Quick Switch Automatically From ade924bf75e31c994cf3fea78a5a401e1f7af56e Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sat, 19 Jul 2025 16:09:16 +0800 Subject: [PATCH 235/243] Update translations Co-authored-by: Jeremy Wu --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 29e8ed0067c..57375696720 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -372,7 +372,7 @@ Quick Switch When Open/Save As dialog opens, quickly navigate to the current path of the file manager. Quick Switch Automatically - Automatically navigate to the path of the current file manager when a file dialog is opened. (Experimental) + When Open/Save As dialog is displayed, automatically navigate to the path of the current file manager. (Experimental) Show Quick Switch Window Display quick switch search window when the open/save dialog is shown to quickly navigate to file/folder locations. Quick Switch Window Position From 608449307d0def1b7350c8dc36bcbb6436f71dc1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 20 Jul 2025 20:19:50 +1000 Subject: [PATCH 236/243] update comments, code references and file names strings --- Flow.Launcher.Core/Plugin/PluginManager.cs | 48 ++-- .../DialogJump.cs} | 208 +++++++++--------- .../DialogJumpPair.cs} | 14 +- .../Models/WindowsDialog.cs | 18 +- .../Models/WindowsExplorer.cs | 12 +- .../UserSettings/Settings.cs | 24 +- ...ickSwitchResult.cs => DialogJumpResult.cs} | 24 +- ...syncQuickSwitch.cs => IAsyncDialogJump.cs} | 10 +- .../{IQuickSwitch.cs => IDialogJump.cs} | 12 +- ...ckSwitchDialog.cs => IDialogJumpDialog.cs} | 16 +- ...itchExplorer.cs => IDialogJumpExplorer.cs} | 10 +- Flow.Launcher/App.xaml.cs | 8 +- Flow.Launcher/Helper/HotKeyMapper.cs | 6 +- Flow.Launcher/HotkeyControl.xaml.cs | 10 +- Flow.Launcher/Languages/en.xaml | 42 ++-- Flow.Launcher/MainWindow.xaml.cs | 46 ++-- .../SettingsPaneGeneralViewModel.cs | 44 ++-- .../ViewModels/SettingsPaneHotkeyViewModel.cs | 8 +- .../Views/SettingsPaneGeneral.xaml | 44 ++-- .../Views/SettingsPaneHotkey.xaml | 10 +- Flow.Launcher/ViewModel/MainViewModel.cs | 96 ++++---- Plugins/Flow.Launcher.Plugin.Explorer/Main.cs | 12 +- 22 files changed, 361 insertions(+), 361 deletions(-) rename Flow.Launcher.Infrastructure/{QuickSwitch/QuickSwitch.cs => DialogJump/DialogJump.cs} (81%) rename Flow.Launcher.Infrastructure/{QuickSwitch/QuickSwitchPair.cs => DialogJump/DialogJumpPair.cs} (75%) rename Flow.Launcher.Infrastructure/{QuickSwitch => DialogJump}/Models/WindowsDialog.cs (94%) rename Flow.Launcher.Infrastructure/{QuickSwitch => DialogJump}/Models/WindowsExplorer.cs (95%) rename Flow.Launcher.Plugin/{QuickSwitchResult.cs => DialogJumpResult.cs} (81%) rename Flow.Launcher.Plugin/Interfaces/{IAsyncQuickSwitch.cs => IAsyncDialogJump.cs} (64%) rename Flow.Launcher.Plugin/Interfaces/{IQuickSwitch.cs => IDialogJump.cs} (53%) rename Flow.Launcher.Plugin/Interfaces/{IQuickSwitchDialog.cs => IDialogJumpDialog.cs} (85%) rename Flow.Launcher.Plugin/Interfaces/{IQuickSwitchExplorer.cs => IDialogJumpExplorer.cs} (76%) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 09b0742aae4..c877894653f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -9,7 +9,7 @@ using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -41,8 +41,8 @@ public static class PluginManager private static IEnumerable _resultUpdatePlugin; private static IEnumerable _translationPlugins; - private static readonly List _quickSwitchExplorerPlugins = new(); - private static readonly List _quickSwitchDialogPlugins = new(); + private static readonly List _dialogJumpExplorerPlugins = new(); + private static readonly List _dialogJumpDialogPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -191,20 +191,20 @@ public static void LoadPlugins(PluginsSettings settings) _resultUpdatePlugin = GetPluginsForInterface(); _translationPlugins = GetPluginsForInterface(); - // Initialize quick switch plugin pairs - foreach (var pair in GetPluginsForInterface()) + // Initialize dialog jump plugin pairs + foreach (var pair in GetPluginsForInterface()) { - _quickSwitchExplorerPlugins.Add(new QuickSwitchExplorerPair + _dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair { - Plugin = (IQuickSwitchExplorer)pair.Plugin, + Plugin = (IDialogJumpExplorer)pair.Plugin, Metadata = pair.Metadata }); } - foreach (var pair in GetPluginsForInterface()) + foreach (var pair in GetPluginsForInterface()) { - _quickSwitchDialogPlugins.Add(new QuickSwitchDialogPair + _dialogJumpDialogPlugins.Add(new DialogJumpDialogPair { - Plugin = (IQuickSwitchDialog)pair.Plugin, + Plugin = (IDialogJumpDialog)pair.Plugin, Metadata = pair.Metadata }); } @@ -310,20 +310,20 @@ public static async Task InitializePluginsAsync() } } - public static ICollection ValidPluginsForQuery(Query query, bool quickSwitch) + public static ICollection ValidPluginsForQuery(Query query, bool dialogJump) { if (query is null) return Array.Empty(); if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { - if (quickSwitch) - return GlobalPlugins.Where(p => p.Plugin is IAsyncQuickSwitch && !PluginModified(p.Metadata.ID)).ToList(); + if (dialogJump) + return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); else return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } - if (quickSwitch && plugin.Plugin is not IAsyncQuickSwitch) + if (dialogJump && plugin.Plugin is not IAsyncDialogJump) return Array.Empty(); if (API.PluginModified(plugin.Metadata.ID)) @@ -413,16 +413,16 @@ public static async Task> QueryHomeForPluginAsync(PluginPair pair, } return results; } - - public static async Task> QueryQuickSwitchForPluginAsync(PluginPair pair, Query query, CancellationToken token) + + public static async Task> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token) { - var results = new List(); + var results = new List(); var metadata = pair.Metadata; try { var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", - async () => results = await ((IAsyncQuickSwitch)pair.Plugin).QueryQuickSwitchAsync(query, token).ConfigureAwait(false)); + async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false)); token.ThrowIfCancellationRequested(); if (results == null) @@ -438,7 +438,7 @@ public static async Task> QueryQuickSwitchForPluginAsync } catch (Exception e) { - API.LogException(ClassName, $"Failed to query quick switch for plugin: {metadata.Name}", e); + API.LogException(ClassName, $"Failed to query dialog jump for plugin: {metadata.Name}", e); return null; } return results; @@ -519,14 +519,14 @@ public static bool IsHomePlugin(string id) return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } - public static IList GetQuickSwitchExplorers() + public static IList GetDialogJumpExplorers() { - return _quickSwitchExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } - public static IList GetQuickSwitchDialogs() + public static IList GetDialogJumpDialogs() { - return _quickSwitchDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } public static bool ActionKeywordRegistered(string actionKeyword) diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs similarity index 81% rename from Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs rename to Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 222466c42ef..6744e7dced9 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -6,7 +6,7 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch.Models; +using Flow.Launcher.Infrastructure.DialogJump.Models; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using NHotkey; @@ -14,38 +14,38 @@ using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; -namespace Flow.Launcher.Infrastructure.QuickSwitch +namespace Flow.Launcher.Infrastructure.DialogJump { - public static class QuickSwitch + public static class DialogJump { #region Public Properties - public static Func ShowQuickSwitchWindowAsync { get; set; } = null; + public static Func ShowDialogJumpWindowAsync { get; set; } = null; - public static Action UpdateQuickSwitchWindow { get; set; } = null; + public static Action UpdateDialogJumpWindow { get; set; } = null; - public static Action ResetQuickSwitchWindow { get; set; } = null; + public static Action ResetDialogJumpWindow { get; set; } = null; - public static Action HideQuickSwitchWindow { get; set; } = null; + public static Action HideDialogJumpWindow { get; set; } = null; - public static QuickSwitchWindowPositions QuickSwitchWindowPosition { get; private set; } + public static DialogJumpWindowPositions DialogJumpWindowPosition { get; private set; } - public static QuickSwitchExplorerPair WindowsQuickSwitchExplorer { get; } = new() + public static DialogJumpExplorerPair WindowsDialogJumpExplorer { get; } = new() { Metadata = new() { - ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the quick switch pairs - Disabled = false // Disabled is for enabling the Windows QuickSwitch explorers & dialogs + ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the dialog jump pairs + Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs }, Plugin = new WindowsExplorer() }; - public static QuickSwitchDialogPair WindowsQuickSwitchDialog { get; } = new() + public static DialogJumpDialogPair WindowsDialogJumpDialog { get; } = new() { Metadata = new() { - ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the quick switch pairs - Disabled = false // Disabled is for enabling the Windows QuickSwitch explorers & dialogs + ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the dialog jump pairs + Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs }, Plugin = new WindowsDialog() }; @@ -54,7 +54,7 @@ public static class QuickSwitch #region Private Fields - private static readonly string ClassName = nameof(QuickSwitch); + private static readonly string ClassName = nameof(DialogJump); private static readonly Settings _settings = Ioc.Default.GetRequiredService(); @@ -64,14 +64,14 @@ public static class QuickSwitch private static HWND _mainWindowHandle = HWND.Null; - private static readonly Dictionary _quickSwitchExplorers = new(); + private static readonly Dictionary _dialogJumpExplorers = new(); - private static QuickSwitchExplorerPair _lastExplorer = null; + private static DialogJumpExplorerPair _lastExplorer = null; private static readonly object _lastExplorerLock = new(); - private static readonly Dictionary _quickSwitchDialogs = new(); + private static readonly Dictionary _dialogJumpDialogs = new(); - private static IQuickSwitchDialogWindow _dialogWindow = null; + private static IDialogJumpDialogWindow _dialogWindow = null; private static readonly object _dialogWindowLock = new(); private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; @@ -105,21 +105,21 @@ public static class QuickSwitch #region Initialize & Setup - public static void InitializeQuickSwitch(IList quickSwitchExplorers, - IList quickSwitchDialogs) + public static void InitializeDialogJump(IList dialogJumpExplorers, + IList dialogJumpDialogs) { if (_initialized) return; - // Initialize quick switch explorers & dialogs - _quickSwitchExplorers.Add(WindowsQuickSwitchExplorer, null); - foreach (var explorer in quickSwitchExplorers) + // Initialize dialog jump explorers & dialogs + _dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); + foreach (var explorer in dialogJumpExplorers) { - _quickSwitchExplorers.Add(explorer, null); + _dialogJumpExplorers.Add(explorer, null); } - _quickSwitchDialogs.Add(WindowsQuickSwitchDialog, null); - foreach (var dialog in quickSwitchDialogs) + _dialogJumpDialogs.Add(WindowsDialogJumpDialog, null); + foreach (var dialog in dialogJumpDialogs) { - _quickSwitchDialogs.Add(dialog, null); + _dialogJumpDialogs.Add(dialog, null); } // Initialize main window handle @@ -127,15 +127,15 @@ public static void InitializeQuickSwitch(IList quickSwi // Initialize timer _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; - _dragMoveTimer.Tick += (s, e) => InvokeUpdateQuickSwitchWindow(); + _dragMoveTimer.Tick += (s, e) => InvokeUpdateDialogJumpWindow(); - // Initialize quick switch window position - QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; + // Initialize dialog jump window position + DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; _initialized = true; } - public static void SetupQuickSwitch(bool enabled) + public static void SetupDialogJump(bool enabled) { if (enabled == _enabled) return; @@ -229,22 +229,22 @@ public static void SetupQuickSwitch(bool enabled) _hideChangeHook.IsNull || _dialogEndChangeHook.IsNull) { - Log.Error(ClassName, "Failed to enable QuickSwitch"); + Log.Error(ClassName, "Failed to enable DialogJump"); return; } } else { // Remove explorer windows - foreach (var explorer in _quickSwitchExplorers.Keys) + foreach (var explorer in _dialogJumpExplorers.Keys) { - _quickSwitchExplorers[explorer] = null; + _dialogJumpExplorers[explorer] = null; } // Remove dialog windows - foreach (var dialog in _quickSwitchDialogs.Keys) + foreach (var dialog in _dialogJumpDialogs.Keys) { - _quickSwitchDialogs[dialog] = null; + _dialogJumpDialogs[dialog] = null; } // Remove dialog window handle @@ -294,10 +294,10 @@ public static void SetupQuickSwitch(bool enabled) // Stop drag move timer _dragMoveTimer?.Stop(); - // Reset quick switch window + // Reset dialog jump window if (dialogWindowExists) { - InvokeResetQuickSwitchWindow(); + InvokeResetDialogJumpWindow(); } } @@ -313,7 +313,7 @@ private static bool RefreshLastExplorer() // Enum windows from the top to the bottom PInvoke.EnumWindows((hWnd, _) => { - foreach (var explorer in _quickSwitchExplorers.Keys) + foreach (var explorer in _dialogJumpExplorers.Keys) { if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled @@ -321,7 +321,7 @@ private static bool RefreshLastExplorer() var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); if (explorerWindow != null) { - _quickSwitchExplorers[explorer] = explorerWindow; + _dialogJumpExplorers[explorer] = explorerWindow; _lastExplorer = explorer; found = true; return false; @@ -342,7 +342,7 @@ private static bool RefreshLastExplorer() public static string GetActiveExplorerPath() { - return RefreshLastExplorer() ? _quickSwitchExplorers[_lastExplorer].GetExplorerPath() : string.Empty; + return RefreshLastExplorer() ? _dialogJumpExplorers[_lastExplorer].GetExplorerPath() : string.Empty; } #endregion @@ -351,30 +351,30 @@ public static string GetActiveExplorerPath() #region Invoke Property Events - private static async Task InvokeShowQuickSwitchWindowAsync(bool dialogWindowChanged) + private static async Task InvokeShowDialogJumpWindowAsync(bool dialogWindowChanged) { - // Show quick switch window - if (_settings.ShowQuickSwitchWindow) + // Show dialog jump window + if (_settings.ShowDialogJumpWindow) { - // Save quick switch window position for one file dialog + // Save dialog jump window position for one file dialog if (dialogWindowChanged) { - QuickSwitchWindowPosition = _settings.QuickSwitchWindowPosition; + DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; } - // Call show quick switch window - IQuickSwitchDialogWindow dialogWindow; + // Call show dialog jump window + IDialogJumpDialogWindow dialogWindow; lock (_dialogWindowLock) { dialogWindow = _dialogWindow; } - if (dialogWindow != null && ShowQuickSwitchWindowAsync != null) + if (dialogWindow != null && ShowDialogJumpWindowAsync != null) { - await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle); + await ShowDialogJumpWindowAsync.Invoke(dialogWindow.Handle); } - // Hook move size event if quick switch window is under dialog & dialog window changed - if (QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + // Hook move size event if dialog jump window is under dialog & dialog window changed + if (DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { if (dialogWindowChanged) { @@ -416,20 +416,20 @@ static unsafe void SetMoveProc(HWND handle) } } - private static void InvokeUpdateQuickSwitchWindow() + private static void InvokeUpdateDialogJumpWindow() { - UpdateQuickSwitchWindow?.Invoke(); + UpdateDialogJumpWindow?.Invoke(); } - private static void InvokeResetQuickSwitchWindow() + private static void InvokeResetDialogJumpWindow() { lock (_dialogWindowLock) { _dialogWindow = null; } - // Reset quick switch window - ResetQuickSwitchWindow?.Invoke(); + // Reset dialog jump window + ResetDialogJumpWindow?.Invoke(); // Stop drag move timer _dragMoveTimer?.Stop(); @@ -442,10 +442,10 @@ private static void InvokeResetQuickSwitchWindow() } } - private static void InvokeHideQuickSwitchWindow() + private static void InvokeHideDialogJumpWindow() { - // Hide quick switch window - HideQuickSwitchWindow?.Invoke(); + // Hide dialog jump window + HideDialogJumpWindow?.Invoke(); // Stop drag move timer _dragMoveTimer?.Stop(); @@ -481,13 +481,13 @@ uint dwmsEventTime // Check if it is a file dialog window var isDialogWindow = false; var dialogWindowChanged = false; - foreach (var dialog in _quickSwitchDialogs.Keys) + foreach (var dialog in _dialogJumpDialogs.Keys) { if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled - IQuickSwitchDialogWindow dialogWindow; - var existingDialogWindow = _quickSwitchDialogs[dialog]; + IDialogJumpDialogWindow dialogWindow; + var existingDialogWindow = _dialogJumpDialogs[dialog]; if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) { // If the dialog window is already in the list, no need to check again @@ -517,7 +517,7 @@ uint dwmsEventTime { Log.Debug(ClassName, $"Dialog Window: {hwnd}"); // Navigate to path - if (_settings.AutoQuickSwitch) + if (_settings.AutoDialogJump) { // Check if we have already switched for this dialog bool alreadySwitched; @@ -526,26 +526,26 @@ uint dwmsEventTime alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); } - // Just show quick switch window + // Just show dialog jump window if (alreadySwitched) { - await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); } - // Show quick switch window after navigating the path + // Show dialog jump window after navigating the path else { if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) { - await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); } } } else { - await InvokeShowQuickSwitchWindowAsync(dialogWindowChanged); + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); } } - // Quick switch window + // Dialog jump window else if (hwnd == _mainWindowHandle) { Log.Debug(ClassName, $"Main Window: {hwnd}"); @@ -562,10 +562,10 @@ uint dwmsEventTime dialogWindowExist = true; } } - if (dialogWindowExist) // Neither quick switch window nor file dialog window is foreground + if (dialogWindowExist) // Neither dialog jump window nor file dialog window is foreground { - // Hide quick switch window until the file dialog window is brought to the foreground - InvokeHideQuickSwitchWindow(); + // Hide dialog jump window until the file dialog window is brought to the foreground + InvokeHideDialogJumpWindow(); } // Check if there are foreground explorer windows @@ -573,7 +573,7 @@ uint dwmsEventTime { lock (_lastExplorerLock) { - foreach (var explorer in _quickSwitchExplorers.Keys) + foreach (var explorer in _dialogJumpExplorers.Keys) { if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled @@ -582,7 +582,7 @@ uint dwmsEventTime if (explorerWindow != null) { Log.Debug(ClassName, $"Explorer window: {hwnd}"); - _quickSwitchExplorers[explorer] = explorerWindow; + _dialogJumpExplorers[explorer] = explorerWindow; _lastExplorer = explorer; break; } @@ -611,7 +611,7 @@ private static void LocationChangeCallback( uint dwmsEventTime ) { - // If the dialog window is moved, update the quick switch window position + // If the dialog window is moved, update the dialog jump window position var dialogWindowExist = false; lock (_dialogWindowLock) { @@ -622,7 +622,7 @@ uint dwmsEventTime } if (dialogWindowExist) { - InvokeUpdateQuickSwitchWindow(); + InvokeUpdateDialogJumpWindow(); } } @@ -636,7 +636,7 @@ private static void MoveSizeCallBack( uint dwmsEventTime ) { - // If the dialog window is moved or resized, update the quick switch window position + // If the dialog window is moved or resized, update the dialog jump window position if (_dragMoveTimer != null) { switch (eventType) @@ -678,7 +678,7 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - InvokeResetQuickSwitchWindow(); + InvokeResetDialogJumpWindow(); } } @@ -709,7 +709,7 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - InvokeResetQuickSwitchWindow(); + InvokeResetDialogJumpWindow(); } } @@ -740,7 +740,7 @@ uint dwmsEventTime { _autoSwitchedDialogs.Remove(hwnd); } - InvokeResetQuickSwitchWindow(); + InvokeResetDialogJumpWindow(); } } @@ -779,7 +779,7 @@ private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = f string path; lock (_lastExplorerLock) { - path = _quickSwitchExplorers[_lastExplorer]?.GetExplorerPath(); + path = _dialogJumpExplorers[_lastExplorer]?.GetExplorerPath(); } // Check path null or empty @@ -824,7 +824,7 @@ private static bool CheckPath(string path, out bool file) } } - private static IQuickSwitchDialogWindowTab GetDialogWindowTab(HWND hwnd) + private static IDialogJumpDialogWindowTab GetDialogWindowTab(HWND hwnd) { var dialogWindow = GetDialogWindow(hwnd); if (dialogWindow == null) return null; @@ -832,7 +832,7 @@ private static IQuickSwitchDialogWindowTab GetDialogWindowTab(HWND hwnd) return dialogWindowTab; } - private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) + private static IDialogJumpDialogWindow GetDialogWindow(HWND hwnd) { // First check dialog window lock (_dialogWindowLock) @@ -844,12 +844,12 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } // Then check all dialog windows - foreach (var dialog in _quickSwitchDialogs.Keys) + foreach (var dialog in _dialogJumpDialogs.Keys) { if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled - var dialogWindow = _quickSwitchDialogs[dialog]; + var dialogWindow = _dialogJumpDialogs[dialog]; if (dialogWindow != null && dialogWindow.Handle == hwnd) { return dialogWindow; @@ -857,13 +857,13 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) } // Finally search for the dialog window again - foreach (var dialog in _quickSwitchDialogs.Keys) + foreach (var dialog in _dialogJumpDialogs.Keys) { if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled - IQuickSwitchDialogWindow dialogWindow; - var existingDialogWindow = _quickSwitchDialogs[dialog]; + IDialogJumpDialogWindow dialogWindow; + var existingDialogWindow = _dialogJumpDialogs[dialog]; if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) { // If the dialog window is already in the list, no need to check again @@ -877,7 +877,7 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) // Update dialog window if found if (dialogWindow != null) { - _quickSwitchDialogs[dialog] = dialogWindow; + _dialogJumpDialogs[dialog] = dialogWindow; return dialogWindow; } } @@ -885,7 +885,7 @@ private static IQuickSwitchDialogWindow GetDialogWindow(HWND hwnd) return null; } - private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dialog, string path, bool isFile, bool auto = false) + private static async Task JumpToPathAsync(IDialogJumpDialogWindowTab dialog, string path, bool isFile, bool auto = false) { // Jump after flow launcher window vanished (after JumpAction returned true) // and the dialog had been in the foreground. @@ -900,17 +900,17 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial bool result; if (isFile) { - switch (_settings.QuickSwitchFileResultBehaviour) + switch (_settings.DialogJumpFileResultBehaviour) { - case QuickSwitchFileResultBehaviours.FullPath: + case DialogJumpFileResultBehaviours.FullPath: Log.Debug(ClassName, $"File Jump FullPath: {path}"); result = FileJump(path, dialog); break; - case QuickSwitchFileResultBehaviours.FullPathOpen: + case DialogJumpFileResultBehaviours.FullPathOpen: Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); result = FileJump(path, dialog, openFile: true); break; - case QuickSwitchFileResultBehaviours.Directory: + case DialogJumpFileResultBehaviours.Directory: Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}"); result = DirJump(Path.GetDirectoryName(path), dialog, auto); break; @@ -945,7 +945,7 @@ private static async Task JumpToPathAsync(IQuickSwitchDialogWindowTab dial } } - private static bool FileJump(string filePath, IQuickSwitchDialogWindowTab dialog, bool openFile = false) + private static bool FileJump(string filePath, IDialogJumpDialogWindowTab dialog, bool openFile = false) { if (!dialog.JumpFile(filePath)) { @@ -962,7 +962,7 @@ private static bool FileJump(string filePath, IQuickSwitchDialogWindowTab dialog return true; } - private static bool DirJump(string dirPath, IQuickSwitchDialogWindowTab dialog, bool auto = false) + private static bool DirJump(string dirPath, IDialogJumpDialogWindowTab dialog, bool auto = false) { if (!dialog.JumpFolder(dirPath, auto)) { @@ -1016,22 +1016,22 @@ public static void Dispose() } // Dispose explorers - foreach (var explorer in _quickSwitchExplorers.Keys) + foreach (var explorer in _dialogJumpExplorers.Keys) { - _quickSwitchExplorers[explorer]?.Dispose(); + _dialogJumpExplorers[explorer]?.Dispose(); } - _quickSwitchExplorers.Clear(); + _dialogJumpExplorers.Clear(); lock (_lastExplorerLock) { _lastExplorer = null; } // Dispose dialogs - foreach (var dialog in _quickSwitchDialogs.Keys) + foreach (var dialog in _dialogJumpDialogs.Keys) { - _quickSwitchDialogs[dialog]?.Dispose(); + _dialogJumpDialogs[dialog]?.Dispose(); } - _quickSwitchDialogs.Clear(); + _dialogJumpDialogs.Clear(); lock (_dialogWindowLock) { _dialogWindow = null; diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs similarity index 75% rename from Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs rename to Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs index 1467afe580d..d1248eac13d 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitchPair.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs @@ -1,10 +1,10 @@ using Flow.Launcher.Plugin; -namespace Flow.Launcher.Infrastructure.QuickSwitch; +namespace Flow.Launcher.Infrastructure.DialogJump; -public class QuickSwitchExplorerPair +public class DialogJumpExplorerPair { - public IQuickSwitchExplorer Plugin { get; init; } + public IDialogJumpExplorer Plugin { get; init; } public PluginMetadata Metadata { get; init; } @@ -15,7 +15,7 @@ public override string ToString() public override bool Equals(object obj) { - if (obj is QuickSwitchExplorerPair r) + if (obj is DialogJumpExplorerPair r) { return string.Equals(r.Metadata.ID, Metadata.ID); } @@ -32,9 +32,9 @@ public override int GetHashCode() } } -public class QuickSwitchDialogPair +public class DialogJumpDialogPair { - public IQuickSwitchDialog Plugin { get; init; } + public IDialogJumpDialog Plugin { get; init; } public PluginMetadata Metadata { get; init; } @@ -45,7 +45,7 @@ public override string ToString() public override bool Equals(object obj) { - if (obj is QuickSwitchDialogPair r) + if (obj is DialogJumpDialogPair r) { return string.Equals(r.Metadata.ID, Metadata.ID); } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs similarity index 94% rename from Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs rename to Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs index 66698abc47e..2f495468abc 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs @@ -8,16 +8,16 @@ using WindowsInput; using WindowsInput.Native; -namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +namespace Flow.Launcher.Infrastructure.DialogJump.Models { /// - /// Class for handling Windows File Dialog instances in QuickSwitch. + /// Class for handling Windows File Dialog instances in DialogJump. /// - public class WindowsDialog : IQuickSwitchDialog + public class WindowsDialog : IDialogJumpDialog { private const string WindowsDialogClassName = "#32770"; - public IQuickSwitchDialogWindow CheckDialogWindow(IntPtr hwnd) + public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd) { // Is it a Win32 dialog box? if (GetClassName(new(hwnd)) == WindowsDialogClassName) @@ -68,13 +68,13 @@ private static DialogType GetFileDialogType(HWND handle) #endregion } - public class WindowsDialogWindow : IQuickSwitchDialogWindow + public class WindowsDialogWindow : IDialogJumpDialogWindow { public IntPtr Handle { get; private set; } = IntPtr.Zero; // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore // So we need to cache the current tab and use the original handle - private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null; + private IDialogJumpDialogWindowTab _currentTab { get; set; } = null; private readonly DialogType _dialogType; @@ -84,7 +84,7 @@ internal WindowsDialogWindow(IntPtr handle, DialogType dialogType) _dialogType = dialogType; } - public IQuickSwitchDialogWindowTab GetCurrentTab() + public IDialogJumpDialogWindowTab GetCurrentTab() { return _currentTab ??= new WindowsDialogTab(Handle, _dialogType); } @@ -95,7 +95,7 @@ public void Dispose() } } - public class WindowsDialogTab : IQuickSwitchDialogWindowTab + public class WindowsDialogTab : IDialogJumpDialogWindowTab { #region Public Properties @@ -148,7 +148,7 @@ public bool JumpFolder(string path, bool auto) { if (auto) { - // Use legacy jump folder method for auto quick switch because file editor is default value. + // Use legacy jump folder method for auto dialog jump because file editor is default value. // After setting path using file editor, we do not need to revert its value. return JumpFolderWithFileEditor(path, false); } diff --git a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs similarity index 95% rename from Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs rename to Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs index 4c068327b29..9461ea46c37 100644 --- a/Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs @@ -7,16 +7,16 @@ using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; -namespace Flow.Launcher.Infrastructure.QuickSwitch.Models +namespace Flow.Launcher.Infrastructure.DialogJump.Models { /// - /// Class for handling Windows Explorer instances in QuickSwitch. + /// Class for handling Windows Explorer instances in DialogJump. /// - public class WindowsExplorer : IQuickSwitchExplorer + public class WindowsExplorer : IDialogJumpExplorer { - public IQuickSwitchExplorerWindow CheckExplorerWindow(IntPtr hwnd) + public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd) { - IQuickSwitchExplorerWindow explorerWindow = null; + IDialogJumpExplorerWindow explorerWindow = null; // Is it from Explorer? var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd)); @@ -78,7 +78,7 @@ public void Dispose() } } - public class WindowsExplorerWindow : IQuickSwitchExplorerWindow + public class WindowsExplorerWindow : IDialogJumpExplorerWindow { public IntPtr Handle { get; } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 4873cb7da29..23f9047fef7 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -86,7 +86,7 @@ public bool ShowOpenResultHotkey public string OpenHistoryHotkey { get; set; } = $"Ctrl+H"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; - public string QuickSwitchHotkey { get; set; } = $"{KeyConstant.Alt} + G"; + public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G"; private string _language = Constant.SystemLanguageCode; public string Language @@ -324,20 +324,20 @@ public CustomBrowserViewModel CustomBrowser } }; - public bool EnableQuickSwitch { get; set; } = true; + public bool EnableDialogJump { get; set; } = true; - public bool AutoQuickSwitch { get; set; } = false; + public bool AutoDialogJump { get; set; } = false; - public bool ShowQuickSwitchWindow { get; set; } = false; + public bool ShowDialogJumpWindow { get; set; } = false; [JsonConverter(typeof(JsonStringEnumConverter))] - public QuickSwitchWindowPositions QuickSwitchWindowPosition { get; set; } = QuickSwitchWindowPositions.UnderDialog; + public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog; [JsonConverter(typeof(JsonStringEnumConverter))] - public QuickSwitchResultBehaviours QuickSwitchResultBehaviour { get; set; } = QuickSwitchResultBehaviours.LeftClick; + public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick; [JsonConverter(typeof(JsonStringEnumConverter))] - public QuickSwitchFileResultBehaviours QuickSwitchFileResultBehaviour { get; set; } = QuickSwitchFileResultBehaviours.FullPath; + public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath; [JsonConverter(typeof(JsonStringEnumConverter))] public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; @@ -562,8 +562,8 @@ public List RegisteredHotkeys list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); - if (!string.IsNullOrEmpty(QuickSwitchHotkey)) - list.Add(new(QuickSwitchHotkey, "quickSwitchHotkey", () => QuickSwitchHotkey = "")); + if (!string.IsNullOrEmpty(DialogJumpHotkey)) + list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = "")); // Custom Query Hotkeys foreach (var customPluginHotkey in CustomPluginHotkeys) @@ -678,19 +678,19 @@ public enum DoublePinyinSchemas XiaoLang } - public enum QuickSwitchWindowPositions + public enum DialogJumpWindowPositions { UnderDialog, FollowDefault } - public enum QuickSwitchResultBehaviours + public enum DialogJumpResultBehaviours { LeftClick, RightClick } - public enum QuickSwitchFileResultBehaviours + public enum DialogJumpFileResultBehaviours { FullPath, FullPathOpen, diff --git a/Flow.Launcher.Plugin/QuickSwitchResult.cs b/Flow.Launcher.Plugin/DialogJumpResult.cs similarity index 81% rename from Flow.Launcher.Plugin/QuickSwitchResult.cs rename to Flow.Launcher.Plugin/DialogJumpResult.cs index 0940bf85fb3..ad544f5358e 100644 --- a/Flow.Launcher.Plugin/QuickSwitchResult.cs +++ b/Flow.Launcher.Plugin/DialogJumpResult.cs @@ -1,22 +1,22 @@ namespace Flow.Launcher.Plugin { /// - /// Describes a result of a executed by a plugin in quick switch window + /// Describes a result of a executed by a plugin in dialog jump window /// - public class QuickSwitchResult : Result + public class DialogJumpResult : Result { /// /// This holds the path which can be provided by plugin to be navigated to the - /// file dialog when records in quick switch window is right clicked on a result. + /// file dialog when records in dialog jump window is right clicked on a result. /// - public required string QuickSwitchPath { get; init; } + public required string DialogJumpPath { get; init; } /// - /// Clones the current quick switch result + /// Clones the current dialog jump result /// - public new QuickSwitchResult Clone() + public new DialogJumpResult Clone() { - return new QuickSwitchResult + return new DialogJumpResult { Title = Title, SubTitle = SubTitle, @@ -46,16 +46,16 @@ public class QuickSwitchResult : Result AddSelectedCount = AddSelectedCount, RecordKey = RecordKey, ShowBadge = ShowBadge, - QuickSwitchPath = QuickSwitchPath + DialogJumpPath = DialogJumpPath }; } /// - /// Convert to . + /// Convert to . /// - public static QuickSwitchResult From(Result result, string quickSwitchPath) + public static DialogJumpResult From(Result result, string dialogJumpPath) { - return new QuickSwitchResult + return new DialogJumpResult { Title = result.Title, SubTitle = result.SubTitle, @@ -85,7 +85,7 @@ public static QuickSwitchResult From(Result result, string quickSwitchPath) AddSelectedCount = result.AddSelectedCount, RecordKey = result.RecordKey, ShowBadge = result.ShowBadge, - QuickSwitchPath = quickSwitchPath + DialogJumpPath = dialogJumpPath }; } } diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs similarity index 64% rename from Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs rename to Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs index 477b44975e7..3ea78f11ff3 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncQuickSwitch.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs @@ -5,20 +5,20 @@ namespace Flow.Launcher.Plugin { /// - /// Asynchronous Quick Switch Model + /// Asynchronous Dialog Jump Model /// - public interface IAsyncQuickSwitch : IFeatures + public interface IAsyncDialogJump : IFeatures { /// - /// Asynchronous querying for quick switch window + /// Asynchronous querying for dialog jump window /// /// /// If the Querying method requires high IO transmission - /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncQuickSwitch interface + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface /// /// Query to search /// Cancel when querying job is obsolete /// - Task> QueryQuickSwitchAsync(Query query, CancellationToken token); + Task> QueryDialogJumpAsync(Query query, CancellationToken token); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs similarity index 53% rename from Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs rename to Flow.Launcher.Plugin/Interfaces/IDialogJump.cs index 5e43a73acf2..27f1be034a8 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitch.cs +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs @@ -5,16 +5,16 @@ namespace Flow.Launcher.Plugin { /// - /// Synchronous Quick Switch Model + /// Synchronous Dialog Jump Model /// /// If the Querying method requires high IO transmission - /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncQuickSwitch interface + /// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface /// /// - public interface IQuickSwitch : IAsyncQuickSwitch + public interface IDialogJump : IAsyncDialogJump { /// - /// Querying for quick switch window + /// Querying for dialog jump window /// /// This method will be called within a Task.Run, /// so please avoid synchrously wait for long. @@ -22,8 +22,8 @@ public interface IQuickSwitch : IAsyncQuickSwitch /// /// Query to search /// - List QueryQuickSwitch(Query query); + List QueryDialogJump(Query query); - Task> IAsyncQuickSwitch.QueryQuickSwitchAsync(Query query, CancellationToken token) => Task.Run(() => QueryQuickSwitch(query)); + Task> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query)); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs similarity index 85% rename from Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs rename to Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs index 2fb498bb541..33ad9ae73da 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchDialog.cs +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs @@ -5,9 +5,9 @@ namespace Flow.Launcher.Plugin { /// - /// Interface for handling file dialog instances in QuickSwitch. + /// Interface for handling file dialog instances in DialogJump. /// - public interface IQuickSwitchDialog : IFeatures, IDisposable + public interface IDialogJumpDialog : IFeatures, IDisposable { /// /// Check if the foreground window is a file dialog instance. @@ -18,13 +18,13 @@ public interface IQuickSwitchDialog : IFeatures, IDisposable /// /// The window if the foreground window is a file dialog instance. Null if it is not. /// - IQuickSwitchDialogWindow? CheckDialogWindow(IntPtr hwnd); + IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd); } /// - /// Interface for handling a specific file dialog window in QuickSwitch. + /// Interface for handling a specific file dialog window in DialogJump. /// - public interface IQuickSwitchDialogWindow : IDisposable + public interface IDialogJumpDialogWindow : IDisposable { /// /// The handle of the dialog window. @@ -35,13 +35,13 @@ public interface IQuickSwitchDialogWindow : IDisposable /// Get the current tab of the dialog window. /// /// - IQuickSwitchDialogWindowTab GetCurrentTab(); + IDialogJumpDialogWindowTab GetCurrentTab(); } /// - /// Interface for handling a specific tab in a file dialog window in QuickSwitch. + /// Interface for handling a specific tab in a file dialog window in DialogJump. /// - public interface IQuickSwitchDialogWindowTab : IDisposable + public interface IDialogJumpDialogWindowTab : IDisposable { /// /// The handle of the dialog tab. diff --git a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs similarity index 76% rename from Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs rename to Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs index ea46e046a58..9a2b879d058 100644 --- a/Flow.Launcher.Plugin/Interfaces/IQuickSwitchExplorer.cs +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs @@ -5,9 +5,9 @@ namespace Flow.Launcher.Plugin { /// - /// Interface for handling file explorer instances in QuickSwitch. + /// Interface for handling file explorer instances in DialogJump. /// - public interface IQuickSwitchExplorer : IFeatures, IDisposable + public interface IDialogJumpExplorer : IFeatures, IDisposable { /// /// Check if the foreground window is a Windows Explorer instance. @@ -18,13 +18,13 @@ public interface IQuickSwitchExplorer : IFeatures, IDisposable /// /// The window if the foreground window is a file explorer instance. Null if it is not. /// - IQuickSwitchExplorerWindow? CheckExplorerWindow(IntPtr hwnd); + IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd); } /// - /// Interface for handling a specific file explorer window in QuickSwitch. + /// Interface for handling a specific file explorer window in DialogJump. /// - public interface IQuickSwitchExplorerWindow : IDisposable + public interface IDialogJumpExplorerWindow : IDisposable { /// /// The handle of the explorer window. diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 8834ab0bcce..4df8cfa0177 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -16,7 +16,7 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -234,8 +234,8 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); - QuickSwitch.InitializeQuickSwitch(PluginManager.GetQuickSwitchExplorers(), PluginManager.GetQuickSwitchDialogs()); - QuickSwitch.SetupQuickSwitch(_settings.EnableQuickSwitch); + DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); + DialogJump.SetupDialogJump(_settings.EnableDialogJump); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -416,7 +416,7 @@ protected virtual void Dispose(bool disposing) // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); - QuickSwitch.Dispose(); + DialogJump.Dispose(); } API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index aa598141f12..86a68475e8d 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -2,7 +2,7 @@ using ChefKeys; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.ViewModel; using NHotkey; @@ -23,9 +23,9 @@ internal static void Initialize() _settings = Ioc.Default.GetService(); SetHotkey(_settings.Hotkey, OnToggleHotkey); - if (_settings.EnableQuickSwitch) + if (_settings.EnableDialogJump) { - SetHotkey(_settings.QuickSwitchHotkey, QuickSwitch.OnToggleHotkey); + SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey); } LoadCustomPluginHotkey(); } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 0bb5bd16e2f..89bfde3497a 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -111,7 +111,7 @@ public enum HotkeyType SelectPrevItemHotkey2, SelectNextItemHotkey, SelectNextItemHotkey2, - QuickSwitchHotkey, + DialogJumpHotkey, } // We can initialize settings in static field because it has been constructed in App constuctor @@ -143,7 +143,7 @@ public string Hotkey HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, - HotkeyType.QuickSwitchHotkey => _settings.QuickSwitchHotkey, + HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey, _ => throw new System.NotImplementedException("Hotkey type not set") }; } @@ -203,8 +203,8 @@ public string Hotkey case HotkeyType.SelectNextItemHotkey2: _settings.SelectNextItemHotkey2 = value; break; - case HotkeyType.QuickSwitchHotkey: - _settings.QuickSwitchHotkey = value; + case HotkeyType.DialogJumpHotkey: + _settings.DialogJumpHotkey = value; break; default: throw new System.NotImplementedException("Hotkey type not set"); diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 73141806fc3..dda5dbc0b39 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -368,27 +368,27 @@ For supported plugins, badges are displayed to help distinguish them more easily. Show Result Badges for Global Query Only Show badges for global query results only - Quick Switch - Enter shortcut to quickly navigate the Open/Save As dialogue to the path of the current file manager. - Quick Switch - When Open/Save As dialog opens, quickly navigate to the current path of the file manager. - Quick Switch Automatically - When Open/Save As dialog is displayed, automatically navigate to the path of the current file manager. (Experimental) - Show Quick Switch Window - Display quick switch search window when the open/save dialog is shown to quickly navigate to file/folder locations. - Quick Switch Window Position - Select position for quick switch window - Fixed under the Open/Save As dialog. Displayed on dialog open and stays until dialogue is closed - Default search window position. Displayed when triggered by search window hotkey - Quick Switch Result Navigation Behaviour - Behaviour to navigate Open/Save As dialog to the selected result path - Left click or Enter key - Right click - Quick Switch File Navigation Behaviour - Behaviour to navigate Open/Save As dialog when the result is a file path - Fill full path in file name box - Fill full path in file name box and open - Fill directory in path box + Dialog Jump + Enter shortcut to quickly navigate the Open/Save As dialogue to the path of the current file manager. + Dialog Jump + When Open/Save As dialog opens, quickly navigate to the current path of the file manager. + Dialog Jump Automatically + When Open/Save As dialog is displayed, automatically navigate to the path of the current file manager. (Experimental) + Show Dialog Jump Window + Display dialog jump search window when the open/save dialog is shown to quickly navigate to file/folder locations. + Dialog Jump Window Position + Select position for dialog jump window + Fixed under the Open/Save As dialog. Displayed on dialog open and stays until dialogue is closed + Default search window position. Displayed when triggered by search window hotkey + Dialog Jump Result Navigation Behaviour + Behaviour to navigate Open/Save As dialog to the selected result path + Left click or Enter key + Right click + Dialog Jump File Navigation Behaviour + Behaviour to navigate Open/Save As dialog when the result is a file path + Fill full path in file name box + Fill full path in file name box and open + Fill directory in path box HTTP Proxy diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index d5bda530c22..1ed287e6037 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; @@ -19,7 +19,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -217,8 +217,8 @@ private void OnLoaded(object sender, RoutedEventArgs e) // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; - // Register quick switch events - InitializeQuickSwitch(); + // Register dialog jump events + InitializeDialogJump(); // View model property changed event _viewModel.PropertyChanged += (o, e) => @@ -232,7 +232,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) if (_viewModel.MainWindowVisibilityStatus) { // Play sound effect before activing the window - if (_settings.UseSound && !_viewModel.IsQuickSwitchWindowUnderDialog()) + if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog()) { SoundPlay(); } @@ -255,7 +255,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) QueryTextBox.Focus(); // Play window animation - if (_settings.UseAnimation && !_viewModel.IsQuickSwitchWindowUnderDialog()) + if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog()) { WindowAnimation(); } @@ -385,7 +385,7 @@ private void OnClosed(object sender, EventArgs e) private void OnLocationChanged(object sender, EventArgs e) { - if (_viewModel.IsQuickSwitchWindowUnderDialog()) + if (_viewModel.IsDialogJumpWindowUnderDialog()) { return; } @@ -399,7 +399,7 @@ private void OnLocationChanged(object sender, EventArgs e) private async void OnDeactivated(object sender, EventArgs e) { - if (_viewModel.IsQuickSwitchWindowUnderDialog()) + if (_viewModel.IsDialogJumpWindowUnderDialog()) { return; } @@ -593,8 +593,8 @@ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref b switch (msg) { case Win32Helper.WM_ENTERSIZEMOVE: - // Do do handle size move event for quick switch window - if (_viewModel.IsQuickSwitchWindowUnderDialog()) + // Do do handle size move event for dialog jump window + if (_viewModel.IsDialogJumpWindowUnderDialog()) { return IntPtr.Zero; } @@ -604,8 +604,8 @@ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref b handled = true; break; case Win32Helper.WM_EXITSIZEMOVE: - // Do do handle size move event for quick switch window - if (_viewModel.IsQuickSwitchWindowUnderDialog()) + // Do do handle size move event for dialog jump window + if (_viewModel.IsDialogJumpWindowUnderDialog()) { return IntPtr.Zero; } @@ -823,10 +823,10 @@ private void InitializeContextMenu() public void UpdatePosition() { // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - if (_viewModel.IsQuickSwitchWindowUnderDialog()) + if (_viewModel.IsDialogJumpWindowUnderDialog()) { - InitializeQuickSwitchPosition(); - InitializeQuickSwitchPosition(); + InitializeDialogJumpPosition(); + InitializeDialogJumpPosition(); } else { @@ -1390,20 +1390,20 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) #endregion - #region Quick Switch + #region Dialog Jump - private void InitializeQuickSwitch() + private void InitializeDialogJump() { - QuickSwitch.ShowQuickSwitchWindowAsync = _viewModel.SetupQuickSwitchAsync; - QuickSwitch.UpdateQuickSwitchWindow = InitializeQuickSwitchPosition; - QuickSwitch.ResetQuickSwitchWindow = _viewModel.ResetQuickSwitch; - QuickSwitch.HideQuickSwitchWindow = _viewModel.HideQuickSwitch; + DialogJump.ShowDialogJumpWindowAsync = _viewModel.SetupDialogJumpAsync; + DialogJump.UpdateDialogJumpWindow = InitializeDialogJumpPosition; + DialogJump.ResetDialogJumpWindow = _viewModel.ResetDialogJump; + DialogJump.HideDialogJumpWindow = _viewModel.HideDialogJump; } - private void InitializeQuickSwitchPosition() + private void InitializeDialogJumpPosition() { if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return; - if (!_viewModel.IsQuickSwitchWindowUnderDialog()) return; + if (!_viewModel.IsDialogJumpWindowUnderDialog()) return; // Get dialog window rect var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index d7966fa981a..2ef44f89006 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; @@ -8,7 +8,7 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -147,39 +147,39 @@ public bool PortableMode public List LastQueryModes { get; } = DropdownDataGeneric.GetValues("LastQuery"); - public bool EnableQuickSwitch + public bool EnableDialogJump { - get => Settings.EnableQuickSwitch; + get => Settings.EnableDialogJump; set { - if (Settings.EnableQuickSwitch != value) + if (Settings.EnableDialogJump != value) { - Settings.EnableQuickSwitch = value; - QuickSwitch.SetupQuickSwitch(value); - if (Settings.EnableQuickSwitch) + Settings.EnableDialogJump = value; + DialogJump.SetupDialogJump(value); + if (Settings.EnableDialogJump) { - HotKeyMapper.SetHotkey(new(Settings.QuickSwitchHotkey), QuickSwitch.OnToggleHotkey); + HotKeyMapper.SetHotkey(new(Settings.DialogJumpHotkey), DialogJump.OnToggleHotkey); } else { - HotKeyMapper.RemoveHotkey(Settings.QuickSwitchHotkey); + HotKeyMapper.RemoveHotkey(Settings.DialogJumpHotkey); } } } } - public class QuickSwitchWindowPositionData : DropdownDataGeneric { } - public class QuickSwitchResultBehaviourData : DropdownDataGeneric { } - public class QuickSwitchFileResultBehaviourData : DropdownDataGeneric { } + public class DialogJumpWindowPositionData : DropdownDataGeneric { } + public class DialogJumpResultBehaviourData : DropdownDataGeneric { } + public class DialogJumpFileResultBehaviourData : DropdownDataGeneric { } - public List QuickSwitchWindowPositions { get; } = - DropdownDataGeneric.GetValues("QuickSwitchWindowPosition"); + public List DialogJumpWindowPositions { get; } = + DropdownDataGeneric.GetValues("DialogJumpWindowPosition"); - public List QuickSwitchResultBehaviours { get; } = - DropdownDataGeneric.GetValues("QuickSwitchResultBehaviour"); + public List DialogJumpResultBehaviours { get; } = + DropdownDataGeneric.GetValues("DialogJumpResultBehaviour"); - public List QuickSwitchFileResultBehaviours { get; } = - DropdownDataGeneric.GetValues("QuickSwitchFileResultBehaviour"); + public List DialogJumpFileResultBehaviours { get; } = + DropdownDataGeneric.GetValues("DialogJumpFileResultBehaviour"); public int SearchDelayTimeValue { @@ -214,9 +214,9 @@ private void UpdateEnumDropdownLocalizations() DropdownDataGeneric.UpdateLabels(SearchPrecisionScores); DropdownDataGeneric.UpdateLabels(LastQueryModes); DropdownDataGeneric.UpdateLabels(DoublePinyinSchemas); - DropdownDataGeneric.UpdateLabels(QuickSwitchWindowPositions); - DropdownDataGeneric.UpdateLabels(QuickSwitchResultBehaviours); - DropdownDataGeneric.UpdateLabels(QuickSwitchFileResultBehaviours); + DropdownDataGeneric.UpdateLabels(DialogJumpWindowPositions); + DropdownDataGeneric.UpdateLabels(DialogJumpResultBehaviours); + DropdownDataGeneric.UpdateLabels(DialogJumpFileResultBehaviours); // Since we are using Binding instead of DynamicResource, we need to manually trigger the update OnPropertyChanged(nameof(AlwaysPreviewToolTip)); } diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs index b6e3651773d..9e6a31dc772 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs @@ -4,7 +4,7 @@ using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.QuickSwitch; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -36,11 +36,11 @@ private void SetTogglingHotkey(HotkeyModel hotkey) } [RelayCommand] - private void SetQuickSwitchHotkey(HotkeyModel hotkey) + private void SetDialogJumpHotkey(HotkeyModel hotkey) { - if (Settings.EnableQuickSwitch) + if (Settings.EnableDialogJump) { - HotKeyMapper.SetHotkey(hotkey, QuickSwitch.OnToggleHotkey); + HotKeyMapper.SetHotkey(hotkey, DialogJump.OnToggleHotkey); } } diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 79a33cd8bab..81e15df6950 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -1,4 +1,4 @@ - + Sub="{DynamicResource dialogJumpToolTip}"> diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index 7a2f892bff1..c6905362926 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -74,15 +74,15 @@ + Sub="{DynamicResource dialogJumpHotkeyToolTip}"> + WindowTitle="{DynamicResource dialogJumpHotkey}" /> _emptyResult = new List(); - private readonly IReadOnlyList _emptyQuickSwitchResult = new List(); + private readonly IReadOnlyList _emptyDialogJumpResult = new List(); private readonly PluginMetadata _historyMetadata = new() { @@ -405,16 +405,16 @@ public void ForwardHistory() [RelayCommand] private void LoadContextMenu() { - // For quick switch and right click mode, we need to navigate to the path - if (_isQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.RightClick) + // For dialog jump and right click mode, we need to navigate to the path + if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.RightClick) { if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { var result = SelectedResults.SelectedItem.Result; - if (result is QuickSwitchResult quickSwitchResult) + if (result is DialogJumpResult dialogJumpResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)); + _ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath)); } } return; @@ -498,17 +498,17 @@ private async Task OpenResultAsync(string index) return; } - // For quick switch and left click mode, we need to navigate to the path - if (_isQuickSwitch && Settings.QuickSwitchResultBehaviour == QuickSwitchResultBehaviours.LeftClick) + // For dialog jump and left click mode, we need to navigate to the path + if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.LeftClick) { Hide(); if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) { - if (result is QuickSwitchResult quickSwitchResult) + if (result is DialogJumpResult dialogJumpResult) { Win32Helper.SetForegroundWindow(DialogWindowHandle); - _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)); + _ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath)); } } } @@ -535,17 +535,17 @@ private async Task OpenResultAsync(string index) } } - private static IReadOnlyList DeepCloneResults(IReadOnlyList results, bool isQuickSwitch, CancellationToken token = default) + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, bool isDialogJump, CancellationToken token = default) { var resultsCopy = new List(); - if (isQuickSwitch) + if (isDialogJump) { foreach (var result in results.ToList()) { if (token.IsCancellationRequested) break; - var resultCopy = ((QuickSwitchResult)result).Clone(); + var resultCopy = ((DialogJumpResult)result).Clone(); resultsCopy.Add(resultCopy); } } @@ -1344,10 +1344,10 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); var currentIsHomeQuery = query.IsHomeQuery; - var currentIsQuickSwitch = _isQuickSwitch; + var currentIsDialogJump = _isDialogJump; - // Do not show home page for quick switch window - if (currentIsHomeQuery && currentIsQuickSwitch) + // Do not show home page for dialog jump window + if (currentIsHomeQuery && currentIsDialogJump) { ClearResults(); return; @@ -1385,7 +1385,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } else { - plugins = PluginManager.ValidPluginsForQuery(query, currentIsQuickSwitch); + plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump); if (plugins.Count == 1) { @@ -1513,8 +1513,8 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) // Task.Yield will force it to run in ThreadPool await Task.Yield(); - IReadOnlyList results = currentIsQuickSwitch ? - await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) : + IReadOnlyList results = currentIsDialogJump ? + await PluginManager.QueryDialogJumpForPluginAsync(plugin, query, token) : currentIsHomeQuery ? await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : await PluginManager.QueryForPluginAsync(plugin, query, token); @@ -1524,12 +1524,12 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : IReadOnlyList resultsCopy; if (results == null) { - resultsCopy = currentIsQuickSwitch ? _emptyQuickSwitchResult : _emptyResult; + resultsCopy = currentIsDialogJump ? _emptyDialogJumpResult : _emptyResult; } else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results, currentIsQuickSwitch, token); + resultsCopy = DeepCloneResults(results, currentIsDialogJump, token); } foreach (var result in resultsCopy) @@ -1824,27 +1824,27 @@ public bool ShouldIgnoreHotkeys() #endregion - #region Quick Switch + #region Dialog Jump public nint DialogWindowHandle { get; private set; } = nint.Zero; - private bool _isQuickSwitch = false; + private bool _isDialogJump = false; private bool _previousMainWindowVisibilityStatus; - private CancellationTokenSource _quickSwitchSource; + private CancellationTokenSource _dialogJumpSource; public void InitializeVisibilityStatus(bool visibilityStatus) { _previousMainWindowVisibilityStatus = visibilityStatus; } - public bool IsQuickSwitchWindowUnderDialog() + public bool IsDialogJumpWindowUnderDialog() { - return _isQuickSwitch && QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog; + return _isDialogJump && DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog; } - public async Task SetupQuickSwitchAsync(nint handle) + public async Task SetupDialogJumpAsync(nint handle) { if (handle == nint.Zero) return; @@ -1854,7 +1854,7 @@ public async Task SetupQuickSwitchAsync(nint handle) { DialogWindowHandle = handle; _previousMainWindowVisibilityStatus = MainWindowVisibilityStatus; - _isQuickSwitch = true; + _isDialogJump = true; dialogWindowHandleChanged = true; @@ -1862,14 +1862,14 @@ public async Task SetupQuickSwitchAsync(nint handle) await Task.Delay(300); } - // If handle is cleared, which means the dialog is closed, clear quick switch state + // If handle is cleared, which means the dialog is closed, clear dialog jump state if (DialogWindowHandle == nint.Zero) { - _isQuickSwitch = false; + _isDialogJump = false; return; } - // Initialize quick switch window + // Initialize dialog jump window if (MainWindowVisibilityStatus) { if (dialogWindowHandleChanged) @@ -1885,7 +1885,7 @@ public async Task SetupQuickSwitchAsync(nint handle) } else { - if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { // We wait for window to be reset before showing it because if window has results, // showing it before resetting will cause flickering when results are clearing @@ -1905,25 +1905,25 @@ public async Task SetupQuickSwitchAsync(nint handle) } } - if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { - // Cancel the previous quick switch task - _quickSwitchSource?.Cancel(); + // Cancel the previous dialog jump task + _dialogJumpSource?.Cancel(); // Create a new cancellation token source - _quickSwitchSource = new CancellationTokenSource(); + _dialogJumpSource = new CancellationTokenSource(); _ = Task.Run(() => { try { // Check task cancellation - if (_quickSwitchSource.Token.IsCancellationRequested) return; + if (_dialogJumpSource.Token.IsCancellationRequested) return; // Check dialog handle if (DialogWindowHandle == nint.Zero) return; - // Wait 150ms to check if quick switch window gets the focus + // Wait 150ms to check if dialog jump window gets the focus var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); if (timeOut) return; @@ -1940,14 +1940,14 @@ public async Task SetupQuickSwitchAsync(nint handle) #pragma warning disable VSTHRD100 // Avoid async void methods - public async void ResetQuickSwitch() + public async void ResetDialogJump() { // Cache original dialog window handle var dialogWindowHandle = DialogWindowHandle; - // Reset the quick switch state + // Reset the dialog jump state DialogWindowHandle = nint.Zero; - _isQuickSwitch = false; + _isDialogJump = false; // If dialog window handle is not set, we should not reset the main window visibility if (dialogWindowHandle == nint.Zero) return; @@ -1989,14 +1989,14 @@ public async void ResetQuickSwitch() #pragma warning restore VSTHRD100 // Avoid async void methods - public void HideQuickSwitch() + public void HideDialogJump() { if (DialogWindowHandle != nint.Zero) { - if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog) + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { // Warning: Main window is already in foreground - // This is because if you click popup menus in other applications to hide quick switch window, + // This is because if you click popup menus in other applications to hide dialog jump window, // they can steal focus before showing main window if (MainWindowVisibilityStatus) { @@ -2045,7 +2045,7 @@ public void Show() Win32Helper.DWMSetCloakForWindow(mainWindow, false); // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !_isQuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -2117,7 +2117,7 @@ public async void Hide(bool reset = true) if (Application.Current?.MainWindow is MainWindow mainWindow) { // Set clock and search icon opacity - var opacity = (Settings.UseAnimation && !_isQuickSwitch) ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -2262,7 +2262,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { _updateSource?.Dispose(); - _quickSwitchSource?.Dispose(); + _dialogJumpSource?.Dispose(); _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index e4b432b2b9a..fbaefa9d66e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Plugin.Explorer.Helper; +using Flow.Launcher.Plugin.Explorer.Helper; using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.ViewModels; @@ -14,7 +14,7 @@ namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncQuickSwitch + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump { internal static PluginInitContext Context { get; set; } @@ -26,7 +26,7 @@ public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, I private SearchManager searchManager; - private static readonly List _emptyQuickSwitchResultList = new(); + private static readonly List _emptyDialogJumpResultList = new(); public Control CreateSettingPanel() { @@ -112,16 +112,16 @@ private static void FillQuickAccessLinkNames() } } - public async Task> QueryQuickSwitchAsync(Query query, CancellationToken token) + public async Task> QueryDialogJumpAsync(Query query, CancellationToken token) { try { var results = await searchManager.SearchAsync(query, token); - return results.Select(r => QuickSwitchResult.From(r, r.CopyText)).ToList(); + return results.Select(r => DialogJumpResult.From(r, r.CopyText)).ToList(); } catch (Exception e) when (e is SearchException or EngineNotAvailableException) { - return _emptyQuickSwitchResultList; + return _emptyDialogJumpResultList; } } } From caa07ec5223b127f215698c645d4be6ee72bd7ab Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:33:37 +0800 Subject: [PATCH 237/243] Add exception handling to async void event callback --- Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 6744e7dced9..13519ea7a92 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -595,6 +595,10 @@ uint dwmsEventTime } } } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to invoke ForegroundChangeCallback", ex); + } finally { _foregroundChangeLock.Release(); From aded72488d860d51275f464fce62692c0fe6eac1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:34:08 +0800 Subject: [PATCH 238/243] Use culture-invariant string comparison --- .../DialogJump/Models/WindowsExplorer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs index 9461ea46c37..e9ed9dae7a5 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs @@ -20,7 +20,7 @@ public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd) // Is it from Explorer? var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd)); - if (processName.ToLower() == "explorer.exe") + if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase)) { EnumerateShellWindows((shellWindow) => { From fd613031b5c097f2ef7e667f762c326a473d7891 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:35:42 +0800 Subject: [PATCH 239/243] Fix missing cancellation token propagation in default implementation --- Flow.Launcher.Plugin/Interfaces/IDialogJump.cs | 2 +- Flow.Launcher.Plugin/Interfaces/IPlugin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs index 27f1be034a8..0c50a0d0b68 100644 --- a/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs @@ -24,6 +24,6 @@ public interface IDialogJump : IAsyncDialogJump /// List QueryDialogJump(Query query); - Task> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query)); + Task> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs index bac93d090cd..cf5a8a5829c 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPlugin.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs @@ -32,6 +32,6 @@ public interface IPlugin : IAsyncPlugin Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context)); - Task> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query)); + Task> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token); } } From 42c5f9e18d0ec901002bf1234ab223717e64c54e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:36:13 +0800 Subject: [PATCH 240/243] Fix code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 42bfff7b0ea..49af0236fb6 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -1927,7 +1927,7 @@ public async Task SetupDialogJumpAsync(nint handle) var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); if (timeOut) return; - // Bring focus back to the the dialog + // Bring focus back to the dialog Win32Helper.SetForegroundWindow(DialogWindowHandle); } catch (Exception e) From ef2e6b05459719d22d8ca9d38ea6a00a00a62ce8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 20 Jul 2025 20:37:30 +1000 Subject: [PATCH 241/243] fix typo --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 +-- .../DialogJump/DialogJump.cs | 34 +++++++++---------- .../DialogJump/Models/WindowsDialog.cs | 4 +-- Flow.Launcher.Plugin/DialogJumpResult.cs | 6 ++-- .../Interfaces/IAsyncDialogJump.cs | 2 +- .../Interfaces/IDialogJump.cs | 2 +- Flow.Launcher/Languages/en.xaml | 16 ++++----- Flow.Launcher/MainWindow.xaml.cs | 4 +-- Flow.Launcher/ViewModel/MainViewModel.cs | 18 +++++----- 9 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index c877894653f..c9a0cb026b0 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -191,7 +191,7 @@ public static void LoadPlugins(PluginsSettings settings) _resultUpdatePlugin = GetPluginsForInterface(); _translationPlugins = GetPluginsForInterface(); - // Initialize dialog jump plugin pairs + // Initialize Dialog Jump plugin pairs foreach (var pair in GetPluginsForInterface()) { _dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair @@ -438,7 +438,7 @@ public static async Task> QueryDialogJumpForPluginAsync(P } catch (Exception e) { - API.LogException(ClassName, $"Failed to query dialog jump for plugin: {metadata.Name}", e); + API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); return null; } return results; diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 6744e7dced9..ee0d9f27a63 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -34,7 +34,7 @@ public static class DialogJump { Metadata = new() { - ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the dialog jump pairs + ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the Dialog Jump pairs Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs }, Plugin = new WindowsExplorer() @@ -44,7 +44,7 @@ public static class DialogJump { Metadata = new() { - ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the dialog jump pairs + ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the Dialog Jump pairs Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs }, Plugin = new WindowsDialog() @@ -110,7 +110,7 @@ public static void InitializeDialogJump(IList dialogJump { if (_initialized) return; - // Initialize dialog jump explorers & dialogs + // Initialize Dialog Jump explorers & dialogs _dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); foreach (var explorer in dialogJumpExplorers) { @@ -129,7 +129,7 @@ public static void InitializeDialogJump(IList dialogJump _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; _dragMoveTimer.Tick += (s, e) => InvokeUpdateDialogJumpWindow(); - // Initialize dialog jump window position + // Initialize Dialog Jump window position DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; _initialized = true; @@ -294,7 +294,7 @@ public static void SetupDialogJump(bool enabled) // Stop drag move timer _dragMoveTimer?.Stop(); - // Reset dialog jump window + // Reset Dialog Jump window if (dialogWindowExists) { InvokeResetDialogJumpWindow(); @@ -353,16 +353,16 @@ public static string GetActiveExplorerPath() private static async Task InvokeShowDialogJumpWindowAsync(bool dialogWindowChanged) { - // Show dialog jump window + // Show Dialog Jump window if (_settings.ShowDialogJumpWindow) { - // Save dialog jump window position for one file dialog + // Save Dialog Jump window position for one file dialog if (dialogWindowChanged) { DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; } - // Call show dialog jump window + // Call show Dialog Jump window IDialogJumpDialogWindow dialogWindow; lock (_dialogWindowLock) { @@ -373,7 +373,7 @@ private static async Task InvokeShowDialogJumpWindowAsync(bool dialogWindowChang await ShowDialogJumpWindowAsync.Invoke(dialogWindow.Handle); } - // Hook move size event if dialog jump window is under dialog & dialog window changed + // Hook move size event if Dialog Jump window is under dialog & dialog window changed if (DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { if (dialogWindowChanged) @@ -428,7 +428,7 @@ private static void InvokeResetDialogJumpWindow() _dialogWindow = null; } - // Reset dialog jump window + // Reset Dialog Jump window ResetDialogJumpWindow?.Invoke(); // Stop drag move timer @@ -444,7 +444,7 @@ private static void InvokeResetDialogJumpWindow() private static void InvokeHideDialogJumpWindow() { - // Hide dialog jump window + // Hide Dialog Jump window HideDialogJumpWindow?.Invoke(); // Stop drag move timer @@ -526,12 +526,12 @@ uint dwmsEventTime alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); } - // Just show dialog jump window + // Just show Dialog Jump window if (alreadySwitched) { await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); } - // Show dialog jump window after navigating the path + // Show Dialog Jump window after navigating the path else { if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) @@ -562,9 +562,9 @@ uint dwmsEventTime dialogWindowExist = true; } } - if (dialogWindowExist) // Neither dialog jump window nor file dialog window is foreground + if (dialogWindowExist) // Neither Dialog Jump window nor file dialog window is foreground { - // Hide dialog jump window until the file dialog window is brought to the foreground + // Hide Dialog Jump window until the file dialog window is brought to the foreground InvokeHideDialogJumpWindow(); } @@ -611,7 +611,7 @@ private static void LocationChangeCallback( uint dwmsEventTime ) { - // If the dialog window is moved, update the dialog jump window position + // If the dialog window is moved, update the Dialog Jump window position var dialogWindowExist = false; lock (_dialogWindowLock) { @@ -636,7 +636,7 @@ private static void MoveSizeCallBack( uint dwmsEventTime ) { - // If the dialog window is moved or resized, update the dialog jump window position + // If the dialog window is moved or resized, update the Dialog Jump window position if (_dragMoveTimer != null) { switch (eventType) diff --git a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs index 2f495468abc..ee4e034337b 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; @@ -148,7 +148,7 @@ public bool JumpFolder(string path, bool auto) { if (auto) { - // Use legacy jump folder method for auto dialog jump because file editor is default value. + // Use legacy jump folder method for auto Dialog Jump because file editor is default value. // After setting path using file editor, we do not need to revert its value. return JumpFolderWithFileEditor(path, false); } diff --git a/Flow.Launcher.Plugin/DialogJumpResult.cs b/Flow.Launcher.Plugin/DialogJumpResult.cs index ad544f5358e..2c9f0c13923 100644 --- a/Flow.Launcher.Plugin/DialogJumpResult.cs +++ b/Flow.Launcher.Plugin/DialogJumpResult.cs @@ -1,18 +1,18 @@ namespace Flow.Launcher.Plugin { /// - /// Describes a result of a executed by a plugin in dialog jump window + /// Describes a result of a executed by a plugin in Dialog Jump window /// public class DialogJumpResult : Result { /// /// This holds the path which can be provided by plugin to be navigated to the - /// file dialog when records in dialog jump window is right clicked on a result. + /// file dialog when records in Dialog Jump window is right clicked on a result. /// public required string DialogJumpPath { get; init; } /// - /// Clones the current dialog jump result + /// Clones the current Dialog Jump result /// public new DialogJumpResult Clone() { diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs index 3ea78f11ff3..e028ebb1264 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs @@ -10,7 +10,7 @@ namespace Flow.Launcher.Plugin public interface IAsyncDialogJump : IFeatures { /// - /// Asynchronous querying for dialog jump window + /// Asynchronous querying for Dialog Jump window /// /// /// If the Querying method requires high IO transmission diff --git a/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs index 27f1be034a8..4b576633af4 100644 --- a/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs @@ -14,7 +14,7 @@ namespace Flow.Launcher.Plugin public interface IDialogJump : IAsyncDialogJump { /// - /// Querying for dialog jump window + /// Querying for Dialog Jump window /// /// This method will be called within a Task.Run, /// so please avoid synchrously wait for long. diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index dda5dbc0b39..2fca6edf0b4 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -369,23 +369,23 @@ Show Result Badges for Global Query Only Show badges for global query results only Dialog Jump - Enter shortcut to quickly navigate the Open/Save As dialogue to the path of the current file manager. + Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager. Dialog Jump - When Open/Save As dialog opens, quickly navigate to the current path of the file manager. + When Open/Save As dialog window opens, quickly navigate to the current path of the file manager. Dialog Jump Automatically - When Open/Save As dialog is displayed, automatically navigate to the path of the current file manager. (Experimental) + When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental) Show Dialog Jump Window - Display dialog jump search window when the open/save dialog is shown to quickly navigate to file/folder locations. + Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations. Dialog Jump Window Position - Select position for dialog jump window - Fixed under the Open/Save As dialog. Displayed on dialog open and stays until dialogue is closed + Select position for the Dialog Jump search window + Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed Default search window position. Displayed when triggered by search window hotkey Dialog Jump Result Navigation Behaviour - Behaviour to navigate Open/Save As dialog to the selected result path + Behaviour to navigate Open/Save As dialog window to the selected result path Left click or Enter key Right click Dialog Jump File Navigation Behaviour - Behaviour to navigate Open/Save As dialog when the result is a file path + Behaviour to navigate Open/Save As dialog window when the result is a file path Fill full path in file name box Fill full path in file name box and open Fill directory in path box diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 1ed287e6037..2ddce81900e 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -217,7 +217,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; - // Register dialog jump events + // Register Dialog Jump events InitializeDialogJump(); // View model property changed event @@ -604,7 +604,7 @@ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref b handled = true; break; case Win32Helper.WM_EXITSIZEMOVE: - // Do do handle size move event for dialog jump window + // Do do handle size move event for Dialog Jump window if (_viewModel.IsDialogJumpWindowUnderDialog()) { return IntPtr.Zero; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 42bfff7b0ea..28ec8583207 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -405,7 +405,7 @@ public void ForwardHistory() [RelayCommand] private void LoadContextMenu() { - // For dialog jump and right click mode, we need to navigate to the path + // For Dialog Jump and right click mode, we need to navigate to the path if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.RightClick) { if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) @@ -498,7 +498,7 @@ private async Task OpenResultAsync(string index) return; } - // For dialog jump and left click mode, we need to navigate to the path + // For Dialog Jump and left click mode, we need to navigate to the path if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.LeftClick) { Hide(); @@ -1346,7 +1346,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var currentIsHomeQuery = query.IsHomeQuery; var currentIsDialogJump = _isDialogJump; - // Do not show home page for dialog jump window + // Do not show home page for Dialog Jump window if (currentIsHomeQuery && currentIsDialogJump) { ClearResults(); @@ -1862,14 +1862,14 @@ public async Task SetupDialogJumpAsync(nint handle) await Task.Delay(300); } - // If handle is cleared, which means the dialog is closed, clear dialog jump state + // If handle is cleared, which means the dialog is closed, clear Dialog Jump state if (DialogWindowHandle == nint.Zero) { _isDialogJump = false; return; } - // Initialize dialog jump window + // Initialize Dialog Jump window if (MainWindowVisibilityStatus) { if (dialogWindowHandleChanged) @@ -1907,7 +1907,7 @@ public async Task SetupDialogJumpAsync(nint handle) if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { - // Cancel the previous dialog jump task + // Cancel the previous Dialog Jump task _dialogJumpSource?.Cancel(); // Create a new cancellation token source @@ -1923,7 +1923,7 @@ public async Task SetupDialogJumpAsync(nint handle) // Check dialog handle if (DialogWindowHandle == nint.Zero) return; - // Wait 150ms to check if dialog jump window gets the focus + // Wait 150ms to check if Dialog Jump window gets the focus var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); if (timeOut) return; @@ -1945,7 +1945,7 @@ public async void ResetDialogJump() // Cache original dialog window handle var dialogWindowHandle = DialogWindowHandle; - // Reset the dialog jump state + // Reset the Dialog Jump state DialogWindowHandle = nint.Zero; _isDialogJump = false; @@ -1996,7 +1996,7 @@ public void HideDialogJump() if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { // Warning: Main window is already in foreground - // This is because if you click popup menus in other applications to hide dialog jump window, + // This is because if you click popup menus in other applications to hide Dialog Jump window, // they can steal focus before showing main window if (MainWindowVisibilityStatus) { From 9079c46acdca35f2cfaf0b14350aaa03a331a408 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:42:42 +0800 Subject: [PATCH 242/243] Log error --- Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 13519ea7a92..d315c74ca36 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -589,9 +589,9 @@ uint dwmsEventTime } } } - catch (System.Exception) + catch (System.Exception ex) { - // Ignored + Log.Exception(ClassName, "An error occurred while checking foreground explorer windows", ex); } } } From 2834c56c7b333586c9e6f1f4a9b96df82f0911b7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 20 Jul 2025 18:47:14 +0800 Subject: [PATCH 243/243] Fix potential deadlock risk with nested Task.Run and await --- .../DialogJump/DialogJump.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index d315c74ca36..35f43b411e3 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -457,7 +457,17 @@ private static void InvokeHideDialogJumpWindow() public static void OnToggleHotkey(object sender, HotkeyEventArgs args) { - _ = Task.Run(() => NavigateDialogPathAsync(PInvoke.GetForegroundWindow())); + _ = Task.Run(async () => + { + try + { + await NavigateDialogPathAsync(PInvoke.GetForegroundWindow()); + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to navigate dialog path", ex); + } + }); } #endregion @@ -534,7 +544,18 @@ uint dwmsEventTime // Show dialog jump window after navigating the path else { - if (!await Task.Run(() => NavigateDialogPathAsync(hwnd, true))) + if (!await Task.Run(async () => + { + try + { + return await NavigateDialogPathAsync(hwnd, true); + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to navigate dialog path", ex); + return false; + } + })) { await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); }