Skip to content

Add explorer-tree-click-expand mod#4767

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

Add explorer-tree-click-expand mod#4767
diegoalejo15 wants to merge 6 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.

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