Skip to content

Add explorer-tree-click-expand mod#4767

Open
diegoalejo15 wants to merge 7 commits into
ramensoftware:mainfrom
diegoalejo15:main
Open

Add explorer-tree-click-expand mod#4767
diegoalejo15 wants to merge 7 commits into
ramensoftware:mainfrom
diegoalejo15:main

Conversation

@diegoalejo15

Copy link
Copy Markdown

By default, in File Explorer's navigation pane (tree view), left-clicking
a folder only selects it; you have to click the small chevron/arrow to
expand it.

This mod adds three click behaviors to the navigation pane tree:

  • Left click on a folder's icon or label: expands it one level (like
    pressing Numpad +). The native chevron behavior is untouched.
  • Ctrl + left click on any folder: collapses the entire tree back to
    its original state (all folders collapsed).
  • Alt + left click on a folder: expands it and all its subfolders
    recursively, down to the last level (like pressing the Numpad * key).

Both Ctrl+click and Alt+click behaviors can be individually enabled or
disabled in the mod's settings. Both are enabled by default.

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Changelog item 1...
  • Changelog item 2...

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • [x ] The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@m417z

m417z commented Jul 18, 2026

Copy link
Copy Markdown
Member

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


1. The low-level hook has no message loop. WH_MOUSE_LL is dispatched by the system sending a message to the thread that called SetWindowsHookEx, so that thread must run a message loop or the callback is never invoked (after LowLevelHooksTimeout the event is simply skipped). Wh_ModInit installs the hook and returns; nothing pumps messages. The same applies to g_hHiddenWnd — its WM_APP_COLLAPSE_ALL / WM_APP_EXPAND_ALL messages are PostMessage'd but never dispatched, so HiddenWndProc never runs either. Both established WH_MOUSE_LL mods install the hook on a dedicated thread that runs a GetMessage loop — see scroll-window-opacity.wh.cpp and mouse-button-remap.wh.cpp. (This becomes moot if you switch to subclassing per item 2, which is the better fix.)

The mod might work when loaded early (e.g. on boot) because the init runs on the main thread, and Explorer creates a message loop later on, but it's incorrect to rely on it.

2. Replace the system-wide mouse hook with subclassing the nav-pane tree. The mod only cares about clicks on Explorer's own navigation-pane SysTreeView32, but WH_MOUSE_LL is a global hook that fires for every mouse event in every process — unnecessary overhead, and the source of items 3 and 4. The idiomatic approach for an Explorer mod is to locate the nav-pane tree and subclass it with WindhawkUtils::SetWindowSubclassFromAnyThread, handling WM_LBUTTONDOWN in the subclass proc. That proc runs on the tree's own UI thread, so TreeView_* / SendMessage calls are same-thread, safe, and non-blocking — and the global hook plus the whole hidden-window machinery disappear. See classic-explorer-treeview.wh.cpp: it locates the tree via FindWindowEx(..., L"SysTreeView32", ...) and subclasses it with SetWindowSubclassFromAnyThread (removing it in cleanup). Note this should stay an explorer.exe mod — don't convert it to a tool mod.

3. Cross-process corruption: TVM_HITTEST / TVM_GETITEM sent to windows in other processes. Because the hook is global, WindowFromPoint(info->pt) can return a SysTreeView32 belonging to another process (RegEdit, common dialogs, any app with a tree view). The mod then SendMessage's TVM_HITTEST / TVM_GETITEM with a pointer to a local TVHITTESTINFO / TVITEM. These are WM_USER-range messages, and SendMessage does not marshal their pointer parameters across process boundaries — the receiving process dereferences the raw pointer value in its own address space, causing garbage reads or a crash in that other app (and the plain-click path's PostMessage(hWnd, TVM_EXPAND, ...) would expand unrelated apps' trees). At minimum you'd have to filter to your own process (GetWindowThreadProcessId(hWnd, &pid); if (pid != GetCurrentProcessId()) return CallNextHookEx(...);), but subclassing (item 2) removes the hazard entirely since you only ever touch your own tree.

4. No blocking work inside a low-level hook. LowLevelMouseProc calls SendMessage(hWnd, TVM_HITTEST, ...) and SendMessage(hWnd, TVM_GETITEM, ...) — synchronous cross-thread sends. WH_MOUSE_LL / WH_KEYBOARD_LL callbacks run on the installing thread and block all system input until they return; a SendMessage into a busy UI thread can stall the whole desktop (or time out and silently drop the event). Low-level hooks must return as fast as possible. Subclassing (item 2) resolves this too, since the hit-testing then happens on the tree's own thread rather than on the input path.

Optional improvements

Minor polish — none of this affects users once the items above are addressed, so it's your call.

  • Add a visual to the README. This is a visible interaction change, so a short GIF (hosted on i.imgur.com or raw.githubusercontent.com) demonstrating left-click / Ctrl+click / Alt+click would help users understand the effect at a glance.
  • RegisterClassW and CreateWindowW return values are unchecked. Minor, and both go away with the subclass redesign (item 2).

@diegoalejo15

Copy link
Copy Markdown
Author

