Skip to content

Quick Switch #1018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 253 commits into
base: dev
Choose a base branch
from
Open

Quick Switch #1018

wants to merge 253 commits into from

Conversation

taooceros
Copy link
Member

@taooceros taooceros commented Feb 10, 2022

Nothing more than quickswitch. We may integrate flow's path system to this feature instead of relying explorer.

Setup Quick Switch

  1. Quick switch key: Alt+G by default
  2. Quick switch automatically
  3. Quick switch window

Use Quick Switch

  1. Open explorer -> Open file dialog -> Use hotkey to navigate to that path.

  2. Open file dialog -> Query window (quick switch window) fixed under file dialog -> Click results to navigate to the selected path

Quick Switch API

Implement new api interfaces to develop third party explorers & dialogs which are for dotnet plugins only.

public interface IAsyncQuickSwitch
{
    /// <summary>
    /// Asynchronous querying for quick switch window
    /// </summary>
    Task<List<QuickSwitchResult>> QueryQuickSwitchAsync(Query query, CancellationToken token);
}
public interface IQuickSwitch : IAsyncQuickSwitch
{
    /// <summary>
    /// Querying for quick switch window
    /// </summary>
    List<QuickSwitchResult> QueryQuickSwitch(Query query);

    Task<List<QuickSwitchResult>> IAsyncQuickSwitch.QueryQuickSwitchAsync(Query query, CancellationToken token) => Task.Run(() => QueryQuickSwitch(query));
}
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin in quick switch window
/// </summary>
public class QuickSwitchResult : Result
{
    /// <summary>
    /// 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.
    /// </summary>
    public required string QuickSwitchPath { get; init; }

    // ...
}

Additionally, Explorer plugin already supports quick switch.

Test

  • Quick switch hotkey can change and apply.
  • Automatically quick switch can work under most cases (since it is an experimental feature)
  • Show quick switch window
  • Quick switch window position can change properly.
  • Quick switch window action can change properly.
  • Explorer path will select the active tab.

Todos

  • https://github.com/listary/Listary.FileAppPlugin/blob/master/docs%2FGetting%20Started.md
  • Support opening file path.
  • Quick switch automatically issue for save as file dialog from @onesounds: For the Open dialog, the path changes correctly at the time the dialog is opened, even without switching focus to File Explorer.
    However, for the Save As dialog, the path does not apply immediately when the dialog is opened. It only works after switching focus to File Explorer and then returning.
  • Third party explorer.
  • Third party dialog.
  • In auto mode, the target program still closes when using "Open." (Previously reported issue) on some devices & apps from @onesounds.
  • Dialogs pop up on many other dialog windows.
  • When the bottom bar mode is active, if a dialog is closed and Flow is exited without being reopened, the window position is saved based on the bottom bar mode location. On the next launch, Flow opens at the last position used by the bottom bar mode.
  • In the "Save As" dialog, Flow automatically inputs the entire file name, which causes unintended saving.
  • Check if plugin is modified before querying (Check if plugin is modified before querying #3791)

Future

  • Add support for automatic quick switch (automatically navigate the file dialog path to the path of the last explorer without triggering the quick switch hotkey), this feature can cause some problems and it is hidden from setting panel.

@taooceros
Copy link
Member Author

Some threading issues seem appearing. Not sure the detailed reason.

@stefnotch
Copy link
Contributor

Okay, my main questions would be

  • What's the intended use-case for this? As in, when does a Flow plugin need to navigate in the actual file explorer, instead of opening a new one with the correct path.
  • Would opening a new file explorer window with the desired path, and closing the old one work? Or does that not handle certain cases? (e.g. the file browser/chooser )

@taooceros
Copy link
Member Author

Okay, my main questions would be

* What's the intended use-case for this? As in, when does a Flow plugin need to navigate in the actual file explorer, instead of opening a new one with the correct path.

I think the major use case is to sync the path of an opened explorer to a open file dialog to select a file easily.

Would opening a new file explorer window with the desired path, and closing the old one work? Or does that not handle certain cases? (e.g. the file browser/chooser )

Sry I don't get the idea.

@stefnotch
Copy link
Contributor

Okay, I understand what this is used for now. I'd have to dig a lot deeper into what the IUIAutomation can do to be able to improve this.

I think the rule of thumb is to avoid sending keyboard events, and instead always use an API if one exists. Keyboard events can be delayed and whatnot.

@taooceros
Copy link
Member Author

Okay, I understand what this is used for now. I'd have to dig a lot deeper into what the IUIAutomation can do to be able to improve this.

I think the rule of thumb is to avoid sending keyboard events, and instead always use an API if one exists. Keyboard events can be delayed and whatnot.

Yeah that's what I would like to see. It is possible to use PInvoke directly without IUIAutomation though, so it will be cool if you are familiar with that as well.

Another thing is the original listary seems implement this feature without changing the textbox and sending an enter signal, so I wonder whether you may have some clues about that.

@stefnotch
Copy link
Contributor

I tried searching for what I could, but that's apparently quite tricky to hook into. So I don't really have a better solution at the moment.

@taooceros
Copy link
Member Author

I tried searching for what I could, but that's apparently quite tricky to hook into. So I don't really have a better solution at the moment.

okay thanks🤣

@stefnotch
Copy link
Contributor

stefnotch commented Aug 17, 2022

There might be a alternate design:

So the file manager has the "quick access" sidebar. Flow could add its own entry there, and that entry always redirects to the currently open folder. An additional advantage might be that it's easier to discover this, compared to a keyboard shortcut.

Screenshot for context:

image

(Note: I have no idea how hard that would be to efficiently pull that off.)

@taooceros
Copy link
Member Author

So you mean to add a entry that redirect to the most recent opened explorer path?🤔Interesting

@stefnotch
Copy link
Contributor

Yep, spot-on.

@taooceros
Copy link
Member Author

Yep, spot-on.

If that's the case, we may be able to create a plugin for it.

@taooceros
Copy link
Member Author

Do you have any docs for that?

@stefnotch
Copy link
Contributor

stefnotch commented Aug 17, 2022

@taooceros I haven't looked into this all that much (just a few cursory google searches)

Programmatic access

Apparently there's a way of programmatically adding folders to the quick access area.

https://stackoverflow.com/questions/30051634/is-it-possible-programmatically-add-folders-to-the-windows-10-quick-access-panel

Special Links folder

https://blogs.msmvps.com/kenlin/2017/06/14/537/

Steps:

  1. Enable a special, built-in folder by setting a value in the system registry. Anything in this folder will land in the "quick access".
  2. Put a shortcut in that folder. (A .lnk shortcut)
  3. And then always update the shortcut's path to point at the currently open file explorer.

Symbolic links or Hardlink

I bet there's some trickery that could be done with those

Extra harddrive

We could add an in-memory harddrive, mount it and provide a single shortcut in there.
This might be a tad tricky though, depending on whether there's an easy API/wrapper or not...

@mcthesw
Copy link

mcthesw commented Oct 15, 2022

Could this be done? I really love this feature.

@VictoriousRaptor VictoriousRaptor linked an issue Nov 2, 2022 that may be closed by this pull request
@VictoriousRaptor VictoriousRaptor linked an issue Nov 27, 2022 that may be closed by this pull request
@stefnotch
Copy link
Contributor

Yet another option would be to add a "switch to" context menu entry

Sort of like how 7zip has a dynamic context menu, except that we'd populate it with the titles of other explorer windows.
image

@stefnotch
Copy link
Contributor

stefnotch commented Jan 8, 2023

Apparently Windows 11 can add files to quick access. That might let us pin a program to quick access

Such a program could then update the list of files in the quick access window.

@rexhibition
Copy link

Really hope we can get the quick switch function :( the Ctrl+G in Listary is so useful

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (5)

598-610: Fix cross-thread timer access.

MoveSizeCallBack is executed on the WinEvent hook thread, not the WPF UI thread where _dragMoveTimer was created. Calling Start()/Stop() directly can raise an InvalidOperationException.

case PInvoke.EVENT_SYSTEM_MOVESIZESTART:
-   _dragMoveTimer.Start();
+   _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Start());
    break;
case PInvoke.EVENT_SYSTEM_MOVESIZEEND:
-   _dragMoveTimer.Stop();
+   _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Stop());
    break;

416-419: Add exception handling to prevent hotkey failures.

The hotkey handler doesn't catch exceptions, which could lead to hotkey failures if the navigation logic throws exceptions, potentially disrupting the application's functionality.

public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
{
-   _ = Task.Run(() => NavigateDialogPathAsync(PInvoke.GetForegroundWindow()));
+   try
+   {
+       _ = Task.Run(async () => 
+       {
+           try 
+           {
+               await NavigateDialogPathAsync(PInvoke.GetForegroundWindow());
+           }
+           catch (Exception ex)
+           {
+               Log.Exception(ClassName, "Error in NavigateDialogPathAsync from hotkey", ex);
+           }
+       });
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Error processing quickswitch hotkey", ex);
+   }
}

791-794: Add null check for Path.GetDirectoryName result.

Path.GetDirectoryName() can return null for root paths or invalid paths, which would cause issues in the DirJump method.

case QuickSwitchFileResultBehaviours.Directory:
    Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}");
