From da393ba7a14e9c60992efa9ba0dfd19f3e105004 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Sun, 12 Jul 2026 20:56:37 -0300 Subject: [PATCH 1/7] Add files via upload --- explorer-tree-click-expand.wh.cpp | 204 ++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 explorer-tree-click-expand.wh.cpp diff --git a/explorer-tree-click-expand.wh.cpp b/explorer-tree-click-expand.wh.cpp new file mode 100644 index 0000000000..a8ffa1709f --- /dev/null +++ b/explorer-tree-click-expand.wh.cpp @@ -0,0 +1,204 @@ +// ==WindhawkMod== +// @id explorer-tree-click-expand +// @name Expand folder on click (nav pane) +// @description Left click expands a folder one level; Ctrl+click collapses the whole tree; Alt+click expands all subfolders +// @version 1.0 +// @author Didi +// @github https://github.com/diegoalejo15 +// @include explorer.exe +// @architecture x86-64 +// ==/WindhawkMod== + +// ==WindhawkModReadme== +/* +# Expand folder on click + +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. +*/ +// ==/WindhawkModReadme== + +// ==WindhawkModSettings== +/* +- ctrlClickCollapseAll: true + $name: Ctrl+Click to collapse all + $description: >- + When enabled, Ctrl + left click on any folder in the navigation pane + collapses the entire tree back to its original state. +- altClickExpandAll: true + $name: Alt+Click to expand all subfolders + $description: >- + When enabled, Alt + left click on a folder in the navigation pane + expands it and all its subfolders recursively (same as pressing the + Numpad * key). +*/ +// ==/WindhawkModSettings== + +#include +#include +#include + +HHOOK g_hMouseHook = nullptr; +HWND g_hHiddenWnd = nullptr; + +bool g_settingCtrlClickCollapseAll = true; +bool g_settingAltClickExpandAll = true; + +constexpr UINT WM_APP_COLLAPSE_ALL = WM_APP + 1; +constexpr UINT WM_APP_EXPAND_ALL = WM_APP + 2; +constexpr wchar_t kHiddenWndClass[] = L"ExplorerTreeClickExpandHiddenWnd"; + +void CollapseAll(HWND hwndTree, HTREEITEM hItem) { + while (hItem) { + HTREEITEM hChild = TreeView_GetChild(hwndTree, hItem); + if (hChild) { + CollapseAll(hwndTree, hChild); + } + TreeView_Expand(hwndTree, hItem, TVE_COLLAPSE); + hItem = TreeView_GetNextSibling(hwndTree, hItem); + } +} + +// Collapses everything in the tree except the very top-level root item +// (e.g. "Desktop"), which stays expanded so its direct children (Home, +// Gallery, Dropbox, etc.) remain visible - matching the tree's original +// out-of-the-box state instead of hiding everything. +void CollapseAllKeepingRoot(HWND hwndTree) { + HTREEITEM hRoot = TreeView_GetRoot(hwndTree); + if (!hRoot) return; + + TreeView_Expand(hwndTree, hRoot, TVE_EXPAND); + CollapseAll(hwndTree, TreeView_GetChild(hwndTree, hRoot)); +} + +void ExpandAll(HWND hwndTree, HTREEITEM hItem) { + // Select the item first - the native "*" (Numpad Multiply) behavior + // expands the currently selected/focused item and all its descendants, + // and it's implemented internally in a fast, non-blocking way (unlike + // manually walking the tree with TVM_EXPAND calls). + TreeView_SelectItem(hwndTree, hItem); + SendMessage(hwndTree, WM_KEYDOWN, VK_MULTIPLY, 0); + SendMessage(hwndTree, WM_KEYUP, VK_MULTIPLY, 0); +} + +void LoadSettings() { + g_settingCtrlClickCollapseAll = Wh_GetIntSetting(L"ctrlClickCollapseAll") != 0; + g_settingAltClickExpandAll = Wh_GetIntSetting(L"altClickExpandAll") != 0; +} + +LRESULT CALLBACK HiddenWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_APP_COLLAPSE_ALL) { + HWND hTreeWnd = (HWND)wParam; + if (hTreeWnd && IsWindow(hTreeWnd)) { + CollapseAllKeepingRoot(hTreeWnd); + } + return 0; + } + if (msg == WM_APP_EXPAND_ALL) { + HWND hTreeWnd = (HWND)wParam; + HTREEITEM hItem = (HTREEITEM)lParam; + if (hTreeWnd && IsWindow(hTreeWnd) && hItem) { + ExpandAll(hTreeWnd, hItem); + } + return 0; + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode == HC_ACTION && wParam == WM_LBUTTONDOWN) { + MSLLHOOKSTRUCT* info = (MSLLHOOKSTRUCT*)lParam; + HWND hWnd = WindowFromPoint(info->pt); + + wchar_t className[64]; + if (hWnd && GetClassNameW(hWnd, className, ARRAYSIZE(className)) && + wcscmp(className, L"SysTreeView32") == 0) { + + POINT clientPt = info->pt; + ScreenToClient(hWnd, &clientPt); + + TVHITTESTINFO hitTest{}; + hitTest.pt = clientPt; + HTREEITEM hItem = (HTREEITEM)SendMessage(hWnd, TVM_HITTEST, 0, (LPARAM)&hitTest); + + // Only act if the click landed on the icon or label, NOT on the + // expand/collapse chevron (that one already toggles natively). + bool onItemBody = (hitTest.flags & (TVHT_ONITEMICON | TVHT_ONITEMLABEL)) != 0; + + if (hItem && onItemBody) { + bool ctrlDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + bool altDown = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; + + if (ctrlDown && g_settingCtrlClickCollapseAll) { + PostMessage(g_hHiddenWnd, WM_APP_COLLAPSE_ALL, (WPARAM)hWnd, 0); + } else if (altDown && g_settingAltClickExpandAll) { + PostMessage(g_hHiddenWnd, WM_APP_EXPAND_ALL, (WPARAM)hWnd, (LPARAM)hItem); + } else if (!ctrlDown && !altDown) { + TVITEM item{}; + item.mask = TVIF_STATE | TVIF_CHILDREN; + item.hItem = hItem; + item.stateMask = TVIS_EXPANDED; + SendMessage(hWnd, TVM_GETITEM, 0, (LPARAM)&item); + + bool hasChildren = item.cChildren != 0; + bool isExpanded = (item.state & TVIS_EXPANDED) != 0; + + if (hasChildren && !isExpanded) { + // Post (non-blocking) so we don't delay the system's + // mouse hook chain. + PostMessage(hWnd, TVM_EXPAND, TVE_EXPAND, (LPARAM)hItem); + } + } + } + } + } + + return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam); +} + +BOOL Wh_ModInit() { + LoadSettings(); + + WNDCLASSW wc{}; + wc.lpfnWndProc = HiddenWndProc; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = kHiddenWndClass; + RegisterClassW(&wc); + + g_hHiddenWnd = CreateWindowW( + kHiddenWndClass, L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr + ); + + g_hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, GetModuleHandle(nullptr), 0); + return g_hMouseHook != nullptr; +} + +void Wh_ModSettingsChanged() { + LoadSettings(); +} + +void Wh_ModUninit() { + if (g_hMouseHook) { + UnhookWindowsHookEx(g_hMouseHook); + g_hMouseHook = nullptr; + } + if (g_hHiddenWnd) { + DestroyWindow(g_hHiddenWnd); + g_hHiddenWnd = nullptr; + } + UnregisterClassW(kHiddenWndClass, GetModuleHandle(nullptr)); +} \ No newline at end of file From 3eea3a19bfb8d732d9ac2a0e608f35ae02fcb008 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Sun, 12 Jul 2026 21:13:08 -0300 Subject: [PATCH 2/7] Update explorer-tree-click-expand.wh.cpp --- explorer-tree-click-expand.wh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/explorer-tree-click-expand.wh.cpp b/explorer-tree-click-expand.wh.cpp index a8ffa1709f..3281a751b2 100644 --- a/explorer-tree-click-expand.wh.cpp +++ b/explorer-tree-click-expand.wh.cpp @@ -1,6 +1,6 @@ // ==WindhawkMod== // @id explorer-tree-click-expand -// @name Expand folder on click (nav pane) +// @name Expand folder on click (Explorer nav pane) // @description Left click expands a folder one level; Ctrl+click collapses the whole tree; Alt+click expands all subfolders // @version 1.0 // @author Didi @@ -201,4 +201,4 @@ void Wh_ModUninit() { g_hHiddenWnd = nullptr; } UnregisterClassW(kHiddenWndClass, GetModuleHandle(nullptr)); -} \ No newline at end of file +} From c539304fb6f8a021a607e3b173356916ea42e780 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Mon, 13 Jul 2026 10:38:13 -0300 Subject: [PATCH 3/7] Update explorer-tree-click-expand.wh.cpp --- explorer-tree-click-expand.wh.cpp | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/explorer-tree-click-expand.wh.cpp b/explorer-tree-click-expand.wh.cpp index 3281a751b2..d3d5618489 100644 --- a/explorer-tree-click-expand.wh.cpp +++ b/explorer-tree-click-expand.wh.cpp @@ -17,7 +17,7 @@ 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: +This mod adds four 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. @@ -25,9 +25,13 @@ This mod adds three click behaviors to the navigation pane tree: 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). +- **Shift + left click** on a folder: fully collapses just that folder, + resetting the expanded state of its subfolders so that when you expand + it again, its children come back collapsed instead of showing + everything you had open before. -Both Ctrl+click and Alt+click behaviors can be individually enabled or -disabled in the mod's settings. Both are enabled by default. +Ctrl+click, Alt+click, and Shift+click behaviors can each be individually +enabled or disabled in the mod's settings. All are enabled by default. */ // ==/WindhawkModReadme== @@ -44,6 +48,12 @@ disabled in the mod's settings. Both are enabled by default. When enabled, Alt + left click on a folder in the navigation pane expands it and all its subfolders recursively (same as pressing the Numpad * key). +- shiftClickCollapseFolder: true + $name: Shift+Click to collapse just that folder + $description: >- + When enabled, Shift + left click on a folder in the navigation pane + fully collapses that single folder, resetting the expanded state of + its subfolders so they come back collapsed the next time it's opened. */ // ==/WindhawkModSettings== @@ -56,6 +66,7 @@ HWND g_hHiddenWnd = nullptr; bool g_settingCtrlClickCollapseAll = true; bool g_settingAltClickExpandAll = true; +bool g_settingShiftClickCollapseFolder = true; constexpr UINT WM_APP_COLLAPSE_ALL = WM_APP + 1; constexpr UINT WM_APP_EXPAND_ALL = WM_APP + 2; @@ -97,6 +108,7 @@ void ExpandAll(HWND hwndTree, HTREEITEM hItem) { void LoadSettings() { g_settingCtrlClickCollapseAll = Wh_GetIntSetting(L"ctrlClickCollapseAll") != 0; g_settingAltClickExpandAll = Wh_GetIntSetting(L"altClickExpandAll") != 0; + g_settingShiftClickCollapseFolder = Wh_GetIntSetting(L"shiftClickCollapseFolder") != 0; } LRESULT CALLBACK HiddenWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { @@ -141,12 +153,22 @@ LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { if (hItem && onItemBody) { bool ctrlDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; bool altDown = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; + bool shiftDown = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; if (ctrlDown && g_settingCtrlClickCollapseAll) { PostMessage(g_hHiddenWnd, WM_APP_COLLAPSE_ALL, (WPARAM)hWnd, 0); } else if (altDown && g_settingAltClickExpandAll) { PostMessage(g_hHiddenWnd, WM_APP_EXPAND_ALL, (WPARAM)hWnd, (LPARAM)hItem); - } else if (!ctrlDown && !altDown) { + } else if (shiftDown && g_settingShiftClickCollapseFolder) { + // Collapse just this one folder. TVE_COLLAPSERESET on top + // of TVE_COLLAPSE also resets the expanded state of its + // children, so re-expanding later starts fresh instead of + // showing every subfolder that was previously open - this + // is a single, non-recursive operation, so it's safe to + // post directly to the tree instead of routing through + // the hidden window. + PostMessage(hWnd, TVM_EXPAND, TVE_COLLAPSE | TVE_COLLAPSERESET, (LPARAM)hItem); + } else if (!ctrlDown && !altDown && !shiftDown) { TVITEM item{}; item.mask = TVIF_STATE | TVIF_CHILDREN; item.hItem = hItem; From 01f176889d4d4d4ac295e2c133ff9a249b27e2c4 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Wed, 15 Jul 2026 08:37:24 -0300 Subject: [PATCH 4/7] Update explorer-tree-click-expand.wh.cpp --- explorer-tree-click-expand.wh.cpp | 51 +++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/explorer-tree-click-expand.wh.cpp b/explorer-tree-click-expand.wh.cpp index d3d5618489..55b2ce1598 100644 --- a/explorer-tree-click-expand.wh.cpp +++ b/explorer-tree-click-expand.wh.cpp @@ -2,7 +2,7 @@ // @id explorer-tree-click-expand // @name Expand folder on click (Explorer nav pane) // @description Left click expands a folder one level; Ctrl+click collapses the whole tree; Alt+click expands all subfolders -// @version 1.0 +// @version 1.1 // @author Didi // @github https://github.com/diegoalejo15 // @include explorer.exe @@ -32,6 +32,13 @@ This mod adds four click behaviors to the navigation pane tree: Ctrl+click, Alt+click, and Shift+click behaviors can each be individually enabled or disabled in the mod's settings. All are enabled by default. + +Only acts on tree views that belong to Explorer itself. Navigation panes +shown inside other programs' Open/Save file dialogs (Cubase, Excel, +etc.) are left completely untouched - talking to a tree view control +living in another program's process turned out to be unreliable (it +could hang or crash the other program), so this mod deliberately stays +out of that territory. */ // ==/WindhawkModReadme== @@ -72,6 +79,44 @@ constexpr UINT WM_APP_COLLAPSE_ALL = WM_APP + 1; constexpr UINT WM_APP_EXPAND_ALL = WM_APP + 2; constexpr wchar_t kHiddenWndClass[] = L"ExplorerTreeClickExpandHiddenWnd"; +// Cache of the last window we checked, so we don't call +// OpenProcess/QueryFullProcessImageName on every single mouse click - +// only when the hovered window actually changes. +HWND g_lastCheckedWnd = nullptr; +bool g_lastCheckedIsExplorer = false; + +// Returns true only if hWnd belongs to an explorer.exe process. This mod +// only ever acts on Explorer's own tree views - navigation panes shown +// inside other programs' Open/Save file dialogs are left completely +// alone, since talking to a tree view living in another program's +// process proved unreliable (it could hang or crash that program). +bool IsExplorerWindow(HWND hWnd) { + if (hWnd == g_lastCheckedWnd) { + return g_lastCheckedIsExplorer; + } + + bool isExplorer = false; + DWORD pid = 0; + GetWindowThreadProcessId(hWnd, &pid); + if (pid) { + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (hProcess) { + wchar_t path[MAX_PATH]; + DWORD size = ARRAYSIZE(path); + if (QueryFullProcessImageNameW(hProcess, 0, path, &size)) { + wchar_t* fileName = wcsrchr(path, L'\\'); + fileName = fileName ? fileName + 1 : path; + isExplorer = _wcsicmp(fileName, L"explorer.exe") == 0; + } + CloseHandle(hProcess); + } + } + + g_lastCheckedWnd = hWnd; + g_lastCheckedIsExplorer = isExplorer; + return isExplorer; +} + void CollapseAll(HWND hwndTree, HTREEITEM hItem) { while (hItem) { HTREEITEM hChild = TreeView_GetChild(hwndTree, hItem); @@ -137,7 +182,8 @@ LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { wchar_t className[64]; if (hWnd && GetClassNameW(hWnd, className, ARRAYSIZE(className)) && - wcscmp(className, L"SysTreeView32") == 0) { + wcscmp(className, L"SysTreeView32") == 0 && + IsExplorerWindow(hWnd)) { POINT clientPt = info->pt; ScreenToClient(hWnd, &clientPt); @@ -223,4 +269,5 @@ void Wh_ModUninit() { g_hHiddenWnd = nullptr; } UnregisterClassW(kHiddenWndClass, GetModuleHandle(nullptr)); + g_lastCheckedWnd = nullptr; } From 07f1943312a72a8222a03f4a5fd2411deae1b754 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Sat, 18 Jul 2026 15:00:22 -0300 Subject: [PATCH 5/7] Rename explorer-tree-click-expand.wh.cpp to mods/explorer-tree-click-expand.wh.cpp --- .../explorer-tree-click-expand.wh.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename explorer-tree-click-expand.wh.cpp => mods/explorer-tree-click-expand.wh.cpp (100%) diff --git a/explorer-tree-click-expand.wh.cpp b/mods/explorer-tree-click-expand.wh.cpp similarity index 100% rename from explorer-tree-click-expand.wh.cpp rename to mods/explorer-tree-click-expand.wh.cpp From 35dbefaf621ded68f6190ff51d1d04417f3c5c0c Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Sun, 19 Jul 2026 10:13:09 -0300 Subject: [PATCH 6/7] Update explorer-tree-click-expand.wh.cpp --- mods/explorer-tree-click-expand.wh.cpp | 331 ++++++++++++++----------- 1 file changed, 187 insertions(+), 144 deletions(-) diff --git a/mods/explorer-tree-click-expand.wh.cpp b/mods/explorer-tree-click-expand.wh.cpp index 55b2ce1598..12ddd1bf09 100644 --- a/mods/explorer-tree-click-expand.wh.cpp +++ b/mods/explorer-tree-click-expand.wh.cpp @@ -2,10 +2,11 @@ // @id explorer-tree-click-expand // @name Expand folder on click (Explorer nav pane) // @description Left click expands a folder one level; Ctrl+click collapses the whole tree; Alt+click expands all subfolders -// @version 1.1 +// @version 1.2 // @author Didi // @github https://github.com/diegoalejo15 // @include explorer.exe +// @compilerOptions -lcomctl32 // @architecture x86-64 // ==/WindhawkMod== @@ -33,12 +34,21 @@ This mod adds four click behaviors to the navigation pane tree: Ctrl+click, Alt+click, and Shift+click behaviors can each be individually enabled or disabled in the mod's settings. All are enabled by default. -Only acts on tree views that belong to Explorer itself. Navigation panes -shown inside other programs' Open/Save file dialogs (Cubase, Excel, -etc.) are left completely untouched - talking to a tree view control -living in another program's process turned out to be unreliable (it -could hang or crash the other program), so this mod deliberately stays -out of that territory. +The mod works by subclassing the navigation pane's tree view control +directly (it's identified by its parent being a `NamespaceTreeControl`, +which is specific to Explorer's nav pane). Tree views shown inside other +programs' Open/Save file dialogs (Cubase, Excel, etc.), or any other tree +view elsewhere in Explorer, are never touched, since only the nav pane's +own tree view control is ever subclassed. + +By default this mod only runs inside `explorer.exe`, so it only affects +the navigation pane of File Explorer windows. Many other programs' Open/ +Save dialogs use the same navigation pane control, so if you'd like this +mod's click behaviors to also work there, you can add those programs' +executable names (e.g. `EXCEL.EXE`) to the mod's process inclusion list +from the "Advanced" tab of this mod's settings in Windhawk - no code +changes needed. The mod only ever touches the process it's running in, so +this is safe to do for as many or as few programs as you'd like. */ // ==/WindhawkModReadme== @@ -64,64 +74,38 @@ out of that territory. */ // ==/WindhawkModSettings== +#include + #include #include #include -HHOOK g_hMouseHook = nullptr; -HWND g_hHiddenWnd = nullptr; +#include +#include +#include bool g_settingCtrlClickCollapseAll = true; bool g_settingAltClickExpandAll = true; bool g_settingShiftClickCollapseFolder = true; -constexpr UINT WM_APP_COLLAPSE_ALL = WM_APP + 1; -constexpr UINT WM_APP_EXPAND_ALL = WM_APP + 2; -constexpr wchar_t kHiddenWndClass[] = L"ExplorerTreeClickExpandHiddenWnd"; - -// Cache of the last window we checked, so we don't call -// OpenProcess/QueryFullProcessImageName on every single mouse click - -// only when the hovered window actually changes. -HWND g_lastCheckedWnd = nullptr; -bool g_lastCheckedIsExplorer = false; - -// Returns true only if hWnd belongs to an explorer.exe process. This mod -// only ever acts on Explorer's own tree views - navigation panes shown -// inside other programs' Open/Save file dialogs are left completely -// alone, since talking to a tree view living in another program's -// process proved unreliable (it could hang or crash that program). -bool IsExplorerWindow(HWND hWnd) { - if (hWnd == g_lastCheckedWnd) { - return g_lastCheckedIsExplorer; - } - - bool isExplorer = false; - DWORD pid = 0; - GetWindowThreadProcessId(hWnd, &pid); - if (pid) { - HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - if (hProcess) { - wchar_t path[MAX_PATH]; - DWORD size = ARRAYSIZE(path); - if (QueryFullProcessImageNameW(hProcess, 0, path, &size)) { - wchar_t* fileName = wcsrchr(path, L'\\'); - fileName = fileName ? fileName + 1 : path; - isExplorer = _wcsicmp(fileName, L"explorer.exe") == 0; - } - CloseHandle(hProcess); - } - } +// The nav pane's tree view is a direct child of a NamespaceTreeControl +// window. This is specific to Explorer's own navigation pane, so checking +// for it lets us subclass exactly (and only) the tree we care about - +// nothing in other processes, and nothing else within Explorer itself. +constexpr WCHAR kNavPaneParentClassName[] = L"NamespaceTreeControl"; +constexpr WCHAR kTreeViewClassName[] = L"SysTreeView32"; - g_lastCheckedWnd = hWnd; - g_lastCheckedIsExplorer = isExplorer; - return isExplorer; -} +// Tracks every tree view we've subclassed so Wh_ModUninit can remove all +// subclasses before the mod DLL is unloaded (otherwise a leftover subclass +// pointing into unloaded code would crash Explorer on the next click). +std::mutex g_subclassedTreesMutex; +std::vector g_subclassedTrees; -void CollapseAll(HWND hwndTree, HTREEITEM hItem) { +void CollapseAllRecursive(HWND hwndTree, HTREEITEM hItem) { while (hItem) { HTREEITEM hChild = TreeView_GetChild(hwndTree, hItem); if (hChild) { - CollapseAll(hwndTree, hChild); + CollapseAllRecursive(hwndTree, hChild); } TreeView_Expand(hwndTree, hItem, TVE_COLLAPSE); hItem = TreeView_GetNextSibling(hwndTree, hItem); @@ -134,17 +118,21 @@ void CollapseAll(HWND hwndTree, HTREEITEM hItem) { // out-of-the-box state instead of hiding everything. void CollapseAllKeepingRoot(HWND hwndTree) { HTREEITEM hRoot = TreeView_GetRoot(hwndTree); - if (!hRoot) return; + if (!hRoot) { + return; + } TreeView_Expand(hwndTree, hRoot, TVE_EXPAND); - CollapseAll(hwndTree, TreeView_GetChild(hwndTree, hRoot)); + CollapseAllRecursive(hwndTree, TreeView_GetChild(hwndTree, hRoot)); } void ExpandAll(HWND hwndTree, HTREEITEM hItem) { // Select the item first - the native "*" (Numpad Multiply) behavior // expands the currently selected/focused item and all its descendants, // and it's implemented internally in a fast, non-blocking way (unlike - // manually walking the tree with TVM_EXPAND calls). + // manually walking the tree with TVM_EXPAND calls). This all runs on + // the tree's own thread now, so these SendMessage calls are same- + // thread and safe. TreeView_SelectItem(hwndTree, hItem); SendMessage(hwndTree, WM_KEYDOWN, VK_MULTIPLY, 0); SendMessage(hwndTree, WM_KEYUP, VK_MULTIPLY, 0); @@ -156,103 +144,158 @@ void LoadSettings() { g_settingShiftClickCollapseFolder = Wh_GetIntSetting(L"shiftClickCollapseFolder") != 0; } -LRESULT CALLBACK HiddenWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - if (msg == WM_APP_COLLAPSE_ALL) { - HWND hTreeWnd = (HWND)wParam; - if (hTreeWnd && IsWindow(hTreeWnd)) { - CollapseAllKeepingRoot(hTreeWnd); - } - return 0; - } - if (msg == WM_APP_EXPAND_ALL) { - HWND hTreeWnd = (HWND)wParam; - HTREEITEM hItem = (HTREEITEM)lParam; - if (hTreeWnd && IsWindow(hTreeWnd) && hItem) { - ExpandAll(hTreeWnd, hItem); - } - return 0; +void ForgetSubclassedTree(HWND hWnd) { + std::lock_guard lock(g_subclassedTreesMutex); + auto it = std::find(g_subclassedTrees.begin(), g_subclassedTrees.end(), hWnd); + if (it != g_subclassedTrees.end()) { + g_subclassedTrees.erase(it); } - return DefWindowProc(hwnd, msg, wParam, lParam); } -LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { - if (nCode == HC_ACTION && wParam == WM_LBUTTONDOWN) { - MSLLHOOKSTRUCT* info = (MSLLHOOKSTRUCT*)lParam; - HWND hWnd = WindowFromPoint(info->pt); - - wchar_t className[64]; - if (hWnd && GetClassNameW(hWnd, className, ARRAYSIZE(className)) && - wcscmp(className, L"SysTreeView32") == 0 && - IsExplorerWindow(hWnd)) { - - POINT clientPt = info->pt; - ScreenToClient(hWnd, &clientPt); - - TVHITTESTINFO hitTest{}; - hitTest.pt = clientPt; - HTREEITEM hItem = (HTREEITEM)SendMessage(hWnd, TVM_HITTEST, 0, (LPARAM)&hitTest); - - // Only act if the click landed on the icon or label, NOT on the - // expand/collapse chevron (that one already toggles natively). - bool onItemBody = (hitTest.flags & (TVHT_ONITEMICON | TVHT_ONITEMLABEL)) != 0; - - if (hItem && onItemBody) { - bool ctrlDown = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; - bool altDown = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; - bool shiftDown = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; - - if (ctrlDown && g_settingCtrlClickCollapseAll) { - PostMessage(g_hHiddenWnd, WM_APP_COLLAPSE_ALL, (WPARAM)hWnd, 0); - } else if (altDown && g_settingAltClickExpandAll) { - PostMessage(g_hHiddenWnd, WM_APP_EXPAND_ALL, (WPARAM)hWnd, (LPARAM)hItem); - } else if (shiftDown && g_settingShiftClickCollapseFolder) { - // Collapse just this one folder. TVE_COLLAPSERESET on top - // of TVE_COLLAPSE also resets the expanded state of its - // children, so re-expanding later starts fresh instead of - // showing every subfolder that was previously open - this - // is a single, non-recursive operation, so it's safe to - // post directly to the tree instead of routing through - // the hidden window. - PostMessage(hWnd, TVM_EXPAND, TVE_COLLAPSE | TVE_COLLAPSERESET, (LPARAM)hItem); - } else if (!ctrlDown && !altDown && !shiftDown) { - TVITEM item{}; - item.mask = TVIF_STATE | TVIF_CHILDREN; - item.hItem = hItem; - item.stateMask = TVIS_EXPANDED; - SendMessage(hWnd, TVM_GETITEM, 0, (LPARAM)&item); - - bool hasChildren = item.cChildren != 0; - bool isExpanded = (item.state & TVIS_EXPANDED) != 0; - - if (hasChildren && !isExpanded) { - // Post (non-blocking) so we don't delay the system's - // mouse hook chain. - PostMessage(hWnd, TVM_EXPAND, TVE_EXPAND, (LPARAM)hItem); - } +LRESULT CALLBACK TreeViewSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, + LPARAM lParam, UINT_PTR uIdSubclass) { + if (uMsg == WM_LBUTTONDOWN) { + POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + + TVHITTESTINFO hitTest{}; + hitTest.pt = pt; + HTREEITEM hItem = TreeView_HitTest(hWnd, &hitTest); + + // Only act if the click landed on the icon or label, NOT on the + // expand/collapse chevron (that one already toggles natively). + bool onItemBody = + (hitTest.flags & (TVHT_ONITEMICON | TVHT_ONITEMLABEL)) != 0; + + if (hItem && onItemBody) { + bool ctrlDown = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + bool altDown = (GetKeyState(VK_MENU) & 0x8000) != 0; + bool shiftDown = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + + // Let the tree handle the click natively first (selection, + // chevron toggle, etc.), then layer our behavior on top - + // matching how a global hook that doesn't consume the message + // would behave. + LRESULT result = DefSubclassProc(hWnd, uMsg, wParam, lParam); + + if (ctrlDown && g_settingCtrlClickCollapseAll) { + CollapseAllKeepingRoot(hWnd); + } else if (altDown && g_settingAltClickExpandAll) { + ExpandAll(hWnd, hItem); + } else if (shiftDown && g_settingShiftClickCollapseFolder) { + // TVE_COLLAPSERESET on top of TVE_COLLAPSE also resets the + // expanded state of the item's children, so re-expanding + // later starts fresh instead of showing every subfolder + // that was previously open. + TreeView_Expand(hWnd, hItem, TVE_COLLAPSE | TVE_COLLAPSERESET); + } else if (!ctrlDown && !altDown && !shiftDown) { + TVITEM item{}; + item.mask = TVIF_STATE | TVIF_CHILDREN; + item.hItem = hItem; + item.stateMask = TVIS_EXPANDED; + TreeView_GetItem(hWnd, &item); + + bool hasChildren = item.cChildren != 0; + bool isExpanded = (item.state & TVIS_EXPANDED) != 0; + + if (hasChildren && !isExpanded) { + TreeView_Expand(hWnd, hItem, TVE_EXPAND); } } + + return result; + } + } else if (uMsg == WM_NCDESTROY) { + ForgetSubclassedTree(hWnd); + WindhawkUtils::RemoveWindowSubclassFromAnyThread(hWnd, TreeViewSubclassProc); + } + + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} + +void TrySubclassTreeView(HWND hTreeWnd) { + HWND hParent = GetParent(hTreeWnd); + if (!hParent) { + return; + } + + WCHAR parentClassName[64]; + if (!GetClassNameW(hParent, parentClassName, ARRAYSIZE(parentClassName)) || + _wcsicmp(parentClassName, kNavPaneParentClassName) != 0) { + return; + } + + { + std::lock_guard lock(g_subclassedTreesMutex); + if (std::find(g_subclassedTrees.begin(), g_subclassedTrees.end(), + hTreeWnd) != g_subclassedTrees.end()) { + return; } } - return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam); + if (WindhawkUtils::SetWindowSubclassFromAnyThread(hTreeWnd, TreeViewSubclassProc, + 0)) { + std::lock_guard lock(g_subclassedTreesMutex); + g_subclassedTrees.push_back(hTreeWnd); + } +} + +using CreateWindowExW_t = decltype(&CreateWindowExW); +CreateWindowExW_t CreateWindowExW_Original; +HWND WINAPI CreateWindowExW_Hook(DWORD dwExStyle, LPCWSTR lpClassName, + LPCWSTR lpWindowName, DWORD dwStyle, int X, + int Y, int nWidth, int nHeight, + HWND hWndParent, HMENU hMenu, + HINSTANCE hInstance, LPVOID lpParam) { + HWND hWnd = CreateWindowExW_Original(dwExStyle, lpClassName, lpWindowName, + dwStyle, X, Y, nWidth, nHeight, + hWndParent, hMenu, hInstance, lpParam); + + // lpClassName can be an atom (a small integer cast to a pointer) rather + // than a real string pointer, so only touch it as a string once we know + // it's not one. + if (hWnd && hWndParent && + ((ULONG_PTR)lpClassName & ~(ULONG_PTR)0xffff) != 0 && + _wcsicmp(lpClassName, kTreeViewClassName) == 0) { + TrySubclassTreeView(hWnd); + } + + return hWnd; +} + +// Finds nav pane tree views that already exist in a given top-level window +// (used at init time, in case Explorer windows were already open before the +// mod was loaded). +BOOL CALLBACK EnumTreeChildProc(HWND hWnd, LPARAM lParam) { + WCHAR className[64]; + if (GetClassNameW(hWnd, className, ARRAYSIZE(className)) && + _wcsicmp(className, kTreeViewClassName) == 0) { + TrySubclassTreeView(hWnd); + } + return TRUE; +} + +BOOL CALLBACK EnumTopLevelWindowsProc(HWND hWnd, LPARAM lParam) { + DWORD pid = 0; + GetWindowThreadProcessId(hWnd, &pid); + if (pid == GetCurrentProcessId()) { + EnumChildWindows(hWnd, EnumTreeChildProc, 0); + } + return TRUE; } BOOL Wh_ModInit() { LoadSettings(); - WNDCLASSW wc{}; - wc.lpfnWndProc = HiddenWndProc; - wc.hInstance = GetModuleHandle(nullptr); - wc.lpszClassName = kHiddenWndClass; - RegisterClassW(&wc); + WindhawkUtils::SetFunctionHook(CreateWindowExW, CreateWindowExW_Hook, + &CreateWindowExW_Original); - g_hHiddenWnd = CreateWindowW( - kHiddenWndClass, L"", 0, 0, 0, 0, 0, - HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr - ); + return TRUE; +} - g_hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, GetModuleHandle(nullptr), 0); - return g_hMouseHook != nullptr; +void Wh_ModAfterInit() { + // Pick up nav pane tree views in Explorer windows that were already + // open when the mod was loaded. + EnumWindows(EnumTopLevelWindowsProc, 0); } void Wh_ModSettingsChanged() { @@ -260,14 +303,14 @@ void Wh_ModSettingsChanged() { } void Wh_ModUninit() { - if (g_hMouseHook) { - UnhookWindowsHookEx(g_hMouseHook); - g_hMouseHook = nullptr; + std::vector trees; + { + std::lock_guard lock(g_subclassedTreesMutex); + trees = g_subclassedTrees; + g_subclassedTrees.clear(); } - if (g_hHiddenWnd) { - DestroyWindow(g_hHiddenWnd); - g_hHiddenWnd = nullptr; + + for (HWND hTree : trees) { + WindhawkUtils::RemoveWindowSubclassFromAnyThread(hTree, TreeViewSubclassProc); } - UnregisterClassW(kHiddenWndClass, GetModuleHandle(nullptr)); - g_lastCheckedWnd = nullptr; } From 5d15dd373f3484dfc398743388c6c14bd2c4d1d1 Mon Sep 17 00:00:00 2001 From: diegoalejo15 Date: Tue, 21 Jul 2026 09:35:04 -0300 Subject: [PATCH 7/7] Update explorer-tree-click-expand.wh.cpp --- mods/explorer-tree-click-expand.wh.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mods/explorer-tree-click-expand.wh.cpp b/mods/explorer-tree-click-expand.wh.cpp index 12ddd1bf09..0c03546cc7 100644 --- a/mods/explorer-tree-click-expand.wh.cpp +++ b/mods/explorer-tree-click-expand.wh.cpp @@ -18,6 +18,8 @@ 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. +![Demo](https://i.imgur.com/9pmZye3.gif) + This mod adds four click behaviors to the navigation pane tree: - **Left click** on a folder's icon or label: expands it one level (like @@ -112,18 +114,17 @@ void CollapseAllRecursive(HWND hwndTree, HTREEITEM hItem) { } } -// Collapses everything in the tree except the very top-level root item -// (e.g. "Desktop"), which stays expanded so its direct children (Home, -// Gallery, Dropbox, etc.) remain visible - matching the tree's original -// out-of-the-box state instead of hiding everything. +// Collapses everything below each top-level root item (e.g. "Home", +// "This PC", "Network" on the default layout, or just "Desktop" when +// "Show all folders" is enabled), keeping the roots themselves visible - +// matching the tree's original out-of-the-box state instead of hiding +// everything. The default nav pane layout has several sibling root items, +// so all of them need to be walked, not just the first one. void CollapseAllKeepingRoot(HWND hwndTree) { - HTREEITEM hRoot = TreeView_GetRoot(hwndTree); - if (!hRoot) { - return; + for (HTREEITEM hRoot = TreeView_GetRoot(hwndTree); hRoot; + hRoot = TreeView_GetNextSibling(hwndTree, hRoot)) { + CollapseAllRecursive(hwndTree, TreeView_GetChild(hwndTree, hRoot)); } - - TreeView_Expand(hwndTree, hRoot, TVE_EXPAND); - CollapseAllRecursive(hwndTree, TreeView_GetChild(hwndTree, hRoot)); } void ExpandAll(HWND hwndTree, HTREEITEM hItem) {