Skip to content

Quickswitch #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 76 commits into
base: dev
Choose a base branch
from
Open

Quickswitch #1018

wants to merge 76 commits into from

Conversation

taooceros
Copy link
Member

@taooceros taooceros commented Feb 10, 2022

Important

All test codes LogDebug in QuickSwitch should be removed.

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
  • Quick switch automatically issue for executing many times: Open file dialog (Auto fill) -> Close -> Open file dialog (Not auto fill)
  • 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.
  1. 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 -> Right click results to navigate to the selected path

Quick Switch API

Implement new api interfaces to let plugin be queried on quick switch window.

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.

TODO

@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.

@onesounds
Copy link
Contributor

The dialog swapping works well. But I think we should improve the search feature that shows up at the bottom. Right now, it’s just a display-only feature. In Listary, when you type a path or filename at the bottom of the dialog, it directly inputs that into the dialog. (In other words, typing a specific path or file and hitting enter inputs it into the dialog.) If we’re not going to support that behavior just yet, I think the “show” feature should be turned off and either hidden or disabled in the UI, otherwise we shouldn't include it in the release.

@Jack251970
Copy link
Contributor

Jack251970 commented Apr 17, 2025

The dialog swapping works well. But I think we should improve the search feature that shows up at the bottom. Right now, it’s just a display-only feature. In Listary, when you type a path or filename at the bottom of the dialog, it directly inputs that into the dialog. (In other words, typing a specific path or file and hitting enter inputs it into the dialog.) If we’re not going to support that behavior just yet, I think the “show” feature should be turned off and either hidden or disabled in the UI, otherwise we shouldn't include it in the release.

You can use right click to open file or directory in dialog. (I override the original context menu feature because I think we do not need to support context menu for quick switch results.) And Explorer plugin already supports quick switch. I am planning release this feature as beta in this version if you think it is good to go.

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: 3

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

488-511: ⚠️ Potential issue

Release COM objects to prevent memory leaks.

The EnumerateShellWindows method creates COM objects (shellWindowsObj and shellWindows) but doesn't release them after use, which can lead to memory leaks if called repeatedly.

Add proper COM object release:

private static unsafe void EnumerateShellWindows(Action<object> 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;

+   try
+   {
        // Enumerate the shell windows
        var count = shellWindows.Count;
        for (var i = 0; i < count; i++)
        {
            action(shellWindows.Item(i));
        }
+   }
+   finally
+   {
+       Marshal.ReleaseComObject(shellWindows);
+       Marshal.ReleaseComObject(shellWindowsObj);
+   }
}

153-159: ⚠️ Potential issue

Add exception handling to prevent hotkey failures.

The OnToggleHotkey method doesn't handle exceptions from NavigateDialogPath, which could cause the application to crash if something goes wrong during navigation.

Add exception handling:

public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
{
    if (_isInitialized)
    {
-       NavigateDialogPath();
+       try
+       {
+           NavigateDialogPath();
+       }
+       catch (Exception ex)
+       {
+           Log.Exception(ClassName, "Error processing quickswitch hotkey", ex);
+       }
    }
}

205-208: 🛠️ Refactor suggestion

Add logging for exception cases to aid troubleshooting.

This catch block silently swallows exceptions without logging them, making it difficult to diagnose issues in production.

Add logging:

catch
{
+   Log.Error(ClassName, "Failed to retrieve or process path from folder view");
    return;
}

402-405: 🛠️ Refactor suggestion

Add logging for COM exceptions in catch block.

Ignoring COM exceptions without logging makes debugging difficult. Consider adding logging to understand what's happening when these errors occur.

Add logging:

catch (COMException ex)
{
-   // Ignored
+   Log.Debug(ClassName, "COM exception while processing shell window: " + ex.Message);
}

409-412: 🛠️ Refactor suggestion

Add logging for caught exceptions.