-   result = DirJump(Path.GetDirectoryName(path), dialog, auto);
+   var dirPath = Path.GetDirectoryName(path);
+   if (string.IsNullOrEmpty(dirPath))
+   {
+       Log.Error(ClassName, $"Could not get directory for file: {path}");
+       return false;
+   }
+   result = DirJump(dirPath, dialog, auto);
    break;

425-560: Add exception handling to async void method.

The ForegroundChangeCallback is marked with SuppressMessage for async void, but lacks comprehensive exception handling. Any unhandled exceptions could terminate the application.

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private static async void ForegroundChangeCallback(
    // parameters...
)
{
+   try 
+   {
        await _foregroundChangeLock.WaitAsync();
        try
        {
            // existing implementation...
        }
        finally
        {
            _foregroundChangeLock.Release();
        }
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Unhandled exception in ForegroundChangeCallback", ex);
+   }
}

134-203: Initialize COM before using shell APIs.

The SetupQuickSwitch method uses Windows shell APIs through RefreshLastExplorer() but never initializes COM. This could cause failures on some systems where COM hasn't been initialized.

if (enabled)
{
+   // Initialize COM for this thread
+   var hr = PInvoke.CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
+   if (hr.Failed)
+   {
+       Log.Error(ClassName, $"Failed to initialize COM: {hr}");
+       return;
+   }
+
    // Check if there are explorer windows and get the topmost one

Remember to balance this with CoUninitialize() in the Dispose() method.

🧹 Nitpick comments (2)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (2)

87-90: Optimize data structure for better performance.

Using List<HWND> for _autoSwitchedDialogs results in O(n) performance for Contains, Add, and Remove operations. Since these operations occur in foreground-change callbacks, consider using HashSet<HWND> for O(1) performance and automatic duplicate prevention.

-private static readonly List<HWND> _autoSwitchedDialogs = new();
+private static readonly HashSet<HWND> _autoSwitchedDialogs = new();

628-628: Fix typo in log message.

There's a spelling error in the log message.

-Log.Debug(ClassName, $"Destory dialog: {hwnd}");
+Log.Debug(ClassName, $"Destroy dialog: {hwnd}");
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6be321 and cf66077.

📒 Files selected for processing (1)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (7)
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3791
File: Flow.Launcher.Core/Plugin/PluginManager.cs:293-295
Timestamp: 2025-07-01T05:46:13.251Z
Learning: In Flow.Launcher.Core/Plugin/PluginManager.cs, when checking if a plugin is modified within the PluginManager class itself, prefer using the internal static PluginModified(string id) method directly rather than going through API.PluginModified() for better performance and architectural design.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:244-247
Timestamp: 2025-06-08T14:12:21.348Z
Learning: In Flow.Launcher, App.NotifyIcon is created before MainWindow creation, so null checks for App.NotifyIcon are not necessary when accessing it from MainWindow code.
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:34:24.926Z
Learning: In Windows Forms dialogs, Windows handles invalid paths and prevents the user from clicking "Ok" if the path is incorrect, so additional path validation is unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3572
File: Flow.Launcher/App.xaml.cs:214-216
Timestamp: 2025-07-06T12:21:37.927Z
Learning: In Flow Launcher, the UpdatePluginManifestAsync method in PluginsManifest.cs already has comprehensive internal try-catch handling that logs exceptions and returns false on failure rather than throwing, making external try-catch wrappers unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:318-318
Timestamp: 2025-06-08T14:12:12.842Z
Learning: In Flow.Launcher, the App.NotifyIcon static property is initialized in the App class before MainWindow creation, so null checks are not needed when accessing App.NotifyIcon in MainWindow lifecycle methods.
Learnt from: onesounds
PR: Flow-Launcher/Flow.Launcher#3394
File: Flow.Launcher/Themes/Darker Glass.xaml:134-141
Timestamp: 2025-03-28T21:12:13.386Z
Learning: In Flow.Launcher, hotkey styling is implemented with a two-component structure: a Border element with style `ItemHotkeyBGStyle` that provides background and border styling, containing a TextBlock with style `ItemHotkeyStyle` that handles the text styling.
🪛 GitHub Actions: Check Spelling
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs

[warning] 272-42: Spell check warning: 'Wnd' is not a recognized word.


[warning] 279-86: Spell check warning: 'Wnd' is not a recognized word.


[warning] 12-14: Spell check warning: 'NHotkey' is not a recognized word.


[warning] 628-51: Spell check warning: 'Destory' is not a recognized word.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: gitStream.cm
  • GitHub Check: build
🔇 Additional comments (1)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1)

713-764: Fix logic error in dialog window search.

The method has flawed logic - it may return the wrong dialog window because it doesn't properly validate the HWND match before returning. Additionally, the cache isn't updated when a dialog window is found.

// Finally search for the dialog window again
foreach (var dialog in _quickSwitchDialogs.Keys)
{
    if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
        dialog.Metadata.Disabled) continue; // Plugin is disabled

    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.Plugin.CheckDialogWindow(hwnd);
    }

    // Update dialog window if found
    if (dialogWindow != null)
    {
        _quickSwitchDialogs[dialog] = dialogWindow;
-       return dialogWindow;
+       // Only return if this dialog window matches the target HWND
+       if (dialogWindow.Handle == hwnd)
+       {
+           return dialogWindow;
+       }
    }
}

return null;
⛔ Skipped due to learnings
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3791
File: Flow.Launcher.Core/Plugin/PluginManager.cs:293-295
Timestamp: 2025-07-01T05:46:13.251Z
Learning: In Flow.Launcher.Core/Plugin/PluginManager.cs, when checking if a plugin is modified within the PluginManager class itself, prefer using the internal static PluginModified(string id) method directly rather than going through API.PluginModified() for better performance and architectural design.

This comment has been minimized.

This comment has been minimized.

@VictoriousRaptor
Copy link
Contributor

Seems like not working well with multi tab explorer in windows 11. It goes for the first tab rather than the active tab.

@Jack251970
Copy link
Member

Seems like not working well with multi tab explorer in windows 11. It goes for the first tab rather than the active tab.

I noticed this problem before but I cannot find the correct API to get the active tab path. Feel free to change the codes in Flow.Launcher.Infrastructure.QuickSwitch.Models.WindowsExplorer in WindowsExplorer.cs

This comment has been minimized.

@VictoriousRaptor
Copy link
Contributor

VictoriousRaptor commented Jul 17, 2025

Seems like not working well with multi tab explorer in windows 11. It goes for the first tab rather than the active tab.

I noticed this problem before but I cannot find the correct API to get the active tab path. Feel free to change the codes in Flow.Launcher.Infrastructure.QuickSwitch.Models.WindowsExplorer in WindowsExplorer.cs

https://github.com/w4po/ExplorerTabUtility/blob/fad736353210d36595c6e6caa77ac7ad9129b3ee/ExplorerTabUtility/Helpers/Helper.cs#L336

https://github.com/w4po/ExplorerTabUtility/blob/fad736353210d36595c6e6caa77ac7ad9129b3ee/ExplorerTabUtility/Hooks/ExplorerWatcher.cs#L736

This might help.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (10)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (10)

134-203: Initialize COM before using Windows shell APIs.

The code uses COM interfaces through shell APIs but never initializes COM. This could cause failures on some systems.

Add COM initialization at the start of SetupQuickSwitch when enabling:

 if (enabled)
 {
+    // Initialize COM for this thread
+    var hr = PInvoke.CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
+    if (hr.Failed)
+    {
+        Log.Error(ClassName, $"Failed to initialize COM: {hr}");
+        return;
+    }
+
     // Check if there are explorer windows and get the topmost one

329-332: Add error handling to ShowQuickSwitchWindowAsync invocation.

The method invokes external code but doesn't include error handling, which could lead to unhandled exceptions disrupting the QuickSwitch functionality.

if (dialogWindow != null && ShowQuickSwitchWindowAsync != null)
{
-   await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle);
+   try
+   {
+       await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle);
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Error showing QuickSwitch window", ex);
+   }
}

416-419: Add exception handling to prevent hotkey failures.

The hotkey handler doesn't catch exceptions, which could lead to hotkey failures if the navigation logic throws exceptions, potentially disrupting the application's functionality.

public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
{
-   _ = Task.Run(() => NavigateDialogPathAsync(PInvoke.GetForegroundWindow()));
+   try
+   {
+       _ = Task.Run(async () => 
+       {
+           try 
+           {
+               await NavigateDialogPathAsync(PInvoke.GetForegroundWindow());
+           }
+           catch (Exception ex)
+           {
+               Log.Exception(ClassName, "Error in NavigateDialogPathAsync from hotkey", ex);
+           }
+       });
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Error processing quickswitch hotkey", ex);
+   }
}

482-485: Incorrect lock object – potential race condition in auto‑switch tracking

The read‑side lock guarding _autoSwitchedDialogs uses _dialogWindowHandleLock, not _autoSwitchedDialogsLock, which means concurrent reads/writes can hit a race when JumpToPath adds to the list.

- lock (_dialogWindowHandleLock)
+ lock (_autoSwitchedDialogsLock)
 {
     alreadySwitched = _autoSwitchedDialogs.Contains(hwnd);
 }

598-608: DispatcherTimer.Start/Stop called from non-UI thread → cross-thread exception risk

MoveSizeCallBack is executed on the WinEvent hook thread, not the WPF UI thread where _dragMoveTimer was created.
Invoking Start()/Stop() directly can raise an InvalidOperationException.

Invoke via the dispatcher that owns the timer:

case PInvoke.EVENT_SYSTEM_MOVESIZESTART:
-    _dragMoveTimer.Start();
+    _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Start());
    break;
case PInvoke.EVENT_SYSTEM_MOVESIZEEND:
-    _dragMoveTimer.Stop();
+    _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Stop());
    break;

743-760: Fix logic error in dialog window search.

The method returns the first dialog window found instead of the matching one, which could return the wrong dialog window.

-return dialogWindow;
+if (dialogWindow != null)
+{
+    _quickSwitchDialogs[dialog] = dialogWindow;
+    return dialogWindow;
+}

766-824: JumpToPathAsync always returns true – success/failure swallowed

result is computed but never returned; the method unconditionally returns true, so callers cannot detect failure. Fix by returning the real status:

-            try
+            bool result = false;
+            try
             {
-                bool result;
+                // … existing code …

                 if (result)
                 {
                     lock (_autoSwitchedDialogsLock)
                     {
                         _autoSwitchedDialogs.Add(new(dialogHandle));
                     }
                 }
             }
             catch (System.Exception e)
             {
                 Log.Exception(ClassName, "Failed to jump to path", e);
             }
             finally
             {
                 _navigationLock.Release();
             }
-            return true;
+            return result;

793-793: Add null check for Path.GetDirectoryName result.

Path.GetDirectoryName() can return null for root paths or invalid paths, which would cause issues in the DirJump method.

case QuickSwitchFileResultBehaviours.Directory:
    Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}");
-   result = DirJump(Path.GetDirectoryName(path), dialog, auto);
+   var dirPath = Path.GetDirectoryName(path);
+   if (string.IsNullOrEmpty(dirPath))
+   {
+       Log.Error(ClassName, $"Could not get directory for file: {path}");
+       return false;
+   }
+   result = DirJump(dirPath, dialog, auto);
    break;

858-918: Enhance resource cleanup in Dispose method.

The Dispose method should include COM uninitialization to balance any CoInitialize calls, and clear the auto-switched dialogs list.

// Stop drag move timer
if (_dragMoveTimer != null)
{
    _dragMoveTimer.Stop();
    _dragMoveTimer = null;
}

+// Clear auto-switched dialogs list
+lock (_autoSwitchedDialogsLock)
+{
+    _autoSwitchedDialogs.Clear();
+}
+
+// Uninitialize COM (ignore failure – nothing we can do)
+try 
+{ 
+    PInvoke.CoUninitialize(); 
+} 
+catch (Exception ex)
+{
+    Log.Debug(ClassName, $"Error uninitializing COM: {ex.Message}");
+}

87-90: Use HashSet for better performance.

The _autoSwitchedDialogs collection performs Contains, Add, and Remove operations which are O(n) with List. Since these operations happen in event callbacks, using HashSet would provide O(1) performance.

Apply this diff to improve performance:

-// A list of all file dialog windows that are auto switched already
-private static readonly List<HWND> _autoSwitchedDialogs = new();
+// A set of all file dialog windows that are auto switched already
+private static readonly HashSet<HWND> _autoSwitchedDialogs = new();
🧹 Nitpick comments (1)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1)

628-628: Fix typo in log message.

-Log.Debug(ClassName, $"Destory dialog: {hwnd}");
+Log.Debug(ClassName, $"Destroy dialog: {hwnd}");
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e8bf1f and 6a3ca10.

