diff --git a/base/applications/CMakeLists.txt b/base/applications/CMakeLists.txt index dccaaa80438..03679ac33f9 100644 --- a/base/applications/CMakeLists.txt +++ b/base/applications/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(atactl) add_subdirectory(cacls) add_subdirectory(calc) add_subdirectory(charmap) +add_subdirectory(cleanmgr) add_subdirectory(clipbrd) add_subdirectory(cmdutils) add_subdirectory(control) diff --git a/base/applications/cleanmgr/CMakeLists.txt b/base/applications/cleanmgr/CMakeLists.txt new file mode 100644 index 00000000000..e3e3c65f6f9 --- /dev/null +++ b/base/applications/cleanmgr/CMakeLists.txt @@ -0,0 +1,9 @@ + +project(cleanmgr) + +# The main application +add_subdirectory(cleanmgr) + +# Cleanup handlers +#add_subdirectory(dataclen) # Data Driven Cleaner + diff --git a/base/applications/cleanmgr/cleanmgr/CCleanupHandler.cpp b/base/applications/cleanmgr/cleanmgr/CCleanupHandler.cpp new file mode 100644 index 00000000000..f04e1346466 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CCleanupHandler.cpp @@ -0,0 +1,212 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: CCleanupHandler implementation + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#include "cleanmgr.h" + + +CCleanupHandler::CCleanupHandler(CRegKey &subKey, const CStringW &keyName, const GUID &guid) + : hSubKey(subKey) + , KeyName(keyName) + , Guid(guid) + , dwFlags(0) + , Priority(0) + , StateFlags(0) + , SpaceUsed(0) + , ShowHandler(true) + , hIcon(NULL) +{ +} + +CCleanupHandler::~CCleanupHandler() +{ + Deactivate(); + ::DestroyIcon(hIcon); +} + +void +CCleanupHandler::Deactivate() +{ + if (Handler) + { + DWORD dwFlags = 0; + Handler->Deactivate(&dwFlags); + if (dwFlags & EVCF_REMOVEFROMLIST) + UNIMPLEMENTED_DBGBREAK(); + } +} + +bool +CCleanupHandler::Initialize(LPCWSTR pcwszVolume) +{ + if (FAILED_UNEXPECTEDLY( + ::CoCreateInstance(Guid, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARG(IEmptyVolumeCache, &Handler)))) + { + return false; + } + + DWORD dwSize = sizeof(Priority); + if (hSubKey.QueryBinaryValue(L"Priority", &Priority, &dwSize) != ERROR_SUCCESS) + { + if (hSubKey.QueryDWORDValue(L"Priority", Priority) != ERROR_SUCCESS) + Priority = 200; + } + + dwSize = sizeof(StateFlags); + if (hSubKey.QueryDWORDValue(L"StateFlags", StateFlags) != ERROR_SUCCESS) + StateFlags = 0; + + WCHAR PathBuffer[MAX_PATH] = {}; + ULONG nChars = _countof(PathBuffer); + if (hSubKey.QueryStringValue(L"IconPath", PathBuffer, &nChars) != ERROR_SUCCESS) + { + CStringW Tmp; + WCHAR GuidStr[50] = {}; + if (StringFromGUID2(Guid, GuidStr, _countof(GuidStr))) + { + Tmp.Format(L"CLSID\\%s\\DefaultIcon", GuidStr); + CRegKey clsid; + nChars = _countof(PathBuffer); + if (clsid.Open(HKEY_CLASSES_ROOT, Tmp, KEY_READ) != ERROR_SUCCESS || + clsid.QueryStringValue(NULL, PathBuffer, &nChars) != ERROR_SUCCESS) + { + PathBuffer[0] = UNICODE_NULL; + } + } + } + if (!PathBuffer[0]) + StringCchCopyW(PathBuffer, _countof(PathBuffer), L"%systemroot%\\system32\\shell32.dll"); + + int Index = 0; + WCHAR *ptr = wcschr(PathBuffer, L','); + if (ptr) + { + *ptr++ = UNICODE_NULL; + Index = wcstol(ptr, NULL, 10); + } + HICON Large, Small; + UINT Result = ExtractIconExW(PathBuffer, Index, &Large, &Small, 1); + if (Result < 1) + Result = ExtractIconExW(L"%systemroot%\\system32\\shell32.dll", 0, &Large, &Small, 1); + if (Result >= 1) + { + hIcon = Small; + if (!hIcon) + { + hIcon = Large; + } + else + { + ::DestroyIcon(Large); + } + } + + // These options should come from the command line + // dwFlags |= EVCF_SETTINGSMODE; + // dwFlags |= EVCF_OUTOFDISKSPACE; + + CComPtr spHandler2; + HRESULT hr = Handler->QueryInterface(IID_PPV_ARG(IEmptyVolumeCache2, &spHandler2)); + if (SUCCEEDED(hr)) + { + hr = spHandler2->InitializeEx( + hSubKey, pcwszVolume, KeyName, &wszDisplayName, &wszDescription, &wszBtnText, &dwFlags); + if (FAILED_UNEXPECTEDLY(hr)) + return false; + + // No files to clean will return S_FALSE; + if (hr != S_OK) + return false; + } + else + { + // Observed behavior: + // When Initialize is called, wszDescription is actually pointing to data + // wszDescription.AllocateBytes(0x400u); + hr = Handler->Initialize(hSubKey, pcwszVolume, &wszDisplayName, &wszDescription, &dwFlags); + if (FAILED_UNEXPECTEDLY(hr)) + return false; + + // No files to clean will return S_FALSE; + if (hr != S_OK) + return false; + + CComPtr spBag; + WCHAR GuidStr[50] = {}; + nChars = _countof(GuidStr); + if (hSubKey.QueryStringValue(L"PropertyBag", GuidStr, &nChars) == ERROR_SUCCESS) + { + GUID guid = {}; + if (!FAILED_UNEXPECTEDLY(CLSIDFromString(GuidStr, &guid))) + { + FAILED_UNEXPECTEDLY( + CoCreateInstance(guid, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARG(IPropertyBag, &spBag))); + } + } + ReadProperty(L"Display", spBag, wszDisplayName); + ReadProperty(L"Description", spBag, wszDescription); + + if (dwFlags & EVCF_HASSETTINGS) + { + ReadProperty(L"AdvancedButtonText", spBag, wszBtnText); + } + } + + if ((dwFlags & EVCF_ENABLEBYDEFAULT) && !(StateFlags & HANDLER_STATE_SELECTED)) + { + StateFlags |= HANDLER_STATE_SELECTED; + } + + // For convenience + if (!wszDisplayName) + SHStrDupW(KeyName, &wszDisplayName); + + return true; +} + +void +CCleanupHandler::ReadProperty(LPCWSTR Name, IPropertyBag *pBag, CComHeapPtr &storage) +{ + if (storage) + return; + + if (pBag) + { + CComVariant tmp; + tmp.vt = VT_BSTR; + HRESULT hr = pBag->Read(Name, &tmp, NULL); + if (!FAILED_UNEXPECTEDLY(hr) && tmp.vt == VT_BSTR) + { + SHStrDupW(tmp.bstrVal, &storage); + } + } + + if (!storage) + { + WCHAR TmpStr[0x200] = {}; + DWORD dwSize = _countof(TmpStr); + + if (hSubKey.QueryStringValue(Name, TmpStr, &dwSize) == ERROR_SUCCESS) + { + WCHAR ResolvedStr[0x200] = {}; + SHLoadIndirectString(TmpStr, ResolvedStr, _countof(ResolvedStr), NULL); + SHStrDupW(ResolvedStr, &storage); + } + } +} + +BOOL +CCleanupHandler::HasSettings() const +{ + return !!(dwFlags & EVCF_HASSETTINGS); +} + +BOOL +CCleanupHandler::DontShowIfZero() const +{ + return !!(dwFlags & EVCF_DONTSHOWIFZERO); +} + diff --git a/base/applications/cleanmgr/cleanmgr/CCleanupHandler.hpp b/base/applications/cleanmgr/cleanmgr/CCleanupHandler.hpp new file mode 100644 index 00000000000..3b829a82c21 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CCleanupHandler.hpp @@ -0,0 +1,48 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: CCleanupHandler definition + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#define HANDLER_STATE_SELECTED 1 + + +struct CCleanupHandler +{ + CCleanupHandler(CRegKey &subKey, const CStringW &keyName, const GUID &guid); + ~CCleanupHandler(); + + void Deactivate(); + + bool + Initialize(LPCWSTR pcwszVolume); + + void + ReadProperty(LPCWSTR Name, IPropertyBag *pBag, CComHeapPtr &storage); + + BOOL + HasSettings() const; + + BOOL + DontShowIfZero() const; + + CRegKey hSubKey; + CStringW KeyName; + GUID Guid; + + CComHeapPtr wszDisplayName; + CComHeapPtr wszDescription; + CComHeapPtr wszBtnText; + + CStringW IconPath; + DWORD dwFlags; + DWORD Priority; + DWORD StateFlags; + + CComPtr Handler; + DWORDLONG SpaceUsed; + bool ShowHandler; + HICON hIcon; +}; + diff --git a/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.cpp b/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.cpp new file mode 100644 index 00000000000..73af85d7974 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.cpp @@ -0,0 +1,163 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: CCleanupHandlerList implementation + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#include "cleanmgr.h" + +void CCleanupHandlerList::LoadHandlers(WCHAR Drive) +{ + m_DriveStr.Format(L"%c:", Drive); + + CRegKey VolumeCaches; + if (VolumeCaches.Open(HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VolumeCaches", KEY_READ) != ERROR_SUCCESS) + return; + + LONG ItemIndex = 0; + WCHAR szKeyName[MAX_PATH]; + + WCHAR wszVolume[] = { Drive, L':', L'\\', UNICODE_NULL }; + + while (TRUE) + { + DWORD dwSize = _countof(szKeyName); + if (VolumeCaches.EnumKey(ItemIndex++, szKeyName, &dwSize) != ERROR_SUCCESS) + { + break; + } + + + CRegKey hSubKey; + if (hSubKey.Open(VolumeCaches, szKeyName, KEY_READ) == ERROR_SUCCESS) + { + WCHAR GuidStr[50] = {}; + dwSize = _countof(GuidStr); + if (hSubKey.QueryStringValue(NULL, GuidStr, &dwSize) != ERROR_SUCCESS) + { + continue; + } + + GUID guid = {}; + if (FAILED_UNEXPECTEDLY(CLSIDFromString(GuidStr, &guid))) + continue; + + CCleanupHandler* handler = new CCleanupHandler(hSubKey, szKeyName, guid); + + if (!handler->Initialize(wszVolume)) + { + delete handler; + continue; + } + + m_Handlers.AddTail(handler); + } + } + + // Sort handlers + BOOL fChanged = m_Handlers.GetCount() > 0; + while (fChanged) + { + fChanged = FALSE; + + for (size_t n = 0; n < m_Handlers.GetCount() - 1; n++) + { + POSITION leftPos = m_Handlers.FindIndex(n); + POSITION rightPos = m_Handlers.FindIndex(n+1); + CCleanupHandler* left = m_Handlers.GetAt(leftPos); + CCleanupHandler* right = m_Handlers.GetAt(rightPos); + + if (right->Priority < left->Priority) + { + m_Handlers.SwapElements(leftPos, rightPos); + fChanged = TRUE; + } + else if (right->Priority == left->Priority) + { + CStringW leftStr(left->wszDisplayName); + if (leftStr.Compare(right->wszDisplayName) > 0) + { + m_Handlers.SwapElements(leftPos, rightPos); + fChanged = TRUE; + } + } + } + } +} + + +DWORDLONG +CCleanupHandlerList::ScanDrive(IEmptyVolumeCacheCallBack *picb) +{ + CProgressDlg progress; + CString Caption; + Caption.Format(IDS_CALCULATING, m_DriveStr.GetString()); + CStringW Title(MAKEINTRESOURCE(IDS_DISK_CLEANUP)); + progress.Start((DWORD)m_Handlers.GetCount(), Title, Caption); + int ItemIndex = 0; + DWORDLONG TotalSpaceUsed = 0; + ForEach( + [&](CCleanupHandler *current) + { + Caption.Format(IDS_SCANNING, current->wszDisplayName.m_pData); + progress.Step(++ItemIndex, Caption); + + HRESULT hr = current->Handler->GetSpaceUsed(¤t->SpaceUsed, picb); + + if (FAILED_UNEXPECTEDLY(hr)) + { + current->ShowHandler = false; + current->StateFlags &= ~HANDLER_STATE_SELECTED; + return; + } + + if (current->SpaceUsed == 0 && current->DontShowIfZero()) + { + current->ShowHandler = false; + current->StateFlags &= ~HANDLER_STATE_SELECTED; + } + TotalSpaceUsed += current->SpaceUsed; + }); + progress.Stop(); + + return TotalSpaceUsed; +} + +void +CCleanupHandlerList::ExecuteCleanup(IEmptyVolumeCacheCallBack *picb) +{ + CProgressDlg progress; + CString Caption; + Caption.Format(IDS_CLEANING_CAPTION, m_DriveStr.GetString()); + + DWORD TotalSelected = 0; + ForEach( + [&](CCleanupHandler *current) + { + if (current->StateFlags & HANDLER_STATE_SELECTED) + TotalSelected++; + }); + + CStringW Title(MAKEINTRESOURCE(IDS_DISK_CLEANUP)); + progress.Start(TotalSelected, Title, Caption); + int ItemIndex = 0; + ForEach( + [&](CCleanupHandler *current) + { + if (!(current->StateFlags & HANDLER_STATE_SELECTED)) + return; + + Caption.Format(IDS_CLEANING, current->wszDisplayName.m_pData); + progress.Step(++ItemIndex, Caption); + + // If there is nothing to clean, we might get STG_E_NOMOREFILES + if (current->SpaceUsed > 0) + { + HRESULT hr = current->Handler->Purge(-1, picb); + if (FAILED_UNEXPECTEDLY(hr)) + return; + } + }); + progress.Stop(); +} diff --git a/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.hpp b/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.hpp new file mode 100644 index 00000000000..55e38502c83 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CCleanupHandlerList.hpp @@ -0,0 +1,31 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: CCleanupHandlerList definition + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + + +class CCleanupHandlerList +{ +private: + CAtlList m_Handlers; + CStringW m_DriveStr; + +public: + + void LoadHandlers(WCHAR Drive); + DWORDLONG ScanDrive(IEmptyVolumeCacheCallBack* picb); + void ExecuteCleanup(IEmptyVolumeCacheCallBack *picb); + + template + void ForEach(Fn callback) + { + for (POSITION it = m_Handlers.GetHeadPosition(); it; m_Handlers.GetNext(it)) + { + CCleanupHandler *current = m_Handlers.GetAt(it); + + callback(current); + } + } +}; diff --git a/base/applications/cleanmgr/cleanmgr/CEmptyVolumeCacheCallBack.hpp b/base/applications/cleanmgr/cleanmgr/CEmptyVolumeCacheCallBack.hpp new file mode 100644 index 00000000000..8811d522dcc --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CEmptyVolumeCacheCallBack.hpp @@ -0,0 +1,54 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: CEmptyVolumeCacheCallBack definition / implementation + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + + +// We don't really use this, but some windows handlers crash without it +struct CEmptyVolumeCacheCallBack + : public IEmptyVolumeCacheCallBack +{ + + STDMETHOD_(ULONG, AddRef)() throw() + { + return 2; + } + STDMETHOD_(ULONG, Release)() throw() + { + return 1; + } + STDMETHOD(QueryInterface)( + REFIID riid, + _COM_Outptr_ void** ppvObject) throw() + { + if (riid == IID_IUnknown || riid == IID_IEmptyVolumeCacheCallBack) + { + *ppvObject = (IUnknown*)this; + return S_OK; + } + *ppvObject = NULL; + return E_NOINTERFACE; + } + + + STDMETHODIMP ScanProgress( + _In_ DWORDLONG dwlSpaceUsed, + _In_ DWORD dwFlags, + _In_ LPCWSTR pcwszStatus) override + { + DPRINT("dwlSpaceUsed: %lld, dwFlags: %x\n", dwlSpaceUsed, dwFlags); + return S_OK; + } + + STDMETHODIMP PurgeProgress( + _In_ DWORDLONG dwlSpaceFreed, + _In_ DWORDLONG dwlSpaceToFree, + _In_ DWORD dwFlags, + _In_ LPCWSTR pcwszStatus) override + { + DPRINT("dwlSpaceFreed: %lld, dwlSpaceToFree: %lld, dwFlags: %x\n", dwlSpaceFreed, dwlSpaceToFree, dwFlags); + return S_OK; + } +}; diff --git a/base/applications/cleanmgr/cleanmgr/CMakeLists.txt b/base/applications/cleanmgr/cleanmgr/CMakeLists.txt new file mode 100644 index 00000000000..f692c04796c --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CMakeLists.txt @@ -0,0 +1,19 @@ + +add_executable(cleanmgr + cleanmgr.cpp + cleanmgr.h + cleanmgr.rc + resource.h + CEmptyVolumeCacheCallBack.hpp + CProgressDlg.hpp + CSelectDriveDlg.cpp + CCleanupHandler.cpp + CCleanupHandler.hpp + CCleanupHandlerList.cpp + CCleanupHandlerList.hpp +) +set_module_type(cleanmgr win32gui UNICODE) +target_link_libraries(cleanmgr uuid cpprt atl_classes) +add_importlibs(cleanmgr shlwapi oleaut32 ole32 shell32 comctl32 user32 advapi32 msvcrt kernel32 ntdll) +add_dependencies(cleanmgr psdk) +add_cd_file(TARGET cleanmgr DESTINATION reactos/system32 FOR all) diff --git a/base/applications/cleanmgr/cleanmgr/CProgressDlg.hpp b/base/applications/cleanmgr/cleanmgr/CProgressDlg.hpp new file mode 100644 index 00000000000..66604de01de --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CProgressDlg.hpp @@ -0,0 +1,50 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Progress dialog implementation + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#pragma once + +class CProgressDlg +{ + CComPtr m_spProgress; + DWORD m_dwTotal = 0; +public: + + ~CProgressDlg() + { + Stop(); + } + + + void Start(DWORD dwTotalSteps, LPCWSTR Title, LPCWSTR Text) + { + HRESULT hr = CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_INPROC, IID_PPV_ARG(IProgressDialog, &m_spProgress)); + if (FAILED_UNEXPECTEDLY(hr)) + return; + + m_dwTotal = dwTotalSteps; + + m_spProgress->SetTitle(Title); + m_spProgress->SetLine(2, Text, TRUE, NULL); + m_spProgress->StartProgressDialog(NULL, NULL, PROGDLG_NOMINIMIZE, NULL); + m_spProgress->SetProgress(0, m_dwTotal); + } + + void Step(DWORD dwProgress, LPCWSTR Text) + { + m_spProgress->SetProgress(dwProgress, m_dwTotal); + m_spProgress->SetLine(1, Text, TRUE, NULL); + } + + void Stop() + { + if (m_spProgress) + { + m_spProgress->StopProgressDialog(); + m_spProgress.Release(); + } + } +}; diff --git a/base/applications/cleanmgr/cleanmgr/CSelectDriveDlg.cpp b/base/applications/cleanmgr/cleanmgr/CSelectDriveDlg.cpp new file mode 100644 index 00000000000..6387bcd87a3 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/CSelectDriveDlg.cpp @@ -0,0 +1,67 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Drive selection dialog + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#include "cleanmgr.h" + +class CSelectDriveDlg : public CDialogImpl +{ +public: + enum { IDD = IDD_SELECTDRIVE }; + + BEGIN_MSG_MAP(CSelectDriveDlg) + MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) + COMMAND_ID_HANDLER(IDOK, OnEndDialog) + COMMAND_ID_HANDLER(IDCANCEL, OnEndDialog) + END_MSG_MAP() + + CSelectDriveDlg() + :m_SelectedDrive(UNICODE_NULL) + { + } + + LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) + { + CWindow cbo = GetDlgItem(IDC_DRIVES); + WCHAR VolumeNameBuffer[MAX_PATH + 1]; + CStringW Tmp; + for (WCHAR Drive = 'A'; Drive <= 'Z'; ++Drive) + { + WCHAR RootPathName[] = { Drive,':','\\',0 }; + UINT Type = GetDriveTypeW(RootPathName); + if (Type == DRIVE_FIXED) + { + GetVolumeInformationW(RootPathName, VolumeNameBuffer, _countof(VolumeNameBuffer), 0, 0, 0, 0, 0); + Tmp.Format(L"%s (%.2s)", VolumeNameBuffer, RootPathName); + + int index = (int)cbo.SendMessage(CB_ADDSTRING, NULL, (LPARAM)Tmp.GetString()); + cbo.SendMessage(CB_SETITEMDATA, index, Drive); + } + } + cbo.SendMessage(CB_SETCURSEL, 0); + return 1; + } + LRESULT OnEndDialog(WORD, WORD wID, HWND, BOOL&) + { + CWindow cbo = GetDlgItem(IDC_DRIVES); + m_SelectedDrive = (WCHAR)cbo.SendMessage(CB_GETITEMDATA, cbo.SendMessage(CB_GETCURSEL)); + EndDialog(wID); + return 0; + } + + WCHAR m_SelectedDrive; +}; + + +void +SelectDrive(WCHAR &Drive) +{ + CSelectDriveDlg dlgSelectDrive; + if (dlgSelectDrive.DoModal() == IDOK) + { + Drive = dlgSelectDrive.m_SelectedDrive; + } +} diff --git a/base/applications/cleanmgr/cleanmgr/cleanmgr.cpp b/base/applications/cleanmgr/cleanmgr/cleanmgr.cpp new file mode 100644 index 00000000000..13505521208 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/cleanmgr.cpp @@ -0,0 +1,296 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Disk cleanup entrypoint + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#include "cleanmgr.h" + +// for listview with extend style LVS_EX_CHECKBOXES, State image 1 is the unchecked box, and state image 2 is the +// checked box. see this: https://docs.microsoft.com/en-us/windows/win32/controls/extended-list-view-styles +#define STATEIMAGETOINDEX(x) (((x)&LVIS_STATEIMAGEMASK) >> 12) +#define STATEIMAGE_UNCHECKED 1 +#define STATEIMAGE_CHECKED 2 + + +struct CCleanMgrProperties : + public CPropertyPageImpl +{ + enum { IDD = IDD_PROPERTIES_MAIN }; + CWindow m_HandlerListControl; + WCHAR m_Drive; + DWORDLONG m_TotalSpaceUsed; + CCleanupHandlerList* m_HandlerList; + bool m_IgnoreChanges = true; + + + CCleanMgrProperties(WCHAR Drive, DWORDLONG TotalSpaceUsed, CCleanupHandlerList *handlerList) + : m_Drive(Drive) + , m_TotalSpaceUsed(TotalSpaceUsed) + , m_HandlerList(handlerList) + { + } + + int OnApply() + { + CStringW Title(MAKEINTRESOURCE(IDS_DISK_CLEANUP)); + CStringW Text(MAKEINTRESOURCE(IDS_CONFIRM_DELETE)); + + if (MessageBoxW(Text, Title, MB_YESNO | MB_ICONQUESTION) != IDYES) + return PSNRET_INVALID; + + return PSNRET_NOERROR; + } + + LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) + { + HICON hIcon = (HICON)::LoadImageW( + _AtlBaseModule.GetResourceInstance(), MAKEINTRESOURCEW(IDI_CLEANMGR), IMAGE_ICON, 0, 0, + LR_DEFAULTSIZE | LR_SHARED); + SendDlgItemMessage(IDC_DISKICON, STM_SETICON, (WPARAM)hIcon); + + m_HandlerListControl = GetDlgItem(IDC_HANDLERLIST); + RECT rc; + m_HandlerListControl.GetClientRect(&rc); + rc.right -= GetSystemMetrics(SM_CXVSCROLL); + + LV_COLUMN column = {}; + column.mask = LVCF_FMT | LVCF_WIDTH; + column.fmt = LVCFMT_LEFT; + column.cx = rc.right * 80 / 100; + ListView_InsertColumn(m_HandlerListControl, 0, &column); + column.fmt = LVCFMT_RIGHT; + column.cx = rc.right * 20 / 100; + + ListView_InsertColumn(m_HandlerListControl, 1, &column); + HIMAGELIST hImagelist = ImageList_Create(16, 16, ILC_MASK | ILC_COLOR32, 1, 1); + ListView_SetImageList(m_HandlerListControl, hImagelist, LVSIL_SMALL); + + ListView_SetExtendedListViewStyleEx(m_HandlerListControl, LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT, LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT); + + m_HandlerList->ForEach( + [&](CCleanupHandler *current) + { + if (!current->ShowHandler) + return; + + LV_ITEM item = {}; + item.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; + item.lParam = (LPARAM)current; + item.pszText = (LPWSTR)current->wszDisplayName; + item.iItem = ListView_GetItemCount(m_HandlerListControl); + item.iImage = ImageList_AddIcon(hImagelist, current->hIcon); + item.iItem = ListView_InsertItem(m_HandlerListControl, &item); + ListView_SetCheckState( + m_HandlerListControl, item.iItem, !!(current->StateFlags & HANDLER_STATE_SELECTED)); + + item.mask = LVIF_TEXT; + WCHAR ByteSize[100] = {}; + StrFormatByteSizeW(current->SpaceUsed, ByteSize, _countof(ByteSize)); + ListView_SetItemText(m_HandlerListControl, item.iItem, 1, ByteSize); + }); + + // Now we should start responding to changes + m_IgnoreChanges = false; + + // Select the first item + ListView_SetItemState(m_HandlerListControl, 0, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); + + UpdateSpaceUsed(); + return TRUE; + } + + CCleanupHandler* GetHandler(int Index) + { + LVITEMW item = {}; + item.iItem = Index; + if (item.iItem >= 0) + { + item.mask = LVIF_PARAM; + ListView_GetItem(m_HandlerListControl, &item); + return (CCleanupHandler*)item.lParam; + } + return nullptr; + } + + + LRESULT OnDetails(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) + { + CCleanupHandler *handler = GetHandler(ListView_GetNextItem(m_HandlerListControl, -1, LVIS_FOCUSED)); + if (handler) + { + handler->Handler->ShowProperties(m_hWnd); + } + return 0L; + } + + LRESULT OnHandlerItemchanged(int idCtrl, LPNMHDR pnmh, BOOL& bHandled) + { + if (idCtrl == IDC_HANDLERLIST) + { + // We are still initializing, don't respond to changes just yet! + if (m_IgnoreChanges) + return 0L; + + LPNMLISTVIEW pnic = (LPNMLISTVIEW)pnmh; + + // We only care about state changes + if (!(pnic->uChanged & LVIF_STATE)) + return 0L; + + + INT ItemIndex = pnic->iItem; + if (ItemIndex == -1 || ItemIndex >= ListView_GetItemCount(pnic->hdr.hwndFrom)) + { + return 0L; + } + + bool GotSelected = (pnic->uNewState & LVIS_SELECTED) && !(pnic->uOldState & LVIS_SELECTED); + if (GotSelected) + { + CWindow DetailsButton = GetDlgItem(IDC_DETAILS); + CCleanupHandler* handler = (CCleanupHandler*)pnic->lParam; + + SetDlgItemText(IDC_DESCRIPTION, handler->wszDescription ? handler->wszDescription : L""); + if (handler->HasSettings()) + { + DetailsButton.ShowWindow(SW_SHOW); + DetailsButton.SetWindowText(handler->wszBtnText); + } + else + { + DetailsButton.ShowWindow(SW_HIDE); + } + } + + int iOldState = STATEIMAGETOINDEX(pnic->uOldState); + int iNewState = STATEIMAGETOINDEX(pnic->uNewState); + + if ((iOldState ^ iNewState) == (STATEIMAGE_UNCHECKED ^ STATEIMAGE_CHECKED)) + { + CCleanupHandler* handler = (CCleanupHandler*)pnic->lParam; + if (iNewState == STATEIMAGE_CHECKED) + handler->StateFlags |= HANDLER_STATE_SELECTED; + else + handler->StateFlags &= ~HANDLER_STATE_SELECTED; + UpdateSpaceUsed(); + } + } + return 0L; + } + + void UpdateSpaceUsed() + { + CStringW tmp; + WCHAR ByteSize[100]; + StrFormatByteSizeW(m_TotalSpaceUsed, ByteSize, _countof(ByteSize)); + + tmp.Format(IDS_TOTAL_CLEANABLE_CAPTION, ByteSize, m_Drive); + SetDlgItemText(IDC_TOTAL_CLEANABLE, tmp); + + DWORDLONG SelectedGained = 0; + + m_HandlerList->ForEach( + [&](CCleanupHandler *current) + { + if (current->StateFlags & HANDLER_STATE_SELECTED) + { + SelectedGained += current->SpaceUsed; + } + }); + + StrFormatByteSizeW(SelectedGained, ByteSize, _countof(ByteSize)); + SetDlgItemText(IDC_SELECTED_GAINED, ByteSize); + } + + BEGIN_MSG_MAP(CCleanMgrProperties) + MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) + COMMAND_ID_HANDLER(IDC_DETAILS, OnDetails) + NOTIFY_HANDLER(IDC_HANDLERLIST, LVN_ITEMCHANGED, OnHandlerItemchanged) + CHAIN_MSG_MAP(CPropertyPageImpl) // Allow the default handler to call 'OnApply' etc + END_MSG_MAP() +}; + + + +class CCleanMgrModule : public ATL::CAtlExeModuleT< CCleanMgrModule > +{ +public: + WCHAR m_Drive = UNICODE_NULL; + + bool ParseCommandLine( + _In_z_ LPCTSTR lpCmdLine, + _Out_ HRESULT* pnRetCode) throw() + { + int argc = 0; + CLocalPtr argv(CommandLineToArgvW(lpCmdLine, &argc)); + + for (int n = 1; n < argc; ++n) + { + if ((argv[n][0] == '/' || argv[n][0] == '-') && towlower(argv[n][1]) == 'd') + { + if (iswalpha(argv[n][2])) + { + m_Drive = towupper(argv[n][2]); + continue; + } + if ((n + 1) < argc) + { + m_Drive = towupper(argv[n + 1][0]); + ++n; + continue; + } + } + } + *pnRetCode = S_OK; + return true; + } + + HRESULT Run(_In_ int nShowCmd) throw() + { + if (m_Drive == UNICODE_NULL) + { + SelectDrive(m_Drive); + } + + if (m_Drive == UNICODE_NULL) + return E_FAIL; + + CCleanupHandlerList Handlers; + CEmptyVolumeCacheCallBack CacheCallBack; + + Handlers.LoadHandlers(m_Drive); + DWORDLONG TotalSpaceUsed = Handlers.ScanDrive(&CacheCallBack); + + CCleanMgrProperties cleanMgr(m_Drive, TotalSpaceUsed, &Handlers); + HPROPSHEETPAGE hpsp[1] = { cleanMgr.Create() }; + + PROPSHEETHEADERW psh = { }; + psh.dwSize = sizeof(psh); + psh.dwFlags = PSH_NOAPPLYNOW | PSH_USEICONID | PSH_NOCONTEXTHELP; + psh.hInstance = _AtlBaseModule.GetResourceInstance(); + psh.pszIcon = MAKEINTRESOURCEW(IDI_CLEANMGR); + CStringW Title; + Title.Format(IDS_PROPERTIES_MAIN_TITLE, m_Drive); + psh.pszCaption = Title; + psh.nPages = _countof(hpsp); + psh.phpage = hpsp; + + if (PropertySheetW(&psh) >= 1) + { + Handlers.ExecuteCleanup(&CacheCallBack); + } + return S_OK; + } +}; + +CCleanMgrModule _AtlModule; + + + +extern "C" int WINAPI wWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, + LPWSTR /*lpCmdLine*/, int nShowCmd) +{ + return _AtlModule.WinMain(nShowCmd); +} diff --git a/base/applications/cleanmgr/cleanmgr/cleanmgr.h b/base/applications/cleanmgr/cleanmgr/cleanmgr.h new file mode 100644 index 00000000000..742a585ba25 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/cleanmgr.h @@ -0,0 +1,66 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Main header file + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#pragma once + +#ifndef STRICT +#define STRICT +#endif + +#define _ATL_APARTMENT_THREADED +#define _ATL_NO_AUTOMATIC_NAMESPACE +#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit +#define ATL_NO_ASSERT_ON_DESTROY_NONEXISTENT_WINDOW + +#define _FORCENAMELESSUNION + +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include +#include + + +using namespace ATL; + +#define NDEBUG +#include +#include +#include + + +template class CLocalPtr + : public CHeapPtr +{ +public: + CLocalPtr() throw() + { + } + + explicit CLocalPtr(_In_ T* pData) throw() : + CHeapPtr(pData) + { + } +}; + +#include "resource.h" +#include "CProgressDlg.hpp" +#include "CCleanupHandler.hpp" +#include "CCleanupHandlerList.hpp" +#include "CEmptyVolumeCacheCallBack.hpp" + +// CSelectDriveDlg.cpp +void +SelectDrive(WCHAR &Drive); diff --git a/base/applications/cleanmgr/cleanmgr/cleanmgr.rc b/base/applications/cleanmgr/cleanmgr/cleanmgr.rc new file mode 100644 index 00000000000..07edd4d0d8a --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/cleanmgr.rc @@ -0,0 +1,64 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Resources + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#include +#include +#include +#include "resource.h" + +#define REACTOS_STR_FILE_DESCRIPTION "ReactOS Disk Cleanup" +#define REACTOS_STR_INTERNAL_NAME "cleanmgr" +#define REACTOS_STR_ORIGINAL_FILENAME "cleanmgr.exe" +#include + +#include + +IDI_CLEANMGR ICON "resources/cleanmgr.ico" + +#pragma code_page(65001) + +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +STRINGTABLE +BEGIN + IDS_PROPERTIES_MAIN_TITLE "Disk Cleanup for (%c:)" + IDS_TOTAL_CLEANABLE_CAPTION "You can use Disk Cleanup to free up to %s of disk space on (%c:)." + IDS_DISK_CLEANUP "Disk Cleanup" + IDS_CONFIRM_DELETE "Are you sure you want to delete these files permanently?" + IDS_CALCULATING "Disk Cleanup is calculating how much space can be gained on (%s)." + IDS_SCANNING "Scanning: %s" + IDS_CLEANING_CAPTION "Disk Cleanup is cleaning up files on %s." + IDS_CLEANING "Cleaning: %s" +END + +IDD_PROPERTIES_MAIN DIALOGEX 0, 0, 235, 215 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CENTER | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Disk Cleanup" +FONT 8, "MS Shell Dlg", 400, 0, 0x0 +BEGIN + CONTROL "",IDC_DISKICON,"Static",SS_ICON,6,6,20,20 + LTEXT "You can use Disk Cleanup to free up to 0.0MB of disk space on drive C.",IDC_TOTAL_CLEANABLE,36,6,192,18 + LTEXT "Files to delete:",IDC_STATIC,6,30,222,8 + CONTROL "",IDC_HANDLERLIST,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOCOLUMNHEADER | WS_BORDER | WS_TABSTOP,6,42,222,66 + LTEXT "Total amount of disk space gained:",IDC_STATIC,6,114,144,8 + RTEXT "",IDC_SELECTED_GAINED,156,114,73,8 + GROUPBOX "Description",IDC_STATIC,6,126,222,84 + LTEXT "",IDC_DESCRIPTION,12,138,210,54 + PUSHBUTTON "Details...",IDC_DETAILS,150,192,74,14 +END + +IDD_SELECTDRIVE DIALOGEX 0, 0, 177, 74 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Disk Cleanup - Select Drive" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,30,48,50,14 + PUSHBUTTON "E&xit",IDCANCEL,96,48,50,14 + LTEXT "Select the drive to clean up.",IDC_STATIC,12,6,150,8 + COMBOBOX IDC_DRIVES,12,24,150,90,CBS_DROPDOWNLIST | CBS_HASSTRINGS | CBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP +END + diff --git a/base/applications/cleanmgr/cleanmgr/resource.h b/base/applications/cleanmgr/cleanmgr/resource.h new file mode 100644 index 00000000000..5edb10b5b22 --- /dev/null +++ b/base/applications/cleanmgr/cleanmgr/resource.h @@ -0,0 +1,30 @@ +/* + * PROJECT: ReactOS Disk Cleanup + * LICENSE: MIT (https://spdx.org/licenses/MIT) + * PURPOSE: Resource definitions + * COPYRIGHT: Copyright 2023-2025 Mark Jansen + */ + +#define IDC_STATIC -1 + +#define IDI_CLEANMGR 100 + +#define IDD_PROPERTIES_MAIN 200 +#define IDC_DISKICON 201 +#define IDC_TOTAL_CLEANABLE 202 +#define IDC_HANDLERLIST 203 +#define IDC_SELECTED_GAINED 204 +#define IDC_DESCRIPTION 205 +#define IDC_DETAILS 206 + +#define IDD_SELECTDRIVE 220 +#define IDC_DRIVES 221 + +#define IDS_PROPERTIES_MAIN_TITLE 1000 +#define IDS_TOTAL_CLEANABLE_CAPTION 1001 +#define IDS_DISK_CLEANUP 1002 +#define IDS_CONFIRM_DELETE 1003 +#define IDS_CALCULATING 1004 +#define IDS_SCANNING 1005 +#define IDS_CLEANING_CAPTION 1006 +#define IDS_CLEANING 1007 diff --git a/base/applications/cleanmgr/cleanmgr/resources/cleanmgr.ico b/base/applications/cleanmgr/cleanmgr/resources/cleanmgr.ico new file mode 100644 index 00000000000..577b6294d14 Binary files /dev/null and b/base/applications/cleanmgr/cleanmgr/resources/cleanmgr.ico differ