The catch block swallows exceptions without logging them, which can make debugging difficult.

Add logging:

catch (System.Exception ex)
{
-   // Ignored
+   Log.Debug(ClassName, "Error while enumerating shell windows: " + ex.Message);
}

133-140: 🛠️ Refactor suggestion

Handle partial hook initialization failure properly.

If one hook fails but others succeed, the currently initialized hooks aren't disposed, leading to a potential resource leak.

Add proper cleanup:

if (_foregroundChangeHook.IsInvalid ||
    _locationChangeHook.IsInvalid ||
    /*_moveSizeHook.IsInvalid ||*/
    _destroyChangeHook.IsInvalid)
{
    Log.Error(ClassName, "Failed to initialize QuickSwitch");
+   // Clean up any hooks that were successfully created
+   DisposeHooks();
    return;
}

+private static void DisposeHooks()
+{
+   if (_foregroundChangeHook != null && !_foregroundChangeHook.IsInvalid)
+   {
+       _foregroundChangeHook.Dispose();
+       _foregroundChangeHook = null;
+   }
+   if (_locationChangeHook != null && !_locationChangeHook.IsInvalid)
+   {
+       _locationChangeHook.Dispose();
+       _locationChangeHook = null;
+   }
+   if (_destroyChangeHook != null && !_destroyChangeHook.IsInvalid)
+   {
+       _destroyChangeHook.Dispose();
+       _destroyChangeHook = null;
+   }
+}

65-151: 🛠️ Refactor suggestion

Initialize COM system explicitly before use.

The code interacts with COM interfaces without ensuring COM is initialized for the current thread, which may cause failures in certain scenarios.

Add COM initialization:

public static void Initialize()
{
    if (_isInitialized) return;

+   // Initialize COM for the current thread
+   try
+   {
+       PInvoke.CoInitialize(null);
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Failed to initialize COM", ex);
+       return;
+   }

    // Rest of the initialization code...

And add corresponding cleanup in the Dispose method:

public static void Dispose()
{
    // Dispose handle
    // ... existing code ...

    // Release ComObjects
    if (_lastExplorerView != null)
    {
        Marshal.ReleaseComObject(_lastExplorerView);
        _lastExplorerView = null;
    }
    
+   // Uninitialize COM for this thread
+   try
+   {
+       PInvoke.CoUninitialize();
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Error uninitializing COM", ex);
+   }
+   
+   _isInitialized = false;
}
🧹 Nitpick comments (1)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1)

219-270: Consider using ThreadPool instead of creating new threads.

Creating a new Thread for each navigation operation can lead to resource overhead. Using ThreadPool would be more efficient, especially if this operation happens frequently.

Replace Thread creation with ThreadPool:

-var t = new Thread(async () =>
+ThreadPool.QueueUserWorkItem(async _ =>
{
    // 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);
    if (timeOut)
    {
        return;
    };

    // Rest of the method remains the same...
});
-t.Start();
return;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a74ad6d and d6e69b2.

📒 Files selected for processing (4)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
  • Flow.Launcher.Infrastructure/Win32Helper.cs (6 hunks)
  • Flow.Launcher/MainWindow.xaml.cs (11 hunks)
  • Flow.Launcher/ViewModel/MainViewModel.cs (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Flow.Launcher/MainWindow.xaml.cs
  • Flow.Launcher.Infrastructure/Win32Helper.cs
  • Flow.Launcher/ViewModel/MainViewModel.cs
🔇 Additional comments (4)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (4)

1-17: LGTM! The imports and namespace organization look good.

The file imports all necessary dependencies for Windows API interaction, COM interop, threading, and UI integration.


288-303: LGTM! The helper method for retrieving window class names is well-implemented.

The method correctly uses fixed blocks and native methods to retrieve window class names safely.


22-62: LGTM! The class structure and static fields are well organized.

The Quick Switch functionality is organized with clear action delegates and proper locking objects for thread safety.


513-544: LGTM! The Dispose method correctly releases resources.

The method properly disposes of event hooks and COM objects. Just make sure to add COM uninitialization as suggested in a previous comment.

Comment on lines +433 to +459
// 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,
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;
}
}
}*/
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider implementing a more reliable window dragging detection system.

The current system for detecting window dragging is commented out, with a TODO noting it needs improvement. The current implementation based on timers may not be reliable.

Research more reliable methods for detecting window dragging in Windows, such as using the Windows UI Automation framework or Windows hooks that specifically track window movement.

Consider:

  1. Using UI Automation events for tracking window movements
  2. Using low-level mouse hooks to detect drag operations
  3. Using a combination of window position monitoring and input state tracking

🏁 Script executed:

#!/bin/bash
# Search for reliable window drag detection implementations
rg -A 5 -B 5 "window.+drag" --glob="*.cs"
rg -A 5 -B 5 "EVENT_SYSTEM_MOVESIZESTART" --glob="*.cs"

Length of output: 2316


Implement a robust window move/size detection mechanism

The commented‐out WinEventHook callback in Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (around lines 433–459) leaves QuickSwitch unaware of dialog drags. Rather than a delayed timer, consider one of the following to retarget the window immediately:

  • Re‑enable and refine the existing SetWinEventHook approach, handling EVENT_SYSTEM_MOVESIZESTART/END directly in MoveSizeCallBack.
  • Use UI Automation’s WindowPattern or StructureChanged events to detect when the dialog’s bounding rectangle changes.
  • Install a WH_CALLWNDPROC hook (via SetWindowsHookEx) to intercept WM_MOVING/WM_SIZING messages from the dialog window.
  • As a fallback, employ a low‑level mouse hook (WH_MOUSE_LL) to track drag start/end over the window’s non‐client area.

Addressing this will eliminate the 1 sec timer delay and ensure QuickSwitch follows the dialog smoothly.

This comment has been minimized.

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: 2

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

222-273: Prefer Task.Run / thread‑pool instead of spawning raw threads each time

Every jump creates a brand‑new Thread, which is expensive (1 MB stack, extra scheduling overhead) and can accumulate if users trigger the hot‑key repeatedly. Using the thread pool keeps resource usage bounded and simplifies the async logic.

-var t = new Thread(async () =>
+_ = Task.Run(async () =>
 {
     ...
 });
-t.Start();

(This suggestion was raised earlier and remains outstanding.)


65-92: 🛠️ Refactor suggestion

⚠️ Potential issue

Add explicit COM initialization to avoid sporadic failures on some systems

Initialize() uses COM objects (IWebBrowser2, IShellWindows) before ensuring COM is initialized for the calling thread. On machines where the STA model is not already in place this can throw RPC_E_CHANGED_MODE or silently fail to marshal objects, breaking quick‑switch detection at startup.

 public static void Initialize()
 {
     if (_isInitialized) return;

+    // Initialise COM for this thread – required before touching shell COM objects
+    var hr = PInvoke.CoInitialize(IntPtr.Zero);
+    if (hr.Failed)
+    {
+        Log.Error(ClassName, $"CoInitialize failed: {hr}");
+        return;
+    }
+
     // Check all foreground windows and check if there are explorer windows

and remember to balance it in Dispose() (see comment further down).


491-514: ⚠️ Potential issue

Release COM objects in EnumerateShellWindows to prevent leaks

CoCreateInstance gives you ownership of both shellWindowsObj and the typed cast shellWindows, and every element returned by shellWindows.Item(i). None of them are released, so every call leaks three COM references.

 var shellWindows = (IShellWindows)shellWindowsObj;

-// Enumerate the shell windows
-var count = shellWindows.Count;
-for (var i = 0; i < count; i++)
-{
-    action(shellWindows.Item(i));
-}
+try
+{
+    var count = shellWindows.Count;
+    for (var i = 0; i < count; i++)
+    {
+        var wnd = shellWindows.Item(i);
+        try
+        {
+            action(wnd);
+        }
+        finally
+        {
+            Marshal.ReleaseComObject(wnd);
+        }
+    }
+}
+finally
+{
+    Marshal.ReleaseComObject(shellWindows);
+    Marshal.ReleaseComObject(shellWindowsObj);
+}

Without this, opening and closing many dialogs during one session will steadily increase private bytes.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d6e69b2 and b3ff92f.

📒 Files selected for processing (5)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs (1 hunks)
  • Flow.Launcher.Infrastructure/Win32Helper.cs (6 hunks)
  • Flow.Launcher/App.xaml.cs (3 hunks)
  • Flow.Launcher/Languages/en.xaml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • Flow.Launcher/Languages/en.xaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • Flow.Launcher/App.xaml.cs
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs
  • Flow.Launcher.Infrastructure/Win32Helper.cs

This comment has been minimized.

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: 1

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

437-463: Implement a robust window move/size detection mechanism.

The commented-out MoveSizeCallBack indicates an unimplemented feature for detecting window dragging. Without proper implementation, the current timer-based approach may not reliably update the quick switch window position during dialog dragging.

Consider implementing one of these alternatives:

  1. Re-enable and refine the existing SetWinEventHook approach to handle EVENT_SYSTEM_MOVESIZESTART/END directly.
  2. Use UI Automation to detect dialog window position changes.
  3. Install a WH_CALLWNDPROC hook to intercept WM_MOVING/WM_SIZING messages.

This would eliminate the timer delay and ensure the quick switch window follows the dialog smoothly.


492-515: 🛠️ Refactor suggestion

Release COM objects to prevent memory leaks.

The EnumerateShellWindows method creates COM objects but doesn't release them, which can lead to memory leaks when called repeatedly during foreground window changes.

Use a try/finally block to ensure proper cleanup:

private static unsafe void EnumerateShellWindows(Action<object> 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;

+   try
+   {
        // Enumerate the shell windows
        var count = shellWindows.Count;
        for (var i = 0; i < count; i++)
        {
            action(shellWindows.Item(i));
        }
+   }
+   finally
+   {
+       Marshal.ReleaseComObject(shellWindows);
+       Marshal.ReleaseComObject(shellWindowsObj);
+   }
}

153-159: 🛠️ Refactor suggestion

Add exception handling to prevent hotkey failures.

The OnToggleHotkey method doesn't handle exceptions, which could cause the application to become unresponsive if an error occurs while processing the hotkey.

Add a try-catch block to prevent exceptions from propagating:

public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
{
    if (_isInitialized)
    {
+       try
+       {
            NavigateDialogPath(Win32Helper.GetForegroundWindowHWND());
+       }
+       catch (Exception ex)
+       {
+           Log.Exception(ClassName, "Error processing quickswitch hotkey", ex);
+       }
    }
}

406-409: 🛠️ Refactor suggestion

Add logging for COM exceptions to aid troubleshooting.

The code catches COMException but doesn't log it, making it difficult to diagnose issues in production.

catch (COMException ex)
{
-   // Ignored
+   Log.Debug(ClassName, $"COM exception when processing shell window: {ex.Message}");
}

413-416: 🛠️ Refactor suggestion

Add logging for caught exceptions.

The catch block swallows exceptions without logging them, which can make debugging difficult.

catch (System.Exception ex)
{
-   // Ignored
+   Log.Debug(ClassName, $"Error while enumerating shell windows: {ex.Message}");
}

177-180: 🛠️ Refactor suggestion

Add detailed logging for COM exceptions.

The COM exception is caught but not logged, making it difficult to diagnose issues with explorer view document access.

catch (COMException ex)
{
+   Log.Debug(ClassName, $"COM exception when accessing explorer view document: {ex.Message}");
    return;
}

65-151: 🛠️ Refactor suggestion

Initialize COM for the current thread before accessing COM interfaces.

The code accesses COM interfaces without ensuring COM is initialized, which may cause failures in certain environments.

public static void Initialize()
{
    if (_isInitialized) return;

+   // Initialize COM for the current thread
+   try
+   {
+       PInvoke.CoInitialize(null);
+   }
+   catch (Exception ex)
+   {
+       Log.Exception(ClassName, "Failed to initialize COM", ex);
+       return;
+   }

    // Check all foreground windows and check if there are explorer windows
    lock (_lastExplorerViewLock)
    {
        // Rest of the implementation...
    }

    // Rest of initialization...

    if (_foregroundChangeHook.IsInvalid ||
        _locationChangeHook.IsInvalid ||
        /*_moveSizeHook.IsInvalid ||*/
        _destroyChangeHook.IsInvalid)
    {
        Log.Error(ClassName, "Failed to initialize QuickSwitch");
+       // Uninitialize COM before returning
+       PInvoke.CoUninitialize();
        return;
    }

    // Rest of initialization...

    _isInitialized = true;
    return;
}

517-557: 🛠️ Refactor suggestion

Add CoUninitialize call in Dispose method.

If CoInitialize is called during initialization, CoUninitialize should be called during cleanup to maintain COM balance.

public static void Dispose()
{
    // Reset initialize flag
    _isInitialized = false;

    // Dispose handle
    if (_foregroundChangeHook != null)
    {
        _foregroundChangeHook.Dispose();
        _foregroundChangeHook = null;
    }
    // Other hook disposals...

    // Release ComObjects
    if (_lastExplorerView != null)
    {
        Marshal.ReleaseComObject(_lastExplorerView);
        _lastExplorerView = null;
    }

    // Stop drag move timer
    if (_dragMoveTimer != null)
    {
        _dragMoveTimer.Stop();
        _dragMoveTimer = null;
    }
    
+   // Uninitialize COM for this thread
+   try
+   {
+       PInvoke.CoUninitialize();
+   }
+   catch (Exception ex)
+   {
+       Log.Debug(ClassName, $"Error uninitializing COM: {ex.Message}");
+   }
}

133-140: 🛠️ Refactor suggestion

Handle partial hook failure to prevent resource leak.

If any of the event hooks is invalid, the code immediately returns without disposing already-created hooks, potentially leading to resource leaks.

if (_foregroundChangeHook.IsInvalid ||
    _locationChangeHook.IsInvalid ||
    /*_moveSizeHook.IsInvalid ||*/
    _destroyChangeHook.IsInvalid)
{
    Log.Error(ClassName, "Failed to initialize QuickSwitch");
+   // Clean up any successfully created hooks
+   DisposeHooks();
    return;
}

+private static void DisposeHooks()
+{
+   if (_foregroundChangeHook != null && !_foregroundChangeHook.IsInvalid)
+   {
+       _foregroundChangeHook.Dispose();
+       _foregroundChangeHook = null;
+   }
+   if (_locationChangeHook != null && !_locationChangeHook.IsInvalid)
+   {
+       _locationChangeHook.Dispose();
+       _locationChangeHook = null;
+   }
+   if (_destroyChangeHook != null && !_destroyChangeHook.IsInvalid)
+   {
+       _destroyChangeHook.Dispose();
+       _destroyChangeHook = null;
+   }
+}

19-62: ⚠️ Potential issue

Handle potentially stale COM references in event callbacks.

The static _lastExplorerView COM reference might become invalid across different callbacks, especially when multiple Explorer windows are opened/closed in quick succession.

Consider implementing a solution that verifies COM references before use:

private static IWebBrowser2 _lastExplorerView = null;

+ // Helper method to safely check if COM reference is still valid
+ private static bool IsComObjectAlive(object comObject)
+ {
+     if (comObject == null) return false;
+     try
+     {
+         // GetHashCode is a simple call that should succeed if object is alive
+         comObject.GetHashCode();
+         return true;
+     }
+     catch (COMException)
+     {
+         return false;
+     }
+ }

// Then use this check before accessing COM objects:
private static void NavigateDialogPath(HWND dialog, Action action = null)
{
    object document = null;
    try
    {
        lock (_lastExplorerViewLock)
        {
+           if (!IsComObjectAlive(_lastExplorerView))
+           {
+               Log.Debug(ClassName, "COM reference to explorer view is no longer valid");
+               _lastExplorerView = null;
+               return;
+           }
            
            if (_lastExplorerView != null)
            {
                // Rest of implementation...
            }
        }
    }
}

221-275: 🛠️ Refactor suggestion

Use ThreadPool instead of creating new threads for JumpToPath operations.

Creating a new thread for each operation is expensive and could lead to thread proliferation if many path jumps are performed in quick succession.

- var t = new Thread(async () =>
+ ThreadPool.QueueUserWorkItem(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)
    {
+       Log.Debug(ClassName, "Timed out waiting for dialog window to become foreground");
        return;
    };

    // Assume that the dialog is in the foreground now
    await _navigationLock.WaitAsync();
    try
    {
        // Rest of the implementation...
    }
    finally
    {
        _navigationLock.Release();
    }
    
    // Invoke action if provided
    action?.Invoke();
-});
-t.Start();
+});
return;
🧹 Nitpick comments (2)
Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (2)

319-370: Refactor dialog window handling for improved readability and maintainability.

The current structure with nested if-else statements and duplicate code blocks for showing the quick switch window makes the code harder to maintain.

Consider extracting common functionality to a separate method:

// File dialog window
if (GetWindowClassName(hwnd) == DialogWindowClassName)
{
    Log.Debug(ClassName, $"Hwnd: {hwnd}");

    lock (_dialogWindowHandleLock)
    {
        _dialogWindowHandle = hwnd;
    }

+   bool showWindowNow = true;
+   Action showWindowAction = () => {
+       if (_settings.ShowQuickSwitchWindow)
+       {
+           ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value);
+           _dragMoveTimer?.Start();
+       }
+   };

    // Navigate to path
    if (_settings.AutoQuickSwitch)
    {
        // Check if we have already switched for this dialog
        bool alreadySwitched;
        lock (_autoSwitchedDialogsLock)
        {
            alreadySwitched = _autoSwitchedDialogs.Contains(hwnd);
        }

        // Just show quick switch window
        if (alreadySwitched)
        {
-           if (_settings.ShowQuickSwitchWindow)
-           {
-               ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value);
-               _dragMoveTimer?.Start();
-           }
+           showWindowAction();
        }
        // Show quick switch window after navigating the path
        else
        {
+           showWindowNow = false;
-           NavigateDialogPath(hwnd, () =>
-           {
-               if (_settings.ShowQuickSwitchWindow)
-               {
-                   ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value);
-                   _dragMoveTimer?.Start();
-               }
-           });
+           NavigateDialogPath(hwnd, showWindowAction);
        }
    }
-   else
+   
+   if (showWindowNow && !_settings.AutoQuickSwitch)
    {
-       // Show quick switch window
-       if (_settings.ShowQuickSwitchWindow)
-       {
-           ShowQuickSwitchWindow?.Invoke(_dialogWindowHandle.Value);
-           _dragMoveTimer?.Start();
-       }
+       showWindowAction();
    }
}

1-16: Consider adding a mechanism to handle COM initialization on a per-thread basis.

The current implementation assumes a single thread context, but the class uses multiple threads (e.g., in JumpToPath). Each thread accessing COM should have its own initialization.

Consider implementing a COM initialization helper:

private static void EnsureComInitialized()
{
    try
    {
        // Initialize COM for the current thread (if not already initialized)
        PInvoke.CoInitialize(null);
    }
    catch (Exception ex)
    {
        // Only log at debug level as this might be called on already initialized threads
        Log.Debug(ClassName, $"Error initializing COM: {ex.Message}");
    }
}

private static void CleanupCom()
{
    try
    {
        PInvoke.CoUninitialize();
    }
    catch (Exception ex)
    {
        Log.Debug(ClassName, $"Error uninitializing COM: {ex.Message}");
    }
}

Then call EnsureComInitialized() at the beginning of methods that need COM and CleanupCom() at the end, particularly in any thread-based operations.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5cac6 and e57816d.

📒 Files selected for processing (1)
  • Flow.Launcher.Infrastructure/QuickSwitch/QuickSwitch.cs (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: gitStream.cm
  • GitHub Check: gitStream.cm
  • GitHub Check: gitStream.cm
  • GitHub Check: gitStream.cm

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: 2

🧹 Nitpick comments (1)
Flow.Launcher.Infrastructure/NativeMethods.txt (1)

58-79: Organize WinEvent, COM and message declarations
The new native methods (SetWinEventHook, SendMessage, etc.), COM interfaces (IShellFolderViewDual2, IShellWindows, IWebBrowser2, CoCreateInstance, CLSCTX), and message/event constants are all appended as a single block. Consider grouping them into labeled sections (e.g., “WinEvent Hooks”, “COM Interfaces & CLSCTX Flags”, “Windows Messages”) and sorting each section alphabetically to improve maintainability and discoverability.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e57816d and 58621b7.

📒 Files selected for processing (2)
  • Flow.Launcher.Infrastructure/NativeMethods.txt (1 hunks)
  • Flow.Launcher.Infrastructure/Win32Helper.cs (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Flow.Launcher.Infrastructure/Win32Helper.cs
🔇 Additional comments (2)
Flow.Launcher.Infrastructure/NativeMethods.txt (2)

56-56: Verify LOCALE_TRANSIENT_KEYBOARD4 constant accuracy
Ensure that LOCALE_TRANSIENT_KEYBOARD4 matches the corresponding value in the Windows SDK (e.g., WinNls.h) and is supported on all targeted Windows versions.


69-72: Validate event coverage for QuickSwitch logic
You’ve added EVENT_OBJECT_DESTROY, EVENT_OBJECT_LOCATIONCHANGE, EVENT_SYSTEM_MOVESIZESTART, and EVENT_SYSTEM_MOVESIZEEND. Verify that these are the exact events your QuickSwitch implementation requires. If you need to track window creation or other lifecycle events, consider adding those here as well.

Comment on lines +58 to +66
SendMessage
EVENT_SYSTEM_FOREGROUND
WINEVENT_OUTOFCONTEXT
WM_KEYDOWN
WM_SETTEXT
IShellFolderViewDual2
CoCreateInstance
CLSCTX
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing UnhookWinEvent P/Invoke
You’ve imported SetWinEventHook for event listening but did not declare UnhookWinEvent, which is required to unhook WinEvent callbacks properly. The UnhookWindowsHookEx API does not apply to WinEvent hooks. Please add the correct declaration for UnhookWinEvent.

Comment on lines +73 to +79
GetFocus
SetFocus
MapVirtualKey
WM_KEYUP
WM_KEYDOWN
GetCurrentThreadId
AttachThreadInput
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate WM_KEYDOWN entry
The WM_KEYDOWN constant is declared twice (once at line 62 and again at line 77). This duplication will cause conflicts in the generated interop code. Please remove the redundant declaration on line 77.

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 Count
❌ check-file-path 1
❌ forbidden-pattern 24
⚠️ ignored-expect-variant 1
⚠️ noisy-file 1
⚠️ non-alpha-in-dictionary 19

See ❌ Event descriptions for more information.

Forbidden patterns 🙅 (1)

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
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.

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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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