📒 Files selected for processing (1)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (7)
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3791
File: Flow.Launcher.Core/Plugin/PluginManager.cs:293-295
Timestamp: 2025-07-01T05:46:13.251Z
Learning: In Flow.Launcher.Core/Plugin/PluginManager.cs, when checking if a plugin is modified within the PluginManager class itself, prefer using the internal static PluginModified(string id) method directly rather than going through API.PluginModified() for better performance and architectural design.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:244-247
Timestamp: 2025-06-08T14:12:21.348Z
Learning: In Flow.Launcher, App.NotifyIcon is created before MainWindow creation, so null checks for App.NotifyIcon are not necessary when accessing it from MainWindow code.
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:34:24.926Z
Learning: In Windows Forms dialogs, Windows handles invalid paths and prevents the user from clicking "Ok" if the path is incorrect, so additional path validation is unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3572
File: Flow.Launcher/App.xaml.cs:214-216
Timestamp: 2025-07-06T12:21:37.947Z
Learning: In Flow Launcher, the UpdatePluginManifestAsync method in PluginsManifest.cs already has comprehensive internal try-catch handling that logs exceptions and returns false on failure rather than throwing, making external try-catch wrappers unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:318-318
Timestamp: 2025-06-08T14:12:12.842Z
Learning: In Flow.Launcher, the App.NotifyIcon static property is initialized in the App class before MainWindow creation, so null checks are not needed when accessing App.NotifyIcon in MainWindow lifecycle methods.
Learnt from: onesounds
PR: Flow-Launcher/Flow.Launcher#3394
File: Flow.Launcher/Themes/Darker Glass.xaml:134-141
Timestamp: 2025-03-28T21:12:13.386Z
Learning: In Flow.Launcher, hotkey styling is implemented with a two-component structure: a Border element with style `ItemHotkeyBGStyle` that provides background and border styling, containing a TextBlock with style `ItemHotkeyStyle` that handles the text styling.
🪛 GitHub Actions: Check Spelling
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs

[warning] 272-272: Spell check warning: Wnd is not a recognized word. (unrecognized-spelling)


[warning] 279-279: Spell check warning: Wnd is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

@Jack251970
Copy link
Member

Seems like not working well with multi tab explorer in windows 11. It goes for the first tab rather than the active tab.

I noticed this problem before but I cannot find the correct API to get the active tab path. Feel free to change the codes in Flow.Launcher.Infrastructure.QuickSwitch.Models.WindowsExplorer in WindowsExplorer.cs

https://github.com/w4po/ExplorerTabUtility/blob/fad736353210d36595c6e6caa77ac7ad9129b3ee/ExplorerTabUtility/Helpers/Helper.cs#L336

https://github.com/w4po/ExplorerTabUtility/blob/fad736353210d36595c6e6caa77ac7ad9129b3ee/ExplorerTabUtility/Hooks/ExplorerWatcher.cs#L736

This might help.

It helps, and now active explorer path feature is implemented

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (9)
Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs (1)

47-73: COM object leak in shell window enumeration.

The EnumerateShellWindows method creates a COM object via CoCreateInstance but never releases it, leading to potential memory leaks.

Apply this fix to properly release the COM object:

 var shellWindows = (IShellWindows)shellWindowsObj;

+try
+{
    // Enumerate the shell windows
    var count = shellWindows.Count;
    for (var i = 0; i < count; i++)
    {
        if (!action(shellWindows.Item(i)))
        {
            return;
        }
    }
+}
+finally
+{
+    Marshal.ReleaseComObject(shellWindows);
+}
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (8)

87-89: Use HashSet for better performance.

The _autoSwitchedDialogs list uses linear search operations (O(n)) for Contains, Add, and Remove. A HashSet<HWND> would provide O(1) performance.

Apply this change:

-private static readonly List<HWND> _autoSwitchedDialogs = new();
+private static readonly HashSet<HWND> _autoSwitchedDialogs = new();

134-203: Missing COM initialization before shell API usage.

The SetupQuickSwitch method uses Windows shell APIs that require COM initialization but doesn't initialize COM, which could cause failures.

Add COM initialization when enabling QuickSwitch:

 if (enabled)
 {
+    // Initialize COM for this thread
+    var hr = PInvoke.CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
+    if (hr.Failed)
+    {
+        Log.Error(ClassName, $"Failed to initialize COM: {hr}");
+        return;
+    }
+
     // Check if there are explorer windows and get the topmost one

329-332: Add error handling to async event invocation.

The ShowQuickSwitchWindowAsync invocation lacks error handling, which could lead to unhandled exceptions.

Wrap the invocation in error handling:

 if (dialogWindow != null && ShowQuickSwitchWindowAsync != null)
 {
-    await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle);
+    try
+    {
+        await ShowQuickSwitchWindowAsync.Invoke(dialogWindow.Handle);
+    }
+    catch (Exception ex)
+    {
+        Log.Exception(ClassName, "Error showing QuickSwitch window", ex);
+    }
 }

362-374: Validate SetWinEventHook success and add cleanup.

The SetMoveProc method doesn't validate if SetWinEventHook succeeds, which could lead to null hook handles.

Add validation and error handling:

 _moveSizeHook = PInvoke.SetWinEventHook(
     PInvoke.EVENT_SYSTEM_MOVESIZESTART,
     PInvoke.EVENT_SYSTEM_MOVESIZEEND,
     PInvoke.GetModuleHandle((PCWSTR)null),
     _moveProc,
     processId,
     threadId,
     PInvoke.WINEVENT_OUTOFCONTEXT);