Thanks for the detailed review! I've pushed a rewrite that addresses all four required items:

  1. No message loop – removed the WH_MOUSE_LL hook and hidden window entirely.
  2. Subclassing instead of a global hook – the mod now hooks CreateWindowExW to detect the nav pane's SysTreeView32 (identified by its parent being a NamespaceTreeControl) and subclasses it directly with WindhawkUtils::SetWindowSubclassFromAnyThread. Already-open Explorer windows are picked up at init via EnumWindows/EnumChildWindows.
  3. Cross-process corruption – no longer possible, since the mod only ever touches windows created within its own process (explorer.exe).
  4. Blocking work in a low-level hook – gone along with the hook itself; hit-testing now happens on the tree's own thread inside the subclass proc.

I also added proper cleanup in Wh_ModUninit (removing all subclasses so nothing points into unloaded code) and a README note explaining that users can add other programs (e.g. Excel, Cubase) to the process inclusion list from the Advanced tab if they want the same behavior in those Open/Save dialogs, since they use the same nav pane control.

Haven't added the optional GIF yet, let me know if that's still worth doing before merge. Ready for another look whenever you have time.

@m417z

m417z commented Jul 20, 2026

Copy link
Copy Markdown
Member

Haven't added the optional GIF yet, let me know if that's still worth doing before merge. Ready for another look whenever you have time.

Sure, why not. Below is another review round. I didn't verify all the items manually. If you think it's OK as is, we can also submit the current version, let me know.

Submission review


1. CollapseAllKeepingRoot only collapses the first root — Ctrl+click "collapse all" doesn't work on the default nav pane layout.

The nav pane tree has multiple top-level (root) items in the default Windows 10/11 layout (Home/Quick access, Gallery, OneDrive, This PC, Network, …). It only has a single "Desktop" root when the "Show all folders" folder option is enabled — which is exactly the configuration the code's comment ("e.g. 'Desktop'") assumes.

void CollapseAllKeepingRoot(HWND hwndTree) {
    HTREEITEM hRoot = TreeView_GetRoot(hwndTree);   // only the FIRST root
    if (!hRoot) {
        return;
    }
    TreeView_Expand(hwndTree, hRoot, TVE_EXPAND);
    CollapseAllRecursive(hwndTree, TreeView_GetChild(hwndTree, hRoot));
}

TreeView_GetRoot returns just the first root item, so on the default layout this only collapses the subtree under the first root (e.g. Home). If the user expanded folders under This PC or Network and Ctrl+clicks, nothing under those roots collapses — the feature effectively does nothing outside the first root. Iterate all root-level siblings instead:

void CollapseAllKeepingRoot(HWND hwndTree) {
    for (HTREEITEM hRoot = TreeView_GetRoot(hwndTree); hRoot;
         hRoot = TreeView_GetNextSibling(hwndTree, hRoot)) {
        // Keep each top-level root expanded, collapse everything below it.
        CollapseAllRecursive(hwndTree, TreeView_GetChild(hwndTree, hRoot));
    }
}

For reference, other nav-pane mods explicitly account for both layouts — e.g. never-auto-expand-explorer-tree-items.wh.cpp#L120-L138 (single-root vs. top-level, where a top-level item's parent is nullptr), and hide-home-gallery-explorer.wh.cpp#L269 iterates root siblings with TVGN_NEXT.

2. Add a screenshot/GIF to the README. This mod has a visible, interactive effect (click → folder expands), which is hard to convey in prose. A short GIF of a left-click expanding one level and Alt+click expanding all would make the catalog entry much clearer. Allowed image hosts are i.imgur.com and raw.githubusercontent.com.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • The subclass proc's 5th parameter is named uIdSubclass but with WindhawkUtils::SetWindowSubclassFromAnyThread that slot actually carries dwRefData (the 0 you pass). Harmless (you don't use it), but the name is misleading — consider dwRefData or dropping the name.
  • In WM_NCDESTROY, calling RemoveWindowSubclassFromAnyThread is unnecessary, it's handled automatically. It also doesn't hurt, just redundant.

Functionality notes

Non-critical observations about the feature behavior itself.

  • Double-click quirk: double-clicking a collapsed folder body fires WM_LBUTTONDOWN (your handler expands it) and then WM_LBUTTONDBLCLK (the tree's native double-click toggles it back to collapsed), so a double-click net-collapses instead of expanding. Single-click already expands, so this is a minor edge case, but worth being aware of.
  • TVE_COLLAPSE | TVE_COLLAPSERESET (shift+click) deletes the item's child items so they're re-enumerated on the next expand. That matches your intended "come back collapsed" behavior, but note it's operating on a tree whose items are owned/managed by the NamespaceTreeControl — worth double-checking it behaves consistently across Explorer versions if you haven't already.
  • The base left-click-expand has no on/off setting (only the Ctrl/Alt/Shift behaviors do). Some users may want the modifier actions without changing what a plain click does — consider a fourth setting to toggle the base expand if that comes up.

@diegoalejo15

Copy link
Copy Markdown
Author

Thanks for the review! I've pushed a new commit addressing everything:

Fixed CollapseAllKeepingRoot — it was only collapsing the subtree under the first root item (TreeView_GetRoot only returns the first one), so on the default nav pane layout (multiple sibling roots: Home, This PC, Network, etc.) Ctrl+click did nothing outside the first root. Now it iterates all top-level sibling roots with TreeView_GetNextSibling and collapses everything under each one, while keeping the roots themselves visible.
Added a demo GIF to the README showing left-click / Ctrl+click / Alt+click / Shift+click in action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants