├── .gitignore ├── DarkMode.cpp ├── DarkMode.h ├── IatHook.h ├── LICENSE ├── README.md ├── TextOnPath ├── PathTextRenderer.cpp ├── PathTextRenderer.h └── license.rtf ├── actions.cpp ├── actions.h ├── data.cpp ├── data.h ├── demo.png ├── dontscan.cpp ├── dontscan.h ├── dpi.cpp ├── dpi.h ├── main.cpp ├── main.h ├── main.ico ├── main.rc ├── manifest_src.xml ├── premake5.lua ├── res.h ├── scan.cpp ├── scan.h ├── sunburst.cpp ├── sunburst.h ├── ui.cpp ├── ui.h ├── version.h └── version.rc /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | tags 3 | *.exe 4 | *.log 5 | *.patch 6 | *.zip 7 | -------------------------------------------------------------------------------- /DarkMode.cpp: -------------------------------------------------------------------------------- 1 | // Derived and adapted from https://github.com/ysc3839/win32-darkmode (MIT License). 2 | 3 | #pragma once 4 | 5 | #define WIN32_LEAN_AND_MEAN 6 | #include 7 | #include 8 | #include "DarkMode.h" 9 | 10 | typedef WORD uint16_t; 11 | #include "IatHook.h" 12 | 13 | #ifndef LOAD_LIBRARY_SEARCH_SYSTEM32 14 | // From libloaderapi.h -- this flag was introduced in Win7. 15 | #define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800 16 | #endif 17 | 18 | enum IMMERSIVE_HC_CACHE_MODE 19 | { 20 | IHCM_USE_CACHED_VALUE, 21 | IHCM_REFRESH 22 | }; 23 | 24 | enum WINDOWCOMPOSITIONATTRIB 25 | { 26 | WCA_UNDEFINED = 0, 27 | WCA_NCRENDERING_ENABLED = 1, 28 | WCA_NCRENDERING_POLICY = 2, 29 | WCA_TRANSITIONS_FORCEDISABLED = 3, 30 | WCA_ALLOW_NCPAINT = 4, 31 | WCA_CAPTION_BUTTON_BOUNDS = 5, 32 | WCA_NONCLIENT_RTL_LAYOUT = 6, 33 | WCA_FORCE_ICONIC_REPRESENTATION = 7, 34 | WCA_EXTENDED_FRAME_BOUNDS = 8, 35 | WCA_HAS_ICONIC_BITMAP = 9, 36 | WCA_THEME_ATTRIBUTES = 10, 37 | WCA_NCRENDERING_EXILED = 11, 38 | WCA_NCADORNMENTINFO = 12, 39 | WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, 40 | WCA_VIDEO_OVERLAY_ACTIVE = 14, 41 | WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, 42 | WCA_DISALLOW_PEEK = 16, 43 | WCA_CLOAK = 17, 44 | WCA_CLOAKED = 18, 45 | WCA_ACCENT_POLICY = 19, 46 | WCA_FREEZE_REPRESENTATION = 20, 47 | WCA_EVER_UNCLOAKED = 21, 48 | WCA_VISUAL_OWNER = 22, 49 | WCA_HOLOGRAPHIC = 23, 50 | WCA_EXCLUDED_FROM_DDA = 24, 51 | WCA_PASSIVEUPDATEMODE = 25, 52 | WCA_USEDARKMODECOLORS = 26, 53 | WCA_LAST = 27 54 | }; 55 | 56 | struct WINDOWCOMPOSITIONATTRIBDATA 57 | { 58 | WINDOWCOMPOSITIONATTRIB Attrib; 59 | PVOID pvData; 60 | SIZE_T cbData; 61 | }; 62 | 63 | using fnRtlGetNtVersionNumbers = void (WINAPI *)(LPDWORD major, LPDWORD minor, LPDWORD build); 64 | using fnSetWindowCompositionAttribute = BOOL (WINAPI *)(HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*); 65 | // 1809 17763 66 | using fnShouldAppsUseDarkMode = bool (WINAPI *)(); // ordinal 132 67 | using fnAllowDarkModeForWindow = bool (WINAPI *)(HWND hWnd, bool allow); // ordinal 133 68 | using fnAllowDarkModeForApp = bool (WINAPI *)(bool allow); // ordinal 135, in 1809 69 | using fnFlushMenuThemes = void (WINAPI *)(); // ordinal 136 70 | using fnRefreshImmersiveColorPolicyState = void (WINAPI *)(); // ordinal 104 71 | using fnIsDarkModeAllowedForWindow = bool (WINAPI *)(HWND hWnd); // ordinal 137 72 | using fnGetIsImmersiveColorUsingHighContrast = bool (WINAPI *)(IMMERSIVE_HC_CACHE_MODE mode); // ordinal 106 73 | using fnOpenNcThemeData = HTHEME(WINAPI *)(HWND hWnd, LPCWSTR pszClassList); // ordinal 49 74 | // 1903 18362 75 | using fnShouldSystemUseDarkMode = bool (WINAPI *)(); // ordinal 138 76 | using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode); // ordinal 135, in 1903 77 | 78 | static fnSetWindowCompositionAttribute _SetWindowCompositionAttribute = nullptr; 79 | static fnShouldAppsUseDarkMode _ShouldAppsUseDarkMode = nullptr; 80 | static fnAllowDarkModeForWindow _AllowDarkModeForWindow = nullptr; 81 | static fnAllowDarkModeForApp _AllowDarkModeForApp = nullptr; 82 | static fnFlushMenuThemes _FlushMenuThemes = nullptr; 83 | static fnRefreshImmersiveColorPolicyState _RefreshImmersiveColorPolicyState = nullptr; 84 | static fnIsDarkModeAllowedForWindow _IsDarkModeAllowedForWindow = nullptr; 85 | static fnGetIsImmersiveColorUsingHighContrast _GetIsImmersiveColorUsingHighContrast = nullptr; 86 | static fnOpenNcThemeData _OpenNcThemeData = nullptr; 87 | // 1903 18362 88 | static fnShouldSystemUseDarkMode _ShouldSystemUseDarkMode = nullptr; 89 | static fnSetPreferredAppMode _SetPreferredAppMode = nullptr; 90 | 91 | static bool s_darkModeSupported = false; 92 | static DWORD s_buildNumber = 0; 93 | 94 | bool IsHighContrast() 95 | { 96 | HIGHCONTRASTW highContrast = { sizeof(highContrast) }; 97 | if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), &highContrast, FALSE)) 98 | return !!(highContrast.dwFlags & HCF_HIGHCONTRASTON); 99 | return false; 100 | } 101 | 102 | static void RefreshTitleBarThemeColor(HWND hWnd) 103 | { 104 | if (!s_darkModeSupported) 105 | return; 106 | 107 | BOOL dark = FALSE; 108 | if (_IsDarkModeAllowedForWindow(hWnd) && 109 | _ShouldAppsUseDarkMode() && 110 | !IsHighContrast()) 111 | { 112 | dark = TRUE; 113 | } 114 | if (s_buildNumber < 18362) 115 | SetPropW(hWnd, L"UseImmersiveDarkModeColors", reinterpret_cast(static_cast(dark))); 116 | else if (_SetWindowCompositionAttribute) 117 | { 118 | WINDOWCOMPOSITIONATTRIBDATA data = { WCA_USEDARKMODECOLORS, &dark, sizeof(dark) }; 119 | _SetWindowCompositionAttribute(hWnd, &data); 120 | } 121 | } 122 | 123 | bool IsColorSchemeChangeMessage(LPARAM lParam) 124 | { 125 | bool is = false; 126 | if (lParam && _stricmp(reinterpret_cast(lParam), "ImmersiveColorSet") == 0) 127 | { 128 | if (_RefreshImmersiveColorPolicyState) 129 | _RefreshImmersiveColorPolicyState(); 130 | is = true; 131 | } 132 | if (_GetIsImmersiveColorUsingHighContrast) 133 | _GetIsImmersiveColorUsingHighContrast(IHCM_REFRESH); 134 | return is; 135 | } 136 | 137 | bool IsColorSchemeChangeMessage(UINT message, LPARAM lParam) 138 | { 139 | if (message == WM_SETTINGCHANGE) 140 | return IsColorSchemeChangeMessage(lParam); 141 | return false; 142 | } 143 | 144 | static void FixDarkScrollBar() 145 | { 146 | HMODULE hComctl = LoadLibraryExW(L"comctl32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); 147 | if (hComctl) 148 | { 149 | auto addr = FindDelayLoadThunkInModule(hComctl, "uxtheme.dll", 49); // OpenNcThemeData 150 | if (addr) 151 | { 152 | DWORD oldProtect; 153 | if (VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), PAGE_READWRITE, &oldProtect)) 154 | { 155 | auto MyOpenThemeData = [](HWND hWnd, LPCWSTR classList) -> HTHEME { 156 | if (wcscmp(classList, L"ScrollBar") == 0) 157 | { 158 | hWnd = nullptr; 159 | classList = L"Explorer::ScrollBar"; 160 | } 161 | return _OpenNcThemeData(hWnd, classList); 162 | }; 163 | 164 | addr->u1.Function = reinterpret_cast(static_cast(MyOpenThemeData)); 165 | VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), oldProtect, &oldProtect); 166 | } 167 | } 168 | } 169 | } 170 | 171 | constexpr bool CheckBuildNumber(DWORD buildNumber) 172 | { 173 | #if 0 174 | return (buildNumber == 17763 || // 1809 175 | buildNumber == 18362 || // 1903 176 | buildNumber == 18363); // 1909 177 | #else 178 | return (buildNumber >= 18362); 179 | #endif 180 | } 181 | 182 | static bool ShouldAppsUseDarkMode() 183 | { 184 | return _ShouldAppsUseDarkMode && _ShouldAppsUseDarkMode(); 185 | } 186 | 187 | bool AllowDarkMode() 188 | { 189 | auto RtlGetNtVersionNumbers = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers")); 190 | if (!RtlGetNtVersionNumbers) 191 | return false; 192 | 193 | DWORD major, minor; 194 | RtlGetNtVersionNumbers(&major, &minor, &s_buildNumber); 195 | s_buildNumber &= ~0xF0000000; 196 | if (major < 10) 197 | return false; 198 | if (major == 10 && !CheckBuildNumber(s_buildNumber)) 199 | return false; 200 | 201 | HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); 202 | if (!hUxtheme) 203 | return false; 204 | 205 | _OpenNcThemeData = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(49))); 206 | _RefreshImmersiveColorPolicyState = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(104))); 207 | _GetIsImmersiveColorUsingHighContrast = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(106))); 208 | _ShouldAppsUseDarkMode = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(132))); 209 | _AllowDarkModeForWindow = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); 210 | 211 | auto ord135 = GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135)); 212 | if (!ord135) 213 | return false; 214 | 215 | if (s_buildNumber < 18362) 216 | _AllowDarkModeForApp = reinterpret_cast(ord135); 217 | else 218 | _SetPreferredAppMode = reinterpret_cast(ord135); 219 | 220 | _IsDarkModeAllowedForWindow = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(137))); 221 | 222 | _SetWindowCompositionAttribute = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute")); 223 | 224 | if (_OpenNcThemeData && 225 | _RefreshImmersiveColorPolicyState && 226 | _ShouldAppsUseDarkMode && 227 | _AllowDarkModeForWindow && 228 | (_AllowDarkModeForApp || _SetPreferredAppMode) && 229 | _IsDarkModeAllowedForWindow) 230 | { 231 | s_darkModeSupported = true; 232 | 233 | if (_AllowDarkModeForApp) 234 | _AllowDarkModeForApp(true); 235 | else if (_SetPreferredAppMode) 236 | _SetPreferredAppMode(PreferredAppMode::AllowDark); 237 | 238 | _RefreshImmersiveColorPolicyState(); 239 | 240 | FixDarkScrollBar(); 241 | } 242 | 243 | return s_darkModeSupported; 244 | } 245 | 246 | bool IsDarkModeSupported() 247 | { 248 | return s_darkModeSupported; 249 | } 250 | 251 | bool ShouldUseDarkMode() 252 | { 253 | if (!s_darkModeSupported) 254 | return false; 255 | 256 | return _ShouldAppsUseDarkMode() && !IsHighContrast(); 257 | } 258 | 259 | bool DarkModeOnThemeChanged(HWND hWnd, DarkModeMode dmm) 260 | { 261 | if (!s_darkModeSupported) 262 | return false; 263 | 264 | const bool fUseDark = ((dmm == DarkModeMode::Light) ? false : 265 | (dmm == DarkModeMode::Dark) ? true : 266 | ShouldUseDarkMode()); 267 | 268 | // HACK: In Elucidisk, the first call returns false, but a second call 269 | // returns true. In another app of mine, the first call returns true. I 270 | // haven't yet been able to figure out what's different between them. 271 | // 272 | // BUT! The return value doesn't seem to be related to success; even when 273 | // fUseDark is true and the function returns false, it still successfully 274 | // applies dark mode. 275 | //const bool fDark = 276 | //_AllowDarkModeForWindow(hWnd, fUseDark) || 277 | _AllowDarkModeForWindow(hWnd, fUseDark); 278 | 279 | RefreshTitleBarThemeColor(hWnd); 280 | return fUseDark; 281 | } 282 | 283 | UINT32 GetForeColor(bool dark_mode) 284 | { 285 | return dark_mode ? 0xc0c0c0 : 0x000000; 286 | } 287 | 288 | UINT32 GetBackColor(bool dark_mode) 289 | { 290 | return dark_mode ? 0x111111 : 0xffffff; 291 | } 292 | -------------------------------------------------------------------------------- /DarkMode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // 1903 18362 4 | enum class PreferredAppMode 5 | { 6 | Default, 7 | AllowDark, 8 | ForceDark, 9 | ForceLight, 10 | Max 11 | }; 12 | 13 | enum class DarkModeMode 14 | { 15 | Auto, 16 | Light, 17 | Dark, 18 | }; 19 | 20 | // Initializes dark mode if possible, and returns whether it was successful. 21 | bool AllowDarkMode(); 22 | bool IsDarkModeSupported(); 23 | 24 | // Returns whether the app should use dark mode. This is a combination of 25 | // whether dark mode is available, plus whether high contrast mode is active. 26 | bool ShouldUseDarkMode(); 27 | 28 | bool IsHighContrast(); 29 | bool IsColorSchemeChangeMessage(LPARAM lParam); 30 | bool IsColorSchemeChangeMessage(UINT message, LPARAM lParam); 31 | bool DarkModeOnThemeChanged(HWND hWnd, DarkModeMode dmm=DarkModeMode::Auto); 32 | 33 | UINT32 GetForeColor(bool dark_mode); 34 | UINT32 GetBackColor(bool dark_mode); 35 | -------------------------------------------------------------------------------- /IatHook.h: -------------------------------------------------------------------------------- 1 | // This file contains code from 2 | // https://github.com/stevemk14ebr/PolyHook_2_0/blob/master/sources/IatHook.cpp 3 | // which is licensed under the MIT License. 4 | // See PolyHook_2_0-LICENSE for more information. 5 | 6 | #pragma once 7 | 8 | #pragma push_macro( "uint16_t" ) 9 | 10 | #if __cplusplus >= 201103L || _MSC_VER >= 1700 11 | # define IATHOOK_CONSTEXPR constexpr 12 | #else 13 | # define IATHOOK_CONSTEXPR const 14 | # define uint16_t WORD 15 | #endif 16 | 17 | typedef struct _IATHOOK_IMAGE_IMPORT_BY_NAME { 18 | WORD Hint; 19 | CHAR Name[1]; 20 | } IATHOOK_IMAGE_IMPORT_BY_NAME, *IATHOOK_PIMAGE_IMPORT_BY_NAME; 21 | 22 | typedef struct _IATHOOK_IMAGE_DELAYLOAD_DESCRIPTOR { 23 | union { 24 | DWORD AllAttributes; 25 | struct { 26 | DWORD RvaBased : 1; // Delay load version 2 27 | DWORD ReservedAttributes : 31; 28 | } DUMMYSTRUCTNAME; 29 | } Attributes; 30 | 31 | DWORD DllNameRVA; // RVA to the name of the target library (NULL-terminate ASCII string) 32 | DWORD ModuleHandleRVA; // RVA to the HMODULE caching location (PHMODULE) 33 | DWORD ImportAddressTableRVA; // RVA to the start of the IAT (PIMAGE_THUNK_DATA) 34 | DWORD ImportNameTableRVA; // RVA to the start of the name table (PIMAGE_THUNK_DATA::AddressOfData) 35 | DWORD BoundImportAddressTableRVA; // RVA to an optional bound IAT 36 | DWORD UnloadInformationTableRVA; // RVA to an optional unload info table 37 | DWORD TimeDateStamp; // 0 if not bound, 38 | // Otherwise, date/time of the target DLL 39 | 40 | } IATHOOK_IMAGE_DELAYLOAD_DESCRIPTOR, *IATHOOK_PIMAGE_DELAYLOAD_DESCRIPTOR; 41 | 42 | 43 | template 44 | IATHOOK_CONSTEXPR T RVA2VA(T1 base, T2 rva) 45 | { 46 | return reinterpret_cast(reinterpret_cast(base) + rva); 47 | } 48 | 49 | template 50 | IATHOOK_CONSTEXPR T DataDirectoryFromModuleBase(void *moduleBase, size_t entryID) 51 | { 52 | auto dosHdr = reinterpret_cast(moduleBase); 53 | auto ntHdr = RVA2VA(moduleBase, dosHdr->e_lfanew); 54 | auto dataDir = ntHdr->OptionalHeader.DataDirectory; 55 | return RVA2VA(moduleBase, dataDir[entryID].VirtualAddress); 56 | } 57 | 58 | PIMAGE_THUNK_DATA FindAddressByName(void *moduleBase, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, const char *funcName) 59 | { 60 | for (; impName->u1.Ordinal; ++impName, ++impAddr) 61 | { 62 | if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal)) 63 | continue; 64 | 65 | auto import = RVA2VA(moduleBase, impName->u1.AddressOfData); 66 | if (strcmp(import->Name, funcName) != 0) 67 | continue; 68 | return impAddr; 69 | } 70 | return nullptr; 71 | } 72 | 73 | PIMAGE_THUNK_DATA FindAddressByOrdinal(void *moduleBase, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, uint16_t ordinal) 74 | { 75 | for (; impName->u1.Ordinal; ++impName, ++impAddr) 76 | { 77 | if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal) && IMAGE_ORDINAL(impName->u1.Ordinal) == ordinal) 78 | return impAddr; 79 | } 80 | return nullptr; 81 | } 82 | 83 | PIMAGE_THUNK_DATA FindIatThunkInModule(void *moduleBase, const char *dllName, const char *funcName) 84 | { 85 | auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_IMPORT); 86 | for (; imports->Name; ++imports) 87 | { 88 | if (_stricmp(RVA2VA(moduleBase, imports->Name), dllName) != 0) 89 | continue; 90 | 91 | auto origThunk = RVA2VA(moduleBase, imports->OriginalFirstThunk); 92 | auto thunk = RVA2VA(moduleBase, imports->FirstThunk); 93 | return FindAddressByName(moduleBase, origThunk, thunk, funcName); 94 | } 95 | return nullptr; 96 | } 97 | 98 | PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void *moduleBase, const char *dllName, const char *funcName) 99 | { 100 | auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); 101 | for (; imports->DllNameRVA; ++imports) 102 | { 103 | if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) 104 | continue; 105 | 106 | auto impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); 107 | auto impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); 108 | return FindAddressByName(moduleBase, impName, impAddr, funcName); 109 | } 110 | return nullptr; 111 | } 112 | 113 | PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void *moduleBase, const char *dllName, uint16_t ordinal) 114 | { 115 | auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); 116 | for (; imports->DllNameRVA; ++imports) 117 | { 118 | if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) 119 | continue; 120 | 121 | auto impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); 122 | auto impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); 123 | return FindAddressByOrdinal(moduleBase, impName, impAddr, ordinal); 124 | } 125 | return nullptr; 126 | } 127 | 128 | #pragma pop_macro( "uint16_t" ) 129 | 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Antos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elucidisk 2 | 3 | This uses a sunburst chart to visualize disk space usage on Windows. 4 | 5 | It is modelled after [Scanner](http://www.steffengerlach.de/freeware/), 6 | [Diskitude](https://madebyevan.com/diskitude/), and other similar disk space 7 | viewers. 8 | 9 | The name "Elucidisk" is a portmanteau of "elucidate" and "disk". 10 | 11 | ![image](https://raw.githubusercontent.com/chrisant996/elucidisk/master/demo.png) 12 | 13 | ## What can it do? 14 | 15 | - Scans drives or folders to find the size used. 16 | - Shows the results in a sunburst chart. 17 | - The chart has several configurable options: 18 | - Use plain colors. 19 | - Use rainbow colors (based on angle in the sunburst). 20 | - Use heatmap colors (based on size). 21 | - Show/hide names of files and directories in the sunburst (if the name fits). 22 | - Show/hide free space for drives. 23 | - Use the actual compressed size for compressed files, instead of the uncompressed size. 24 | - Show arcs with proportional area (e.g. two arcs for 50 GB directories will have the same area, even if they are at different distances from the center). 25 | - Show size comparison bar when hovering over an arc (the comparison bars are always in the center ring, so their sizes are comparable even when Proportional Area is turned off). 26 | - Show combined summary chart for all local drives. 27 | - Right click on an arc for a context menu of available actions. 28 | - Right click elsewhere for a context menu of configurable options (or press Shift-F10 or Apps key). 29 | 30 | Please feel free to [open 31 | issues](https://github.com/chrisant996/elucidisk/issues) for suggestions, 32 | problem reports, or other feedback. 33 | 34 | If you want to contribute, fork the repo and create a topic branch, and send a 35 | pull request for your topic branch. Also, consider opening an issue first and 36 | discussing the contribution you want to make. 37 | 38 | ## Why was it created? 39 | 40 | When viewing a sunburst chart of disk space usage, I want the free space on a 41 | disk to show up in the chart. The only sunburst disk space visualizer I could 42 | find that includes the free space is Scanner. 43 | 44 | I also wanted a few improvements to the user interface, such as highlighting 45 | the arc under the mouse pointer and showing names of directories/files when 46 | the name fits in the arc. 47 | 48 | I wanted to use the MIT license. Most disk space visualizers are either 49 | proprietary or use a "viral" version of GPL license. 50 | 51 | So, I wrote my own. 52 | 53 | It is written in C++ and uses DirectX for rendering. 54 | 55 | ## Building Elucidisk 56 | 57 | Elucidisk uses [Premake](http://premake.github.io) to generate Visual Studio solutions. Note that Premake >= 5.0.0-beta1 is required. 58 | 59 | 1. Cd to your clone of elucidisk. 60 | 2. Run premake5.exe toolchain (where toolchain is one of Premake's actions - see `premake5.exe --help`). 61 | 3. Build scripts will be generated in .build\\toolchain. For example `.build\vs2019\elucidisk.sln`. 62 | 4. Call your toolchain of choice (Visual Studio, msbuild.exe, etc). 63 | 64 | -------------------------------------------------------------------------------- /TextOnPath/PathTextRenderer.cpp: -------------------------------------------------------------------------------- 1 | //// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 2 | //// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 3 | //// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 4 | //// PARTICULAR PURPOSE. 5 | //// 6 | //// Copyright (c) Microsoft Corporation. All rights reserved 7 | 8 | // MICROSOFT LIMITED PUBLIC LICENSE version 1.1 9 | 10 | // From sample source from Win81App for animated text on a path. 11 | // https://github.com/uri247/Win81App/tree/master/Direct2D%20and%20DirectWrite%20animated%20text%20on%20a%20path%20sample 12 | 13 | // BEGIN_CHANGE 14 | #if 0 15 | #include "pch.h" 16 | #else 17 | #include "../main.h" 18 | #include 19 | #include 20 | #include 21 | #endif 22 | // END_CHANGE 23 | #include "PathTextRenderer.h" 24 | 25 | // An identity matrix for use by IDWritePixelSnapping::GetCurrentTransform. 26 | const DWRITE_MATRIX identityTransform = 27 | { 28 | 1, 0, 29 | 0, 1, 30 | 0, 0 31 | }; 32 | 33 | PathTextRenderer::PathTextRenderer(FLOAT pixelsPerDip) : 34 | m_pixelsPerDip(pixelsPerDip), 35 | // BEGIN_CHANGE 36 | #if 0 37 | m_ref(0) 38 | #else 39 | m_ref(1) 40 | #endif 41 | // END_CHANGE 42 | { 43 | } 44 | 45 | // BEGIN_CHANGE 46 | HRESULT PathTextRenderer::TestFit(void* clientDrawingContext, IDWriteTextLayout* pLayout) 47 | { 48 | m_measure = true; 49 | m_fits = true; 50 | const HRESULT hr = pLayout->Draw(clientDrawingContext, this, 0, 0); 51 | m_measure = false; 52 | if (FAILED(hr)) 53 | return hr; 54 | if (!m_fits) 55 | return S_FALSE; 56 | return S_OK; 57 | } 58 | // END_CHANGE 59 | 60 | // 61 | // Draws a given glyph run along the geometry specified 62 | // in the given clientDrawingEffect. 63 | // 64 | // This method calculates the horizontal displacement 65 | // of each glyph cluster in the run, then calculates the 66 | // tangent vector of the geometry at each of those distances. 67 | // It then renders the glyph cluster using the offset and angle 68 | // defined by that tangent, thereby placing each cluster on 69 | // the path and also rotated to the path. 70 | // 71 | HRESULT PathTextRenderer::DrawGlyphRun( 72 | _In_opt_ void* clientDrawingContext, 73 | FLOAT baselineOriginX, 74 | FLOAT baselineOriginY, 75 | DWRITE_MEASURING_MODE measuringMode, 76 | _In_ DWRITE_GLYPH_RUN const* glyphRun, 77 | _In_ DWRITE_GLYPH_RUN_DESCRIPTION const* glyphRunDescription, 78 | _In_opt_ IUnknown* clientDrawingEffect 79 | ) 80 | { 81 | if (clientDrawingContext == nullptr) 82 | { 83 | return S_OK; 84 | } 85 | 86 | // Since we use our own custom renderer and explicitly set the effect 87 | // on the layout, we know exactly what the parameter is and can 88 | // safely cast it directly. 89 | PathTextDrawingContext* dc = static_cast(clientDrawingContext); 90 | 91 | // Store any existing transformation in the render target. 92 | D2D1_MATRIX_3X2_F originalTransform; 93 | dc->d2DContext->GetTransform(&originalTransform); 94 | 95 | // Compute the length of the geometry. 96 | FLOAT maxLength; 97 | // BEGIN_CHANGE 98 | #if 0 99 | DX::ThrowIfFailed( 100 | #else 101 | { 102 | HRESULT hr = ( 103 | #endif 104 | // END_CHANGE 105 | dc->geometry->ComputeLength( 106 | originalTransform, &maxLength) 107 | ); 108 | // BEGIN_CHANGE 109 | if (FAILED(hr)) return hr; 110 | // Allow a little padding for readability. 111 | maxLength -= 2.0f * m_pixelsPerDip / 96.0f; 112 | } 113 | // END_CHANGE 114 | 115 | // Set up a partial glyph run that we can modify. 116 | DWRITE_GLYPH_RUN partialGlyphRun = *glyphRun; 117 | 118 | // Determine whether the text is LTR or RTL. 119 | BOOL leftToRight = (glyphRun->bidiLevel % 2 == 0); 120 | 121 | // Set the initial length along the path. 122 | FLOAT length = baselineOriginX; 123 | 124 | // Set the index of the first glyph in the current glyph cluster. 125 | UINT firstGlyphIdx = 0; 126 | 127 | while (firstGlyphIdx < glyphRun->glyphCount) 128 | { 129 | // Compute the number of glyphs in this cluster and the total cluster width. 130 | UINT numGlyphsInCluster = 0; 131 | UINT i = firstGlyphIdx; 132 | FLOAT clusterWidth = 0; 133 | // BEGIN_CHANGE 134 | #if 0 135 | while (glyphRunDescription->clusterMap[i] == glyphRunDescription->clusterMap[firstGlyphIdx] && 136 | i < glyphRun->glyphCount) 137 | #else 138 | while (i < glyphRun->glyphCount && 139 | glyphRunDescription->clusterMap[i] == glyphRunDescription->clusterMap[firstGlyphIdx]) 140 | #endif 141 | // END_CHANGE 142 | { 143 | clusterWidth += glyphRun->glyphAdvances[i]; 144 | i++; 145 | numGlyphsInCluster++; 146 | } 147 | 148 | // Compute the cluster's midpoint along the path. 149 | FLOAT midpoint = leftToRight ? 150 | (length + (clusterWidth / 2)) : 151 | (length - (clusterWidth / 2)); 152 | 153 | // Only render this cluster if it's within the path. 154 | // BEGIN_CHANGE 155 | #if 0 156 | if (midpoint < maxLength) 157 | #else 158 | FLOAT endpoint = leftToRight ? 159 | (length + clusterWidth) : 160 | (length - clusterWidth); 161 | if (endpoint > maxLength) 162 | { 163 | m_fits = false; 164 | break; 165 | } 166 | #endif 167 | // END_CHANGE 168 | { 169 | // Compute the offset and tangent at the cluster's midpoint. 170 | D2D1_POINT_2F offset; 171 | D2D1_POINT_2F tangent; 172 | // BEGIN_CHANGE 173 | #if 0 174 | DX::ThrowIfFailed( 175 | #else 176 | { 177 | HRESULT hr = ( 178 | #endif 179 | // END_CHANGE 180 | dc->geometry->ComputePointAtLength( 181 | midpoint, 182 | D2D1::IdentityMatrix(), 183 | &offset, 184 | &tangent 185 | ) 186 | ); 187 | // BEGIN_CHANGE 188 | if (FAILED(hr)) return hr; 189 | } 190 | // END_CHANGE 191 | 192 | // Create a rotation matrix to align the cluster to the path. 193 | // Alternatively, we could use the D2D1::Matrix3x2F::Rotation() 194 | // helper, but this is more efficient since we already have cos(t) 195 | // and sin(t). 196 | D2D1_MATRIX_3X2_F rotation = D2D1::Matrix3x2F( 197 | tangent.x, 198 | tangent.y, 199 | -tangent.y, 200 | tangent.x, 201 | (offset.x * (1.0f - tangent.x) + offset.y * tangent.y), 202 | (offset.y * (1.0f - tangent.x) - offset.x * tangent.y) 203 | ); 204 | 205 | // Create a translation matrix to center the cluster on the tangent point. 206 | D2D1_MATRIX_3X2_F translation = leftToRight ? 207 | D2D1::Matrix3x2F::Translation(-clusterWidth/2, 0) : // LTR --> nudge it left 208 | D2D1::Matrix3x2F::Translation(clusterWidth/2, 0); // RTL --> nudge it right 209 | 210 | // Apply the transformations (in the proper order). 211 | dc->d2DContext->SetTransform(translation * rotation * originalTransform); 212 | 213 | // Draw the transformed glyph cluster. 214 | partialGlyphRun.glyphCount = numGlyphsInCluster; 215 | // BEGIN_CHANGE 216 | if (!m_measure) 217 | // END_CHANGE 218 | dc->d2DContext->DrawGlyphRun( 219 | D2D1::Point2F(offset.x, offset.y), 220 | &partialGlyphRun, 221 | dc->brush 222 | ); 223 | } 224 | 225 | // Advance to the next cluster. 226 | length = leftToRight ? (length + clusterWidth) : (length - clusterWidth); 227 | partialGlyphRun.glyphIndices += numGlyphsInCluster; 228 | partialGlyphRun.glyphAdvances += numGlyphsInCluster; 229 | 230 | if (partialGlyphRun.glyphOffsets != nullptr) 231 | { 232 | partialGlyphRun.glyphOffsets += numGlyphsInCluster; 233 | } 234 | 235 | firstGlyphIdx += numGlyphsInCluster; 236 | } 237 | 238 | // Restore the original transformation. 239 | dc->d2DContext->SetTransform(originalTransform); 240 | 241 | return S_OK; 242 | } 243 | 244 | HRESULT PathTextRenderer::DrawUnderline( 245 | _In_opt_ void* clientDrawingContext, 246 | FLOAT baselineOriginX, 247 | FLOAT baselineOriginY, 248 | _In_ DWRITE_UNDERLINE const* underline, 249 | _In_opt_ IUnknown* clientDrawingEffect 250 | ) 251 | { 252 | // We don't use underline in this application. 253 | return E_NOTIMPL; 254 | } 255 | 256 | HRESULT PathTextRenderer::DrawStrikethrough( 257 | _In_opt_ void* clientDrawingContext, 258 | FLOAT baselineOriginX, 259 | FLOAT baselineOriginY, 260 | _In_ DWRITE_STRIKETHROUGH const* strikethrough, 261 | _In_opt_ IUnknown* clientDrawingEffect 262 | ) 263 | { 264 | // We don't use strikethrough in this application. 265 | return E_NOTIMPL; 266 | } 267 | 268 | HRESULT PathTextRenderer::DrawInlineObject( 269 | _In_opt_ void* clientDrawingContext, 270 | FLOAT originX, 271 | FLOAT originY, 272 | IDWriteInlineObject* inlineObject, 273 | BOOL isSideways, 274 | BOOL isRightToLeft, 275 | _In_opt_ IUnknown* clientDrawingEffect 276 | ) 277 | { 278 | // We don't use inline objects in this application. 279 | return E_NOTIMPL; 280 | } 281 | 282 | // 283 | // IDWritePixelSnapping methods 284 | // 285 | HRESULT PathTextRenderer::IsPixelSnappingDisabled( 286 | _In_opt_ void* clientDrawingContext, 287 | _Out_ BOOL* isDisabled 288 | ) 289 | { 290 | *isDisabled = FALSE; 291 | return S_OK; 292 | } 293 | 294 | HRESULT PathTextRenderer::GetCurrentTransform( 295 | _In_opt_ void* clientDrawingContext, 296 | _Out_ DWRITE_MATRIX* transform 297 | ) 298 | { 299 | *transform = identityTransform; 300 | return S_OK; 301 | } 302 | 303 | HRESULT PathTextRenderer::GetPixelsPerDip( 304 | _In_opt_ void* clientDrawingContext, 305 | _Out_ FLOAT* pixelsPerDip 306 | ) 307 | { 308 | *pixelsPerDip = m_pixelsPerDip; 309 | return S_OK; 310 | } 311 | 312 | // 313 | // IUnknown methods 314 | // 315 | // These use a basic, non-thread-safe implementation of the 316 | // standard reference-counting logic. 317 | // 318 | HRESULT PathTextRenderer::QueryInterface( 319 | REFIID riid, 320 | _Outptr_ void** object 321 | ) 322 | { 323 | *object = nullptr; 324 | return E_NOTIMPL; 325 | } 326 | 327 | ULONG PathTextRenderer::AddRef() 328 | { 329 | m_ref++; 330 | 331 | return m_ref; 332 | } 333 | 334 | ULONG PathTextRenderer::Release() 335 | { 336 | m_ref--; 337 | 338 | if (m_ref == 0) 339 | { 340 | delete this; 341 | return 0; 342 | } 343 | 344 | return m_ref; 345 | } 346 | -------------------------------------------------------------------------------- /TextOnPath/PathTextRenderer.h: -------------------------------------------------------------------------------- 1 | //// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 2 | //// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 3 | //// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 4 | //// PARTICULAR PURPOSE. 5 | //// 6 | //// Copyright (c) Microsoft Corporation. All rights reserved 7 | 8 | // MICROSOFT LIMITED PUBLIC LICENSE version 1.1 9 | // From sample source from Win81App for animated text on a path. 10 | // https://github.com/uri247/Win81App/tree/master/Direct2D%20and%20DirectWrite%20animated%20text%20on%20a%20path%20sample 11 | 12 | #pragma once 13 | 14 | struct PathTextDrawingContext 15 | { 16 | SPI d2DContext; 17 | SPI geometry; 18 | SPI brush; 19 | }; 20 | 21 | class PathTextRenderer : public IDWriteTextRenderer 22 | { 23 | public: 24 | PathTextRenderer( 25 | FLOAT pixelsPerDip 26 | ); 27 | 28 | // BEGIN_CHANGE 29 | STDMETHOD(TestFit)( 30 | void* clientDrawingContext, 31 | IDWriteTextLayout* pLayout 32 | ); 33 | // END_CHANGE 34 | 35 | STDMETHOD(DrawGlyphRun)( 36 | _In_opt_ void* clientDrawingContext, 37 | FLOAT baselineOriginX, 38 | FLOAT baselineOriginY, 39 | DWRITE_MEASURING_MODE measuringMode, 40 | _In_ DWRITE_GLYPH_RUN const* glyphRun, 41 | _In_ DWRITE_GLYPH_RUN_DESCRIPTION const* glyphRunDescription, 42 | _In_opt_ IUnknown* clientDrawingEffect 43 | ) override; 44 | 45 | STDMETHOD(DrawUnderline)( 46 | _In_opt_ void* clientDrawingContext, 47 | FLOAT baselineOriginX, 48 | FLOAT baselineOriginY, 49 | _In_ DWRITE_UNDERLINE const* underline, 50 | _In_opt_ IUnknown* clientDrawingEffect 51 | ) override; 52 | 53 | STDMETHOD(DrawStrikethrough)( 54 | _In_opt_ void* clientDrawingContext, 55 | FLOAT baselineOriginX, 56 | FLOAT baselineOriginY, 57 | _In_ DWRITE_STRIKETHROUGH const* strikethrough, 58 | _In_opt_ IUnknown* clientDrawingEffect 59 | ) override; 60 | 61 | STDMETHOD(DrawInlineObject)( 62 | _In_opt_ void* clientDrawingContext, 63 | FLOAT originX, 64 | FLOAT originY, 65 | IDWriteInlineObject* inlineObject, 66 | BOOL isSideways, 67 | BOOL isRightToLeft, 68 | _In_opt_ IUnknown* clientDrawingEffect 69 | ) override; 70 | 71 | STDMETHOD(IsPixelSnappingDisabled)( 72 | _In_opt_ void* clientDrawingContext, 73 | _Out_ BOOL* isDisabled 74 | ) override; 75 | 76 | STDMETHOD(GetCurrentTransform)( 77 | _In_opt_ void* clientDrawingContext, 78 | _Out_ DWRITE_MATRIX* transform 79 | ) override; 80 | 81 | STDMETHOD(GetPixelsPerDip)( 82 | _In_opt_ void* clientDrawingContext, 83 | _Out_ FLOAT* pixelsPerDip 84 | ) override; 85 | 86 | STDMETHOD(QueryInterface)( 87 | REFIID riid, 88 | _Outptr_ void** object 89 | ) override; 90 | 91 | STDMETHOD_(ULONG, AddRef)() override; 92 | 93 | STDMETHOD_(ULONG, Release)() override; 94 | 95 | private: 96 | FLOAT m_pixelsPerDip; // Number of pixels per DIP. 97 | UINT m_ref; // Reference count for AddRef and Release. 98 | // BEGIN_CHANGE 99 | bool m_measure = false; // Measure instead of drawing. 100 | bool m_fits = false; // Whether the text fits. 101 | // END_CHANGE 102 | }; 103 | -------------------------------------------------------------------------------- /TextOnPath/license.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\uc1\htmautsp\deff2{\fonttbl{\f0\fcharset0 Times New Roman;}{\f2\fcharset0 MS Shell Dlg;}}{\colortbl\red0\green0\blue0;\red255\green255\blue255;}\loch\hich\dbch\pard\plain\ltrpar\itap0{\lang1033\fs16\f2\cf0 \cf0\ql{\f2 \line \li0\ri0\sa0\sb0\fi0\ql\par} 2 | {\fs40\f2 {\ltrch MICROSOFT LIMITED PUBLIC LICENSE version 1.1}\li0\ri0\sa0\sb0\fi0\ql\par} 3 | {\f2 \line {\ltrch ----------------------}\line \li0\ri0\sa0\sb0\fi0\ql\par} 4 | {\f2 {\ltrch This license governs use of code marked as \ldblquote sample\rdblquote or \ldblquote example\rdblquote available on this web site without a license agreement, as provided under the section above titled \ldblquote NOTICE SPECIFIC TO SOFTWARE AVAILABLE ON THIS WEB SITE.\rdblquote If you use such code (the \ldblquote software\rdblquote ), you accept this license. If you do not accept the license, do not use the software.}\li0\ri0\sa0\sb0\fi0\ql\par} 5 | {\f2 \line \li0\ri0\sa0\sb0\fi0\ql\par} 6 | {\f2 {\ltrch 1. Definitions}\li0\ri0\sa0\sb0\fi0\ql\par} 7 | {\f2 {\ltrch The terms \ldblquote reproduce,\rdblquote \ldblquote reproduction,\rdblquote \ldblquote derivative works,\rdblquote and \ldblquote distribution\rdblquote have the same meaning here as under U.S. copyright law. }\li0\ri0\sa0\sb0\fi0\ql\par} 8 | {\f2 {\ltrch A \ldblquote contribution\rdblquote is the original software, or any additions or changes to the software.}\li0\ri0\sa0\sb0\fi0\ql\par} 9 | {\f2 {\ltrch A \ldblquote contributor\rdblquote is any person that distributes its contribution under this license.}\li0\ri0\sa0\sb0\fi0\ql\par} 10 | {\f2 {\ltrch \ldblquote Licensed patents\rdblquote are a contributor\rquote s patent claims that read directly on its contribution.}\li0\ri0\sa0\sb0\fi0\ql\par} 11 | {\f2 \line \li0\ri0\sa0\sb0\fi0\ql\par} 12 | {\f2 {\ltrch 2. Grant of Rights}\li0\ri0\sa0\sb0\fi0\ql\par} 13 | {\f2 {\ltrch (A) Copyright Grant - Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.}\li0\ri0\sa0\sb0\fi0\ql\par} 14 | {\f2 {\ltrch (B) Patent Grant - Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.}\li0\ri0\sa0\sb0\fi0\ql\par} 15 | {\f2 \line \li0\ri0\sa0\sb0\fi0\ql\par} 16 | {\f2 {\ltrch 3. Conditions and Limitations}\li0\ri0\sa0\sb0\fi0\ql\par} 17 | {\f2 {\ltrch (A) No Trademark License- This license does not grant you rights to use any contributors\rquote name, logo, or trademarks.}\li0\ri0\sa0\sb0\fi0\ql\par} 18 | {\f2 {\ltrch (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.}\li0\ri0\sa0\sb0\fi0\ql\par} 19 | {\f2 {\ltrch (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.}\li0\ri0\sa0\sb0\fi0\ql\par} 20 | {\f2 {\ltrch (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.}\li0\ri0\sa0\sb0\fi0\ql\par} 21 | {\f2 {\ltrch (E) The software is licensed \ldblquote as-is.\rdblquote You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.}\li0\ri0\sa0\sb0\fi0\ql\par} 22 | {\f2 {\ltrch (F) Platform Limitation - The licenses granted in sections 2(A) and 2(B) extend only to the software or derivative works that you create that run directly on a Microsoft Windows operating system product, Microsoft run-time technology (such as the .NET Framework or Silverlight), or Microsoft application platform (such as Microsoft Office or Microsoft Dynamics).}\li0\ri0\sa0\sb0\fi0\ql\par} 23 | {\f2 \line \li0\ri0\sa0\sb0\fi0\ql\par} 24 | } 25 | } -------------------------------------------------------------------------------- /actions.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include "data.h" 6 | #include "actions.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | static BOOL CALLBACK FindTreeViewCallback(HWND hwnd, LPARAM lParam); 13 | static int CALLBACK BFF_Callback(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData); 14 | static bool ShellDeleteInternal(HWND hwnd, const WCHAR* path, bool permanent); 15 | 16 | //---------------------------------------------------------------------------- 17 | // Shell functions. 18 | 19 | void ShellOpen(HWND hwnd, const WCHAR* path) 20 | { 21 | ShellExecute(hwnd, nullptr, path, nullptr, nullptr, SW_NORMAL); 22 | } 23 | 24 | void ShellOpenRecycleBin(HWND hwnd) 25 | { 26 | ShellExecute(hwnd, nullptr, TEXT("shell:RecycleBinFolder"), nullptr, nullptr, SW_NORMAL); 27 | } 28 | 29 | bool ShellRecycle(HWND hwnd, const WCHAR* path) 30 | { 31 | return ShellDeleteInternal(hwnd, path, false/*permanent*/); 32 | } 33 | 34 | bool ShellDelete(HWND hwnd, const WCHAR* path) 35 | { 36 | return ShellDeleteInternal(hwnd, path, true/*permanent*/); 37 | } 38 | 39 | bool ShellEmptyRecycleBin(HWND hwnd, const WCHAR* path) 40 | { 41 | if (is_drive(path)) 42 | { 43 | if (SUCCEEDED(SHEmptyRecycleBin(hwnd, path, 0))) 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | bool ShellBrowseForFolder(HWND hwnd, const WCHAR* title, std::wstring& inout) 51 | { 52 | ThreadDpiAwarenessContext dpiContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); 53 | 54 | WCHAR sz[2048]; 55 | 56 | // First try the IFileDialog common dialog. Usage here is based on the 57 | // Windows v7.1 SDK Sample "winui\shell\appplatform\CommonFileDialogModes". 58 | 59 | do // Not really a loop, just a convenient way to break out to fallback code. 60 | { 61 | SPI spfd; 62 | if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spfd)))) 63 | break; 64 | 65 | DWORD dwOptions; 66 | if (FAILED(spfd->GetOptions(&dwOptions))) 67 | break; 68 | 69 | spfd->SetOptions(dwOptions|FOS_PICKFOLDERS|FOS_FORCEFILESYSTEM|FOS_NOREADONLYRETURN|FOS_DONTADDTORECENT); 70 | 71 | SPI spsiFolder; 72 | if (inout.length()) 73 | SHCreateItemFromParsingName(inout.c_str(), 0, IID_PPV_ARGS(&spsiFolder)); 74 | else 75 | SHGetKnownFolderItem(FOLDERID_Documents, KF_FLAG_DEFAULT, 0, IID_PPV_ARGS(&spsiFolder)); 76 | if (spsiFolder) 77 | spfd->SetFolder(spsiFolder); 78 | if (title && *title) 79 | spfd->SetTitle(title); 80 | 81 | HRESULT hr; 82 | 83 | hr = spfd->Show(hwnd); 84 | if (FAILED(hr)) 85 | { 86 | if (hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) 87 | { 88 | LShellError: 89 | DWORD const dwFlags = FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS; 90 | DWORD cch = FormatMessage(dwFlags, 0, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), sz, _countof(sz), 0); 91 | if (!cch) 92 | { 93 | if (hr < 65536) 94 | swprintf_s(sz, _countof(sz), TEXT("Error %u."), hr); 95 | else 96 | swprintf_s(sz, _countof(sz), TEXT("Error 0x%08X."), hr); 97 | } 98 | MessageBox(hwnd, sz, TEXT("Elucidisk"), MB_OK|MB_ICONERROR); 99 | } 100 | return false; 101 | } 102 | 103 | SPI spsi; 104 | hr = spfd->GetResult(&spsi); 105 | if (FAILED(hr)) 106 | goto LShellError; 107 | 108 | LPWSTR pszName; 109 | hr = spsi->GetDisplayName(SIGDN_FILESYSPATH, &pszName); 110 | if (FAILED(hr)) 111 | goto LShellError; 112 | 113 | inout = pszName; 114 | CoTaskMemFree(pszName); 115 | return true; 116 | } 117 | while (false); 118 | 119 | // Fall back to use the legacy folder picker. 120 | // 121 | // Usage here is based on: 122 | // "https://msdn.microsoft.com/en-us/library/windows/desktop/bb762115(v=vs.85).aspx" 123 | 124 | BROWSEINFO bi = {}; 125 | bi.hwndOwner = hwnd; 126 | bi.pszDisplayName = sz; 127 | bi.lpszTitle = title; 128 | bi.ulFlags = BIF_RETURNONLYFSDIRS|BIF_EDITBOX|BIF_VALIDATE|BIF_NEWDIALOGSTYLE|BIF_NONEWFOLDERBUTTON; 129 | bi.lpfn = BFF_Callback; 130 | bi.lParam = LPARAM(inout.c_str()); 131 | 132 | LPITEMIDLIST pidl; 133 | pidl = SHBrowseForFolder(&bi); 134 | if (!pidl) 135 | return false; 136 | 137 | if (!SHGetPathFromIDList(pidl, sz)) 138 | return false; 139 | 140 | inout = sz; 141 | return true; 142 | } 143 | 144 | //---------------------------------------------------------------------------- 145 | // Helpers. 146 | 147 | static BOOL CALLBACK FindTreeViewCallback(HWND hwnd, LPARAM lParam) 148 | { 149 | WCHAR szClassName[MAX_PATH] = {}; 150 | GetClassName(hwnd, szClassName, _countof(szClassName)); 151 | szClassName[_countof(szClassName) - 1] = 0; 152 | 153 | if (wcsicmp(szClassName, TEXT("SysTreeView32")) == 0) 154 | { 155 | HWND* phwnd = (HWND*)lParam; 156 | if (phwnd) 157 | *phwnd = hwnd; 158 | return false; 159 | } 160 | 161 | return true; 162 | } 163 | 164 | static int CALLBACK BFF_Callback(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) 165 | { 166 | static bool s_fProcessEnsureVisible = false; 167 | 168 | switch (uMsg) 169 | { 170 | case BFFM_INITIALIZED: 171 | { 172 | #pragma warning(push) 173 | #pragma warning(disable : 4996) // GetVersion is deprecated, but we'll continue to use it. 174 | #pragma warning(disable : 28159) // GetVersion is deprecated, but we'll continue to use it. 175 | OSVERSIONINFO osvi; 176 | osvi.dwOSVersionInfoSize = sizeof(osvi); 177 | s_fProcessEnsureVisible = (GetVersionEx(&osvi) && 178 | (osvi.dwMajorVersion > 6 || 179 | (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion >= 1))); 180 | #pragma warning(pop) 181 | SendMessage(hwnd, BFFM_SETSELECTION, true, lpData); 182 | } 183 | break; 184 | 185 | case BFFM_SELCHANGED: 186 | if (s_fProcessEnsureVisible) 187 | { 188 | s_fProcessEnsureVisible = false; 189 | 190 | HWND hwndTree = 0; 191 | HTREEITEM hItem = 0; 192 | EnumChildWindows(hwnd, FindTreeViewCallback, LPARAM(&hwndTree)); 193 | if (hwndTree) 194 | hItem = TreeView_GetSelection(hwndTree); 195 | if (hItem) 196 | TreeView_EnsureVisible(hwndTree, hItem); 197 | } 198 | break; 199 | } 200 | 201 | return 0; 202 | } 203 | 204 | enum class CautionLevel { Normal, SystemFile, SystemDir, SpecialDir, Windows, Error }; 205 | 206 | static CautionLevel AssessCautionLevel(const WCHAR* _path) 207 | { 208 | std::wstring path = _path; 209 | strip_separator(path); 210 | if (path.empty()) 211 | return CautionLevel::Error; 212 | 213 | // Disallow attempting to delete a drive. 214 | if (is_drive(path.c_str())) 215 | return CautionLevel::Error; 216 | 217 | WIN32_FIND_DATA fd; 218 | HANDLE hFind = FindFirstFile(path.c_str(), &fd); 219 | if (hFind == INVALID_HANDLE_VALUE) 220 | return CautionLevel::Error; 221 | 222 | CautionLevel caution = CautionLevel::Normal; 223 | 224 | FindClose(hFind); 225 | if (fd.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) 226 | caution = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? CautionLevel::SystemDir : CautionLevel::SystemFile; 227 | 228 | struct QuirkySpecialFolder 229 | { 230 | CautionLevel caution; 231 | const KNOWNFOLDERID* kid; 232 | const WCHAR* path; 233 | bool children; 234 | bool recursive; 235 | }; 236 | 237 | static const QuirkySpecialFolder c_quirky[] = 238 | { 239 | { CautionLevel::Windows, &FOLDERID_Windows }, 240 | { CautionLevel::Windows, &FOLDERID_Windows, nullptr, true/*children*/, true/*recursive*/ }, 241 | { CautionLevel::SpecialDir, &FOLDERID_Profile, TEXT("AppData") }, 242 | { CautionLevel::SpecialDir, &FOLDERID_UserProfiles, nullptr, true/*children*/ }, 243 | }; 244 | 245 | static const KNOWNFOLDERID c_kids[] = 246 | { 247 | FOLDERID_UserProfiles, 248 | 249 | FOLDERID_AccountPictures, 250 | FOLDERID_CameraRoll, 251 | FOLDERID_Contacts, 252 | FOLDERID_Desktop, 253 | FOLDERID_Documents, 254 | FOLDERID_Downloads, 255 | FOLDERID_Favorites, 256 | FOLDERID_Fonts, 257 | FOLDERID_Links, 258 | FOLDERID_Music, 259 | FOLDERID_Pictures, 260 | FOLDERID_Playlists, 261 | FOLDERID_Videos, 262 | 263 | FOLDERID_Profile, 264 | FOLDERID_LocalAppData, 265 | FOLDERID_LocalAppDataLow, 266 | FOLDERID_RoamingAppData, 267 | FOLDERID_AppDataDesktop, 268 | FOLDERID_AppDataDocuments, 269 | FOLDERID_AppDataFavorites, 270 | FOLDERID_AppDataProgramData, 271 | 272 | FOLDERID_Programs, 273 | FOLDERID_ProgramData, 274 | FOLDERID_ProgramFilesX64, 275 | FOLDERID_ProgramFilesX86, 276 | FOLDERID_ProgramFilesCommonX64, 277 | FOLDERID_ProgramFilesCommonX86, 278 | FOLDERID_UserProgramFiles, 279 | FOLDERID_UserProgramFilesCommon, 280 | 281 | FOLDERID_StartMenu, 282 | FOLDERID_StartMenuAllPrograms, 283 | FOLDERID_CommonStartMenu, 284 | FOLDERID_SendTo, 285 | 286 | FOLDERID_SkyDrive, 287 | FOLDERID_SkyDriveCameraRoll, 288 | FOLDERID_SkyDriveDocuments, 289 | FOLDERID_SkyDriveMusic, 290 | FOLDERID_SkyDrivePictures, 291 | }; 292 | 293 | std::wstring tmp; 294 | for (const auto& quirky : c_quirky) 295 | { 296 | tmp.clear(); 297 | 298 | WCHAR* pszPath = nullptr; 299 | const HRESULT hr = SHGetKnownFolderPath(*quirky.kid, KF_FLAG_DONT_VERIFY|KF_FLAG_NO_ALIAS, nullptr, &pszPath); 300 | if (SUCCEEDED(hr)) 301 | tmp = pszPath; 302 | if (pszPath) 303 | CoTaskMemFree(pszPath); 304 | 305 | if (!tmp.empty()) 306 | { 307 | if (quirky.path) 308 | { 309 | tmp.append(TEXT("\\")); 310 | tmp.append(quirky.path); 311 | } 312 | 313 | bool match = false; 314 | if (quirky.children) 315 | { 316 | tmp.append(TEXT("\\")); 317 | if (!wcsnicmp(tmp.c_str(), path.c_str(), tmp.length())) 318 | { 319 | if (quirky.recursive) 320 | match = true; 321 | else 322 | match = !wcschr(path.c_str() + tmp.length(), '\\'); 323 | } 324 | } 325 | else 326 | { 327 | match = !wcsicmp(tmp.c_str(), path.c_str()); 328 | } 329 | 330 | if (match) 331 | { 332 | caution = quirky.caution; 333 | break; 334 | } 335 | } 336 | } 337 | 338 | for (const auto& kid : c_kids) 339 | { 340 | WCHAR* pszPath = nullptr; 341 | const HRESULT hr = SHGetKnownFolderPath(kid, KF_FLAG_DONT_VERIFY|KF_FLAG_NO_ALIAS, nullptr, &pszPath); 342 | const bool special = (SUCCEEDED(hr) && !wcsicmp(pszPath, path.c_str())); 343 | if (pszPath) 344 | CoTaskMemFree(pszPath); 345 | 346 | if (special) 347 | { 348 | caution = CautionLevel::SpecialDir; 349 | break; 350 | } 351 | } 352 | 353 | return caution; 354 | } 355 | 356 | static bool ShellDeleteInternal(HWND hwnd, const WCHAR* _path, bool permanent) 357 | { 358 | #if 0 359 | if (!permanent) 360 | { 361 | WCHAR message[2048]; 362 | swprintf_s(message, _countof(message), TEXT("Are you sure you want to move \"%s\" to the Recycle Bin?"), _path); 363 | if (MessageBox(hwnd, message, TEXT("Confirm Recycle"), MB_YESNOCANCEL|MB_ICONQUESTION) != IDYES) 364 | return false; 365 | } 366 | #endif 367 | 368 | CautionLevel caution = AssessCautionLevel(_path); 369 | if (caution != CautionLevel::Normal) 370 | { 371 | WCHAR message[2048]; 372 | const WCHAR* title = nullptr; 373 | const WCHAR* format = nullptr; 374 | switch (caution) 375 | { 376 | case CautionLevel::SystemFile: 377 | title = TEXT("Caution - System File"); 378 | format = TEXT("\"%s\" is a System File.%s"); 379 | break; 380 | case CautionLevel::SystemDir: 381 | title = TEXT("Caution - System Directory"); 382 | format = TEXT("\"%s\" is a System Directory.%s"); 383 | break; 384 | case CautionLevel::SpecialDir: 385 | title = TEXT("Caution - Special Directory"); 386 | format = TEXT("\"%s\" is a Special Directory.%s"); 387 | break; 388 | case CautionLevel::Windows: 389 | MessageBox(hwnd, TEXT("Sorry, deleting core Windows OS files and directories is too dangerous."), TEXT("Caution - Operating System Directories"), MB_OK|MB_ICONSTOP); 390 | return false; 391 | default: 392 | assert(false); 393 | MessageBeep(0xffffffff); 394 | return false; 395 | } 396 | 397 | swprintf_s(message, _countof(message), format, _path, TEXT("\r\n\r\nAre you sure you want to continue?"), _path); 398 | if (MessageBox(hwnd, message, title, MB_YESNOCANCEL|MB_ICONWARNING) != IDYES) 399 | return false; 400 | } 401 | 402 | #ifdef DEBUG 403 | if (MessageBox(hwnd, TEXT("FIRST EXTRA CONFIRMATION IN DEBUG BUILDS!"), TEXT("Caution - First Chance"), MB_YESNOCANCEL|MB_ICONWARNING) != IDYES) 404 | return false; 405 | if (MessageBox(hwnd, TEXT("LAST EXTRA CONFIRMATION IN DEBUG BUILDS!"), TEXT("Caution - Last Chance"), MB_YESNOCANCEL|MB_ICONWARNING) != IDYES) 406 | return false; 407 | #endif 408 | 409 | const size_t len = wcslen(_path); 410 | // NOTE: calloc zero-fills, satisfying the double nul termination required 411 | // by SHFileOperation. 412 | WCHAR* pathzz = (WCHAR*)calloc(len + 2, sizeof(*pathzz)); 413 | memcpy(pathzz, _path, len * sizeof(*pathzz)); 414 | 415 | SHFILEOPSTRUCT op = { 0 }; 416 | op.hwnd = hwnd; 417 | op.wFunc = FO_DELETE; 418 | op.pFrom = pathzz; 419 | op.fFlags = FOF_NO_CONNECTED_ELEMENTS|FOF_SIMPLEPROGRESS|FOF_WANTNUKEWARNING; 420 | op.lpszProgressTitle = permanent ? TEXT("Deleting") : TEXT("Recycling"); 421 | 422 | if (!permanent) 423 | op.fFlags |= FOF_ALLOWUNDO; 424 | 425 | const int nError = SHFileOperation(&op); 426 | if (nError) 427 | return false; 428 | 429 | return true; 430 | } 431 | 432 | -------------------------------------------------------------------------------- /actions.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | class SH_SHFree { protected: void Free(LPITEMIDLIST pidl) { SHFree(pidl); } }; 9 | typedef SH SPIDL; 10 | 11 | void ShellOpen(HWND hwnd, const WCHAR* path); 12 | void ShellOpenRecycleBin(HWND hwnd); 13 | bool ShellRecycle(HWND hwnd, const WCHAR* path); 14 | bool ShellDelete(HWND hwnd, const WCHAR* path); 15 | bool ShellEmptyRecycleBin(HWND hwnd, const WCHAR* path); 16 | bool ShellBrowseForFolder(HWND hwnd, const WCHAR* title, std::wstring& inout); 17 | 18 | -------------------------------------------------------------------------------- /data.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include "data.h" 6 | #include 7 | #include 8 | 9 | #ifdef DEBUG 10 | static thread_local bool s_make_fake = false; 11 | 12 | bool SetFake(bool fake) 13 | { 14 | const bool was = s_make_fake; 15 | s_make_fake = fake; 16 | return was; 17 | } 18 | #endif 19 | 20 | void ensure_separator(std::wstring& path) 21 | { 22 | if (path.length()) 23 | { 24 | const WCHAR ch = path.c_str()[path.length() - 1]; 25 | if (ch != '/' && ch != '\\') 26 | path.append(TEXT("\\")); 27 | } 28 | } 29 | 30 | void strip_separator(std::wstring& path) 31 | { 32 | while (path.length() && is_separator(path.c_str()[path.length() - 1])) 33 | path.resize(path.length() - 1); 34 | } 35 | 36 | void skip_separators(const WCHAR*& path) 37 | { 38 | while (is_separator(*path)) 39 | ++path; 40 | } 41 | 42 | void skip_nonseparators(const WCHAR*& path) 43 | { 44 | while (*path && !is_separator(*path)) 45 | ++path; 46 | } 47 | 48 | static void build_full_path(std::wstring& path, const std::shared_ptr& node) 49 | { 50 | if (!node) 51 | { 52 | path.clear(); 53 | } 54 | else if (node->AsFreeSpace()) 55 | { 56 | path = node->GetName(); 57 | } 58 | else 59 | { 60 | const DirNode* dir = node->AsDir(); 61 | if (dir && dir->IsRecycleBin()) 62 | { 63 | const auto parent = dir->GetParent(); 64 | path = dir->GetName(); 65 | if (parent) 66 | { 67 | path.append(TEXT(" on ")); 68 | path.append(parent->GetName()); 69 | strip_separator(path); 70 | } 71 | } 72 | else 73 | { 74 | build_full_path(path, node->GetParent()); 75 | path.append(node->GetName()); 76 | if (dir) 77 | ensure_separator(path); 78 | } 79 | } 80 | } 81 | 82 | bool is_root_finished(const std::shared_ptr& node) 83 | { 84 | DirNode* dir = node->AsDir(); 85 | std::shared_ptr parent; 86 | for (parent = dir ? std::static_pointer_cast(node) : node->GetParent(); parent; parent = parent->GetParent()) 87 | { 88 | if (!parent->IsFinished()) 89 | return false; 90 | } 91 | return true; 92 | } 93 | 94 | bool is_drive(const WCHAR* path) 95 | { 96 | // FUTURE: Recognize UNC shares and on \\?\X: drives. But that might not 97 | // be sufficient to support FreeSpace and RecycleBin for those. 98 | return (path[0] && path[1] == ':' && (!path[2] || 99 | (is_separator(path[2]) && !path[3]))); 100 | } 101 | 102 | bool is_subst(const WCHAR* path) 103 | { 104 | std::wstring device = path; 105 | strip_separator(device); 106 | 107 | WCHAR szTargetPath[1024]; 108 | if (QueryDosDevice(device.c_str(), szTargetPath, _countof(szTargetPath))) 109 | return (wcsnicmp(szTargetPath, TEXT("\\??\\"), 4) == 0); 110 | 111 | return false; 112 | } 113 | 114 | unsigned int has_io_prefix(const WCHAR* path) 115 | { 116 | const WCHAR* p = path; 117 | if (!is_separator(*(p++))) 118 | return 0; 119 | if (!is_separator(*(p++))) 120 | return 0; 121 | if (*(p++) != '?') 122 | return 0; 123 | if (!is_separator(*(p++))) 124 | return 0; 125 | skip_separators(p); 126 | return static_cast(p - path); 127 | } 128 | 129 | bool is_unc(const WCHAR* path, const WCHAR** past_unc) 130 | { 131 | if (!is_separator(path[0]) || !is_separator(path[1])) 132 | return false; 133 | 134 | const WCHAR* const in = path; 135 | skip_separators(path); 136 | size_t leading = static_cast(path - in); 137 | 138 | // Check for device namespace. 139 | if (path[0] == '.' && (!path[1] || is_separator(path[1]))) 140 | return false; 141 | 142 | // Check for \\?\UNC\ namespace. 143 | if (leading == 2 && path[0] == '?' && is_separator(path[1])) 144 | { 145 | ++path; 146 | skip_separators(path); 147 | 148 | if (*path != 'U' && *path != 'u') 149 | return false; 150 | ++path; 151 | if (*path != 'N' && *path != 'n') 152 | return false; 153 | ++path; 154 | if (*path != 'C' && *path != 'c') 155 | return false; 156 | ++path; 157 | 158 | if (!is_separator(*path)) 159 | return false; 160 | skip_separators(path); 161 | } 162 | 163 | if (past_unc) 164 | { 165 | // Skip server name. 166 | skip_nonseparators(path); 167 | while (*path && !is_separator(*path)) 168 | ++path; 169 | 170 | // Skip separator. 171 | skip_separators(path); 172 | 173 | // Skip share name. 174 | skip_nonseparators(path); 175 | 176 | *past_unc = path; 177 | } 178 | return true; 179 | } 180 | 181 | bool get_drivelike_prefix(const WCHAR* _path, std::wstring& out) 182 | { 183 | const WCHAR* path = _path; 184 | 185 | const WCHAR* past_unc; 186 | if (is_unc(path, &past_unc)) 187 | { 188 | if (is_separator(*past_unc)) 189 | ++past_unc; 190 | out.clear(); 191 | out.append(path, past_unc - path); 192 | return true; 193 | } 194 | 195 | unsigned int iop = has_io_prefix(path); 196 | if (iop > 0) 197 | path += iop; 198 | 199 | if (path[0] && path[1] == ':' && unsigned(towlower(path[0]) - 'a') <= ('z' - 'a')) 200 | { 201 | out.clear(); 202 | out.append(_path, iop); 203 | out.append(path, 2 + is_separator(path[2])); 204 | return true; 205 | } 206 | 207 | return false; 208 | } 209 | 210 | static std::wstring make_free_space_name(const WCHAR* drive) 211 | { 212 | std::wstring name; 213 | name.append(TEXT("Free on ")); 214 | name.append(drive); 215 | return name; 216 | } 217 | 218 | #ifdef DEBUG 219 | static volatile LONG s_cNodes = 0; 220 | LONG CountNodes() { return s_cNodes; } 221 | #endif 222 | 223 | Node::Node(const WCHAR* name, const std::shared_ptr& parent) 224 | : m_name(name) 225 | , m_parent(parent) 226 | #ifdef DEBUG 227 | , m_fake(s_make_fake) 228 | #endif 229 | { 230 | #ifdef DEBUG 231 | InterlockedIncrement(&s_cNodes); 232 | #endif 233 | } 234 | 235 | Node::~Node() 236 | { 237 | #ifdef DEBUG 238 | InterlockedDecrement(&s_cNodes); 239 | #endif 240 | } 241 | 242 | bool Node::IsParentFinished() const 243 | { 244 | std::shared_ptr parent = GetParent(); 245 | return parent && parent->IsFinished(); 246 | } 247 | 248 | void Node::GetFullPath(std::wstring& out) const 249 | { 250 | std::shared_ptr self = shared_from_this(); 251 | build_full_path(out, self); 252 | } 253 | 254 | std::vector> DirNode::CopyDirs(bool include_recycle) const 255 | { 256 | std::lock_guard lock(m_node_mutex); 257 | 258 | std::vector> dirs = m_dirs; 259 | if (include_recycle && GetRecycleBin()) 260 | dirs.emplace_back(GetRecycleBin()); 261 | return dirs; 262 | } 263 | 264 | std::vector> DirNode::CopyFiles() const 265 | { 266 | std::lock_guard lock(m_node_mutex); 267 | 268 | return m_files; 269 | } 270 | 271 | ULONGLONG DirNode::GetEffectiveSize() const 272 | { 273 | if (!GetFreeSpace()) 274 | return GetSize(); 275 | else 276 | return std::max(GetFreeSpace()->GetUsedSize(), GetSize()); 277 | } 278 | 279 | std::shared_ptr DirNode::AddDir(const WCHAR* name) 280 | { 281 | std::shared_ptr parent(std::static_pointer_cast(shared_from_this())); 282 | std::shared_ptr dir = std::make_shared(name, parent); 283 | 284 | { 285 | std::lock_guard lock(m_node_mutex); 286 | 287 | m_dirs.emplace_back(dir); 288 | 289 | m_count_dirs++; 290 | 291 | std::shared_ptr parent(m_parent.lock()); 292 | while (parent) 293 | { 294 | parent->m_count_dirs++; 295 | parent = parent->m_parent.lock(); 296 | } 297 | } 298 | 299 | return dir; 300 | } 301 | 302 | std::shared_ptr DirNode::AddFile(const WCHAR* name, ULONGLONG size) 303 | { 304 | std::shared_ptr parent(std::static_pointer_cast(shared_from_this())); 305 | std::shared_ptr file = std::make_shared(name, size, parent); 306 | 307 | { 308 | std::lock_guard lock(m_node_mutex); 309 | 310 | m_files.emplace_back(file); 311 | 312 | m_size += size; 313 | m_count_files++; 314 | 315 | std::shared_ptr parent(m_parent.lock()); 316 | while (parent) 317 | { 318 | parent->m_size += size; 319 | parent->m_count_files++; 320 | parent = parent->m_parent.lock(); 321 | } 322 | } 323 | 324 | return file; 325 | } 326 | 327 | void DirNode::DeleteChild(const std::shared_ptr& node) 328 | { 329 | std::lock_guard lock(m_node_mutex); 330 | 331 | if (node->AsDir()) 332 | { 333 | DirNode* dir = node->AsDir(); 334 | if (dir->IsRecycleBin()) 335 | { 336 | assert(false); 337 | return; 338 | } 339 | 340 | for (auto iter = m_dirs.begin(); iter != m_dirs.end(); ++iter) 341 | { 342 | if (iter->get() == dir) 343 | { 344 | std::shared_ptr parent(std::static_pointer_cast(shared_from_this())); 345 | while (parent) 346 | { 347 | parent->m_size -= dir->GetSize(); 348 | parent->m_count_dirs -= dir->CountDirs(); 349 | parent->m_count_files -= dir->CountFiles(); 350 | parent = parent->m_parent.lock(); 351 | } 352 | 353 | m_dirs.erase(iter); 354 | return; 355 | } 356 | } 357 | } 358 | else 359 | { 360 | FileNode* file = node->AsFile(); 361 | for (auto iter = m_files.begin(); iter != m_files.end(); ++iter) 362 | { 363 | if (iter->get() == file) 364 | { 365 | std::shared_ptr parent(std::static_pointer_cast(shared_from_this())); 366 | while (parent) 367 | { 368 | parent->m_size -= file->GetSize(); 369 | parent->m_count_files--; 370 | parent = parent->m_parent.lock(); 371 | } 372 | 373 | m_files.erase(iter); 374 | return; 375 | } 376 | } 377 | } 378 | } 379 | 380 | void DirNode::Clear() 381 | { 382 | std::lock_guard lock(m_node_mutex); 383 | 384 | #ifdef DEBUG 385 | if (IsFake()) 386 | { 387 | assert(false); 388 | return; 389 | } 390 | #endif 391 | 392 | std::shared_ptr parent(GetParent()); 393 | while (parent) 394 | { 395 | parent->m_size -= GetSize(); 396 | parent->m_count_dirs -= CountDirs(); 397 | parent->m_count_files -= CountFiles(); 398 | 399 | std::shared_ptr up = parent->m_parent.lock(); 400 | if (!up) 401 | parent->m_finished = false; 402 | parent = up; 403 | } 404 | 405 | m_dirs.clear(); 406 | m_files.clear(); 407 | m_count_dirs = 0; 408 | m_count_files = 0; 409 | m_size = 0; 410 | 411 | if (AsDrive()) 412 | AsDrive()->AddFreeSpace(); 413 | 414 | m_finished = false; 415 | } 416 | 417 | void DirNode::UpdateRecycleBinMetadata(ULONGLONG size) 418 | { 419 | assert(!IsFake()); 420 | 421 | GetParent()->m_size -= m_size; 422 | m_size = size; 423 | GetParent()->m_size += m_size; 424 | } 425 | 426 | void RecycleBinNode::UpdateRecycleBin(std::recursive_mutex& ui_mutex) 427 | { 428 | assert(!IsFake()); 429 | 430 | const WCHAR* drive = GetParent()->GetName(); 431 | ULONGLONG size = 0; 432 | 433 | SHQUERYRBINFO info = { sizeof(info) }; 434 | if (SUCCEEDED(SHQueryRecycleBin(drive, &info))) 435 | size = info.i64Size; 436 | 437 | std::lock_guard lock(ui_mutex); 438 | UpdateRecycleBinMetadata(size); 439 | } 440 | 441 | FreeSpaceNode::FreeSpaceNode(const WCHAR* drive, ULONGLONG free, ULONGLONG total, const std::shared_ptr& parent) 442 | : Node(make_free_space_name(drive).c_str(), parent) 443 | , m_free(free) 444 | , m_total(total) 445 | { 446 | } 447 | 448 | void DriveNode::AddRecycleBin() 449 | { 450 | assert(!IsFake()); 451 | 452 | // Skip SUBST drives. 453 | if (is_subst(GetName())) 454 | return; 455 | 456 | const std::shared_ptr parent = std::static_pointer_cast(shared_from_this()); 457 | { 458 | std::lock_guard lock(m_node_mutex); 459 | 460 | m_recycle = std::make_shared(parent); 461 | } 462 | } 463 | 464 | void DriveNode::AddFreeSpace() 465 | { 466 | assert(!IsFake()); 467 | 468 | // Skip SUBST drives. 469 | if (is_subst(GetName())) 470 | return; 471 | 472 | DWORD sectors_per_cluster; 473 | DWORD bytes_per_sector; 474 | DWORD free_clusters; 475 | DWORD total_clusters; 476 | if (GetDiskFreeSpace(GetName(), §ors_per_cluster, &bytes_per_sector, &free_clusters, &total_clusters)) 477 | { 478 | const ULONGLONG bytes_per_cluster = sectors_per_cluster * bytes_per_sector; 479 | const ULONGLONG free = ULONGLONG(free_clusters) * bytes_per_cluster; 480 | const ULONGLONG total = ULONGLONG(total_clusters) * bytes_per_cluster; 481 | 482 | AddFreeSpace(free, total); 483 | } 484 | } 485 | 486 | void DriveNode::AddFreeSpace(ULONGLONG free, ULONGLONG total) 487 | { 488 | const std::shared_ptr parent(std::static_pointer_cast(shared_from_this())); 489 | { 490 | std::lock_guard lock(m_node_mutex); 491 | 492 | m_free = std::make_shared(GetName(), free, total, parent); 493 | } 494 | } 495 | 496 | -------------------------------------------------------------------------------- /data.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | // Node represents a directory or file. 5 | // 6 | // DirNode contains other DirNode and FileNode instances. 7 | // Querying and adding children are threadsafe operations. 8 | // 9 | // FileNode contains info about the file. 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #undef GetFreeSpace 18 | 19 | class DirNode; 20 | class FileNode; 21 | class RecycleBinNode; 22 | class FreeSpaceNode; 23 | class DriveNode; 24 | 25 | #ifdef DEBUG 26 | LONG CountNodes(); 27 | bool SetFake(bool fake); 28 | #endif 29 | 30 | class Node : public std::enable_shared_from_this 31 | { 32 | public: 33 | Node(const WCHAR* name, const std::shared_ptr& parent); 34 | virtual ~Node(); 35 | std::shared_ptr GetParent() const { return m_parent.lock(); } 36 | bool IsParentFinished() const; 37 | const WCHAR* GetName() const { return m_name.c_str(); } 38 | void GetFullPath(std::wstring& out) const; 39 | virtual DirNode* AsDir() { return nullptr; } 40 | virtual const DirNode* AsDir() const { return nullptr; } 41 | virtual FileNode* AsFile() { return nullptr; } 42 | virtual const FileNode* AsFile() const { return nullptr; } 43 | virtual RecycleBinNode* AsRecycleBin() { return nullptr; } 44 | virtual const RecycleBinNode* AsRecycleBin() const { return nullptr; } 45 | virtual const FreeSpaceNode* AsFreeSpace() const { return nullptr; } 46 | virtual DriveNode* AsDrive() { return nullptr; } 47 | virtual const DriveNode* AsDrive() const { return nullptr; } 48 | void SetCompressed(bool compressed=true) { m_compressed = compressed; } 49 | bool IsCompressed() const { return m_compressed; } 50 | void SetSparse(bool sparse=true) { m_sparse = sparse; } 51 | bool IsSparse() const { return m_sparse; } 52 | virtual bool IsRecycleBin() const { return false; } 53 | virtual bool IsDrive() const { return false; } 54 | #ifdef DEBUG 55 | bool IsFake() const { return m_fake; } 56 | #endif 57 | protected: 58 | const std::weak_ptr m_parent; 59 | const std::wstring m_name; 60 | bool m_compressed = false; 61 | bool m_sparse = false; 62 | #ifdef DEBUG 63 | const bool m_fake = false; 64 | #endif 65 | }; 66 | 67 | class DirNode : public Node 68 | { 69 | public: 70 | DirNode(const WCHAR* name) : Node(name, nullptr) {} 71 | DirNode(const WCHAR* name, const std::shared_ptr& parent) : Node(name, parent) {} 72 | DirNode* AsDir() override { return this; } 73 | const DirNode* AsDir() const override { return this; } 74 | ULONGLONG CountDirs(bool include_recycle=false) const { return m_count_dirs + (include_recycle && GetRecycleBin()); } 75 | ULONGLONG CountFiles() const { return m_count_files; } 76 | std::vector> CopyDirs(bool include_recycle=false) const; 77 | std::vector> CopyFiles() const; 78 | virtual std::shared_ptr GetRecycleBin() const { return nullptr; } 79 | virtual std::shared_ptr GetFreeSpace() const { return nullptr; } 80 | ULONGLONG GetSize() const { return m_size; } 81 | ULONGLONG GetEffectiveSize() const; 82 | void Hide(bool hide=true) { m_hide = hide; } 83 | bool IsHidden() const { return m_hide; } 84 | std::shared_ptr AddDir(const WCHAR* name); 85 | std::shared_ptr AddFile(const WCHAR* name, ULONGLONG size); 86 | void DeleteChild(const std::shared_ptr& node); 87 | void Clear(); 88 | void Finish() { m_finished = true; } 89 | bool IsFinished() const { return m_finished; } 90 | protected: 91 | void UpdateRecycleBinMetadata(ULONGLONG size); 92 | mutable std::recursive_mutex m_node_mutex; 93 | private: 94 | std::vector> m_dirs; 95 | std::vector> m_files; 96 | ULONGLONG m_count_dirs = 0; 97 | ULONGLONG m_count_files = 0; 98 | ULONGLONG m_size = 0; 99 | bool m_finished = false; 100 | bool m_hide = false; 101 | }; 102 | 103 | class FileNode : public Node 104 | { 105 | public: 106 | FileNode(const WCHAR* name, ULONGLONG size, const std::shared_ptr& parent) : Node(name, parent), m_size(size) {} 107 | FileNode* AsFile() override { return this; } 108 | const FileNode* AsFile() const override { return this; } 109 | ULONGLONG GetSize() const { return m_size; } 110 | private: 111 | const ULONGLONG m_size; 112 | }; 113 | 114 | class RecycleBinNode : public DirNode 115 | { 116 | public: 117 | RecycleBinNode(const std::shared_ptr& parent) : DirNode(TEXT("Recycle Bin"), parent) {} 118 | virtual RecycleBinNode* AsRecycleBin() { return this; } 119 | void UpdateRecycleBin(std::recursive_mutex& ui_mutex); 120 | bool IsRecycleBin() const override { return true; } 121 | }; 122 | 123 | class FreeSpaceNode : public Node 124 | { 125 | public: 126 | FreeSpaceNode(const WCHAR* drive, ULONGLONG free, ULONGLONG total, const std::shared_ptr& parent); 127 | const FreeSpaceNode* AsFreeSpace() const override { return this; } 128 | ULONGLONG GetFreeSize() const { return m_free; } 129 | ULONGLONG GetUsedSize() const { return m_total - m_free; } 130 | ULONGLONG GetTotalSize() const { return m_total; } 131 | private: 132 | const ULONGLONG m_free; 133 | const ULONGLONG m_total; 134 | }; 135 | 136 | class DriveNode : public DirNode 137 | { 138 | public: 139 | DriveNode(const WCHAR* name) : DirNode(name, nullptr) {} 140 | virtual DriveNode* AsDrive() { return this; } 141 | virtual const DriveNode* AsDrive() const { return this; } 142 | void AddRecycleBin(); 143 | void AddFreeSpace(); 144 | void AddFreeSpace(ULONGLONG free, ULONGLONG total); 145 | virtual std::shared_ptr GetRecycleBin() const override { return m_recycle; } 146 | virtual std::shared_ptr GetFreeSpace() const override { return m_free; } 147 | bool IsDrive() const override { return true; } 148 | private: 149 | std::shared_ptr m_recycle; 150 | std::shared_ptr m_free; 151 | }; 152 | 153 | inline bool is_separator(const WCHAR ch) { return ch == '/' || ch == '\\'; } 154 | void ensure_separator(std::wstring& path); 155 | void strip_separator(std::wstring& path); 156 | void skip_separators(const WCHAR*& path); 157 | void skip_nonseparators(const WCHAR*& path); 158 | unsigned int has_io_prefix(const WCHAR* path); 159 | 160 | bool is_root_finished(const std::shared_ptr& node); 161 | bool is_drive(const WCHAR* path); 162 | bool is_subst(const WCHAR* path); 163 | bool is_unc(const WCHAR* path, const WCHAR** past_unc); 164 | bool get_drivelike_prefix(const WCHAR* path, std::wstring&out); 165 | 166 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisant996/elucidisk/20402912bb06906dcfd01883886ff8560d59fdac/demo.png -------------------------------------------------------------------------------- /dontscan.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include "dontscan.h" 6 | #include "actions.h" 7 | #include "res.h" 8 | #include "version.h" 9 | #include 10 | #include 11 | 12 | class DontScanDlg 13 | { 14 | public: 15 | INT_PTR DoModal(HINSTANCE hinst, UINT idd, HWND hwndParent); 16 | INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam); 17 | 18 | protected: 19 | void ReadDirectories(); 20 | bool WriteDirectories(); 21 | int GetCaret() const; 22 | int GetSelection() const; 23 | void SetSelection(int index, bool select=true); 24 | bool GetItem(int index, std::wstring& item) const; 25 | void InsertItem(const WCHAR* item); 26 | void RemoveItem(int index); 27 | void UpdateButtons() const; 28 | static INT_PTR CALLBACK StaticDlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); 29 | 30 | private: 31 | HINSTANCE m_hinst = 0; 32 | HWND m_hwnd = 0; 33 | HWND m_hwndListView = 0; 34 | std::vector m_orig; 35 | bool m_dirty = false; 36 | }; 37 | 38 | INT_PTR DontScanDlg::DoModal(HINSTANCE hinst, UINT idd, HWND hwndParent) 39 | { 40 | assert(!m_hwnd); 41 | 42 | ThreadDpiAwarenessContext dpiContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); 43 | 44 | m_hinst = hinst; 45 | const INT_PTR nRet = DialogBoxParam(hinst, MAKEINTRESOURCE(idd), hwndParent, StaticDlgProc, LPARAM(this)); 46 | 47 | return nRet; 48 | } 49 | 50 | INT_PTR DontScanDlg::DlgProc(UINT msg, WPARAM wParam, LPARAM lParam) 51 | { 52 | switch (msg) 53 | { 54 | case WM_INITDIALOG: 55 | { 56 | // TODO: Use system theming when available. 57 | RECT rcCtrl; 58 | HWND hwndPlaceholder = GetDlgItem(m_hwnd, IDC_DONTSCAN_LIST); 59 | GetWindowRect(hwndPlaceholder, &rcCtrl); 60 | MapWindowRect(0, m_hwnd, &rcCtrl); 61 | 62 | m_hwndListView = CreateWindow(WC_LISTVIEW, TEXT(""), WS_TABSTOP|WS_BORDER|WS_VISIBLE|WS_CHILD|LVS_SINGLESEL|LVS_SHOWSELALWAYS|LVS_NOSORTHEADER|LVS_REPORT|LVS_SORTASCENDING, 63 | rcCtrl.left, rcCtrl.top, rcCtrl.right - rcCtrl.left, rcCtrl.bottom - rcCtrl.top, m_hwnd, (HMENU)IDC_DONTSCAN_LIST, m_hinst, NULL); 64 | if (!m_hwndListView) 65 | return -1; 66 | 67 | SetWindowPos(m_hwndListView, hwndPlaceholder, 0, 0, 0, 0, SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOSIZE); 68 | DestroyWindow(hwndPlaceholder); 69 | ListView_SetExtendedListViewStyle(m_hwndListView, LVS_EX_FULLROWSELECT|LVS_EX_INFOTIP|LVS_EX_DOUBLEBUFFER); 70 | 71 | GetClientRect(m_hwndListView, &rcCtrl); 72 | 73 | LVCOLUMN lvc = {}; 74 | lvc.mask = LVCF_FMT|LVCF_TEXT|LVCF_WIDTH; 75 | lvc.fmt = LVCFMT_LEFT; 76 | lvc.cx = rcCtrl.right - rcCtrl.left - GetSystemMetrics(SM_CXVSCROLL); 77 | lvc.pszText = TEXT("Directory"); 78 | ListView_InsertColumn(m_hwndListView, 0, &lvc); 79 | 80 | ReadDirectories(); 81 | UpdateButtons(); 82 | } 83 | break; 84 | 85 | case WM_COMMAND: 86 | { 87 | const WORD id = GET_WM_COMMAND_ID(wParam, lParam); 88 | const HWND hwnd = GET_WM_COMMAND_HWND(wParam, lParam); 89 | const WORD code = GET_WM_COMMAND_CMD(wParam, lParam); 90 | switch (id) 91 | { 92 | case IDC_DONTSCAN_ADD: 93 | { 94 | std::wstring add; 95 | if (ShellBrowseForFolder(m_hwnd, TEXT("Add Folder"), add)) 96 | { 97 | InsertItem(add.c_str()); 98 | UpdateButtons(); 99 | } 100 | } 101 | break; 102 | case IDC_DONTSCAN_REMOVE: 103 | { 104 | const int index = GetSelection(); 105 | if (index >= 0) 106 | { 107 | RemoveItem(index); 108 | UpdateButtons(); 109 | } 110 | } 111 | break; 112 | case IDOK: 113 | EndDialog(m_hwnd, WriteDirectories()); 114 | break; 115 | case IDCANCEL: 116 | EndDialog(m_hwnd, false); 117 | break; 118 | default: 119 | return false; 120 | } 121 | } 122 | break; 123 | 124 | case WM_NOTIFY: 125 | { 126 | NMHDR* pnmhdr = (NMHDR*)lParam; 127 | NMLISTVIEW* pnml = reinterpret_cast(pnmhdr); 128 | 129 | if (pnmhdr->idFrom == IDC_DONTSCAN_LIST) 130 | { 131 | switch (pnmhdr->code) 132 | { 133 | case LVN_ITEMCHANGED: 134 | UpdateButtons(); 135 | return true; 136 | } 137 | } 138 | 139 | return false; 140 | } 141 | break; 142 | 143 | default: 144 | return false; 145 | } 146 | 147 | return true; 148 | } 149 | 150 | void DontScanDlg::ReadDirectories() 151 | { 152 | ReadRegStrings(TEXT("DontScanDirectories"), m_orig); 153 | 154 | SendMessage(m_hwndListView, WM_SETREDRAW, false, 0); 155 | 156 | ListView_DeleteAllItems(m_hwndListView); 157 | for (const auto& dir : m_orig) 158 | InsertItem(dir.c_str()); 159 | 160 | SetSelection(0, false); 161 | 162 | SendMessage(m_hwndListView, WM_SETREDRAW, true, 0); 163 | InvalidateRect(m_hwndListView, nullptr, false); 164 | } 165 | 166 | bool DontScanDlg::WriteDirectories() 167 | { 168 | std::vector dirs; 169 | 170 | const int count = ListView_GetItemCount(m_hwndListView); 171 | if (count >= 0) 172 | { 173 | WCHAR dir[1024]; 174 | for (int index = 0; index < count; ++index) 175 | { 176 | ListView_GetItemText(m_hwndListView, index, 0, dir, _countof(dir)); 177 | dirs.emplace_back(dir); 178 | } 179 | } 180 | 181 | bool changed = (m_orig.size() != dirs.size()); 182 | for (size_t index = 0; !changed && index < dirs.size(); ++index) 183 | changed = !!wcscmp(m_orig[index].c_str(), dirs[index].c_str()); 184 | 185 | if (changed) 186 | WriteRegStrings(TEXT("DontScanDirectories"), dirs); 187 | 188 | return changed; 189 | } 190 | 191 | int DontScanDlg::GetCaret() const 192 | { 193 | return ListView_GetNextItem(m_hwndListView, -1, LVIS_FOCUSED); 194 | } 195 | 196 | int DontScanDlg::GetSelection() const 197 | { 198 | return ListView_GetNextItem(m_hwndListView, -1, LVIS_SELECTED); 199 | } 200 | 201 | void DontScanDlg::SetSelection(int index, bool select) 202 | { 203 | // Clear the focused state from whichever item currently has it. This 204 | // works around what seems to be a bug in ListView_SetItemState, where the 205 | // focused state is only cleared from an item that has both LVIS_SELECTED 206 | // and LVIS_FOCUSED. 207 | const int caret = ListView_GetNextItem(m_hwndListView, -1, LVNI_FOCUSED); 208 | if (caret >= 0 ) 209 | ListView_SetItemState(m_hwndListView, caret, 0, LVIS_SELECTED|LVIS_FOCUSED); 210 | const DWORD bits = select ? LVIS_SELECTED|LVIS_FOCUSED : LVIS_FOCUSED; 211 | const DWORD mask = LVIS_SELECTED|LVIS_FOCUSED; 212 | ListView_SetItemState(m_hwndListView, index, bits, mask); 213 | } 214 | 215 | bool DontScanDlg::GetItem(int index, std::wstring& item) const 216 | { 217 | WCHAR text[1024]; 218 | LVITEM lvi = {}; 219 | lvi.mask = LVIF_TEXT; 220 | lvi.iItem = index; 221 | lvi.pszText = text; 222 | lvi.cchTextMax = _countof(text); 223 | if (!ListView_GetItem(m_hwndListView, &lvi)) 224 | return false; 225 | item = text; 226 | return true; 227 | } 228 | 229 | void DontScanDlg::InsertItem(const WCHAR* item) 230 | { 231 | LVITEM lvi = {}; 232 | lvi.mask = LVIF_TEXT; 233 | lvi.pszText = const_cast(item); 234 | const int index = ListView_InsertItem(m_hwndListView, &lvi); 235 | 236 | assert(index >= 0); 237 | if (index >= 0) 238 | { 239 | #ifdef DEBUG 240 | std::wstring verify; 241 | assert(GetItem(index, verify)); 242 | assert(!wcscmp(item, verify.c_str())); 243 | #endif 244 | SetSelection(index); 245 | } 246 | } 247 | 248 | void DontScanDlg::RemoveItem(int index) 249 | { 250 | #ifdef DEBUG 251 | const bool deleted = 252 | #endif 253 | ListView_DeleteItem(m_hwndListView, index); 254 | 255 | #ifdef DEBUG 256 | assert(deleted); 257 | #endif 258 | 259 | const int count = ListView_GetItemCount(m_hwndListView); 260 | if (count > 0) 261 | SetSelection(std::min(index, count - 1)); 262 | } 263 | 264 | static void EnableControl(HWND hwndCtrl, bool enable=true) 265 | { 266 | if (!enable && GetFocus() == hwndCtrl) 267 | SendMessage(hwndCtrl, WM_NEXTDLGCTL, 0, 0); 268 | EnableWindow(hwndCtrl, enable); 269 | } 270 | 271 | void DontScanDlg::UpdateButtons() const 272 | { 273 | EnableControl(GetDlgItem(m_hwnd, IDC_DONTSCAN_REMOVE), GetSelection() >= 0); 274 | } 275 | 276 | INT_PTR DontScanDlg::StaticDlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 277 | { 278 | DontScanDlg* pThis = nullptr; 279 | 280 | if (msg == WM_INITDIALOG) 281 | { 282 | // Set the "this" pointer. 283 | pThis = reinterpret_cast(lParam); 284 | SetWindowLongPtr(hwnd, DWLP_USER, DWORD_PTR(pThis)); 285 | pThis->m_hwnd = hwnd; 286 | } 287 | else 288 | { 289 | pThis = reinterpret_cast(GetWindowLongPtr(hwnd, DWLP_USER)); 290 | } 291 | 292 | if (pThis) 293 | { 294 | assert(pThis->m_hwnd == hwnd); 295 | 296 | if (msg == WM_DESTROY) 297 | { 298 | return true; 299 | } 300 | else if (msg == WM_NCDESTROY) 301 | { 302 | SetWindowLongPtr(hwnd, DWLP_USER, 0); 303 | pThis->m_hwnd = 0; 304 | return true; 305 | } 306 | 307 | const INT_PTR lResult = pThis->DlgProc(msg, wParam, lParam); 308 | 309 | // Must return actual result in order for things like 310 | // WM_CTLCOLORLISTBOX to work. 311 | if (lResult || msg == WM_INITDIALOG) 312 | return lResult; 313 | } 314 | 315 | return false; 316 | } 317 | 318 | bool ConfigureDontScanFiles(HINSTANCE hinst, HWND hwndParent) 319 | { 320 | DontScanDlg dlg; 321 | return dlg.DoModal(hinst, IDD_CONFIG_DONTSCAN, hwndParent); 322 | } 323 | 324 | -------------------------------------------------------------------------------- /dontscan.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | bool ConfigureDontScanFiles(HINSTANCE hinst, HWND hwnd); 7 | 8 | -------------------------------------------------------------------------------- /dpi.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include 6 | #include 7 | 8 | #ifndef ILC_COLORMASK 9 | #define ILC_COLORMASK 0x00FE 10 | #endif 11 | 12 | #ifndef BORDERX_PEN 13 | #define BORDERX_PEN 32 14 | #endif 15 | 16 | WORD __GetHdcDpi(HDC hdc) 17 | { 18 | const WORD dxLogPixels = static_cast(GetDeviceCaps(hdc, LOGPIXELSX)); 19 | #ifdef DEBUG 20 | const WORD dyLogPixels = static_cast(GetDeviceCaps(hdc, LOGPIXELSY)); 21 | assert(dxLogPixels == dyLogPixels); 22 | #endif 23 | return dxLogPixels; 24 | } 25 | 26 | class User32 27 | { 28 | public: 29 | User32(); 30 | 31 | WORD GetDpiForSystem(); 32 | WORD GetDpiForWindow(HWND hwnd); 33 | int GetSystemMetricsForDpi(int nIndex, UINT dpi); 34 | bool SystemParametersInfoForDpi(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT dpi); 35 | bool IsValidDpiAwarenessContext(DPI_AWARENESS_CONTEXT context); 36 | bool AreDpiAwarenessContextsEqual(DPI_AWARENESS_CONTEXT contextA, DPI_AWARENESS_CONTEXT contextB); 37 | DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context); 38 | DPI_AWARENESS_CONTEXT GetWindowDpiAwarenessContext(HWND hwnd); 39 | bool EnableNonClientDpiScaling(HWND hwnd); 40 | bool EnablePerMonitorMenuScaling(); 41 | 42 | private: 43 | bool Initialize(); 44 | 45 | private: 46 | DWORD m_dwErr = 0; 47 | HMODULE m_hLib = 0; 48 | bool m_fInitialized = false; 49 | union 50 | { 51 | FARPROC proc[10]; 52 | struct 53 | { 54 | UINT (WINAPI* GetDpiForSystem)(); 55 | UINT (WINAPI* GetDpiForWindow)(HWND hwnd); 56 | int (WINAPI* GetSystemMetricsForDpi)(int nIndex, UINT dpi); 57 | BOOL (WINAPI* SystemParametersInfoForDpi)(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT dpi); 58 | BOOL (WINAPI* IsValidDpiAwarenessContext)(DPI_AWARENESS_CONTEXT context); 59 | BOOL (WINAPI* AreDpiAwarenessContextsEqual)(DPI_AWARENESS_CONTEXT contextA, DPI_AWARENESS_CONTEXT contextB); 60 | DPI_AWARENESS_CONTEXT (WINAPI* SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT context); 61 | DPI_AWARENESS_CONTEXT (WINAPI* GetWindowDpiAwarenessContext)(HWND hwnd); 62 | BOOL (WINAPI* EnableNonClientDpiScaling)(HWND hwnd); 63 | BOOL (WINAPI* EnablePerMonitorMenuScaling)(); 64 | }; 65 | } m_user32; 66 | }; 67 | 68 | User32 g_user32; 69 | 70 | User32::User32() 71 | { 72 | ZeroMemory(&m_user32, sizeof(m_user32)); 73 | 74 | Initialize(); 75 | 76 | static_assert(_countof(m_user32.proc) == sizeof(m_user32) / sizeof(FARPROC), "mismatched FARPROC struct"); 77 | } 78 | 79 | bool User32::Initialize() 80 | { 81 | if (!m_fInitialized) 82 | { 83 | m_fInitialized = true; 84 | m_hLib = LoadLibrary(TEXT("user32.dll")); 85 | if (!m_hLib) 86 | { 87 | m_dwErr = GetLastError(); 88 | } 89 | else 90 | { 91 | size_t cProcs = 0; 92 | 93 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "GetDpiForSystem"))) 94 | m_dwErr = GetLastError(); 95 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "GetDpiForWindow"))) 96 | m_dwErr = GetLastError(); 97 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "GetSystemMetricsForDpi"))) 98 | m_dwErr = GetLastError(); 99 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "SystemParametersInfoForDpi"))) 100 | m_dwErr = GetLastError(); 101 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "IsValidDpiAwarenessContext"))) 102 | m_dwErr = GetLastError(); 103 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "AreDpiAwarenessContextsEqual"))) 104 | m_dwErr = GetLastError(); 105 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "SetThreadDpiAwarenessContext"))) 106 | m_dwErr = GetLastError(); 107 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "GetWindowDpiAwarenessContext"))) 108 | m_dwErr = GetLastError(); 109 | if (!(m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "EnableNonClientDpiScaling"))) 110 | m_dwErr = GetLastError(); 111 | m_user32.proc[cProcs++] = GetProcAddress(m_hLib, "EnablePerMonitorMenuScaling"); // Optional: not an error if it's missing. 112 | assert(_countof(m_user32.proc) == cProcs); 113 | } 114 | } 115 | 116 | return !m_dwErr; 117 | } 118 | 119 | WORD User32::GetDpiForSystem() 120 | { 121 | if (m_user32.GetDpiForSystem) 122 | return m_user32.GetDpiForSystem(); 123 | 124 | const HDC hdc = GetDC(0); 125 | const WORD dpi = __GetHdcDpi(hdc); 126 | ReleaseDC(0, hdc); 127 | return dpi; 128 | } 129 | 130 | WORD User32::GetDpiForWindow(HWND hwnd) 131 | { 132 | if (m_user32.GetDpiForWindow) 133 | return m_user32.GetDpiForWindow(hwnd); 134 | 135 | const HDC hdc = GetDC(hwnd); 136 | const WORD dpi = __GetHdcDpi(hdc); 137 | ReleaseDC(hwnd, hdc); 138 | return dpi; 139 | } 140 | 141 | int User32::GetSystemMetricsForDpi(int nIndex, UINT dpi) 142 | { 143 | if (m_user32.GetSystemMetricsForDpi) 144 | { 145 | // Scale these ourselves because the OS doesn't seem to return them scaled. ?! 146 | if (nIndex == SM_CXFOCUSBORDER || nIndex == SM_CYFOCUSBORDER) 147 | return HIDPIMulDiv(GetSystemMetrics(nIndex), dpi, 96); 148 | 149 | return m_user32.GetSystemMetricsForDpi(nIndex, dpi); 150 | } 151 | 152 | return GetSystemMetrics(nIndex); 153 | } 154 | 155 | bool User32::SystemParametersInfoForDpi(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT _dpi) 156 | { 157 | DpiScaler dpi(static_cast(_dpi)); 158 | DpiScaler dpiSystem(__GetDpiForSystem()); 159 | 160 | switch (uiAction) 161 | { 162 | case SPI_GETICONTITLELOGFONT: 163 | if (SystemParametersInfo(uiAction, uiParam, pvParam, fWinIni)) 164 | { 165 | const LPLOGFONT plf = LPLOGFONT(pvParam); 166 | plf->lfHeight = dpi.ScaleFrom(plf->lfHeight, dpiSystem); 167 | return true; 168 | } 169 | break; 170 | 171 | case SPI_GETICONMETRICS: 172 | if (SystemParametersInfo(uiAction, uiParam, pvParam, fWinIni)) 173 | { 174 | const LPICONMETRICS pim = LPICONMETRICS(pvParam); 175 | pim->lfFont.lfHeight = dpi.ScaleFrom(pim->lfFont.lfHeight, dpiSystem); 176 | return true; 177 | } 178 | break; 179 | 180 | case SPI_GETNONCLIENTMETRICS: 181 | if (SystemParametersInfo(uiAction, uiParam, pvParam, fWinIni)) 182 | { 183 | const LPNONCLIENTMETRICS pncm = LPNONCLIENTMETRICS(pvParam); 184 | pncm->lfCaptionFont.lfHeight = dpi.ScaleFrom(pncm->lfCaptionFont.lfHeight, dpiSystem); 185 | pncm->lfMenuFont.lfHeight = dpi.ScaleFrom(pncm->lfMenuFont.lfHeight, dpiSystem); 186 | pncm->lfMessageFont.lfHeight = dpi.ScaleFrom(pncm->lfMessageFont.lfHeight, dpiSystem); 187 | pncm->lfSmCaptionFont.lfHeight = dpi.ScaleFrom(pncm->lfSmCaptionFont.lfHeight, dpiSystem); 188 | pncm->lfStatusFont.lfHeight = dpi.ScaleFrom(pncm->lfStatusFont.lfHeight, dpiSystem); 189 | return true; 190 | } 191 | break; 192 | } 193 | 194 | return false; 195 | } 196 | 197 | bool User32::IsValidDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 198 | { 199 | if (m_user32.IsValidDpiAwarenessContext) 200 | return m_user32.IsValidDpiAwarenessContext(context); 201 | 202 | return false; 203 | } 204 | 205 | bool User32::AreDpiAwarenessContextsEqual(DPI_AWARENESS_CONTEXT contextA, DPI_AWARENESS_CONTEXT contextB) 206 | { 207 | if (m_user32.AreDpiAwarenessContextsEqual) 208 | return m_user32.AreDpiAwarenessContextsEqual(contextA, contextB); 209 | 210 | return (contextA == contextB); 211 | } 212 | 213 | DPI_AWARENESS_CONTEXT User32::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 214 | { 215 | if (m_user32.SetThreadDpiAwarenessContext) 216 | return m_user32.SetThreadDpiAwarenessContext(context); 217 | 218 | return DPI_AWARENESS_CONTEXT_UNAWARE; 219 | } 220 | 221 | DPI_AWARENESS_CONTEXT User32::GetWindowDpiAwarenessContext(HWND hwnd) 222 | { 223 | if (m_user32.GetWindowDpiAwarenessContext) 224 | return m_user32.GetWindowDpiAwarenessContext(hwnd); 225 | 226 | return DPI_AWARENESS_CONTEXT_UNAWARE; 227 | } 228 | 229 | bool User32::EnableNonClientDpiScaling(HWND hwnd) 230 | { 231 | if (m_user32.EnableNonClientDpiScaling) 232 | return m_user32.EnableNonClientDpiScaling(hwnd); 233 | 234 | return true; 235 | } 236 | 237 | bool User32::EnablePerMonitorMenuScaling() 238 | { 239 | if (m_user32.EnablePerMonitorMenuScaling) 240 | return m_user32.EnablePerMonitorMenuScaling(); 241 | 242 | return false; 243 | } 244 | 245 | WORD __GetDpiForSystem() 246 | { 247 | return WORD(g_user32.GetDpiForSystem()); 248 | } 249 | 250 | WORD __GetDpiForWindow(HWND hwnd) 251 | { 252 | return WORD(g_user32.GetDpiForWindow(hwnd)); 253 | } 254 | 255 | int __GetSystemMetricsForDpi(int nIndex, UINT dpi) 256 | { 257 | return g_user32.GetSystemMetricsForDpi(nIndex, dpi); 258 | } 259 | 260 | bool __SystemParametersInfoForDpi(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT dpi) 261 | { 262 | return g_user32.SystemParametersInfoForDpi(uiAction, uiParam, pvParam, fWinIni, dpi); 263 | } 264 | 265 | bool __IsValidDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 266 | { 267 | return g_user32.IsValidDpiAwarenessContext(context); 268 | } 269 | 270 | bool __AreDpiAwarenessContextsEqual(DPI_AWARENESS_CONTEXT contextA, DPI_AWARENESS_CONTEXT contextB) 271 | { 272 | return g_user32.AreDpiAwarenessContextsEqual(contextA, contextB); 273 | } 274 | 275 | DPI_AWARENESS_CONTEXT __SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 276 | { 277 | return g_user32.SetThreadDpiAwarenessContext(context); 278 | } 279 | 280 | DPI_AWARENESS_CONTEXT __GetWindowDpiAwarenessContext(HWND hwnd) 281 | { 282 | return g_user32.GetWindowDpiAwarenessContext(hwnd); 283 | } 284 | 285 | bool __EnableNonClientDpiScaling(HWND hwnd) 286 | { 287 | return g_user32.EnableNonClientDpiScaling(hwnd); 288 | } 289 | 290 | bool __EnablePerMonitorMenuScaling() 291 | { 292 | return g_user32.EnablePerMonitorMenuScaling(); 293 | } 294 | 295 | bool __IsHwndPerMonitorAware(HWND hwnd) 296 | { 297 | const DPI_AWARENESS_CONTEXT context = __GetWindowDpiAwarenessContext(hwnd); 298 | 299 | return (__AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE) || 300 | __AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)); 301 | } 302 | 303 | ThreadDpiAwarenessContext::ThreadDpiAwarenessContext(const bool fUsePerMonitorAwareness) 304 | { 305 | DPI_AWARENESS_CONTEXT const context = fUsePerMonitorAwareness ? DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE : DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; 306 | 307 | m_fRestore = true; 308 | m_context = __SetThreadDpiAwarenessContext(context); 309 | } 310 | 311 | ThreadDpiAwarenessContext::ThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context) 312 | { 313 | if (context == DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 && !__IsValidDpiAwarenessContext(context)) 314 | context = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE; 315 | 316 | m_fRestore = true; 317 | m_context = __SetThreadDpiAwarenessContext(context); 318 | } 319 | 320 | void ThreadDpiAwarenessContext::Restore() 321 | { 322 | if (m_fRestore) 323 | { 324 | __SetThreadDpiAwarenessContext(m_context); 325 | m_fRestore = false; 326 | } 327 | } 328 | 329 | // HIDPISIGN and HIDPIABS ensure correct rounding for negative numbers passed 330 | // into HIDPIMulDiv as x (round -1.5 to -1, 2.5 to round to 2, etc). This is 331 | // done by taking the absolute value of x and multiplying the result by the 332 | // sign of x. Y and z should never be negative, as y is the dpi of the 333 | // device, and z is always 96 (100%). 334 | inline int HIDPISIGN(int x) 335 | { 336 | return (x < 0) ? -1 : 1; 337 | } 338 | inline int HIDPIABS(int x) 339 | { 340 | return (x < 0) ? -x : x; 341 | } 342 | 343 | int HIDPIMulDiv(int x, int y, int z) 344 | { 345 | assert(y); 346 | assert(z); 347 | // >>1 rounds up at 0.5, >>2 rounds up at 0.75, >>3 rounds up at 0.875 348 | //return (((HIDPIABS(x) * y) + (z >> 1)) / z) * HIDPISIGN(x); 349 | //return (((HIDPIABS(x) * y) + (z >> 2)) / z) * HIDPISIGN(x); 350 | return (((HIDPIABS(x) * y) + (z >> 3)) / z) * HIDPISIGN(x); 351 | } 352 | 353 | static float s_textScaleFactor = 0.0; 354 | 355 | static float GetTextScaleFactor() 356 | { 357 | if (s_textScaleFactor == 0.0) 358 | { 359 | s_textScaleFactor = 1.0; 360 | 361 | // I can't find any API that exposes the Text Scale Factor. 362 | // It's stored in the registry as the factor times 100 here: 363 | // Software\\Microsoft\\Accessibility\\TextScaleFactor 364 | SHKEY shkey; 365 | if (RegOpenKeyEx(HKEY_CURRENT_USER, TEXT("Software\\Microsoft\\Accessibility"), 0, KEY_READ, &shkey) == ERROR_SUCCESS) 366 | { 367 | DWORD dwType; 368 | DWORD dwValue; 369 | DWORD dwSize = sizeof(dwValue); 370 | if (RegQueryValueEx(shkey, TEXT("TextScaleFactor"), 0, &dwType, LPBYTE(&dwValue), &dwSize) == ERROR_SUCCESS && 371 | REG_DWORD == dwType && 372 | sizeof(dwValue) == dwSize) 373 | { 374 | s_textScaleFactor = float(dwValue) / 100; 375 | } 376 | } 377 | } 378 | 379 | return s_textScaleFactor; 380 | } 381 | 382 | bool HIDPI_OnWmSettingChange() 383 | { 384 | const float oldTextScaleFactor = s_textScaleFactor; 385 | s_textScaleFactor = 0.0; 386 | return (GetTextScaleFactor() != oldTextScaleFactor); 387 | } 388 | 389 | DpiScaler::DpiScaler() 390 | { 391 | m_logPixels = 96; 392 | #ifdef DEBUG 393 | m_fTextScaling = false; 394 | #endif 395 | } 396 | 397 | DpiScaler::DpiScaler(WORD dpi) 398 | { 399 | assert(dpi); 400 | m_logPixels = dpi; 401 | #ifdef DEBUG 402 | m_fTextScaling = false; 403 | #endif 404 | } 405 | 406 | DpiScaler::DpiScaler(WPARAM wParam) 407 | { 408 | assert(wParam); 409 | assert(LOWORD(wParam)); 410 | m_logPixels = LOWORD(wParam); 411 | #ifdef DEBUG 412 | m_fTextScaling = false; 413 | #endif 414 | } 415 | 416 | DpiScaler::DpiScaler(const DpiScaler& dpi) 417 | { 418 | assert(!dpi.IsTextScaling()); 419 | m_logPixels = dpi.m_logPixels; 420 | #ifdef DEBUG 421 | m_fTextScaling = dpi.m_fTextScaling; 422 | #endif 423 | } 424 | 425 | DpiScaler::DpiScaler(const DpiScaler& dpi, bool fTextScaling) 426 | { 427 | assert(!dpi.IsTextScaling()); 428 | m_logPixels = fTextScaling ? WORD(GetTextScaleFactor() * dpi.m_logPixels) : dpi.m_logPixels; 429 | #ifdef DEBUG 430 | m_fTextScaling = dpi.m_fTextScaling; 431 | #endif 432 | } 433 | 434 | DpiScaler::DpiScaler(DpiScaler&& dpi) 435 | { 436 | assert(!dpi.IsTextScaling()); 437 | m_logPixels = dpi.m_logPixels; 438 | #ifdef DEBUG 439 | m_fTextScaling = dpi.m_fTextScaling; 440 | #endif 441 | } 442 | 443 | bool DpiScaler::IsDpiEqual(UINT dpi) const 444 | { 445 | assert(dpi); 446 | return dpi == m_logPixels; 447 | } 448 | 449 | bool DpiScaler::IsDpiEqual(const DpiScaler& dpi) const 450 | { 451 | return (dpi.m_logPixels == m_logPixels); 452 | } 453 | 454 | DpiScaler& DpiScaler::operator=(WORD dpi) 455 | { 456 | assert(dpi); 457 | m_logPixels = dpi; 458 | #ifdef DEBUG 459 | m_fTextScaling = false; 460 | #endif 461 | return *this; 462 | } 463 | 464 | DpiScaler& DpiScaler::operator=(const DpiScaler& dpi) 465 | { 466 | m_logPixels = dpi.m_logPixels; 467 | #ifdef DEBUG 468 | m_fTextScaling = dpi.m_fTextScaling; 469 | #endif 470 | return *this; 471 | } 472 | 473 | DpiScaler& DpiScaler::operator=(DpiScaler&& dpi) 474 | { 475 | m_logPixels = dpi.m_logPixels; 476 | #ifdef DEBUG 477 | m_fTextScaling = dpi.m_fTextScaling; 478 | #endif 479 | return *this; 480 | } 481 | 482 | void DpiScaler::OnDpiChanged(const DpiScaler& dpi) 483 | { 484 | m_logPixels = dpi.m_logPixels; 485 | #ifdef DEBUG 486 | m_fTextScaling = dpi.m_fTextScaling; 487 | #endif 488 | } 489 | 490 | void DpiScaler::OnDpiChanged(const DpiScaler& dpi, bool fTextScaling) 491 | { 492 | m_logPixels = fTextScaling ? WORD(GetTextScaleFactor() * dpi.m_logPixels) : dpi.m_logPixels; 493 | #ifdef DEBUG 494 | m_fTextScaling = fTextScaling; 495 | #endif 496 | } 497 | 498 | int DpiScaler::Scale(int n) const 499 | { 500 | return HIDPIMulDiv(n, m_logPixels, 96); 501 | } 502 | 503 | float DpiScaler::ScaleF(float n) const 504 | { 505 | return n * float(m_logPixels) / 96.0f; 506 | } 507 | 508 | int DpiScaler::ScaleTo(int n, DWORD dpi) const 509 | { 510 | assert(dpi); 511 | return HIDPIMulDiv(n, dpi, m_logPixels); 512 | } 513 | 514 | int DpiScaler::ScaleTo(int n, const DpiScaler& dpi) const 515 | { 516 | return HIDPIMulDiv(n, dpi.m_logPixels, m_logPixels); 517 | } 518 | 519 | int DpiScaler::ScaleFrom(int n, DWORD dpi) const 520 | { 521 | assert(dpi); 522 | return HIDPIMulDiv(n, m_logPixels, dpi); 523 | } 524 | 525 | int DpiScaler::ScaleFrom(int n, const DpiScaler& dpi ) const 526 | { 527 | return HIDPIMulDiv(n, m_logPixels, dpi.m_logPixels); 528 | } 529 | 530 | int DpiScaler::PointSizeToHeight(int nPointSize) const 531 | { 532 | assert(nPointSize >= 1); 533 | return -MulDiv(nPointSize, m_logPixels, 72); 534 | } 535 | 536 | int DpiScaler::PointSizeToHeight(float pointSize) const 537 | { 538 | assert(pointSize >= 1); 539 | return -MulDiv(int(pointSize * 10), m_logPixels, 720); 540 | } 541 | 542 | int DpiScaler::GetSystemMetrics(int nIndex) const 543 | { 544 | return __GetSystemMetricsForDpi(nIndex, m_logPixels); 545 | } 546 | 547 | bool DpiScaler::SystemParametersInfo(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni) const 548 | { 549 | return __SystemParametersInfoForDpi(uiAction, uiParam, pvParam, fWinIni, m_logPixels); 550 | } 551 | 552 | WPARAM DpiScaler::MakeWParam() const 553 | { 554 | return MAKELONG(m_logPixels, m_logPixels); 555 | } 556 | 557 | static void InitBmiForRgbaDibSection(BITMAPINFO& bmi, LONG cx, LONG cy) 558 | { 559 | ZeroMemory(&bmi, sizeof(bmi)); 560 | bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); 561 | bmi.bmiHeader.biWidth = cx; 562 | bmi.bmiHeader.biHeight = 0 - cy; // negative = top-down DIB 563 | bmi.bmiHeader.biPlanes = 1; 564 | bmi.bmiHeader.biBitCount = 32; 565 | bmi.bmiHeader.biCompression = BI_RGB; 566 | } 567 | 568 | static HBITMAP ScaleWicBitmapToBitmap(IWICImagingFactory* pFactory, IWICBitmap* pBmp, int cx, int cy) 569 | { 570 | BITMAPINFO bmi = {}; 571 | bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); 572 | bmi.bmiHeader.biWidth = cx; 573 | bmi.bmiHeader.biHeight = 0 - cy; // negative = top-down DIB 574 | bmi.bmiHeader.biPlanes = 1; 575 | bmi.bmiHeader.biBitCount = 32; 576 | bmi.bmiHeader.biCompression = BI_RGB; 577 | 578 | BYTE* pBits; 579 | HBITMAP hBmp = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, (void**)&pBits, nullptr, 0); 580 | SPI spScaler; 581 | bool ok = false; 582 | 583 | do 584 | { 585 | if (!hBmp) 586 | break; 587 | 588 | if (FAILED(pFactory->CreateBitmapScaler(&spScaler))) 589 | break; 590 | 591 | WICBitmapInterpolationMode const mode = WICBitmapInterpolationModeFant; 592 | if (FAILED(spScaler->Initialize(pBmp, cx, cy, mode))) 593 | break; 594 | 595 | WICRect rect = { 0, 0, cx, cy }; 596 | if (FAILED(spScaler->CopyPixels(&rect, cx * 4, cx * cy * 4, pBits))) 597 | break; 598 | 599 | ok = true; 600 | } 601 | while (false); 602 | 603 | if (!ok) 604 | { 605 | DeleteBitmap(hBmp); 606 | hBmp = 0; 607 | } 608 | 609 | return hBmp; 610 | } 611 | 612 | bool HIDPI_StretchBitmap(HBITMAP* phbm, int cxDstImg, int cyDstImg, int cColumns, int cRows, COLORREF& crTransparent) 613 | { 614 | // Check for caller errors. 615 | if (!phbm || !*phbm) 616 | return false; 617 | if (!cxDstImg && !cyDstImg) 618 | return false; 619 | if (!cColumns || !cRows) 620 | return false; 621 | 622 | // Get the bitmap object attributes. 623 | BITMAP bm; 624 | if ((sizeof(bm) != GetObject(*phbm, sizeof(bm), &bm))) 625 | return false; 626 | 627 | // The bitmap dimensions must be a multiple of the columns and rows. 628 | assert(!(bm.bmWidth % cColumns) && !(bm.bmHeight % cRows)); 629 | 630 | const int cxSrcImg = bm.bmWidth / cColumns; 631 | const int cySrcImg = bm.bmHeight / cRows; 632 | 633 | // If one dimension was 0, infer it based on the other values. 634 | if (!cxDstImg) 635 | cxDstImg = HIDPIMulDiv(cyDstImg, cxSrcImg, cySrcImg); 636 | else if(!cyDstImg) 637 | cyDstImg = HIDPIMulDiv(cxDstImg, cySrcImg, cxSrcImg); 638 | 639 | // If the dimensions don't match, the bitmap needs to be scaled. 640 | #ifndef DEBUG // Debug builds always scale, to ensure this code path is well exercised. 641 | if (cxSrcImg != cxDstImg || cySrcImg != cyDstImg) 642 | #endif 643 | { 644 | // Create GDI and WIC objects to perform the scaling. 645 | const HDC hdcSrc = CreateCompatibleDC(0); 646 | const HDC hdcDst = CreateCompatibleDC(0); 647 | const HDC hdcResize = CreateCompatibleDC(0); 648 | 649 | SPI spFactory; 650 | CoCreateInstance(CLSID_WICImagingFactory, 0, CLSCTX_INPROC_SERVER, __uuidof(IWICImagingFactory), (void**)&spFactory); 651 | 652 | // Ensure the input bitmap uses an alpha channel for transparency. 653 | HBITMAP hbmpInput = *phbm; 654 | HBITMAP hbmpConverted = 0; 655 | if (crTransparent != CLR_NONE) 656 | { 657 | BYTE* pbBits; 658 | BITMAPINFO bmi; 659 | InitBmiForRgbaDibSection(bmi, bm.bmWidth, bm.bmHeight); 660 | hbmpConverted = CreateDIBSection(hdcSrc, &bmi, DIB_RGB_COLORS, (void**)&pbBits, 0, 0); 661 | 662 | if (hbmpConverted && 663 | GetDIBits(hdcSrc, hbmpInput, 0, bm.bmHeight, pbBits, &bmi, DIB_RGB_COLORS)) 664 | { 665 | const BYTE bR = GetRValue(crTransparent); 666 | const BYTE bG = GetGValue(crTransparent); 667 | const BYTE bB = GetBValue(crTransparent); 668 | for (size_t c = bm.bmWidth * bm.bmHeight; c--;) 669 | { 670 | if (pbBits[0] == bB && pbBits[1] == bG && pbBits[2] == bR) 671 | pbBits[3] = 0; 672 | else 673 | pbBits[3] = 255; 674 | pbBits += 4; 675 | } 676 | 677 | hbmpInput = hbmpConverted; 678 | crTransparent = CLR_NONE; // Clue in the caller what's just happened! 679 | } 680 | else 681 | { 682 | // Couldn't get the device independent bits, so we can't 683 | // massage the bitmap into the format required by WIC. So 684 | // give up on using WIC and fall back to using StretchBlt, 685 | // which uses an ugly pixel-doubling algorithm. 686 | assert(false); 687 | spFactory.Release(); 688 | } 689 | } 690 | 691 | // Create the WIC factory and bitmap. 692 | SPI spBmpFull; 693 | if (spFactory) 694 | { 695 | if (FAILED(spFactory->CreateBitmapFromHBITMAP(hbmpInput, 0, WICBitmapUseAlpha, &spBmpFull))) 696 | spFactory.Release(); 697 | } 698 | 699 | // Create GDI objects to perform the scaling. 700 | BITMAPINFO bmi; 701 | InitBmiForRgbaDibSection(bmi, cxDstImg * cColumns, cyDstImg * cRows); 702 | const HBITMAP hbmOldSrc = SelectBitmap(hdcSrc, hbmpInput); 703 | const HBITMAP hbmNew = CreateDIBSection(hdcSrc, &bmi, DIB_RGB_COLORS, 0, 0, 0); 704 | const HBITMAP hbmOldDst = SelectBitmap(hdcDst, hbmNew); 705 | 706 | // Iterate through the tiles in the grid, stretching each one 707 | // individually to avoid rounding errors from 'bleeding' between 708 | // tiles. 709 | for (int jj = 0, yDest = 0, yBmp = 0; jj < cRows; jj++, yDest += cyDstImg, yBmp += cySrcImg) 710 | for (int ii = 0, xDest = 0, xBmp = 0; ii < cColumns; ii++, xDest += cxDstImg, xBmp += cxSrcImg) 711 | if (spFactory) 712 | { 713 | SPI spBmp; 714 | if (SUCCEEDED(spFactory->CreateBitmapFromSourceRect(spBmpFull, xBmp, yBmp, cxSrcImg, cySrcImg, &spBmp))) 715 | { 716 | HBITMAP hBmp = ScaleWicBitmapToBitmap(spFactory, spBmp, cxDstImg, cyDstImg); 717 | if (hBmp) 718 | { 719 | const HBITMAP hbmOldResize = SelectBitmap(hdcResize, hBmp); 720 | BitBlt(hdcDst, xDest, yDest, cxDstImg, cyDstImg, hdcResize, 0, 0, SRCCOPY); 721 | SelectBitmap(hdcResize, hbmOldResize); 722 | DeleteBitmap(hBmp); 723 | } 724 | } 725 | } 726 | else 727 | { 728 | StretchBlt(hdcDst, xDest, yDest, cxDstImg, cyDstImg, hdcSrc, xBmp, yBmp, cxSrcImg, cySrcImg, SRCCOPY); 729 | } 730 | 731 | //spBmpFull.Release(); 732 | //spFactory.Release(); 733 | 734 | // Free the GDI objects we created. 735 | SelectBitmap(hdcSrc, hbmOldSrc); 736 | SelectBitmap(hdcDst, hbmOldDst); 737 | DeleteDC(hdcSrc); 738 | DeleteDC(hdcDst); 739 | DeleteDC(hdcResize); 740 | if (hbmpConverted) 741 | DeleteBitmap(hbmpConverted); 742 | 743 | // Delete the passed in bitmap and return the bitmap we created. 744 | DeleteBitmap(*phbm); 745 | *phbm = hbmNew; 746 | } 747 | 748 | return true; 749 | } 750 | 751 | static bool HIDPI_StretchIcon_Internal(HICON hiconIn, HICON* phiconOut, int cxIcon, int cyIcon) 752 | { 753 | *phiconOut = 0; 754 | 755 | const HDC hdc = CreateCompatibleDC(0); 756 | 757 | const HBITMAP hbmMask = CreateCompatibleBitmap(hdc, cxIcon, cyIcon); 758 | const HBITMAP hbmOldMask = HBITMAP(SelectObject(hdc, hbmMask)); 759 | const bool fMaskOk = DrawIconEx(hdc, 0, 0, hiconIn, cxIcon, cyIcon, 0, 0, DI_MASK); 760 | SelectObject(hdc, hbmOldMask); 761 | 762 | const HBITMAP hbmImage = CreateBitmap(cxIcon, cyIcon, 1, GetDeviceCaps(hdc, BITSPIXEL), 0); 763 | const HBITMAP hbmOldImage = HBITMAP(SelectObject(hdc, hbmImage)); 764 | const bool fImageOk = DrawIconEx(hdc, 0, 0, hiconIn, cxIcon, cyIcon, 0, 0, DI_IMAGE); 765 | SelectObject(hdc, hbmOldImage); 766 | 767 | if (fMaskOk && fImageOk) 768 | { 769 | ICONINFO iconinfo; 770 | 771 | iconinfo.fIcon = true; 772 | iconinfo.hbmColor = hbmImage; 773 | iconinfo.hbmMask = hbmMask; 774 | 775 | *phiconOut = CreateIconIndirect(&iconinfo); 776 | } 777 | 778 | DeleteBitmap(hbmImage); 779 | DeleteBitmap(hbmMask); 780 | DeleteDC(hdc); 781 | 782 | return (fMaskOk && fImageOk && *phiconOut); 783 | } 784 | 785 | bool HIDPI_StretchIcon(const DpiScaler& dpi, HICON* phic, int cxIcon, int cyIcon) 786 | { 787 | HICON hiconOut; 788 | 789 | if (HIDPI_StretchIcon_Internal(*phic, &hiconOut, cxIcon, cyIcon)) 790 | { 791 | DestroyIcon( *phic ); 792 | *phic = hiconOut; 793 | return true; 794 | } 795 | 796 | return false; 797 | } 798 | 799 | bool HIDPI_GetBitmapLogPixels(HINSTANCE hinst, UINT idb, int* pdxLogPixels, int* pdyLogPixels, int* pnBitsPerPixel) 800 | { 801 | *pdxLogPixels = 0; 802 | *pdyLogPixels = 0; 803 | *pnBitsPerPixel = 0; 804 | 805 | // Note that MSDN says there is no cleanup needed after FindResource, 806 | // LoadResource, or LockResource. 807 | const HRSRC hResource = FindResource(hinst, MAKEINTRESOURCE(idb), RT_BITMAP); 808 | if (!hResource) 809 | return false; 810 | 811 | const HGLOBAL hResourceBitmap = LoadResource(hinst, hResource); 812 | if (!hResourceBitmap) 813 | return false; 814 | 815 | const BITMAPINFO* const pBitmapInfo = static_cast(LockResource(hResourceBitmap)); 816 | if (!pBitmapInfo) 817 | return false; 818 | 819 | // There are at least three common values of PslsPerMeter that occur for 820 | // 96 DPI bitmaps: 821 | // 822 | // 0 = The bitmap doesn't set this value. 823 | // 2834 = 72 DPI. 824 | // 3780 = 96 DPI. 825 | // 826 | // For simplicity, treat all values under 3780 as 96 DPI bitmaps. 827 | const int cxPelsPerMeter = (pBitmapInfo->bmiHeader.biXPelsPerMeter < 3780) ? 3780 : pBitmapInfo->bmiHeader.biXPelsPerMeter; 828 | const int cyPelsPerMeter = (pBitmapInfo->bmiHeader.biYPelsPerMeter < 3780) ? 3780 : pBitmapInfo->bmiHeader.biYPelsPerMeter; 829 | 830 | // The formula for converting PelsPerMeter to LogPixels(DPI) is: 831 | // 832 | // LogPixels = PelsPerMeter / 39.37 833 | // 834 | // Where: 835 | // 836 | // PelsPerMeter = Pixels per meter. 837 | // LogPixels = Pixels per inch. 838 | // 839 | // Round up, otherwise things can get cut off. 840 | *pdxLogPixels = int((cxPelsPerMeter * 100 + 1968) / 3937); 841 | *pdyLogPixels = int((cyPelsPerMeter * 100 + 1968) / 3937); 842 | 843 | *pnBitsPerPixel = int(pBitmapInfo->bmiHeader.biPlanes * pBitmapInfo->bmiHeader.biBitCount); 844 | 845 | return true; 846 | } 847 | 848 | HIMAGELIST HIDPI_ImageList_LoadImage(HINSTANCE hinst, const int cxTarget, const int cyTarget, UINT idb, const int cxNative, int cGrow, COLORREF crMask, const UINT uType, const UINT uFlags) 849 | { 850 | // If the image type is not IMAGE_BITMAP, or if the caller doesn't care 851 | // about the dimensions of the image, then just use the ones in the file. 852 | if ((uType != IMAGE_BITMAP) || !cxNative || !cxTarget) 853 | return ImageList_LoadImage(hinst, MAKEINTRESOURCE(idb), cxNative, cGrow, crMask, uType, uFlags); 854 | 855 | // Get the bitmap dimensions. 856 | int cxBmpLogPixels; 857 | int cyBmpLogPixels; 858 | int nBitsPerPixel; 859 | if (!HIDPI_GetBitmapLogPixels(hinst, idb, &cxBmpLogPixels, &cyBmpLogPixels, &nBitsPerPixel)) 860 | return 0; 861 | 862 | // Load the bitmap image. 863 | BITMAP bm; 864 | HIMAGELIST himl = 0; 865 | HBITMAP hbmImage = HBITMAP(LoadImage(hinst, MAKEINTRESOURCE(idb), uType, 0, 0, uFlags)); 866 | 867 | if (hbmImage && sizeof(bm) == GetObject(hbmImage, sizeof(bm), &bm)) 868 | { 869 | // Windows says DDPI currently will always be square pixels. 870 | assert(cxBmpLogPixels == cyBmpLogPixels); 871 | 872 | // Bitmap width should be an integral multiple of image width. If not, 873 | // then either the bitmap is wrong or the passed in cxNative is wrong. 874 | assert(!(bm.bmWidth % cxNative)); 875 | 876 | // If the input bitmap has an alpha channel already, then ignore the 877 | // crMask since the OS implementation of ImageList_LoadImage ignores 878 | // it in that case. 879 | if (nBitsPerPixel >= 32) 880 | crMask = CLR_NONE; 881 | 882 | const int cImages = bm.bmWidth / cxNative; 883 | const int cxImage = cxTarget; 884 | const int cy = cxTarget; 885 | const COLORREF crOldMask = crMask; 886 | 887 | // Stretching the bitmap in one action can cause individual images to 888 | // be stretched to wrong place/dimensions. Even when the target DPI 889 | // is an integral multiple of 96 there's still the potential for 890 | // interpolation to "bleed" pixels between adjacent images (a real 891 | // problem, not just hypothetical). Therefore we stretch individual 892 | // images separately to make sure each one is stretched properly. 893 | HIDPI_StretchBitmap(&hbmImage, cxImage, cy, cImages, 1, crMask); 894 | 895 | if (crOldMask != crMask) 896 | GetObject(hbmImage, sizeof(bm), &bm); 897 | 898 | UINT flags = 0; 899 | 900 | if (crMask != CLR_NONE) 901 | { 902 | // ILC_MASK is important for supporting CLR_DEFAULT. 903 | flags |= ILC_MASK; 904 | } 905 | 906 | if (bm.bmBits) 907 | { 908 | // ILC_COLORMASK bits are important when merging ImageLists. 909 | flags |= ( bm.bmBitsPixel & ILC_COLORMASK ); 910 | } 911 | 912 | // The bitmap MUST be de-selected from the DC in order to create the 913 | // image list of the size asked for. 914 | himl = ImageList_Create(cxImage, cy, flags, cImages, cGrow); 915 | 916 | if (himl) 917 | { 918 | int cAdded; 919 | 920 | if (CLR_NONE == crMask) 921 | cAdded = ImageList_Add(himl, hbmImage, 0); 922 | else 923 | cAdded = ImageList_AddMasked(himl, hbmImage, crMask); 924 | 925 | if (cAdded < 0) 926 | { 927 | ImageList_Destroy(himl); 928 | himl = 0; 929 | } 930 | } 931 | } 932 | 933 | DeleteBitmap(hbmImage); 934 | return himl; 935 | } 936 | 937 | int __MessageBox(__in_opt HWND hWnd, __in_opt LPCTSTR lpText, __in_opt LPCTSTR lpCaption, __in UINT uType) 938 | { 939 | ThreadDpiAwarenessContext dpiContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); 940 | return MessageBox(hWnd, lpText, lpCaption, uType); 941 | } 942 | 943 | -------------------------------------------------------------------------------- /dpi.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #ifndef WM_DPICHANGED 7 | #define WM_DPICHANGED 0x02E0 8 | #endif 9 | 10 | #define WMU_DPICHANGED (WM_USER + 9997) // Specialized internal use. 11 | #define WMU_REFRESHDPI (WM_USER + 9998) // Specialized internal use. 12 | 13 | #ifndef DPI_AWARENESS_CONTEXT_UNAWARE 14 | DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); 15 | #define DPI_AWARENESS_CONTEXT_UNAWARE (DPI_AWARENESS_CONTEXT(-1)) 16 | #define DPI_AWARENESS_CONTEXT_SYSTEM_AWARE (DPI_AWARENESS_CONTEXT(-2)) 17 | #define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT(-3)) 18 | #define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT(-4)) 19 | #endif // !DPI_AWARENESS_CONTEXT_UNAWARE 20 | 21 | int HIDPIMulDiv(int x, int y, int z); 22 | 23 | WORD __GetHdcDpi(HDC hdc); 24 | WORD __GetDpiForSystem(); 25 | WORD __GetDpiForWindow(HWND hwnd); 26 | bool __IsHwndPerMonitorAware(HWND hwnd); 27 | 28 | int __GetSystemMetricsForDpi(int nIndex, UINT dpi); 29 | bool __SystemParametersInfoForDpi(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni, UINT dpi); 30 | 31 | bool __EnableNonClientDpiScaling(HWND hwnd); 32 | bool __EnablePerMonitorMenuScaling(); 33 | 34 | class ThreadDpiAwarenessContext 35 | { 36 | public: 37 | ThreadDpiAwarenessContext(bool fUsePerMonitorAwareness); 38 | ThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context); 39 | ~ThreadDpiAwarenessContext() { Restore(); } 40 | 41 | void Restore(); 42 | 43 | private: 44 | DPI_AWARENESS_CONTEXT m_context; 45 | bool m_fRestore; 46 | }; 47 | 48 | class DpiScaler 49 | { 50 | public: 51 | DpiScaler(); 52 | explicit DpiScaler(WORD dpi); 53 | explicit DpiScaler(WPARAM wParam); 54 | explicit DpiScaler(const DpiScaler& dpi); 55 | explicit DpiScaler(const DpiScaler& dpi, bool fTextScaling); 56 | explicit DpiScaler(DpiScaler&& dpi); 57 | 58 | bool IsDpiEqual(UINT dpi) const; 59 | bool IsDpiEqual(const DpiScaler& dpi) const; 60 | bool operator==(UINT dpi) const { return IsDpiEqual( dpi ); } 61 | bool operator==(const DpiScaler& dpi ) const { return IsDpiEqual( dpi ); } 62 | 63 | DpiScaler& operator=(WORD dpi); 64 | DpiScaler& operator=(const DpiScaler& dpi); 65 | DpiScaler& operator=(DpiScaler&& dpi); 66 | void OnDpiChanged(const DpiScaler& dpi); 67 | void OnDpiChanged(const DpiScaler& dpi, bool fTextScaling); 68 | 69 | int Scale(int n) const; 70 | float ScaleF(float n) const; 71 | 72 | int ScaleTo(int n, DWORD dpi) const; 73 | int ScaleTo(int n, const DpiScaler& dpi) const; 74 | int ScaleFrom(int n, DWORD dpi) const; 75 | int ScaleFrom(int n, const DpiScaler& dpi) const; 76 | 77 | int PointSizeToHeight(int nPointSize) const; 78 | int PointSizeToHeight(float pointSize) const; 79 | 80 | int GetSystemMetrics(int nIndex) const; 81 | bool SystemParametersInfo(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni) const; 82 | 83 | WPARAM MakeWParam() const; 84 | 85 | private: 86 | #ifdef DEBUG 87 | bool IsTextScaling() const { return m_fTextScaling; } 88 | #endif 89 | 90 | private: 91 | WORD m_logPixels; 92 | #ifdef DEBUG 93 | bool m_fTextScaling; 94 | #endif 95 | }; 96 | 97 | /* 98 | * HIDPI_StretchBitmap 99 | * 100 | * Stretches a bitmap containing a grid of images. There are cColumns 101 | * images per row and cRows rows per bitmap. Each image is scaled 102 | * individually, so that there are no artifacts with non-integral scaling 103 | * factors. If the bitmap contains only one image, set cColumns and 104 | * cRows to 1. 105 | * 106 | * Args: 107 | * 108 | * phbm - A pointer to the bitmap to be scaled. 109 | * cxDstImg - The width of each image after scaling. 110 | * cyDstImg - The height of each image after scaling. 111 | * cColumns - The number of images per row. This value should 112 | * evenly divide the width of the bitmap. 113 | * cRows - The number of rows in the bitmap. This value should 114 | * evenly divide the height of the bitmap. 115 | * crTransparent - [in/out] The transparent color if no alpha channel. 116 | * CLR_NONE on input or output indicates alpha channel 117 | * is used for transparency (or that it's opaque). 118 | * 119 | * Rets: 120 | * 121 | * Returns true on success, false on failure. 122 | * 123 | * If any scaling has occured, the bitmap pointed to by phbm is deleted 124 | * and is replaced by a new bitmap handle. 125 | */ 126 | bool HIDPI_StretchBitmap(HBITMAP* phbm, int cxDstImg, int cyDstImg, int cColumns, int cRows, COLORREF& crTransparent); 127 | 128 | /* 129 | * HIDPI_GetBitmapLogPixels 130 | * 131 | * Retrieves the DPI fields of the specified bitmap. 132 | * 133 | * Args: 134 | * 135 | * hinst - The HINSTANCE of the bitmap resource. 136 | * idb - The ID of the bitmap resource. 137 | * pdxLogPixels - The returned value for the horizontal DPI field of 138 | * the bitmap. This value is never less than 96. 139 | * pdyLogPixels - The returned value for the vertical DPI field of 140 | * the bitmap. This value is never less than 96. 141 | * 142 | * Rets: 143 | * 144 | * Returns true on success, false on failure. 145 | */ 146 | bool HIDPI_GetBitmapLogPixels(HINSTANCE hinst, UINT idb, int* pdxLogPixels, int* pdyLogPixels); 147 | 148 | /* 149 | * HIDPI_StretchIcon 150 | * 151 | * Stretches an icon to the specified size. 152 | * 153 | * Args: 154 | * 155 | * dpi - The encapsulated DPI. 156 | * phic - The icon to stretch. 157 | * cxIcon - The desired width of the icon. 158 | * cyIcon - The desired height of the icon. 159 | * 160 | * Rets: 161 | * 162 | * Returns true on success, false on failure. 163 | * 164 | * If any stretching occurred, the icon pointed to by phic is deleted and 165 | * is replaced by a new icon handle. 166 | */ 167 | bool HIDPI_StretchIcon(const DpiScaler& dpi, HICON* phic, int cxIcon, int cyIcon); 168 | 169 | /* 170 | * HIDPI_ImageList_LoadImage 171 | * 172 | * This function operates identically to ImageList_LoadImage, except it 173 | * also performs scaling if needed. 174 | * 175 | * Args/Rets: See the MSDN documentation for ImageList_LoadImage. 176 | */ 177 | HIMAGELIST HIDPI_ImageList_LoadImage(HINSTANCE hinst, int cxTarget, int cyTarget, UINT idb, int cxNative, int cGrow, COLORREF crMask, UINT uType, UINT uFlags); 178 | 179 | /* 180 | * HIDPI_OnWmSettingChange 181 | * 182 | * Call this on WM_SETTINGCHANGE in top level windows to check whether 183 | * the Text Scale Factor has changed. 184 | * 185 | * Rets: 186 | * 187 | * Returns true if the Text Scale Factor has changed, otherwise false. 188 | */ 189 | bool HIDPI_OnWmSettingChange(); 190 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include "data.h" 6 | #include "scan.h" 7 | #include "ui.h" 8 | #include "sunburst.h" 9 | #include "DarkMode.h" 10 | #include 11 | #include 12 | 13 | static const WCHAR c_reg_root[] = TEXT("Software\\Elucidisk"); 14 | 15 | bool g_use_compressed_size = false; 16 | bool g_show_free_space = true; 17 | bool g_show_names = true; 18 | bool g_show_comparison_bar = true; 19 | bool g_show_proportional_area = true; 20 | bool g_show_dontscan_anyway = false; 21 | long g_color_mode = CM_RAINBOW; 22 | long g_syscolor_mode = SCM_AUTO; 23 | #ifdef DEBUG 24 | long g_fake_data = FDM_REAL; 25 | #endif 26 | 27 | int PASCAL WinMain( 28 | _In_ HINSTANCE hinstCurrent, 29 | _In_opt_ HINSTANCE /*hinstPrevious*/, 30 | _In_ LPSTR /*lpszCmdLine*/, 31 | _In_ int /*nCmdShow*/) 32 | { 33 | MSG msg = { 0 }; 34 | 35 | int argc; 36 | const WCHAR **argv = const_cast(CommandLineToArgvW(GetCommandLine(), &argc)); 37 | 38 | if (argc) 39 | { 40 | argc--; 41 | argv++; 42 | } 43 | 44 | // Options (there are no options yet). 45 | 46 | // FUTURE: An option to generate the .ico file programmatically using D2D. 47 | 48 | // Create UI. 49 | 50 | INITCOMMONCONTROLSEX icce = { sizeof(icce), ICC_LISTVIEW_CLASSES }; 51 | 52 | CoInitialize(0); 53 | 54 | InitCommonControls(); 55 | InitCommonControlsEx(&icce); 56 | InitializeD2D(); 57 | InitializeDWrite(); 58 | 59 | AllowDarkMode(); 60 | 61 | g_use_compressed_size = !!ReadRegLong(TEXT("UseCompressedSize"), false); 62 | g_show_free_space = !!ReadRegLong(TEXT("ShowFreeSpace"), true); 63 | g_show_names = !!ReadRegLong(TEXT("ShowNames"), true); 64 | g_show_comparison_bar = !!ReadRegLong(TEXT("ShowComparisonBar"), true); 65 | g_show_proportional_area = !!ReadRegLong(TEXT("ShowProportionalArea"), true); 66 | g_show_dontscan_anyway = !!ReadRegLong(TEXT("ShowDontScanAnyway"), false); 67 | g_color_mode = ReadRegLong(TEXT("ColorMode"), CM_RAINBOW); 68 | g_syscolor_mode = ReadRegLong(TEXT("SysColorMode"), SCM_AUTO); 69 | #ifdef DEBUG 70 | g_fake_data = ReadRegLong(TEXT("DbgFakeData"), FDM_REAL); 71 | #endif 72 | 73 | const HWND hwnd = MakeUi(hinstCurrent, argc, argv); 74 | 75 | // Main message loop. 76 | 77 | if (hwnd) 78 | { 79 | while (true) 80 | { 81 | if (!GetMessage(&msg, nullptr, 0, 0)) 82 | break; 83 | TranslateMessage(&msg); 84 | DispatchMessage(&msg); 85 | } 86 | } 87 | 88 | // Cleanup. 89 | 90 | { 91 | MSG tmp; 92 | do {} while(PeekMessage(&tmp, 0, WM_QUIT, WM_QUIT, PM_REMOVE)); 93 | } 94 | 95 | return int(msg.wParam); 96 | } 97 | 98 | LONG ReadRegLong(const WCHAR* name, LONG default_value) 99 | { 100 | HKEY hkey; 101 | LONG ret = default_value; 102 | 103 | if (ERROR_SUCCESS == RegOpenKey(HKEY_CURRENT_USER, c_reg_root, &hkey)) 104 | { 105 | DWORD type; 106 | LONG value; 107 | DWORD cb = sizeof(value); 108 | if (ERROR_SUCCESS == RegQueryValueEx(hkey, name, 0, &type, reinterpret_cast(&value), &cb) && 109 | type == REG_DWORD && 110 | cb == sizeof(value)) 111 | { 112 | ret = value; 113 | } 114 | RegCloseKey(hkey); 115 | } 116 | 117 | return ret; 118 | } 119 | 120 | void WriteRegLong(const WCHAR* name, LONG value) 121 | { 122 | HKEY hkey; 123 | 124 | if (ERROR_SUCCESS == RegCreateKey(HKEY_CURRENT_USER, c_reg_root, &hkey)) 125 | { 126 | RegSetValueEx(hkey, name, 0, REG_DWORD, reinterpret_cast(&value), sizeof(value)); 127 | RegCloseKey(hkey); 128 | } 129 | } 130 | 131 | bool ReadRegStrings(const WCHAR* name, std::vector& out) 132 | { 133 | bool ok = false; 134 | DWORD cb = 0; 135 | out.clear(); 136 | if (ERROR_SUCCESS == RegGetValue(HKEY_CURRENT_USER, c_reg_root, name, RRF_RT_REG_MULTI_SZ, nullptr, nullptr, &cb)) 137 | { 138 | WCHAR* data = (WCHAR*)calloc(cb, 1); 139 | if (ERROR_SUCCESS == RegGetValue(HKEY_CURRENT_USER, c_reg_root, name, RRF_RT_REG_MULTI_SZ, nullptr, data, &cb)) 140 | { 141 | for (const WCHAR* s = data; *s;) 142 | { 143 | const size_t len = wcslen(s); 144 | out.emplace_back(s); 145 | s += len + 1; 146 | if (!*s) 147 | break; 148 | } 149 | ok = true; 150 | } 151 | free(data); 152 | } 153 | return ok; 154 | } 155 | 156 | void WriteRegStrings(const WCHAR* name, const std::vector& in) 157 | { 158 | DWORD cb = sizeof(WCHAR); 159 | for (const auto& s : in) 160 | cb += DWORD((s.length() + 1) * sizeof(WCHAR)); 161 | 162 | WCHAR* data = (WCHAR*)calloc(cb, 1); 163 | WCHAR* to = data; 164 | for (const auto& s : in) 165 | { 166 | memcpy(to, s.c_str(), (s.length() + 1) * sizeof(*s.c_str())); 167 | to += s.length() + 1; 168 | } 169 | 170 | HKEY hkey; 171 | if (ERROR_SUCCESS == RegCreateKey(HKEY_CURRENT_USER, c_reg_root, &hkey)) 172 | { 173 | RegSetValueEx(hkey, name, 0, REG_MULTI_SZ, reinterpret_cast(data), cb); 174 | RegCloseKey(hkey); 175 | } 176 | 177 | free(data); 178 | } 179 | 180 | void MakeMenuPretty(HMENU hmenu) 181 | { 182 | MENUITEMINFO mii = {}; 183 | mii.cbSize = sizeof(mii); 184 | #ifdef MIIM_FTYPE 185 | mii.fMask = MIIM_FTYPE|MIIM_SUBMENU; 186 | #else 187 | mii.fMask = MIIM_TYPE|MIIM_SUBMENU; 188 | #endif 189 | 190 | bool fPrevSep = true; 191 | for (int ii = 0; true; ii++) 192 | { 193 | const bool fEnd = !GetMenuItemInfo(hmenu, ii, true, &mii); 194 | 195 | if (fEnd || (mii.fType & MFT_SEPARATOR)) 196 | { 197 | if (fPrevSep) 198 | { 199 | DeleteMenu(hmenu, ii - fEnd, MF_BYPOSITION); 200 | ii--; 201 | } 202 | 203 | if (fEnd) 204 | break; 205 | 206 | fPrevSep = true; 207 | } 208 | else 209 | { 210 | fPrevSep = false; 211 | } 212 | 213 | if (mii.hSubMenu) 214 | MakeMenuPretty(mii.hSubMenu); 215 | } 216 | } 217 | 218 | UnitScale AutoUnitScale(ULONGLONG size) 219 | { 220 | size /= ULONGLONG(10) * 1024 * 1024; 221 | if (!size) 222 | return UnitScale::KB; 223 | 224 | size /= 1024; 225 | if (!size) 226 | return UnitScale::MB; 227 | 228 | return UnitScale::GB; 229 | } 230 | 231 | void FormatSize(const ULONGLONG _size, std::wstring& text, std::wstring& units, UnitScale scale, int places) 232 | { 233 | WCHAR sz[100]; 234 | double size = double(_size); 235 | 236 | if (scale == UnitScale::Auto) 237 | scale = AutoUnitScale(_size); 238 | 239 | switch (scale) 240 | { 241 | case UnitScale::KB: 242 | units = TEXT("KB"); 243 | size /= 1024; 244 | break; 245 | case UnitScale::MB: 246 | units = TEXT("MB"); 247 | size /= 1024; 248 | size /= 1024; 249 | break; 250 | default: 251 | units = TEXT("GB"); 252 | size /= 1024; 253 | size /= 1024; 254 | size /= 1024; 255 | break; 256 | } 257 | 258 | if (places < 0) 259 | { 260 | if (size >= 100.0f) 261 | places = 0; 262 | else if (size >= 10.0f) 263 | places = 1; 264 | else if (size >= 1.0f) 265 | places = 2; 266 | else 267 | places = 3; 268 | } 269 | 270 | swprintf_s(sz, _countof(sz), TEXT("%.*f"), places, size); 271 | text = sz; 272 | } 273 | 274 | void FormatCount(const ULONGLONG count, std::wstring& text) 275 | { 276 | WCHAR sz[100]; 277 | swprintf_s(sz, _countof(sz), TEXT("%llu"), count); 278 | 279 | WCHAR* commas = sz + _countof(sz); 280 | *(--commas) = '\0'; 281 | 282 | size_t ii = wcslen(sz); 283 | for (int count = 0; ii--; ++count) 284 | { 285 | if (count == 3) 286 | { 287 | count = 0; 288 | *(--commas) = ','; 289 | } 290 | *(--commas) = sz[ii]; 291 | } 292 | 293 | text = commas; 294 | } 295 | 296 | -------------------------------------------------------------------------------- /main.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #define WIN32_LEAN_AND_MEAN 7 | #include 8 | #include 9 | #include "dpi.h" 10 | #include 11 | #include 12 | #include 13 | 14 | LONG ReadRegLong(const WCHAR* name, LONG default_value); 15 | void WriteRegLong(const WCHAR* name, LONG value); 16 | 17 | bool ReadRegStrings(const WCHAR* name, std::vector& out); 18 | void WriteRegStrings(const WCHAR* name, const std::vector& in); 19 | 20 | extern bool g_use_compressed_size; 21 | extern bool g_show_free_space; 22 | extern bool g_show_names; 23 | extern bool g_show_comparison_bar; 24 | extern bool g_show_proportional_area; 25 | extern bool g_show_dontscan_anyway; 26 | extern long g_color_mode; 27 | extern long g_syscolor_mode; 28 | enum ColorMode { CM_PLAIN, CM_RAINBOW, CM_HEATMAP }; 29 | enum SysColorMode { SCM_AUTO, SCM_LIGHT, SCM_DARK }; 30 | #ifdef DEBUG 31 | extern long g_fake_data; 32 | enum FakeDataMode { FDM_REAL, FDM_SIMULATED, FDM_COLORWHEEL, FDM_EMPTYDRIVE, FDM_ONLYDIRS }; 33 | #endif 34 | 35 | void MakeMenuPretty(HMENU hmenu); 36 | 37 | enum class UnitScale { Auto, KB, MB, GB }; 38 | UnitScale AutoUnitScale(ULONGLONG size); 39 | void FormatSize(ULONGLONG size, std::wstring& text, std::wstring& units, UnitScale scale=UnitScale::Auto, int places=-1); 40 | void FormatCount(ULONGLONG count, std::wstring& text); 41 | 42 | //---------------------------------------------------------------------------- 43 | // Smart pointer for AddRef/Release refcounting. 44 | 45 | template struct RemoveConst { typedef T type; }; 46 | template struct RemoveConst { typedef T type; }; 47 | 48 | template 49 | class PrivateRelease : public IFace 50 | { 51 | private: 52 | STDMETHODIMP_(ULONG) Release(); // Force Release to be private to prevent "spfoo->Release()". 53 | ~PrivateRelease(); // Avoid compiler warning/error when destructor is private. 54 | }; 55 | 56 | // Using a macro for this allows you to give your editor's tagging engine a 57 | // preprocessor hint to help it not get confused. 58 | #define SPI_TAGGINGTRICK(IFace) PrivateRelease 59 | 60 | template 61 | class SPI 62 | { 63 | typedef typename RemoveConst::type ICastRemoveConst; 64 | 65 | public: 66 | SPI() { m_p = 0; } 67 | #if 0 68 | // Disabled because of ambiguity between "SPI spFoo = new Foo" 69 | // and "SPI spFoo( spPtrToCopy )". One should not AddRef, but 70 | // the other sometimes should and sometimes should not. But both end 71 | // up using the constructor. So for now we can't allow either form. 72 | SPI(IFace *p) { m_p = p; if (m_p) m_p->AddRef(); } 73 | #endif 74 | ~SPI() { if (m_p) RemoveConst(m_p)->Release(); } 75 | SPI(SPI&& other) { m_p = other.m_p; other.m_p = 0; } 76 | operator IFace*() const { return m_p; } 77 | SPI_TAGGINGTRICK(IFace)* Pointer() const { return static_cast*>(m_p); } 78 | SPI_TAGGINGTRICK(IFace)* operator->() const { return static_cast*>(RemoveConst(m_p)); } 79 | IFace** operator &() { assert(!m_p); return &m_p; } 80 | IFace* operator=(IFace* p) { assert(!m_p); return m_p = p; } 81 | IFace* operator=(SPI&& other) { assert(!m_p); m_p = other.m_p; other.m_p = 0; return m_p; } 82 | IFace* Transfer() { return Detach(); } 83 | IFace* Copy() const { if (m_p) RemoveConst(m_p)->AddRef(); return m_p; } 84 | void Release() { Attach(0); } 85 | void Set(IFace* p) { if (p && m_p != p) RemoveConst(p)->AddRef(); Attach(p); } 86 | void Attach(IFace* p) { ICast* pRelease = static_cast(m_p); m_p = p; if (pRelease && pRelease != p) RemoveConst(pRelease)->Release(); } 87 | IFace* Detach() { IFace* p = m_p; m_p = 0; return p; } 88 | bool operator!() { return !m_p; } 89 | 90 | IFace** UnsafeAddress() { return &m_p; } 91 | 92 | protected: 93 | static ICastRemoveConst* RemoveConst(ICast* p) { return const_cast(static_cast(p)); } 94 | 95 | protected: 96 | IFace* m_p; 97 | 98 | private: 99 | SPI& operator=(SPI const& sp) = delete; 100 | SPI(SPI const&) = delete; 101 | }; 102 | 103 | struct IUnknown; 104 | 105 | template 106 | class SPQI : public SPI 107 | { 108 | public: 109 | SPQI() : SPI() {} 110 | #if 0 111 | // See comment in SPI... 112 | SPQI(IFace* p) : SPI(p) {} 113 | #endif 114 | SPQI(SPQI&& other) { SPI::operator=(std::move(other)); } 115 | 116 | bool FQuery(IUnknown* punk) { return SUCCEEDED(HrQuery(punk)); } 117 | HRESULT HrQuery(IUnknown* punk) { assert(!m_p); return punk->QueryInterface(iid, (void**)&m_p); } 118 | 119 | IFace* operator=(IFace* p) { return SPI::operator=(p); } 120 | IFace* operator=(SPQI&& other) { return SPI::operator=(std::move(other)); } 121 | 122 | private: 123 | SPQI& operator=(SPQI const& sp) = delete; 124 | SPQI(SPQI const&) = delete; 125 | }; 126 | 127 | //---------------------------------------------------------------------------- 128 | // Smart handle. 129 | 130 | // NOTE: a compiler bug forces us to use DWORD_PTR instead of Type. 131 | template 132 | class SH : public Subclass 133 | { 134 | public: 135 | SH(Type h = Type(EmptyValue)) { m_h = h; } 136 | ~SH() { if (Type(EmptyValue) != m_h) Subclass::Free(m_h); } 137 | SH(SH&& other) { m_h = other.m_h; other.m_h = Type(EmptyValue); } 138 | operator Type() const { return m_h; } 139 | Type Handle() const { return m_h; } 140 | Type* operator&() { assert(Type(EmptyValue) == m_h); return &m_h; } 141 | Type operator=(Type h) { assert(Type(EmptyValue) == m_h); return m_h = h; } 142 | Type operator=(SH&& other) { assert(Type(EmptyValue) == m_h); m_h = other.m_h; other.m_h = 0; return m_h; } 143 | void Set(Type h) { if (Type(EmptyValue) != m_h) Subclass::Free(m_h); m_h = h; } 144 | Type Transfer() { return Detach(); } 145 | void Free() { if (Type(EmptyValue) != m_h) Subclass::Free(m_h); m_h = Type(EmptyValue); } 146 | void Close() { Free(); } 147 | void Attach(Type h) { if (m_h != h) Free(); m_h = h; } 148 | Type Detach() { Type h = m_h; m_h = Type(EmptyValue); return h; } 149 | bool operator!() const { static_assert(EmptyValue == 0, "operator! requires empty value == 0"); return !m_h; } 150 | bool IsEmpty() const { return EmptyValue == reinterpret_cast(m_h); } 151 | 152 | Type* UnsafeAddress() { return &m_h; } 153 | 154 | protected: 155 | Type m_h; 156 | 157 | private: 158 | SH &operator=(SH const& sh) = delete; 159 | SH(SH const&) = delete; 160 | }; 161 | 162 | class SH_CloseHandle { protected: void Free(HANDLE h) { CloseHandle(h); } }; 163 | class SH_FindClose { protected: void Free(HANDLE h) { FindClose(h); } }; 164 | class SH_RegCloseKey { protected: void Free(HKEY hkey) { RegCloseKey(hkey); } }; 165 | class SH_DeleteObject { protected: void Free(HGDIOBJ hobj) { DeleteObject(hobj); } }; 166 | class SH_DestroyCursor { protected: void Free(HCURSOR hcur) { DestroyCursor(hcur); } }; 167 | class SH_DestroyIcon { protected: void Free(HICON hicon) { DestroyIcon(hicon); } }; 168 | 169 | typedef SH SHandle; 170 | typedef SH SFileHandle; 171 | typedef SH SFindHandle; 172 | typedef SH SHKEY; 173 | typedef SH SHPEN; 174 | typedef SH SHBRUSH; 175 | typedef SH SHBITMAP; 176 | typedef SH SHFONT; 177 | typedef SH SHCURSOR; 178 | typedef SH SHICON; 179 | 180 | -------------------------------------------------------------------------------- /main.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisant996/elucidisk/20402912bb06906dcfd01883886ff8560d59fdac/main.ico -------------------------------------------------------------------------------- /main.rc: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include "version.h" 4 | #include "version.rc" 5 | #include "res.h" 6 | 7 | IDI_MAIN ICON DISCARDABLE "main.ico" 8 | 9 | 1 24 "manifest.xml" 10 | 11 | IDR_CONTEXT_MENU MENU DISCARDABLE 12 | BEGIN 13 | POPUP "" 14 | BEGIN 15 | MENUITEM "&Rescan", IDM_RESCAN 16 | MENUITEM "&Open File", IDM_OPEN_FILE 17 | MENUITEM "&Open in Explorer", IDM_OPEN_DIRECTORY 18 | MENUITEM SEPARATOR 19 | MENUITEM "&Hide", IDM_HIDE_DIRECTORY 20 | MENUITEM "&Show", IDM_SHOW_DIRECTORY 21 | MENUITEM SEPARATOR 22 | MENUITEM "Rec&ycle", IDM_RECYCLE_ENTRY 23 | MENUITEM "&Delete", IDM_DELETE_ENTRY 24 | MENUITEM "&Empty Recycle Bin", IDM_EMPTY_RECYCLEBIN 25 | END 26 | POPUP "" 27 | BEGIN 28 | MENUITEM "&Plain Colors", IDM_OPTION_PLAIN 29 | MENUITEM "&Rainbow Colors (by angle)", IDM_OPTION_RAINBOW 30 | MENUITEM "&Heatmap Colors (by size)", IDM_OPTION_HEATMAP 31 | MENUITEM SEPARATOR 32 | MENUITEM "Show &Names", IDM_OPTION_NAMES 33 | MENUITEM "Show &Free Space", IDM_OPTION_FREESPACE 34 | MENUITEM "Show &Compressed Sizes", IDM_OPTION_COMPRESSED 35 | MENUITEM "Show Proportional &Area", IDM_OPTION_PROPORTION 36 | MENUITEM "Show Size Comparison &Bar", IDM_OPTION_COMPBAR 37 | MENUITEM SEPARATOR 38 | MENUITEM "Do Not Scan These Directories...", IDM_OPTION_DONTSCAN 39 | MENUITEM " ...But Scan Them Anyway", IDM_OPTION_SCANDONTSCAN 40 | #ifdef DEBUG 41 | MENUITEM SEPARATOR 42 | MENUITEM "Use Real Data", IDM_OPTION_REALDATA 43 | MENUITEM "Use Simulated Data", IDM_OPTION_SIMULATED 44 | MENUITEM "Use Color Wheel Data", IDM_OPTION_COLORWHEEL 45 | MENUITEM "Use Empty Drive Data", IDM_OPTION_EMPTYDRIVE 46 | MENUITEM "Use Only Dirs Data", IDM_OPTION_ONLYDIRS 47 | MENUITEM "Use Oklab Color Space", IDM_OPTION_OKLAB 48 | #endif 49 | MENUITEM SEPARATOR 50 | POPUP "Color Mode" 51 | BEGIN 52 | MENUITEM "Let Windows Choose", IDM_OPTION_AUTOCOLOR 53 | MENUITEM "Light Mode", IDM_OPTION_LIGHTMODE 54 | MENUITEM "Dark Mode", IDM_OPTION_DARKMODE 55 | END 56 | END 57 | END 58 | 59 | IDD_CONFIG_DONTSCAN DIALOG DISCARDABLE 0, 0, 252, 188 60 | STYLE DS_MODALFRAME | DS_SHELLFONT | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU 61 | CAPTION "Do Not Scan" 62 | FONT 8, "MS Shell Dlg" 63 | BEGIN 64 | LTEXT "Do not scan these &directories:",IDC_STATIC1,4,4,148,12 65 | 66 | LTEXT "",IDC_DONTSCAN_LIST,4,16,244,150 67 | 68 | PUSHBUTTON "&Add",IDC_DONTSCAN_ADD,4,170,44,14 69 | PUSHBUTTON "&Remove",IDC_DONTSCAN_REMOVE,52,170,44,14 70 | DEFPUSHBUTTON "OK",IDOK,156,170,44,14 71 | PUSHBUTTON "Cancel",IDCANCEL,204,170,44,14 72 | END 73 | 74 | -------------------------------------------------------------------------------- /manifest_src.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 12 | Disk Space Usage Viewer 13 | 14 | 15 | True 16 | PerMonitorV2, PerMonitor 17 | SegmentHeap 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /premake5.lua: -------------------------------------------------------------------------------- 1 | local to = ".build/"..(_ACTION or "nullaction") 2 | 3 | if _ACTION == "gmake2" then 4 | error("Use `premake5 gmake` instead; gmake2 neglects to link resources.") 5 | end 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | local function init_configuration(cfg) 10 | filter {cfg} 11 | defines("BUILD_"..cfg:upper()) 12 | targetdir(to.."/bin/%{cfg.buildcfg}/%{cfg.platform}") 13 | objdir(to.."/obj/") 14 | end 15 | 16 | -------------------------------------------------------------------------------- 17 | local function define_exe(name, exekind) 18 | project(name) 19 | flags("fatalwarnings") 20 | language("c++") 21 | kind(exekind or "consoleapp") 22 | end 23 | 24 | 25 | -------------------------------------------------------------------------------- 26 | workspace("elucidisk") 27 | configurations({"debug", "release"}) 28 | platforms({"x64"}) 29 | location(to) 30 | 31 | characterset("Unicode") 32 | flags("NoManifest") 33 | staticruntime("on") 34 | symbols("on") 35 | exceptionhandling("off") 36 | 37 | init_configuration("release") 38 | init_configuration("debug") 39 | 40 | filter "debug" 41 | rtti("on") 42 | optimize("off") 43 | defines("DEBUG") 44 | defines("_DEBUG") 45 | 46 | filter "release" 47 | rtti("off") 48 | optimize("full") 49 | omitframepointer("on") 50 | defines("NDEBUG") 51 | 52 | filter {"release", "action:vs*"} 53 | flags("LinkTimeOptimization") 54 | 55 | filter "action:vs*" 56 | defines("_HAS_EXCEPTIONS=0") 57 | 58 | -------------------------------------------------------------------------------- 59 | project("elucidisk") 60 | targetname("elucidisk") 61 | kind("windowedapp") 62 | links("comctl32") 63 | links("d2d1") 64 | links("dwrite") 65 | 66 | language("c++") 67 | flags("fatalwarnings") 68 | 69 | includedirs(".build/vs2022/bin") -- for the generated manifest.xml 70 | files("*.cpp") 71 | files("textonpath/*.cpp") 72 | files("main.rc") 73 | 74 | filter "action:vs*" 75 | defines("_CRT_SECURE_NO_WARNINGS") 76 | defines("_CRT_NONSTDC_NO_WARNINGS") 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- 81 | local any_warnings_or_failures = nil 82 | local msbuild_locations = { 83 | "c:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\MSBuild\\Current\\Bin", 84 | "c:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\MSBuild\\Current\\Bin", 85 | "c:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Enterprise\\MSBuild\\Current\\Bin", 86 | } 87 | 88 | -------------------------------------------------------------------------------- 89 | local release_manifest = { 90 | "elucidisk.exe", 91 | } 92 | 93 | -------------------------------------------------------------------------------- 94 | -- Some timestamp services for code signing: 95 | -- http://timestamp.digicert.com 96 | -- http://time.certum.pl 97 | -- http://sha256timestamp.ws.symantec.com/sha256/timestamp 98 | -- http://timestamp.comodoca.com/authenticode 99 | -- http://timestamp.comodoca.com 100 | -- http://timestamp.sectigo.com 101 | -- http://timestamp.globalsign.com 102 | -- http://tsa.starfieldtech.com 103 | -- http://timestamp.entrust.net/TSS/RFC3161sha2TS 104 | -- http://tsa.swisssign.net 105 | local cert_name = "Open Source Developer, Christopher Antos" 106 | --local timestamp_service = "http://timestamp.digicert.com" 107 | local timestamp_service = "http://time.certum.pl" 108 | local sign_command = string.format(' sign /n "%s" /fd sha256 /td sha256 /tr %s ', cert_name, timestamp_service) 109 | local verify_command = string.format(' verify /pa ') 110 | 111 | -------------------------------------------------------------------------------- 112 | local function warn(msg) 113 | print("\x1b[0;33;1mWARNING: " .. msg.."\x1b[m") 114 | any_warnings_or_failures = true 115 | end 116 | 117 | -------------------------------------------------------------------------------- 118 | local function failed(msg) 119 | print("\x1b[0;31;1mFAILED: " .. msg.."\x1b[m") 120 | any_warnings_or_failures = true 121 | end 122 | 123 | -------------------------------------------------------------------------------- 124 | local exec_lead = "\n" 125 | local function exec(cmd, silent) 126 | print(exec_lead .. "## EXEC: " .. cmd) 127 | 128 | if silent then 129 | cmd = "1>nul 2>nul "..cmd 130 | else 131 | -- cmd = "1>nul "..cmd 132 | end 133 | 134 | -- Premake replaces os.execute() with a version that runs path.normalize() 135 | -- which converts \ to /. That can cause problems when executing some 136 | -- programs such as cmd.exe. 137 | local prev_norm = path.normalize 138 | path.normalize = function (x) return x end 139 | local _, _, ret = os.execute(cmd) 140 | path.normalize = prev_norm 141 | 142 | return ret == 0 143 | end 144 | 145 | -------------------------------------------------------------------------------- 146 | local function exec_with_retry(cmd, tries, delay, silent) 147 | while tries > 0 do 148 | if exec(cmd, silent) then 149 | return true 150 | end 151 | 152 | tries = tries - 1 153 | 154 | if tries > 0 then 155 | print("... waiting to retry ...") 156 | local target = os.clock() + delay 157 | while os.clock() < target do 158 | -- Busy wait, but this is such a rare case that it's not worth 159 | -- trying to be more efficient. 160 | end 161 | end 162 | end 163 | 164 | return false 165 | end 166 | 167 | -------------------------------------------------------------------------------- 168 | local function mkdir(dir) 169 | if os.isdir(dir) then 170 | return 171 | end 172 | 173 | local ret = exec("md " .. path.translate(dir), true) 174 | if not ret then 175 | error("Failed to create directory '" .. dir .. "' ("..tostring(ret)..")", 2) 176 | end 177 | end 178 | 179 | -------------------------------------------------------------------------------- 180 | local function rmdir(dir) 181 | if not os.isdir(dir) then 182 | return 183 | end 184 | 185 | return exec("rd /q /s " .. path.translate(dir), true) 186 | end 187 | 188 | -------------------------------------------------------------------------------- 189 | local function unlink(file) 190 | return exec("del /q " .. path.translate(file), true) 191 | end 192 | 193 | -------------------------------------------------------------------------------- 194 | local function copy(src, dest) 195 | src = path.translate(src) 196 | dest = path.translate(dest) 197 | return exec("copy /y " .. src .. " " .. dest, true) 198 | end 199 | 200 | -------------------------------------------------------------------------------- 201 | local function rename(src, dest) 202 | src = path.translate(src) 203 | return exec("ren " .. src .. " " .. dest, true) 204 | end 205 | 206 | -------------------------------------------------------------------------------- 207 | local function file_exists(name) 208 | local f = io.open(name, "r") 209 | if f ~= nil then 210 | io.close(f) 211 | return true 212 | end 213 | return false 214 | end 215 | 216 | -------------------------------------------------------------------------------- 217 | local function have_required_tool(name, fallback) 218 | local vsver 219 | if name == "msbuild" then 220 | local opt_vsver = _OPTIONS["vsver"] 221 | if opt_vsver and opt_vsver:find("^vs") then 222 | vsver = opt_vsver:sub(3) 223 | end 224 | end 225 | 226 | if not vsver then 227 | if exec("where " .. name, true) then 228 | return name 229 | end 230 | end 231 | 232 | if fallback then 233 | local t 234 | if type(fallback) == "table" then 235 | t = fallback 236 | else 237 | t = { fallback } 238 | end 239 | for _,dir in ipairs(t) do 240 | if not vsver or dir:find(vsver) then 241 | local file = dir.."\\"..name..".exe" 242 | if file_exists(file) then 243 | return '"'..file..'"' 244 | end 245 | end 246 | end 247 | end 248 | 249 | return nil 250 | end 251 | 252 | local function parse_version_file() 253 | local ver_file = io.open("version.h") 254 | if not ver_file then 255 | error("Failed to open version.h file") 256 | end 257 | local vmaj, vmin 258 | for line in ver_file:lines() do 259 | if not vmaj then 260 | vmaj = line:match("VERSION_MAJOR%s+([^%s]+)") 261 | end 262 | if not vmin then 263 | vmin = line:match("VERSION_MINOR%s+([^%s]+)") 264 | end 265 | end 266 | ver_file:close() 267 | if not (vmaj and vmin) then 268 | error("Failed to get version info") 269 | end 270 | return vmaj .. "." .. vmin 271 | end 272 | 273 | -------------------------------------------------------------------------------- 274 | newaction { 275 | trigger = "release", 276 | description = "Elucidisk: Creates a release of Elucidisk", 277 | execute = function () 278 | local premake = _PREMAKE_COMMAND 279 | local root_dir = path.getabsolute(".build/release") .. "/" 280 | 281 | -- Check we have the tools we need. 282 | local have_msbuild = have_required_tool("msbuild", msbuild_locations) 283 | local have_7z = have_required_tool("7z", { "c:\\Program Files\\7-Zip", "c:\\Program Files (x86)\\7-Zip" }) 284 | local have_signtool = have_required_tool("signtool", { "c:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22000.0\\x64" }) 285 | 286 | -- Clone repo in release folder and checkout the specified version 287 | local code_dir = root_dir .. "~working/" 288 | rmdir(code_dir) 289 | mkdir(code_dir) 290 | 291 | exec("git clone . " .. code_dir) 292 | if not os.chdir(code_dir) then 293 | error("Failed to chdir to '" .. code_dir .. "'") 294 | end 295 | exec("git checkout " .. (_OPTIONS["commit"] or "HEAD")) 296 | 297 | -- Build the code. 298 | local x64_ok = true 299 | local toolchain = "ERROR" 300 | local build_code = function (target) 301 | if have_msbuild then 302 | target = target or "build" 303 | 304 | toolchain = _OPTIONS["vsver"] or "vs2022" 305 | exec(premake .. " " .. toolchain) 306 | os.chdir(".build/" .. toolchain) 307 | 308 | x64_ok = exec(have_msbuild .. " /m /v:q /p:configuration=release /p:platform=x64 elucidisk.sln /t:" .. target) 309 | 310 | os.chdir("../..") 311 | else 312 | error("Unable to locate msbuild.exe") 313 | end 314 | end 315 | 316 | -- Build everything. 317 | exec(premake .. " manifest") 318 | build_code() 319 | 320 | local src = path.getabsolute(".build/" .. toolchain .. "/bin/release").."/" 321 | 322 | -- Do a coarse check to make sure there's a build available. 323 | if not os.isdir(src .. ".") or not x64_ok then 324 | error("There's no build available in '" .. src .. "'") 325 | end 326 | 327 | -- Now we can sign the files. 328 | local sign = not _OPTIONS["nosign"] 329 | local signed_ok -- nil means unknown, false means failed, true means ok. 330 | local function sign_files(file_table) 331 | local orig_dir = os.getcwd() 332 | os.chdir(src) 333 | 334 | local files = "" 335 | for _, file in ipairs(file_table) do 336 | files = files .. " " .. file 337 | end 338 | 339 | -- Sectigo requests to wait 15 seconds between timestamps. 340 | -- Digicert and Certum don't mention any delay, so for now 341 | -- just let signtool do the signatures and timestamps without 342 | -- imposing external delays. 343 | signed_ok = (exec('"' .. have_signtool .. sign_command .. files .. '"') and signed_ok ~= false) or false 344 | -- Note: FAILS: cmd.exe /c "program" args "more args" 345 | -- SUCCEEDS: cmd.exe /c ""program" args "more args"" 346 | 347 | -- Verify the signatures. 348 | signed_ok = (exec(have_signtool .. verify_command .. files) and signed_ok ~= false) or false 349 | 350 | os.chdir(orig_dir) 351 | end 352 | 353 | if sign then 354 | sign_files({"x64\\elucidisk.exe"}) 355 | end 356 | 357 | -- Parse version. 358 | local version = parse_version_file() 359 | 360 | -- Now we know the version we can create our output directory. 361 | local target_dir = root_dir .. os.date("%Y%m%d_%H%M%S") .. "_" .. version .. "/" 362 | rmdir(target_dir) 363 | mkdir(target_dir) 364 | 365 | -- Package the release and the pdbs separately. 366 | os.chdir(src .. "/x64") 367 | if have_7z then 368 | --exec(have_7z .. " a -r " .. target_dir .. "elucidisk-v" .. version .. "-symbols.zip *.pdb") 369 | exec(have_7z .. " a -r " .. target_dir .. "elucidisk-v" .. version .. ".zip *.exe") 370 | end 371 | 372 | -- Tidy up code directory. 373 | os.chdir(code_dir) 374 | rmdir(".build") 375 | rmdir(".git") 376 | unlink(".gitignore") 377 | 378 | -- Report some facts about what just happened. 379 | print("\n\n") 380 | if not have_7z then warn("7-ZIP NOT FOUND -- Packing to .zip files was skipped.") end 381 | if not x64_ok then failed("x64 BUILD FAILED") end 382 | if sign and not signed_ok then 383 | failed("signing FAILED") 384 | end 385 | if not any_warnings_or_failures then 386 | print("\x1b[0;32;1mRelease " .. version .. " built successfully.\x1b[m") 387 | end 388 | end 389 | } 390 | 391 | -------------------------------------------------------------------------------- 392 | newoption { 393 | trigger = "nosign", 394 | description = "Elucidisk: don't sign the release files" 395 | } 396 | 397 | -------------------------------------------------------------------------------- 398 | newaction { 399 | trigger = "manifest", 400 | description = "Elucidisk: generate app manifest", 401 | execute = function () 402 | toolchain = _OPTIONS["vsver"] or "vs2022" 403 | local outdir = path.getabsolute(".build/" .. toolchain .. "/bin").."/" 404 | 405 | local version = parse_version_file() 406 | 407 | local src = io.open("manifest_src.xml") 408 | if not src then 409 | error("Failed to open manifest_src.xml input file") 410 | end 411 | local dst = io.open(outdir .. "manifest.xml", "w") 412 | if not dst then 413 | error("Failed to open manifest.xml output file") 414 | end 415 | 416 | for line in src:lines("*L") do 417 | line = line:gsub("%%VERSION%%", version) 418 | dst:write(line) 419 | end 420 | 421 | src:close() 422 | dst:close() 423 | 424 | print("Generated manifest.xml in " .. outdir:gsub("/", "\\") .. ".") 425 | end 426 | } 427 | 428 | -------------------------------------------------------------------------------- /res.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | //---------------------------------------------------------------------------- 7 | // Icons. 8 | 9 | #define IDI_MAIN 1 10 | 11 | //---------------------------------------------------------------------------- 12 | // Context Menus. 13 | 14 | #define IDR_CONTEXT_MENU 1000 15 | 16 | //---------------------------------------------------------------------------- 17 | // Dialogs. 18 | 19 | #define IDD_CONFIG_DONTSCAN 1000 20 | 21 | //---------------------------------------------------------------------------- 22 | // Menu Commands. 23 | 24 | #define IDM_OPEN_DIRECTORY 2000 25 | #define IDM_OPEN_FILE 2001 26 | #define IDM_RECYCLE_ENTRY 2002 27 | #define IDM_DELETE_ENTRY 2003 28 | #define IDM_HIDE_DIRECTORY 2004 29 | #define IDM_SHOW_DIRECTORY 2005 30 | #define IDM_EMPTY_RECYCLEBIN 2006 31 | #define IDM_RESCAN 2007 32 | 33 | #define IDM_OPTION_COMPRESSED 2100 34 | #define IDM_OPTION_FREESPACE 2101 35 | #define IDM_OPTION_NAMES 2102 36 | #define IDM_OPTION_COMPBAR 2103 37 | #define IDM_OPTION_PROPORTION 2104 38 | #define IDM_OPTION_DONTSCAN 2105 39 | #define IDM_OPTION_SCANDONTSCAN 2106 40 | 41 | #define IDM_OPTION_AUTOCOLOR 2160 42 | #define IDM_OPTION_LIGHTMODE 2161 43 | #define IDM_OPTION_DARKMODE 2162 44 | 45 | #define IDM_OPTION_PLAIN 2170 46 | #define IDM_OPTION_RAINBOW 2171 47 | #define IDM_OPTION_HEATMAP 2172 48 | 49 | #ifdef DEBUG 50 | #define IDM_OPTION_REALDATA 2180 51 | #define IDM_OPTION_SIMULATED 2181 52 | #define IDM_OPTION_COLORWHEEL 2182 53 | #define IDM_OPTION_EMPTYDRIVE 2183 54 | #define IDM_OPTION_ONLYDIRS 2184 55 | #define IDM_OPTION_OKLAB 2185 56 | #endif 57 | 58 | #define IDM_REFRESH 2200 59 | #define IDM_BACK 2201 60 | #define IDM_UP 2202 61 | #define IDM_SUMMARY 2203 62 | #define IDM_FOLDER 2204 63 | #define IDM_APPWIZ 2205 64 | 65 | #define IDM_DRIVE_FIRST 2300 66 | #define IDM_DRIVE_LAST 2349 67 | 68 | //---------------------------------------------------------------------------- 69 | // Generic Controls. 70 | 71 | #define IDC_STATIC1 501 72 | #define IDC_STATIC2 502 73 | #define IDC_STATIC3 503 74 | #define IDC_STATIC4 504 75 | #define IDC_STATIC5 505 76 | 77 | #define IDC_BUTTON1 521 78 | #define IDC_BUTTON2 522 79 | #define IDC_BUTTON3 523 80 | #define IDC_BUTTON4 524 81 | #define IDC_BUTTON5 525 82 | 83 | #define IDC_LIST1 541 84 | #define IDC_LIST2 542 85 | #define IDC_LIST3 543 86 | #define IDC_LIST4 544 87 | #define IDC_LIST5 545 88 | 89 | //---------------------------------------------------------------------------- 90 | // Dialog Controls. 91 | 92 | #define IDC_DONTSCAN_ADD IDC_BUTTON1 93 | #define IDC_DONTSCAN_REMOVE IDC_BUTTON2 94 | #define IDC_DONTSCAN_LIST IDC_LIST1 95 | 96 | -------------------------------------------------------------------------------- /scan.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #include "main.h" 5 | #include "data.h" 6 | #include "scan.h" 7 | #include 8 | 9 | static void get_drive(const WCHAR* path, std::wstring& out) 10 | { 11 | out.clear(); 12 | 13 | path += has_io_prefix(path); 14 | 15 | if (path[0] && path[1] == ':' && unsigned(towlower(path[0]) - 'a') <= ('z' - 'a')) 16 | { 17 | WCHAR tmp[3] = { towupper(path[0]), ':' }; 18 | out = tmp; 19 | } 20 | } 21 | 22 | static void capitalize_drive_part(std::wstring& inout) 23 | { 24 | std::wstring out; 25 | const WCHAR* path = inout.c_str(); 26 | 27 | unsigned int pfxlen = has_io_prefix(path); 28 | out.append(path, pfxlen); 29 | path += pfxlen; 30 | 31 | std::wstring drive; 32 | get_drive(path, drive); 33 | out.append(drive.c_str()); 34 | path += drive.length(); 35 | 36 | out.append(path); 37 | 38 | inout = std::move(out); 39 | } 40 | 41 | std::shared_ptr MakeRoot(const WCHAR* _path) 42 | { 43 | std::wstring path; 44 | if (!_path) 45 | { 46 | const DWORD needed = GetCurrentDirectory(0, nullptr); 47 | if (needed) 48 | { 49 | WCHAR* buffer = new WCHAR[needed]; 50 | if (buffer) 51 | { 52 | const DWORD used = GetCurrentDirectory(needed, buffer); 53 | if (used > 0 && used < needed) 54 | get_drive(buffer, path); 55 | delete [] buffer; 56 | } 57 | } 58 | if (path.empty()) 59 | path = TEXT("."); 60 | } 61 | else 62 | { 63 | path = _path; 64 | } 65 | 66 | if (path.empty()) 67 | return nullptr; 68 | 69 | // Everyone knows about "*" and "?" wildcards. But Windows actually 70 | // supports FIVE wildcards! 71 | // 72 | // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-_fsrtl_advanced_fcb_header-fsrtlisnameinexpression 73 | // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-_fsrtl_advanced_fcb_header-fsrtldoesnamecontainwildcards 74 | // 75 | // The ntifs.h header file shows the definitions of DOS_DOT, DOS_QM, and 76 | // DOS_STAR. 77 | // 78 | // Here is the full table of wildcards: 79 | // asterisk * Matches zero or more characters. 80 | // question mark ? Matches a single character. 81 | // DOS_DOT < Matches either a period or zero characters beyond 82 | // the name string. 83 | // DOS_QM > Matches any single character or, upon encountering 84 | // a period or end of name string, advances the 85 | // expression to the end of the set of contiguous 86 | // DOS_QMs ('>' characters). 87 | // DOS_STAR " Matches zero or more characters until encountering 88 | // and matching the final . in the name. 89 | if (wcspbrk(path.c_str(), TEXT("*?<>\""))) 90 | return nullptr; 91 | 92 | ensure_separator(path); 93 | 94 | const DWORD needed = GetFullPathName(path.c_str(), 0, nullptr, nullptr); 95 | if (needed) 96 | { 97 | WCHAR* buffer = new WCHAR[needed]; 98 | if (buffer) 99 | { 100 | const DWORD used = GetFullPathName(path.c_str(), needed, buffer, nullptr); 101 | if (used > 0 && used < needed) 102 | { 103 | path = buffer; 104 | ensure_separator(path); 105 | } 106 | delete [] buffer; 107 | } 108 | } 109 | 110 | capitalize_drive_part(path); 111 | 112 | std::shared_ptr root; 113 | if (is_drive(path.c_str())) 114 | root = std::make_shared(path.c_str()); 115 | else 116 | root = std::make_shared(path.c_str()); 117 | 118 | return root; 119 | } 120 | 121 | #ifdef DEBUG 122 | void AddColorWheelDir(const std::shared_ptr parent, const WCHAR* name, int depth, ScanContext& context) 123 | { 124 | depth--; 125 | 126 | if (!depth) 127 | { 128 | std::lock_guard lock(context.mutex); 129 | 130 | parent->AddFile(TEXT("x"), 1024); 131 | } 132 | else 133 | { 134 | AddColorWheelDir(parent->AddDir(name), name, depth, context); 135 | } 136 | 137 | parent->Finish(); 138 | } 139 | 140 | static void FakeScan(const std::shared_ptr root, size_t index, bool include_free_space, ScanContext& context) 141 | { 142 | switch (g_fake_data) 143 | { 144 | case FDM_COLORWHEEL: 145 | for (int ii = 0; ii < 360; ii += 10) 146 | { 147 | WCHAR sz[100]; 148 | swprintf_s(sz, _countof(sz), TEXT("%u to %u"), ii, ii + 10); 149 | AddColorWheelDir(root, sz, ii ? 10 : 11, context); 150 | } 151 | break; 152 | 153 | default: 154 | case FDM_SIMULATED: 155 | { 156 | static const ULONGLONG units = 1024; 157 | 158 | std::vector> dirs; 159 | 160 | if (include_free_space) 161 | { 162 | DriveNode* drive = root->AsDrive(); 163 | dirs.emplace_back(root->AddDir(TEXT("Abc"))); 164 | dirs.emplace_back(root->AddDir(TEXT("Def"))); 165 | 166 | std::lock_guard lock(context.mutex); 167 | 168 | if (drive) 169 | drive->AddFreeSpace(1000 * units, 2000 * units); 170 | } 171 | else if (root->GetParent() && root->GetParent()->GetParent()) 172 | { 173 | return; 174 | } 175 | else 176 | { 177 | std::lock_guard lock(context.mutex); 178 | 179 | root->AddFile(TEXT("Red"), 4000 * units); 180 | root->AddFile(TEXT("Green"), 8000 * units); 181 | if (index > 0) 182 | { 183 | std::shared_ptr d = root->AddDir(TEXT("Blue")); 184 | d->AddFile(TEXT("Lightning"), 12000 * units); 185 | d->Finish(); 186 | } 187 | } 188 | 189 | for (size_t ii = 0; ii < dirs.size(); ++ii) 190 | FakeScan(dirs[ii], ii, false, context); 191 | } 192 | break; 193 | 194 | case FDM_EMPTYDRIVE: 195 | break; 196 | 197 | case FDM_ONLYDIRS: 198 | { 199 | std::lock_guard lock(context.mutex); 200 | 201 | std::vector> dirs; 202 | dirs.emplace_back(root->AddDir(TEXT("Abc"))); 203 | dirs.emplace_back(root->AddDir(TEXT("Def"))); 204 | dirs.emplace_back(root->AddDir(TEXT("Ghi"))); 205 | 206 | for (auto& dir : dirs) 207 | dir->Finish(); 208 | } 209 | break; 210 | } 211 | 212 | root->Finish(); 213 | } 214 | #endif 215 | 216 | void Scan(const std::shared_ptr& root, const LONG this_generation, volatile LONG* current_generation, ScanContext& context) 217 | { 218 | if (root->AsRecycleBin()) 219 | { 220 | std::lock_guard lock(context.mutex); 221 | 222 | context.current = root; 223 | root->AsRecycleBin()->UpdateRecycleBin(context.mutex); 224 | root->Finish(); 225 | return; 226 | } 227 | 228 | DriveNode* drive = (root->AsDrive() && !is_subst(root->GetName())) ? root->AsDrive() : nullptr; 229 | 230 | std::wstring find; 231 | root->GetFullPath(find); 232 | ensure_separator(find); 233 | 234 | #ifdef DEBUG 235 | if (g_fake_data) 236 | { 237 | const bool was = SetFake(true); 238 | FakeScan(root, 0, true, context); 239 | SetFake(was); 240 | return; 241 | } 242 | #endif 243 | 244 | const bool use_compressed_size = context.use_compressed_size; 245 | const size_t base_path_len = find.length(); 246 | find.append(TEXT("*")); 247 | 248 | std::vector> dirs; 249 | std::wstring test(find); 250 | 251 | WIN32_FIND_DATA fd; 252 | HANDLE hFind = FindFirstFile(find.c_str(), &fd); 253 | if (hFind != INVALID_HANDLE_VALUE) 254 | { 255 | DWORD tick = GetTickCount(); 256 | ULONGLONG num = 0; 257 | 258 | do 259 | { 260 | std::lock_guard lock(context.mutex); 261 | 262 | const bool compressed = (use_compressed_size && (fd.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)); 263 | 264 | if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 265 | { 266 | if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) 267 | continue; 268 | if (!wcscmp(fd.cFileName, TEXT(".")) || !wcscmp(fd.cFileName, TEXT(".."))) 269 | continue; 270 | if (drive && !wcsicmp(fd.cFileName, TEXT("$recycle.bin"))) 271 | continue; 272 | 273 | if (context.dontscan.size()) 274 | { 275 | bool match = false; 276 | test.resize(base_path_len); 277 | test.append(fd.cFileName); 278 | ensure_separator(test); 279 | for (const auto& ignore : context.dontscan) 280 | { 281 | match = !wcsicmp(ignore.c_str(), test.c_str()); 282 | if (match) 283 | break; 284 | } 285 | if (match) 286 | continue; 287 | } 288 | 289 | dirs.emplace_back(root->AddDir(fd.cFileName)); 290 | assert(dirs.back()); 291 | 292 | if (compressed) 293 | dirs.back()->SetCompressed(); 294 | 295 | if (++num > 50 || GetTickCount() - tick > 50) 296 | { 297 | context.current = dirs.back(); 298 | LResetFeedbackInterval: 299 | tick = GetTickCount(); 300 | num = 0; 301 | } 302 | } 303 | else 304 | { 305 | ULARGE_INTEGER uli; 306 | if (compressed || (fd.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) 307 | { 308 | find.resize(base_path_len); 309 | find.append(fd.cFileName); 310 | uli.LowPart = GetCompressedFileSize(find.c_str(), &uli.HighPart); 311 | } 312 | else 313 | { 314 | uli.HighPart = fd.nFileSizeHigh; 315 | uli.LowPart = fd.nFileSizeLow; 316 | } 317 | 318 | std::shared_ptr file = root->AddFile(fd.cFileName, uli.QuadPart); 319 | assert(file); 320 | 321 | if (compressed) 322 | file->SetCompressed(); 323 | if (fd.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) 324 | file->SetSparse(); 325 | 326 | if (++num > 50 || GetTickCount() - tick > 50) 327 | { 328 | context.current = file; 329 | goto LResetFeedbackInterval; 330 | } 331 | } 332 | } 333 | while (this_generation == *current_generation && FindNextFile(hFind, &fd)); 334 | 335 | FindClose(hFind); 336 | hFind = INVALID_HANDLE_VALUE; 337 | } 338 | 339 | for (const auto dir : dirs) 340 | { 341 | if (this_generation != *current_generation) 342 | break; 343 | Scan(dir, this_generation, current_generation, context); 344 | } 345 | 346 | if (this_generation == *current_generation && drive) 347 | { 348 | drive->AddRecycleBin(); 349 | const auto recycle = drive->GetRecycleBin(); 350 | 351 | if (recycle) 352 | { 353 | context.current = recycle; 354 | recycle->UpdateRecycleBin(context.mutex); 355 | recycle->Finish(); 356 | } 357 | } 358 | 359 | root->Finish(); 360 | } 361 | 362 | -------------------------------------------------------------------------------- /scan.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | class DirNode; 9 | 10 | struct ScanContext 11 | { 12 | std::recursive_mutex& mutex; 13 | std::shared_ptr& current; 14 | bool use_compressed_size = false; 15 | std::vector dontscan; 16 | }; 17 | 18 | std::shared_ptr MakeRoot(const WCHAR* path); 19 | void Scan(const std::shared_ptr& root, LONG this_generation, volatile LONG* current_generation, ScanContext& context); 20 | 21 | -------------------------------------------------------------------------------- /sunburst.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #include "data.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "TextOnPath/PathTextRenderer.h" 14 | #include 15 | 16 | //#define USE_CHART_OUTLINE // Experimenting with this off. 17 | 18 | #define MAX_SUNBURST_DEPTH 20 19 | 20 | class DirNode; 21 | struct SunburstMetrics; 22 | class Sunburst; 23 | 24 | HRESULT InitializeD2D(); 25 | HRESULT InitializeDWrite(); 26 | 27 | bool GetD2DFactory(ID2D1Factory** ppFactory); 28 | bool GetDWriteFactory(IDWriteFactory2** ppFactory); 29 | 30 | #ifdef DEBUG 31 | bool GetUseOklab(); 32 | void SetUseOklab(bool use); 33 | #endif 34 | 35 | enum WriteTextOptions 36 | { 37 | WTO_NONE = 0x0000, 38 | WTO_REMEMBER_METRICS = 0x0001, 39 | WTO_CLIP = 0x0002, 40 | WTO_HCENTER = 0x0004, 41 | WTO_VCENTER = 0x0008, 42 | WTO_RIGHT_ALIGN = 0x0010, 43 | WTO_BOTTOM_ALIGN = 0x0020, 44 | 45 | WTO_UNDERLINE = 0x8000, 46 | }; 47 | DEFINE_ENUM_FLAG_OPERATORS(WriteTextOptions); 48 | 49 | struct Shortened 50 | { 51 | std::wstring m_text; 52 | FLOAT m_extent = 0.0f; 53 | size_t m_orig_offset = 0; 54 | }; 55 | 56 | class DirectHwndRenderTarget 57 | { 58 | struct Resources 59 | { 60 | HRESULT Init(HWND hwnd, const D2D1_SIZE_U& size, const DpiScaler& dpi, bool dark_mode); 61 | 62 | SPI m_spFactory; 63 | SPI m_spDWriteFactory; 64 | 65 | SPI m_spTarget; 66 | SPQI m_spContext; 67 | 68 | SPI m_spLineBrush; 69 | SPI m_spFileLineBrush; 70 | SPI m_spFillBrush; 71 | SPI m_spOutlineBrush; 72 | SPI m_spOutlineBrush2; 73 | SPI m_spTextBrush; 74 | 75 | SPI m_spRoundedStroke; 76 | SPI m_spBevelStroke; 77 | 78 | SPI m_spTextFormat; 79 | SPI m_spHeaderTextFormat; 80 | SPI m_spAppInfoTextFormat; 81 | FLOAT m_fontSize = 0.0f; 82 | FLOAT m_headerFontSize = 0.0f; 83 | FLOAT m_appInfoFontSize = 0.0f; 84 | 85 | SPI m_spArcTextFormat; 86 | SPI m_spRenderingParams; 87 | SPI m_spPathTextRenderer; 88 | FLOAT m_arcFontSize = 0.0f; 89 | 90 | D2D1_POINT_2F m_lastTextPosition = D2D1::Point2F(); 91 | D2D1_SIZE_F m_lastTextSize = D2D1::SizeF(); 92 | }; 93 | 94 | public: 95 | DirectHwndRenderTarget(); 96 | ~DirectHwndRenderTarget(); 97 | 98 | HRESULT CreateDeviceResources(HWND hwnd, const DpiScaler& dpi, bool dark_mode); 99 | HRESULT ResizeDeviceResources(); 100 | void ReleaseDeviceResources(); 101 | 102 | ID2D1Factory* Factory() const { return m_resources->m_spFactory; } 103 | IDWriteFactory* DWriteFactory() const { return m_resources->m_spDWriteFactory; } 104 | ID2D1RenderTarget* Target() const { return m_resources->m_spTarget; } 105 | 106 | ID2D1SolidColorBrush* LineBrush() const { return m_resources->m_spLineBrush; } 107 | ID2D1SolidColorBrush* FileLineBrush() const { return m_resources->m_spFileLineBrush; } 108 | ID2D1SolidColorBrush* FillBrush() const { return m_resources->m_spFillBrush; } 109 | ID2D1SolidColorBrush* OutlineBrush() const { return m_resources->m_spOutlineBrush; } 110 | ID2D1SolidColorBrush* OutlineBrush2() const { return m_resources->m_spOutlineBrush2; } 111 | ID2D1SolidColorBrush* TextBrush() const { return m_resources->m_spTextBrush; } 112 | 113 | ID2D1StrokeStyle* RoundedStrokeStyle() const { return m_resources->m_spRoundedStroke; } 114 | ID2D1StrokeStyle* BevelStrokeStyle() const { return m_resources->m_spBevelStroke; } 115 | 116 | IDWriteTextFormat* TextFormat() const { return m_resources->m_spTextFormat; } 117 | FLOAT FontSize() const { return m_resources->m_fontSize; } 118 | IDWriteTextFormat* HeaderTextFormat() const { return m_resources->m_spHeaderTextFormat; } 119 | FLOAT HeaderFontSize() const { return m_resources->m_headerFontSize; } 120 | IDWriteTextFormat* AppInfoTextFormat() const { return m_resources->m_spAppInfoTextFormat; } 121 | FLOAT AppInfoFontSize() const { return m_resources->m_appInfoFontSize; } 122 | 123 | ID2D1DeviceContext* Context() const { return m_resources->m_spContext; } 124 | IDWriteTextFormat* ArcTextFormat() const { return m_resources->m_spArcTextFormat; } 125 | PathTextRenderer* ArcTextRenderer() const { return m_resources->m_spPathTextRenderer; } 126 | FLOAT ArcFontSize() const { return m_resources->m_arcFontSize; } 127 | 128 | bool CreateTextFormat(FLOAT fontsize, DWRITE_FONT_WEIGHT weight, IDWriteTextFormat** ppTextFormat) const; 129 | 130 | bool ShortenText(IDWriteTextFormat* format, const D2D1_RECT_F& rect, const WCHAR* text, size_t len, FLOAT target, Shortened& out, int ellipsis=1); 131 | bool MeasureText(IDWriteTextFormat* format, const D2D1_RECT_F& rect, const WCHAR* text, size_t len, D2D1_SIZE_F& size, IDWriteTextLayout** ppLayout=nullptr); 132 | bool MeasureText(IDWriteTextFormat* format, const D2D1_RECT_F& rect, const std::wstring& text, D2D1_SIZE_F& size, IDWriteTextLayout** ppLayout=nullptr); 133 | bool WriteText(IDWriteTextFormat* format, FLOAT x, FLOAT y, const D2D1_RECT_F& rect, const WCHAR* text, size_t len, WriteTextOptions options=WTO_NONE, IDWriteTextLayout* pLayout=nullptr); 134 | bool WriteText(IDWriteTextFormat* format, FLOAT x, FLOAT y, const D2D1_RECT_F& rect, const std::wstring& text, WriteTextOptions options=WTO_NONE, IDWriteTextLayout* pLayout=nullptr); 135 | D2D1_POINT_2F LastTextPosition() const { return m_resources->m_lastTextPosition; } 136 | D2D1_SIZE_F LastTextSize() const { return m_resources->m_lastTextSize; } 137 | 138 | private: 139 | HWND m_hwnd = 0; 140 | std::unique_ptr m_resources; 141 | }; 142 | 143 | struct SunburstMetrics 144 | { 145 | SunburstMetrics(const Sunburst& sunburst); 146 | SunburstMetrics(const DpiScaler& dpi, const D2D1_RECT_F& bounds, FLOAT max_extent); 147 | FLOAT get_thickness(size_t depth) const; 148 | 149 | const FLOAT stroke; 150 | const FLOAT margin; 151 | const FLOAT indicator_thickness; 152 | const FLOAT boundary_radius; 153 | const FLOAT center_radius; 154 | const FLOAT max_radius; 155 | const FLOAT range_radius; 156 | const FLOAT min_arc; 157 | 158 | private: 159 | FLOAT thicknesses[MAX_SUNBURST_DEPTH]; 160 | }; 161 | 162 | class Sunburst 163 | { 164 | friend struct SunburstMetrics; 165 | 166 | struct Arc 167 | { 168 | float m_start; 169 | float m_end; 170 | std::shared_ptr m_node; 171 | }; 172 | 173 | struct HighlightInfo 174 | { 175 | Arc m_arc; 176 | SPI m_geometry; 177 | size_t m_depth; 178 | FLOAT m_arctext_radius; 179 | bool m_show_names; 180 | }; 181 | 182 | public: 183 | Sunburst(); 184 | ~Sunburst(); 185 | 186 | bool OnDpiChanged(const DpiScaler& dpi); 187 | void UseDarkMode(bool dark) { m_dark_mode = dark; } 188 | bool SetBounds(const D2D1_RECT_F& rect, FLOAT max_extent); 189 | void BuildRings(const SunburstMetrics& mx, const std::vector>& roots); 190 | void RenderRings(DirectHwndRenderTarget& target, const SunburstMetrics& mx, const std::shared_ptr& highlight); 191 | void FormatSize(ULONGLONG size, std::wstring& text, std::wstring& units, int places=-1); 192 | std::shared_ptr HitTest(const SunburstMetrics& mx, POINT pt, bool* is_free=nullptr); 193 | 194 | protected: 195 | D2D1_COLOR_F MakeColor(const Arc& arc, size_t depth, bool highlight); 196 | D2D1_COLOR_F MakeRootColor(bool highlight, bool free); 197 | static void MakeArc(std::vector& arcs, FLOAT outer_radius, FLOAT min_arc, const std::shared_ptr& node, ULONGLONG size, double& sweep, double total, float start, float span, double convert=1.0f); 198 | std::vector NextRing(const std::vector& parent_ring, FLOAT outer_radius, FLOAT min_arc); 199 | void AddArcToSink(ID2D1GeometrySink* pSink, bool counter_clockwise, FLOAT start, FLOAT end, const D2D1_POINT_2F& end_point, FLOAT radius); 200 | bool MakeArcGeometry(DirectHwndRenderTarget& target, FLOAT start, FLOAT end, FLOAT inner_radius, FLOAT outer_radius, ID2D1Geometry** ppGeometry); 201 | void DrawArcText(DirectHwndRenderTarget& target, const Arc& arc, FLOAT radius); 202 | 203 | private: 204 | void RenderRingsInternal(DirectHwndRenderTarget& target, const SunburstMetrics& mx, const std::shared_ptr& highlight, bool files, HighlightInfo& highlightInfo); 205 | int DrawArcTextInternal(DirectHwndRenderTarget& target, IDWriteFactory* pFactory, const WCHAR* text, UINT32 length, FLOAT start, FLOAT end, FLOAT radius, bool only_test_fit=false); 206 | 207 | private: 208 | DpiScaler m_dpi; 209 | DpiScaler m_dpiWithTextScaling; 210 | FLOAT m_min_arc_text_len = 0; 211 | FLOAT m_max_extent = 0; 212 | D2D1_RECT_F m_bounds = D2D1::RectF(); 213 | D2D1_POINT_2F m_center = D2D1::Point2F(); 214 | UnitScale m_units = UnitScale::MB; 215 | bool m_dark_mode = false; 216 | 217 | std::vector> m_roots; 218 | std::vector> m_rings; 219 | std::vector m_start_angles; 220 | std::vector m_free_angles; 221 | }; 222 | 223 | -------------------------------------------------------------------------------- /ui.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | HWND MakeUi(HINSTANCE hinst, int argc, const WCHAR** argv); 7 | 8 | -------------------------------------------------------------------------------- /version.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Christopher Antos 2 | // License: http://opensource.org/licenses/MIT 3 | 4 | #pragma once 5 | 6 | #define VERSION_MAJOR 1 7 | #define VERSION_MINOR 4 8 | 9 | #define COPYRIGHT_STR "Copyright (C) 2023-2025 Christopher Antos" 10 | 11 | #define IND_VER2( a, b ) #a ## "." ## #b 12 | #define IND_VER4( a, b, c, d ) L#a ## L"." ## L#b ## L"." ## L#c ## L"." ## L#d 13 | #define DOT_VER2( a, b ) IND_VER2( a, b ) 14 | #define DOT_VER4( a, b, c, d ) IND_VER4( a, b, c, d ) 15 | 16 | #define RC_VERSION VERSION_MAJOR, VERSION_MINOR, 0, 0 17 | #define RC_VERSION_STR DOT_VER4( VERSION_MAJOR, VERSION_MINOR, 0, 0 ) 18 | 19 | #define VERSION_STR DOT_VER2( VERSION_MAJOR, VERSION_MINOR ) 20 | 21 | -------------------------------------------------------------------------------- /version.rc: -------------------------------------------------------------------------------- 1 | #include "version.h" 2 | 3 | 1 VERSIONINFO 4 | FILEVERSION RC_VERSION 5 | PRODUCTVERSION RC_VERSION 6 | FILEFLAGSMASK 0x3fL 7 | #ifdef _DEBUG 8 | FILEFLAGS 0x1L 9 | #else 10 | FILEFLAGS 0x0L 11 | #endif 12 | FILEOS 0x40004L 13 | FILETYPE 0x1L 14 | FILESUBTYPE 0x0L 15 | BEGIN 16 | BLOCK "StringFileInfo" 17 | BEGIN 18 | BLOCK "040904b0" 19 | BEGIN 20 | VALUE "Comments", "Disk space usage visualizer for Windows" 21 | VALUE "CompanyName", "Christopher Antos" 22 | VALUE "FileDescription", "Elucidisk" 23 | VALUE "FileVersion", RC_VERSION_STR 24 | VALUE "InternalName", "Elucidisk" 25 | VALUE "LegalCopyright", COPYRIGHT_STR 26 | VALUE "OriginalFilename", "elucidisk.exe" 27 | VALUE "ProductName", "Elucidisk" 28 | VALUE "ProductVersion", RC_VERSION_STR 29 | END 30 | END 31 | BLOCK "VarFileInfo" 32 | BEGIN 33 | VALUE "Translation", 0x409, 1200 34 | END 35 | END 36 | --------------------------------------------------------------------------------