+
+if (_moveSizeHook.IsNull)
+{
+    Log.Error(ClassName, $"Failed to hook move/size events for dialog {handle}");
+    return;
+}

416-419: Missing exception handling in hotkey handler.

The OnToggleHotkey method lacks exception handling, which could cause hotkey failures and disrupt application functionality.

Add comprehensive exception handling:

 public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
 {
-    _ = Task.Run(() => NavigateDialogPathAsync(PInvoke.GetForegroundWindow()));
+    try
+    {
+        _ = Task.Run(async () => 
+        {
+            try 
+            {
+                await NavigateDialogPathAsync(PInvoke.GetForegroundWindow());
+            }
+            catch (Exception ex)
+            {
+                Log.Exception(ClassName, "Error in NavigateDialogPathAsync from hotkey", ex);
+            }
+        });
+    }
+    catch (Exception ex)
+    {
+        Log.Exception(ClassName, "Error processing quickswitch hotkey", ex);
+    }
 }

598-608: Cross-thread timer access in MoveSizeCallBack.

The MoveSizeCallBack is executed on a Win32 event hook thread, but _dragMoveTimer operations require UI thread access.

Use dispatcher to ensure thread safety:

 case PInvoke.EVENT_SYSTEM_MOVESIZESTART:
-    _dragMoveTimer.Start();
+    _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Start());
     break;
 case PInvoke.EVENT_SYSTEM_MOVESIZEEND:
-    _dragMoveTimer.Stop();
+    _dragMoveTimer.Dispatcher.BeginInvoke(() => _dragMoveTimer.Stop());
     break;

801-804: Missing null check for Path.GetDirectoryName.

Path.GetDirectoryName() can return null for root paths, which would cause issues in the DirJump method.

Add null validation:

 case QuickSwitchFileResultBehaviours.Directory:
     Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}");
-    result = DirJump(Path.GetDirectoryName(path), dialog, auto);
+    var dirPath = Path.GetDirectoryName(path);
+    if (string.IsNullOrEmpty(dirPath))
+    {
+        Log.Error(ClassName, $"Could not get directory for file: {path}");
+        return false;
+    }
+    result = DirJump(dirPath, dialog, auto);
     break;

868-928: Incomplete resource cleanup in Dispose method.

The Dispose method doesn't clean up all resources properly, potentially leaving COM uninitialized and other resources.

Add comprehensive cleanup:

 // Stop drag move timer
 if (_dragMoveTimer != null)
 {
     _dragMoveTimer.Stop();
     _dragMoveTimer = null;
 }
+
+// Clear auto-switched dialogs list
+lock (_autoSwitchedDialogsLock)
+{
+    _autoSwitchedDialogs.Clear();
+}
+
+// Uninitialize COM (ignore failure – nothing we can do)
+try 
+{ 
+    PInvoke.CoUninitialize(); 
+} 
+catch (Exception ex)
+{
+    Log.Debug(ClassName, $"Error uninitializing COM: {ex.Message}");
+}
🧹 Nitpick comments (1)
Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs (1)

75-78: Empty Dispose method should be implemented.

The Dispose method is empty but should handle cleanup if needed or be marked as intentionally empty.

