22// @id icon-resource-redirect
33// @name Resource Redirect
44// @description Define alternative files for loading various resources (e.g. icons in imageres.dll) for simple theming without having to modify system files
5- // @version 1.2.5
5+ // @version 1.3
66// @author m417z
77// @github https://github.com/m417z
88// @twitter https://twitter.com/m417z
2626Define alternative files for loading various resources (e.g. icons in
2727imageres.dll) for simple theming without having to modify system files.
2828
29- **Note**: This mod requires Windhawk v1.6 or newer.
30-
3129## Icon themes
3230
33- A collection of community contributed icon theme packs can be found in the
34- [Resource Redirect icon
35- themes](https://github.com/ramensoftware/resource-redirect-icon-themes)
36- repository. An icon theme can be selected in the mod's settings.
31+ An icon theme from the collection of community-contributed icon theme packs can
32+ be selected in the mod's settings. Screenshots of some of the icon themes can be
33+ found in the [Resource Redirect icon
34+ themes](https://github.com/ramensoftware/resource-redirect-icon-themes#themes)
35+ repository.
3736
3837An icon theme can also be installed manually by downloading it and specifying
3938its path in the mod's settings. For details, refer to the guide in the
@@ -42,10 +41,24 @@ repository.
4241A short demonstration can be found [here on
4342YouTube](https://youtu.be/irzVmKHB83E).
4443
44+ ## Disable folder thumbnails
45+
46+ The mod has an option to disable thumbnails in Explorer folders, making folders
47+ use a generic folder icon instead of showing a preview of their contents. Some
48+ icon themes may look better with this option enabled, while other themes have
49+ proper support for styling folder thumbnails. Folder thumbnails are only
50+ replaced when the "Redirect all loaded resources" option is enabled (see below).
51+
52+ ## Redirect all loaded resources (experimental)
53+
54+ The option to redirect all resources can be enabled in the settings. In this
55+ case, redirection won't be limited to the resource types and loading methods
56+ listed above. This option might become the default in the future.
57+
4558## Theme paths
4659
4760Theme paths can be set in the settings. A theme path is a folder with
48- alternative resource files, and the `theme.ini` file that contains redirection
61+ alternative resource files and a `theme.ini` file that contains redirection
4962rules. For example, the `theme.ini` file may contain the following:
5063
5164```
@@ -54,8 +67,8 @@ rules. For example, the `theme.ini` file may contain the following:
5467%SystemRoot%\System32\imageres.dll=imageres.dll
5568```
5669
57- In this case, the folder must also contain the `explorer.exe`, `imageres.dll`
58- files which will be used as the redirection resource files.
70+ In this case, the folder must also contain the `explorer.exe` and `imageres.dll`
71+ files, which will be used as the redirection resource files.
5972
6073Alternatively, the theme path can be the `.ini` file itself.
6174
@@ -69,19 +82,13 @@ The mod supports the following resource types and loading methods:
6982* Cursors loaded with the `LoadCursorW` function.
7083* Bitmaps loaded with the `LoadBitmapW` function.
7184* Menus loaded with the `LoadMenuW` function.
72- * Dialogs loaded with the `DialogBoxParamW`, `CreateDialogParamW` functions.
85+ * Dialogs loaded with the `DialogBoxParamW` and `CreateDialogParamW` functions.
7386* Strings loaded with the `LoadStringW` function.
7487* GDI+ images (e.g. PNGs) loaded with the `SHCreateStreamOnModuleResourceW`
7588 function.
7689* DirectUI resources (usually `UIFILE` and `XML`) loaded with the
7790 `SetXMLFromResource` function.
7891
79- ## Redirect all loaded resources (experimental)
80-
81- The option to redirect all resources can be enabled in the settings. In this
82- case, redirection won't be limited to the resource types and loading methods
83- listed above. This option might become the default in the future.
84-
8592## Choosing the redirected resource file
8693
8794For some files, Windows has additional .mui and/or .mun resource files. For
@@ -279,7 +286,27 @@ The resource lookup order then becomes:
279286 - Windows 11 New Folders Purple|themes/icons/niivu/Windows%2011%20New%20Folders%20Purple.zip: Windows 11 New Folders Purple (by niivu)
280287 - Windows 11 New Folders Slate|themes/icons/niivu/Windows%2011%20New%20Folders%20Slate.zip: Windows 11 New Folders Slate (by niivu)
281288 - Windows 11 New Folders Yellow|themes/icons/niivu/Windows%2011%20New%20Folders%20Yellow.zip: Windows 11 New Folders Yellow (by niivu)
289+ - PaneVista|themes/icons/SoftwareType/PaneVista.zip: 'PaneVista (by SoftwareType, credit: ImSwordQueen)'
282290 - Pane7|themes/icons/ImSwordQueen/Pane7.zip: Pane7 (by ImSwordQueen)
291+ - Pane8.1|themes/icons/NicSonic/Pane8.1.zip: Pane8.1 (by NicSonic)
292+ - Minecraft Axolotle|themes/icons/Wasiabbas4pk/Minecraft%20Axolotle.zip: 'Minecraft Axolotle (by WasiXGamer, credit: dalps/minecraft11)'
293+ - Minecraft Fox|themes/icons/Wasiabbas4pk/Minecraft%20Fox.zip: 'Minecraft Fox (by WasiXGamer, credit: dalps/minecraft11)'
294+ - Minecraft TNT|themes/icons/Wasiabbas4pk/Minecraft%20TNT.zip: 'Minecraft TNT (by WasiXGamer, credit: dalps/minecraft11)'
295+ - Minecraft Panda|themes/icons/Wasiabbas4pk/Minecraft%20Panda.zip: 'Minecraft Panda (by WasiXGamer, credit: dalps/minecraft11)'
296+ - Minecraft Creeper|themes/icons/Wasiabbas4pk/Minecraft%20Creeper.zip: 'Minecraft Creeper (by WasiXGamer, credit: dalps/minecraft11)'
297+ - Minecraft Ender Dragon|themes/icons/Wasiabbas4pk/Minecraft%20Ender%20Dragon.zip: 'Minecraft Ender Dragon (by WasiXGamer, credit: dalps/minecraft11)'
298+ - Minecraft Shulker (Blue)|themes/icons/Wasiabbas4pk/Minecraft%20Shulker%28Blue%29.zip: 'Minecraft Shulker (Blue) (by WasiXGamer, credit: dalps/minecraft11)'
299+ - Minecraft Shulker (Black)|themes/icons/Wasiabbas4pk/Minecraft%20Shulker%28Black%29.zip: 'Minecraft Shulker (Black) (by WasiXGamer, credit: dalps/minecraft11)'
300+ - disableThumbnails: false
301+ $name: Disable folder thumbnails
302+ $description: >-
303+ Make Explorer folders use a generic folder icon instead of showing
304+ thumbnails of their contents. This works better with some icon themes.
305+ - allResourceRedirect: false
306+ $name: Redirect all loaded resources (experimental)
307+ $description: >-
308+ Try to redirect all loaded resources, not only the supported resources
309+ that are listed in the description.
283310- themePaths: [""]
284311 $name: Theme paths
285312 $description: >-
@@ -296,17 +323,12 @@ The resource lookup order then becomes:
296323 $name: The redirection resource file
297324 $description: The custom resource file that will be used instead.
298325 $name: Redirection resource paths
299- - allResourceRedirect: false
300- $name: Redirect all loaded resources (experimental)
301- $description: >-
302- Try to redirect all loaded resources, not only the supported resources
303- that are listed in the description.
304326- themeFolder: ""
305327 $name: Theme folder (deprecated)
306328 $description: >-
307329 A folder with alternative resource files and theme.ini.
308330
309- This option will be removed in the future, please use the new "Theme paths"
331+ This option will be removed in the future. Please use the new "Theme paths"
310332 option above.
311333*/
312334// ==/WindhawkModSettings==
@@ -332,12 +354,23 @@ The resource lookup order then becomes:
332354#include < unordered_map>
333355#include < vector>
334356
357+ using namespace std ::string_view_literals;
358+
335359#ifndef LR_EXACTSIZEONLY
336360#define LR_EXACTSIZEONLY 0x10000
337361#endif
338362
363+ #ifndef STATUS_SUCCESS
364+ #define STATUS_SUCCESS ((NTSTATUS )0x00000000L )
365+ #endif
366+
367+ #ifndef STATUS_BUFFER_TOO_SMALL
368+ #define STATUS_BUFFER_TOO_SMALL ((NTSTATUS )0xC0000023L )
369+ #endif
370+
339371struct {
340372 WindhawkUtils::StringSetting iconTheme;
373+ std::atomic<bool > disableThumbnails;
341374 bool allResourceRedirect;
342375} g_settings;
343376
@@ -2553,8 +2586,180 @@ std::wstring GetIconThemePath(std::wstring_view iconTheme) {
25532586 return targetPath;
25542587}
25552588
2589+ bool StartsWithCaseInsensitive (std::wstring_view str,
2590+ std::wstring_view prefix) {
2591+ return str.size () >= prefix.size () &&
2592+ _wcsnicmp (str.data (), prefix.data (), prefix.size ()) == 0 ;
2593+ }
2594+
2595+ // https://github.com/valinet/wh-mods/blob/61319815c7e018e392a08077dc364559548ade02/mods/valinet-unserver.wh.cpp#L95
2596+ // https://stackoverflow.com/questions/937044/determine-path-to-registry-key-from-hkey-handle-in-c
2597+ std::wstring GetPathFromHKEY (HKEY key) {
2598+ if (!key) {
2599+ return {};
2600+ }
2601+
2602+ using NtQueryKey_t = NTSTATUS (NTAPI *)(
2603+ HANDLE KeyHandle, int KeyInformationClass, PVOID KeyInformation,
2604+ ULONG Length, PULONG ResultLength);
2605+ static NtQueryKey_t pNtQueryKey = []() {
2606+ HMODULE hNtdll = GetModuleHandle (L" ntdll.dll" );
2607+ if (hNtdll) {
2608+ return (NtQueryKey_t)GetProcAddress (hNtdll, " NtQueryKey" );
2609+ }
2610+ return (NtQueryKey_t) nullptr ;
2611+ }();
2612+
2613+ if (!pNtQueryKey) {
2614+ return {};
2615+ }
2616+
2617+ constexpr int kKeyNameInformation = 3 ;
2618+
2619+ ULONG size = 0 ;
2620+ NTSTATUS result = pNtQueryKey (key, kKeyNameInformation , nullptr , 0 , &size);
2621+ if (result != STATUS_BUFFER_TOO_SMALL ) {
2622+ return {};
2623+ }
2624+
2625+ std::vector<BYTE > buffer (size);
2626+ result = pNtQueryKey (key, kKeyNameInformation , buffer.data (), size, &size);
2627+ if (result != STATUS_SUCCESS || size < sizeof (ULONG )) {
2628+ return {};
2629+ }
2630+
2631+ // The buffer contains a KEY_NAME_INFORMATION structure:
2632+ // ULONG NameLength (4 bytes) + WCHAR Name[1].
2633+ ULONG nameLength = *reinterpret_cast <ULONG *>(buffer.data ());
2634+ if (size < sizeof (ULONG ) + nameLength) {
2635+ return {};
2636+ }
2637+
2638+ PCWSTR name = reinterpret_cast <PCWSTR >(buffer.data () + sizeof (ULONG ));
2639+ return std::wstring (name, nameLength / sizeof (WCHAR ));
2640+ }
2641+
2642+ bool MatchesClassSubkey (HKEY hKey, std::wstring_view classSubKey) {
2643+ std::wstring keyPath = GetPathFromHKEY (hKey);
2644+ std::wstring_view keyPathSuffix = keyPath;
2645+
2646+ constexpr std::wstring_view kRegistryMachinePrefix =
2647+ L" \\ REGISTRY\\ MACHINE\\ SOFTWARE\\ Classes\\ " sv;
2648+ constexpr std::wstring_view kRegistryUserPrefix = L" \\ REGISTRY\\ USER\\ " sv;
2649+
2650+ if (StartsWithCaseInsensitive (keyPathSuffix, kRegistryMachinePrefix )) {
2651+ // Remove "\REGISTRY\MACHINE\SOFTWARE\Classes\" prefix.
2652+ keyPathSuffix.remove_prefix (kRegistryMachinePrefix .size ());
2653+ } else if (StartsWithCaseInsensitive (keyPathSuffix, kRegistryUserPrefix )) {
2654+ // Remove "\REGISTRY\USER\" prefix.
2655+ keyPathSuffix.remove_prefix (kRegistryUserPrefix .size ());
2656+
2657+ // Remove "<SID>_Classes\" prefix for non-empty SID.
2658+ size_t firstBackslash = keyPathSuffix.find (L' \\ ' );
2659+ if (firstBackslash == std::wstring_view::npos) {
2660+ return false ;
2661+ }
2662+
2663+ constexpr std::wstring_view kClassesSuffix = L" _Classes" sv;
2664+ if (firstBackslash > kClassesSuffix .size () &&
2665+ _wcsnicmp (
2666+ keyPathSuffix.data () + firstBackslash - kClassesSuffix .size (),
2667+ kClassesSuffix .data (), kClassesSuffix .size ()) == 0 ) {
2668+ keyPathSuffix.remove_prefix (firstBackslash + 1 );
2669+ } else {
2670+ return false ;
2671+ }
2672+ } else {
2673+ return false ;
2674+ }
2675+
2676+ return keyPathSuffix.size () == classSubKey.size () &&
2677+ _wcsnicmp (keyPathSuffix.data (), classSubKey.data (),
2678+ classSubKey.size ()) == 0 ;
2679+ }
2680+
2681+ using RegQueryValueExW_t = decltype (&RegQueryValueExW);
2682+ RegQueryValueExW_t RegQueryValueExW_Original;
2683+ LSTATUS WINAPI RegQueryValueExW_Hook (HKEY hKey,
2684+ LPCWSTR lpValueName,
2685+ LPDWORD lpReserved,
2686+ LPDWORD lpType,
2687+ LPBYTE lpData,
2688+ LPDWORD lpcbData) {
2689+ // Disable thumbnails in Explorer folders by providing the "Logo" value for
2690+ // the "AllFolders\Shell" key. When this value is present, Explorer uses a
2691+ // generic folder icon instead of showing thumbnails of the folder contents.
2692+ // This is equivalent to setting the "Logo" value below to "imageres.dll,-3"
2693+ // (REG_SZ), but is applied dynamically by the mod:
2694+ //
2695+ // HKCU\Software\Classes\Local
2696+ // Settings\Software\Microsoft\Windows\Shell\Bags\AllFolders\Shell
2697+ if (!g_settings.disableThumbnails ) {
2698+ return RegQueryValueExW_Original (hKey, lpValueName, lpReserved, lpType,
2699+ lpData, lpcbData);
2700+ }
2701+
2702+ constexpr WCHAR kValueName [] = L" Logo" ;
2703+ constexpr std::wstring_view kClassSubkey =
2704+ L" Local Settings\\ Software\\ Microsoft\\ Windows\\ Shell\\ Bags\\ AllFolders\\ Shell" sv;
2705+ constexpr std::wstring_view kReplacementData = L" imageres.dll,-3" sv;
2706+
2707+ // Save the original buffer size before calling the original function, as
2708+ // lpcbData is overwritten with the actual data size on output.
2709+ DWORD dataBufferSize = (lpData && lpcbData) ? *lpcbData : 0 ;
2710+
2711+ LSTATUS ret = RegQueryValueExW_Original (hKey, lpValueName, lpReserved,
2712+ lpType, lpData, lpcbData);
2713+ if (ret != ERROR_SUCCESS && ret != ERROR_MORE_DATA &&
2714+ ret != ERROR_FILE_NOT_FOUND ) {
2715+ return ret;
2716+ }
2717+
2718+ if (!lpValueName || _wcsicmp (lpValueName, kValueName ) != 0 ) {
2719+ return ret;
2720+ }
2721+
2722+ if (!MatchesClassSubkey (hKey, kClassSubkey )) {
2723+ return ret;
2724+ }
2725+
2726+ Wh_Log (L" Providing %s value to disable folder thumbnails" , kValueName );
2727+
2728+ DWORD requiredSize = (kReplacementData .size () + 1 ) * sizeof (WCHAR );
2729+
2730+ if (lpType) {
2731+ *lpType = REG_SZ ;
2732+ }
2733+
2734+ if (!lpData || !lpcbData) {
2735+ if (lpData && !lpcbData) {
2736+ // Shouldn't happen per documentation.
2737+ return ret;
2738+ }
2739+ if (lpcbData) {
2740+ *lpcbData = requiredSize;
2741+ }
2742+ return ERROR_SUCCESS ;
2743+ }
2744+
2745+ if (dataBufferSize < requiredSize) {
2746+ Wh_Log (L" Not enough space for %s, available=%u" , kValueName ,
2747+ dataBufferSize);
2748+ *lpcbData = requiredSize;
2749+ return ERROR_MORE_DATA ;
2750+ }
2751+
2752+ memcpy (lpData, kReplacementData .data (),
2753+ kReplacementData .size () * sizeof (WCHAR ));
2754+ reinterpret_cast <PWSTR >(lpData)[kReplacementData .size ()] = L' \0 ' ;
2755+ *lpcbData = requiredSize;
2756+
2757+ return ERROR_SUCCESS ;
2758+ }
2759+
25562760void LoadSettings () {
25572761 g_settings.iconTheme = WindhawkUtils::StringSetting::make (L" iconTheme" );
2762+ g_settings.disableThumbnails = Wh_GetIntSetting (L" disableThumbnails" );
25582763 g_settings.allResourceRedirect = Wh_GetIntSetting (L" allResourceRedirect" );
25592764
25602765 std::unordered_map<std::wstring, std::vector<std::wstring>> paths;
@@ -2831,6 +3036,16 @@ BOOL Wh_ModInit() {
28313036 }
28323037 }
28333038
3039+ if (kernelBaseModule) {
3040+ auto pRegQueryValueExW = (RegQueryValueExW_t)GetProcAddress (
3041+ kernelBaseModule, " RegQueryValueExW" );
3042+ if (pRegQueryValueExW) {
3043+ WindhawkUtils::SetFunctionHook (pRegQueryValueExW,
3044+ RegQueryValueExW_Hook,
3045+ &RegQueryValueExW_Original);
3046+ }
3047+ }
3048+
28343049 // The functions below use FindResourceEx, LoadResource, SizeofResource.
28353050 HMODULE shcoreModule =
28363051 LoadLibraryEx (L" shcore.dll" , nullptr , LOAD_LIBRARY_SEARCH_SYSTEM32 );
@@ -2925,6 +3140,7 @@ BOOL Wh_ModSettingsChanged(BOOL* bReload) {
29253140 Wh_Log (L" >" );
29263141
29273142 auto prevIconTheme = std::move (g_settings.iconTheme );
3143+ bool prevDisableThumbnails = g_settings.disableThumbnails ;
29283144 int prevAllResourceRedirect = g_settings.allResourceRedirect ;
29293145
29303146 LoadSettings ();
@@ -2937,7 +3153,8 @@ BOOL Wh_ModSettingsChanged(BOOL* bReload) {
29373153 FreeAndClearRedirectedModules ();
29383154
29393155 if (DoesCurrentProcessOwnTaskbar ()) {
2940- if (wcscmp (g_settings.iconTheme , prevIconTheme) != 0 ) {
3156+ if (wcscmp (g_settings.iconTheme , prevIconTheme) != 0 ||
3157+ g_settings.disableThumbnails != prevDisableThumbnails) {
29413158 PromptToClearCache ();
29423159 }
29433160
0 commit comments