Consider adding a comment to clarify this is intentionally empty:

 public void Dispose()
 {
-    
+    // No resources to dispose in this implementation
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3ca10 and b89d0ab.

📒 Files selected for processing (2)
  • Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs (1 hunks)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs (1)
Learnt from: Koisu-unavailable
PR: Flow-Launcher/Flow.Launcher#3770
File: Flow.Launcher/ViewModel/MainViewModel.cs:0-0
Timestamp: 2025-06-24T19:06:48.344Z
Learning: In Flow.Launcher's Explorer plugin results, the SubTitle property always contains the directory containing the file. For file results, Title contains the filename and SubTitle contains the parent directory. For directory results, SubTitle contains the directory path itself.
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (7)
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3791
File: Flow.Launcher.Core/Plugin/PluginManager.cs:293-295
Timestamp: 2025-07-01T05:46:13.251Z
Learning: In Flow.Launcher.Core/Plugin/PluginManager.cs, when checking if a plugin is modified within the PluginManager class itself, prefer using the internal static PluginModified(string id) method directly rather than going through API.PluginModified() for better performance and architectural design.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:244-247
Timestamp: 2025-06-08T14:12:21.348Z
Learning: In Flow.Launcher, App.NotifyIcon is created before MainWindow creation, so null checks for App.NotifyIcon are not necessary when accessing it from MainWindow code.
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:34:24.926Z
Learning: In Windows Forms dialogs, Windows handles invalid paths and prevents the user from clicking "Ok" if the path is incorrect, so additional path validation is unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3572
File: Flow.Launcher/App.xaml.cs:214-216
Timestamp: 2025-07-06T12:21:37.947Z
Learning: In Flow Launcher, the UpdatePluginManifestAsync method in PluginsManifest.cs already has comprehensive internal try-catch handling that logs exceptions and returns false on failure rather than throwing, making external try-catch wrappers unnecessary.
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3672
File: Flow.Launcher/MainWindow.xaml.cs:318-318
Timestamp: 2025-06-08T14:12:12.842Z
Learning: In Flow.Launcher, the App.NotifyIcon static property is initialized in the App class before MainWindow creation, so null checks are not needed when accessing App.NotifyIcon in MainWindow lifecycle methods.
Learnt from: onesounds
PR: Flow-Launcher/Flow.Launcher#3394
File: Flow.Launcher/Themes/Darker Glass.xaml:134-141
Timestamp: 2025-03-28T21:12:13.386Z
Learning: In Flow.Launcher, hotkey styling is implemented with a two-component structure: a Border element with style `ItemHotkeyBGStyle` that provides background and border styling, containing a TextBlock with style `ItemHotkeyStyle` that handles the text styling.
🪛 GitHub Actions: Check Spelling
Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs

[warning] 29-29: IWeb is not a recognized word. (unrecognized-spelling)


[warning] 57-57: iid is not a recognized word. (unrecognized-spelling)


[warning] 122-122: IWeb is not a recognized word. (unrecognized-spelling)


[warning] 126-126: IWeb is not a recognized word. (unrecognized-spelling)


[warning] 133-133: IWeb is not a recognized word. (unrecognized-spelling)


[warning] 142-142: Wnd is not a recognized word. (unrecognized-spelling)


[warning] 250-250: IUnknown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
Flow.Launcher.Infrastructure/QuickSwitch/Models/WindowsExplorer.cs (7)

15-45: Class structure and interface implementation look good.

The WindowsExplorer class properly implements IQuickSwitchExplorer with appropriate process name checking and shell window enumeration.


92-104: Path retrieval logic is sound.

The GetExplorerPath method correctly handles the active tab detection and path retrieval with proper null checks.


122-171: COM object release is properly handled in finally block.

The GetExplorerByTabHandle method correctly releases the shellBrowser COM object in the finally block, preventing resource leaks.


173-187: STA thread creation pattern is appropriate.

The StartSTAThread method correctly creates an STA thread for COM operations that require specific apartment threading.


189-216: Dynamic COM access with proper error handling.

The GetLocation method uses dynamic typing appropriately to access COM properties that aren't available in static typing, with proper exception handling.


218-232: Path normalization logic is comprehensive.

The NormalizeLocation method handles various Windows path formats including environment variables and special shell locations.


237-260: COM interface definitions are correctly structured.

The IServiceProvider and IShellBrowser interfaces are properly defined with correct GUIDs and method signatures for COM interop.

Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1)

81-84: Delegate rooting prevents garbage collection issues.

The static delegate fields (_fgProc, _locProc, _desProc) properly root the callback methods to prevent garbage collection from breaking the Win32 event hooks.

<system:String x:Key="autoQuickSwitch">Quick Switch Automatically</system:String>
<system:String x:Key="autoQuickSwitchToolTip">Automatically navigate to the path of the current file manager when a file dialog is opened. (Experimental)</system:String>
<system:String x:Key="showQuickSwitchWindow">Show Quick Switch Window</system:String>
<system:String x:Key="showQuickSwitchWindowToolTip">Show quick switch search window when file dialogs are opened to navigate its path (Only work for file open dialog).</system:String>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been able to use it for file save dialogue as well, can you also confirm?

Jack251970 and others added 2 commits July 17, 2025 19:12
Co-authored-by: Jeremy Wu <[email protected]>
Co-authored-by: Jeremy Wu <[email protected]>

This comment has been minimized.

Copy link

@check-spelling-bot Report

🔴 Please review

See the 📂 files view, the 📜action log, or 📝 job summary for details.

❌ Errors and Warnings Count
❌ forbidden-pattern 17
⚠️ non-alpha-in-dictionary 2

See ❌ Event descriptions for more information.

Forbidden patterns 🙅 (2)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

s.b. workaround(s)

\bwork[- ]arounds?\b

Reject duplicate words

\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

// 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);
IReadOnlyList<Result> resultsCopy;
if (e.Results == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? When will it be null?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
20 min review bug Something isn't working enhancement New feature or request kind/ux related to user experience listary from listary review in progress Indicates that a review is in progress for this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Quick Switch like Listary output search result to file selection dialog?
8 participants