From e1d5dd263a8bb0819d30dde3ee3ac947fd638da4 Mon Sep 17 00:00:00 2001 From: Chris Davis Date: Thu, 17 Oct 2019 20:57:19 -0700 Subject: [PATCH] Initial add of PowerRename from SmartRename repo (#499) * Initial add of PowerRename from SmartRename repo --- PowerToys.sln | 54 + installer/PowerToysSetup/Product.wxs | 12 + src/modules/powerrename/README.md | 2 + .../powerrename/dll/PowerRenameExt.cpp | 147 +++ .../powerrename/dll/PowerRenameExt.def | 5 + src/modules/powerrename/dll/PowerRenameExt.h | 61 + src/modules/powerrename/dll/PowerRenameExt.rc | 111 ++ .../powerrename/dll/PowerRenameExt.vcxproj | 190 ++++ .../dll/PowerRenameExt.vcxproj.filters | 52 + src/modules/powerrename/dll/dllmain.cpp | 252 +++++ src/modules/powerrename/dll/resource.h | 12 + src/modules/powerrename/dll/stdafx.cpp | 2 + src/modules/powerrename/dll/stdafx.h | 26 + src/modules/powerrename/dll/targetver.h | 8 + src/modules/powerrename/lib/Helpers.cpp | 288 +++++ src/modules/powerrename/lib/Helpers.h | 10 + .../powerrename/lib/PowerRenameInterfaces.h | 109 ++ .../powerrename/lib/PowerRenameItem.cpp | 221 ++++ src/modules/powerrename/lib/PowerRenameItem.h | 60 + .../powerrename/lib/PowerRenameLib.vcxproj | 167 +++ .../powerrename/lib/PowerRenameManager.cpp | 1000 +++++++++++++++++ .../powerrename/lib/PowerRenameManager.h | 120 ++ .../powerrename/lib/PowerRenameRegEx.cpp | 301 +++++ .../powerrename/lib/PowerRenameRegEx.h | 58 + src/modules/powerrename/lib/srwlock.h | 79 ++ src/modules/powerrename/lib/stdafx.cpp | 2 + src/modules/powerrename/lib/stdafx.h | 20 + src/modules/powerrename/lib/targetver.h | 7 + .../powerrename/testapp/PowerRenameTest.cpp | 62 + .../powerrename/testapp/PowerRenameTest.h | 3 + .../powerrename/testapp/PowerRenameTest.ico | Bin 0 -> 46227 bytes .../powerrename/testapp/PowerRenameTest.rc | 71 ++ .../testapp/PowerRenameTest.vcxproj | 188 ++++ .../testapp/PowerRenameTest.vcxproj.filters | 44 + src/modules/powerrename/testapp/resource.h | 19 + src/modules/powerrename/testapp/small.ico | Bin 0 -> 46227 bytes src/modules/powerrename/testapp/stdafx.cpp | 1 + src/modules/powerrename/testapp/stdafx.h | 16 + src/modules/powerrename/testapp/targetver.h | 8 + src/modules/powerrename/ui/PowerRenameUI.cpp | 930 +++++++++++++++ src/modules/powerrename/ui/PowerRenameUI.h | 133 +++ src/modules/powerrename/ui/PowerRenameUI.rc | 165 +++ .../powerrename/ui/PowerRenameUI.vcxproj | 187 +++ src/modules/powerrename/ui/Rename.ico | Bin 0 -> 108583 bytes src/modules/powerrename/ui/resource.h | Bin 0 -> 3888 bytes src/modules/powerrename/ui/stdafx.cpp | 1 + src/modules/powerrename/ui/stdafx.h | 14 + src/modules/powerrename/ui/targetver.h | 8 + .../unittests/MockPowerRenameItem.cpp | 34 + .../unittests/MockPowerRenameItem.h | 12 + .../MockPowerRenameManagerEvents.cpp | 92 ++ .../unittests/MockPowerRenameManagerEvents.h | 43 + .../unittests/MockPowerRenameRegExEvents.cpp | 69 ++ .../unittests/MockPowerRenameRegExEvents.h | 36 + .../unittests/PowerRenameLibUnitTests.vcxproj | 198 ++++ .../unittests/PowerRenameManagerTests.cpp | 241 ++++ .../unittests/PowerRenameRegExTests.cpp | 331 ++++++ .../powerrename/unittests/TestFileHelper.cpp | 73 ++ .../powerrename/unittests/TestFileHelper.h | 25 + src/modules/powerrename/unittests/stdafx.cpp | 1 + src/modules/powerrename/unittests/stdafx.h | 9 + src/modules/powerrename/unittests/targetver.h | 10 + src/runner/main.cpp | 3 +- 63 files changed, 6402 insertions(+), 1 deletion(-) create mode 100644 src/modules/powerrename/README.md create mode 100644 src/modules/powerrename/dll/PowerRenameExt.cpp create mode 100644 src/modules/powerrename/dll/PowerRenameExt.def create mode 100644 src/modules/powerrename/dll/PowerRenameExt.h create mode 100644 src/modules/powerrename/dll/PowerRenameExt.rc create mode 100644 src/modules/powerrename/dll/PowerRenameExt.vcxproj create mode 100644 src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters create mode 100644 src/modules/powerrename/dll/dllmain.cpp create mode 100644 src/modules/powerrename/dll/resource.h create mode 100644 src/modules/powerrename/dll/stdafx.cpp create mode 100644 src/modules/powerrename/dll/stdafx.h create mode 100644 src/modules/powerrename/dll/targetver.h create mode 100644 src/modules/powerrename/lib/Helpers.cpp create mode 100644 src/modules/powerrename/lib/Helpers.h create mode 100644 src/modules/powerrename/lib/PowerRenameInterfaces.h create mode 100644 src/modules/powerrename/lib/PowerRenameItem.cpp create mode 100644 src/modules/powerrename/lib/PowerRenameItem.h create mode 100644 src/modules/powerrename/lib/PowerRenameLib.vcxproj create mode 100644 src/modules/powerrename/lib/PowerRenameManager.cpp create mode 100644 src/modules/powerrename/lib/PowerRenameManager.h create mode 100644 src/modules/powerrename/lib/PowerRenameRegEx.cpp create mode 100644 src/modules/powerrename/lib/PowerRenameRegEx.h create mode 100644 src/modules/powerrename/lib/srwlock.h create mode 100644 src/modules/powerrename/lib/stdafx.cpp create mode 100644 src/modules/powerrename/lib/stdafx.h create mode 100644 src/modules/powerrename/lib/targetver.h create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.cpp create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.h create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.ico create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.rc create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.vcxproj create mode 100644 src/modules/powerrename/testapp/PowerRenameTest.vcxproj.filters create mode 100644 src/modules/powerrename/testapp/resource.h create mode 100644 src/modules/powerrename/testapp/small.ico create mode 100644 src/modules/powerrename/testapp/stdafx.cpp create mode 100644 src/modules/powerrename/testapp/stdafx.h create mode 100644 src/modules/powerrename/testapp/targetver.h create mode 100644 src/modules/powerrename/ui/PowerRenameUI.cpp create mode 100644 src/modules/powerrename/ui/PowerRenameUI.h create mode 100644 src/modules/powerrename/ui/PowerRenameUI.rc create mode 100644 src/modules/powerrename/ui/PowerRenameUI.vcxproj create mode 100644 src/modules/powerrename/ui/Rename.ico create mode 100644 src/modules/powerrename/ui/resource.h create mode 100644 src/modules/powerrename/ui/stdafx.cpp create mode 100644 src/modules/powerrename/ui/stdafx.h create mode 100644 src/modules/powerrename/ui/targetver.h create mode 100644 src/modules/powerrename/unittests/MockPowerRenameItem.cpp create mode 100644 src/modules/powerrename/unittests/MockPowerRenameItem.h create mode 100644 src/modules/powerrename/unittests/MockPowerRenameManagerEvents.cpp create mode 100644 src/modules/powerrename/unittests/MockPowerRenameManagerEvents.h create mode 100644 src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp create mode 100644 src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h create mode 100644 src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj create mode 100644 src/modules/powerrename/unittests/PowerRenameManagerTests.cpp create mode 100644 src/modules/powerrename/unittests/PowerRenameRegExTests.cpp create mode 100644 src/modules/powerrename/unittests/TestFileHelper.cpp create mode 100644 src/modules/powerrename/unittests/TestFileHelper.h create mode 100644 src/modules/powerrename/unittests/stdafx.cpp create mode 100644 src/modules/powerrename/unittests/stdafx.h create mode 100644 src/modules/powerrename/unittests/targetver.h diff --git a/PowerToys.sln b/PowerToys.sln index b7817c2918..68b6454c54 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -49,6 +49,34 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests-CommonLib", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameExt", "src\modules\powerrename\dll\PowerRenameExt.vcxproj", "{B25AC7A5-FB9F-4789-B392-D5C85E948670}" + ProjectSection(ProjectDependencies) = postProject + {0E072714-D127-460B-AFAD-B4C40B412798} = {0E072714-D127-460B-AFAD-B4C40B412798} + {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameLib", "src\modules\powerrename\lib\PowerRenameLib.vcxproj", "{51920F1F-C28C-4ADF-8660-4238766796C2}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUI", "src\modules\powerrename\ui\PowerRenameUI.vcxproj", "{0E072714-D127-460B-AFAD-B4C40B412798}" + ProjectSection(ProjectDependencies) = postProject + {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameTest", "src\modules\powerrename\testapp\PowerRenameTest.vcxproj", "{A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}" + ProjectSection(ProjectDependencies) = postProject + {0E072714-D127-460B-AFAD-B4C40B412798} = {0E072714-D127-460B-AFAD-B4C40B412798} + {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUnitTests", "src\modules\powerrename\unittests\PowerRenameLibUnitTests.vcxproj", "{2151F984-E006-4A9F-92EF-C6DDE3DC8413}" + ProjectSection(ProjectDependencies) = postProject + {0E072714-D127-460B-AFAD-B4C40B412798} = {0E072714-D127-460B-AFAD-B4C40B412798} + {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} + {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {B25AC7A5-FB9F-4789-B392-D5C85E948670} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -97,6 +125,26 @@ Global {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.Build.0 = Debug|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.ActiveCfg = Release|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.Build.0 = Release|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.ActiveCfg = Debug|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.Build.0 = Debug|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.ActiveCfg = Release|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.Build.0 = Release|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.ActiveCfg = Debug|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.Build.0 = Debug|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.ActiveCfg = Release|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.Build.0 = Release|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Debug|x64.ActiveCfg = Debug|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Debug|x64.Build.0 = Debug|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Release|x64.ActiveCfg = Release|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Release|x64.Build.0 = Release|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.ActiveCfg = Debug|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.Build.0 = Debug|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.ActiveCfg = Release|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.Build.0 = Release|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.ActiveCfg = Debug|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.Build.0 = Debug|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.ActiveCfg = Release|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -113,6 +161,12 @@ Global {4E577735-DFAB-41AF-8A6E-B6E8872A2928} = {1FAF749F-0D6F-4BF5-A563-31A4B5279D27} {1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482} {5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} + {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {51920F1F-C28C-4ADF-8660-4238766796C2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {0E072714-D127-460B-AFAD-B4C40B412798} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {2151F984-E006-4A9F-92EF-C6DDE3DC8413} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 33a5d28cf2..3d796e8303 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -232,6 +232,17 @@ + + + + + + + + + + + @@ -272,6 +283,7 @@ + diff --git a/src/modules/powerrename/README.md b/src/modules/powerrename/README.md new file mode 100644 index 0000000000..0e191fafc5 --- /dev/null +++ b/src/modules/powerrename/README.md @@ -0,0 +1,2 @@ +# PowerRename +A Windows Shell Extension for more advanced renaming using search and replace or regular expressions diff --git a/src/modules/powerrename/dll/PowerRenameExt.cpp b/src/modules/powerrename/dll/PowerRenameExt.cpp new file mode 100644 index 0000000000..4e9c7ea4c9 --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.cpp @@ -0,0 +1,147 @@ +#include "stdafx.h" +#include "PowerRenameExt.h" +#include +#include +#include +#include "resource.h" + +extern HINSTANCE g_hInst; + +HWND g_hwndParent = 0; + + +const wchar_t powerRenameRegPath[] = L"Software\\Microsoft\\PowerRename"; +const wchar_t powerRenameRegEnabledName[] = L"Enabled"; + +bool CPowerRenameMenu::IsEnabled() +{ + DWORD type = REG_DWORD; + DWORD dwEnabled = 0; + DWORD cb = sizeof(dwEnabled); + SHGetValue(HKEY_CURRENT_USER, powerRenameRegPath, powerRenameRegEnabledName, &type, &dwEnabled, &cb); + return (dwEnabled == 0) ? false : true; +} + +bool CPowerRenameMenu::SetEnabled(_In_ bool enabled) +{ + DWORD dwEnabled = enabled ? 1 : 0; + return SUCCEEDED(HRESULT_FROM_WIN32(SHSetValueW(HKEY_CURRENT_USER, powerRenameRegPath, powerRenameRegEnabledName, REG_DWORD, &dwEnabled, sizeof(dwEnabled)))); +} + +CPowerRenameMenu::CPowerRenameMenu() +{ + DllAddRef(); +} + +CPowerRenameMenu::~CPowerRenameMenu() +{ + m_spdo = nullptr; + DllRelease(); +} + +HRESULT CPowerRenameMenu::s_CreateInstance(_In_opt_ IUnknown*, _In_ REFIID riid, _Outptr_ void **ppv) +{ + *ppv = nullptr; + HRESULT hr = E_OUTOFMEMORY; + CPowerRenameMenu *pprm = new CPowerRenameMenu(); + if (pprm) + { + hr = pprm->QueryInterface(riid, ppv); + pprm->Release(); + } + return hr; +} + +// IShellExtInit +HRESULT CPowerRenameMenu::Initialize(_In_opt_ PCIDLIST_ABSOLUTE, _In_ IDataObject *pdtobj, HKEY) +{ + // Check if we have disabled ourselves + if (!IsEnabled()) + return E_FAIL; + + // Cache the data object to be used later + m_spdo = pdtobj; + return S_OK; +} + +// IContextMenu +HRESULT CPowerRenameMenu::QueryContextMenu(HMENU hMenu, UINT index, UINT uIDFirst, UINT, UINT uFlags) +{ + // Check if we have disabled ourselves + if (!IsEnabled()) + return E_FAIL; + + HRESULT hr = E_UNEXPECTED; + if (m_spdo) + { + if ((uFlags & ~CMF_OPTIMIZEFORINVOKE) && (uFlags & ~(CMF_DEFAULTONLY | CMF_VERBSONLY))) + { + wchar_t menuName[64] = { 0 }; + LoadString(g_hInst, IDS_POWERRENAME, menuName, ARRAYSIZE(menuName)); + InsertMenu(hMenu, index, MF_STRING | MF_BYPOSITION, uIDFirst++, menuName); + hr = MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1); + } + } + + return hr; +} + +HRESULT CPowerRenameMenu::InvokeCommand(_In_ LPCMINVOKECOMMANDINFO pici) +{ + // Check if we have disabled ourselves + if (!IsEnabled()) + return E_FAIL; + + HRESULT hr = E_FAIL; + + if ((IS_INTRESOURCE(pici->lpVerb)) && + (LOWORD(pici->lpVerb) == 0)) + { + IStream* pstrm = nullptr; + if (SUCCEEDED(CoMarshalInterThreadInterfaceInStream(__uuidof(m_spdo), m_spdo, &pstrm))) + { + if (!SHCreateThread(s_PowerRenameUIThreadProc, pstrm, CTF_COINIT | CTF_PROCESS_REF, nullptr)) + { + pstrm->Release(); // if we failed to create the thread, then we must release the stream + } + } + } + + return hr; +} + +DWORD WINAPI CPowerRenameMenu::s_PowerRenameUIThreadProc(_In_ void* pData) +{ + IStream* pstrm = static_cast(pData); + CComPtr spdo; + if (SUCCEEDED(CoGetInterfaceAndReleaseStream(pstrm, IID_PPV_ARGS(&spdo)))) + { + // Create the smart rename manager + CComPtr spsrm; + if (SUCCEEDED(CPowerRenameManager::s_CreateInstance(&spsrm))) + { + // Create the factory for our items + CComPtr spsrif; + if (SUCCEEDED(CPowerRenameItem::s_CreateInstance(nullptr, IID_PPV_ARGS(&spsrif)))) + { + // Pass the factory to the manager + if (SUCCEEDED(spsrm->put_smartRenameItemFactory(spsrif))) + { + // Create the smart rename UI instance and pass the smart rename manager + CComPtr spsrui; + if (SUCCEEDED(CPowerRenameUI::s_CreateInstance(spsrm, spdo, false, &spsrui))) + { + // Call blocks until we are done + spsrui->Show(); + spsrui->Close(); + } + } + } + + // Need to call shutdown to break circular dependencies + spsrm->Shutdown(); + } + } + + return 0; +} diff --git a/src/modules/powerrename/dll/PowerRenameExt.def b/src/modules/powerrename/dll/PowerRenameExt.def new file mode 100644 index 0000000000..52302809ab --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.def @@ -0,0 +1,5 @@ +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE \ No newline at end of file diff --git a/src/modules/powerrename/dll/PowerRenameExt.h b/src/modules/powerrename/dll/PowerRenameExt.h new file mode 100644 index 0000000000..cefbab5455 --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.h @@ -0,0 +1,61 @@ +#pragma once +#include "stdafx.h" + +class CPowerRenameMenu : + public IShellExtInit, + public IContextMenu +{ +public: + CPowerRenameMenu(); + + // IUnknown + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ void** ppv) + { + static const QITAB qit[] = + { + QITABENT(CPowerRenameMenu, IShellExtInit), + QITABENT(CPowerRenameMenu, IContextMenu), + { 0, 0 }, + }; + return QISearch(this, qit, riid, ppv); + } + + IFACEMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&m_refCount); + } + + IFACEMETHODIMP_(ULONG) Release() + { + LONG refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) + { + delete this; + } + return refCount; + } + + // IShellExtInit + STDMETHODIMP Initialize(_In_opt_ PCIDLIST_ABSOLUTE pidlFolder, _In_ IDataObject* pdto, HKEY hkProgID); + + // IContextMenu + STDMETHODIMP QueryContextMenu(HMENU hMenu, UINT index, UINT uIDFirst, UINT uIDLast, UINT uFlags); + STDMETHODIMP InvokeCommand(_In_ LPCMINVOKECOMMANDINFO pCMI); + STDMETHODIMP GetCommandString(UINT_PTR, UINT, _In_opt_ UINT*, _In_ LPSTR, UINT) + { + return E_NOTIMPL; + } + + static HRESULT s_CreateInstance(_In_opt_ IUnknown* punkOuter, _In_ REFIID riid, _Outptr_ void** ppv); + static DWORD WINAPI s_PowerRenameUIThreadProc(_In_ void* pData); + + static bool SetEnabled(_In_ bool enabled); + static bool IsEnabled(); + +private: + ~CPowerRenameMenu(); + + long m_refCount = 1; + CComPtr m_spdo; +}; + diff --git a/src/modules/powerrename/dll/PowerRenameExt.rc b/src/modules/powerrename/dll/PowerRenameExt.rc new file mode 100644 index 0000000000..8b0eb6a61b --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.rc @@ -0,0 +1,111 @@ +ÿþ// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,0,0,1 + PRODUCTVERSION 1,0,0,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Microsoft Corp." + VALUE "FileDescription", "PowerRename PowerToy" + VALUE "FileVersion", "1.0.0.1" + VALUE "InternalName", "PowerRenameExt.dll" + VALUE "LegalCopyright", "Copyright (C) 2019" + VALUE "OriginalFilename", "PowerRenameExt.dll" + VALUE "ProductName", "PowerRename PowerToy" + VALUE "ProductVersion", "1.0.0.1" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_POWERRENAME "Power Rename" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + + \ No newline at end of file diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj new file mode 100644 index 0000000000..70c952a12d --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj @@ -0,0 +1,190 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {B25AC7A5-FB9F-4789-B392-D5C85E948670} + PowerRenameExt + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + .dll + ..\lib\;..\ui\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\modules\ + $(ProjectName) + $(SolutionDir)$(Platform)\$(Configuration)\;$(LibraryPath) + + + ..\lib\;..\ui\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\modules\ + $(ProjectName) + $(SolutionDir)$(Platform)\$(Configuration)\;$(LibraryPath) + + + ..\lib\;..\ui\;$(IncludePath) + $(ProjectName) + $(SolutionDir)$(Platform)\$(Configuration)\modules\ + $(SolutionDir)$(Platform)\$(Configuration)\;$(LibraryPath) + + + ..\lib\;..\ui\;$(IncludePath) + $(ProjectName) + $(SolutionDir)$(Platform)\$(Configuration)\modules\ + $(SolutionDir)$(Platform)\$(Configuration)\;$(LibraryPath) + + + + Level3 + Disabled + true + true + stdcpp17 + MultiThreadedDebug + ..\;..\..\..\common;..\..\..\common\telemetry;..\..\;%(AdditionalIncludeDirectories) + + + $(SolutionDir)$(Platform)\$(Configuration)\PowerRenameLib.lib;$(SolutionDir)$(Platform)\$(Configuration)\PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(SolutionDir)$(Platform)\$(Configuration)\..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + PowerRenameExt.def + + + + + Level3 + Disabled + true + true + stdcpplatest + MultiThreadedDebug + ..\;..\..\..\common;..\..\..\common\telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories) + + + $(SolutionDir)$(Platform)\$(Configuration)\PowerRenameLib.lib;$(SolutionDir)$(Platform)\$(Configuration)\PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(SolutionDir)$(Platform)\$(Configuration)\..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + PowerRenameExt.def + + + + + Level3 + MaxSpeed + true + true + true + true + stdcpp17 + MultiThreaded + ..\;..\..\..\common;..\..\..\common\telemetry;..\..\;%(AdditionalIncludeDirectories) + + + true + true + $(SolutionDir)$(Platform)\$(Configuration)\PowerRenameLib.lib;$(SolutionDir)$(Platform)\$(Configuration)\PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(SolutionDir)$(Platform)\$(Configuration)\..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + PowerRenameExt.def + + + + + Level3 + MaxSpeed + true + true + true + true + stdcpplatest + MultiThreaded + ..\;..\..\..\common;..\..\..\common\telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories) + + + true + true + $(SolutionDir)$(Platform)\$(Configuration)\PowerRenameLib.lib;$(SolutionDir)$(Platform)\$(Configuration)\PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(SolutionDir)$(Platform)\$(Configuration)\..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + PowerRenameExt.def + + + + + + + + + + + + + + + + + + + + + + {74485049-c722-400f-abe5-86ac52d929b3} + + + + + + \ No newline at end of file diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters new file mode 100644 index 0000000000..b1a31797b4 --- /dev/null +++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters @@ -0,0 +1,52 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + Source Files + + + \ No newline at end of file diff --git a/src/modules/powerrename/dll/dllmain.cpp b/src/modules/powerrename/dll/dllmain.cpp new file mode 100644 index 0000000000..13b35b3600 --- /dev/null +++ b/src/modules/powerrename/dll/dllmain.cpp @@ -0,0 +1,252 @@ +#include "stdafx.h" +#include "PowerRenameExt.h" +#include +#include + +DWORD g_dwModuleRefCount = 0; +HINSTANCE g_hInst = 0; + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +class CSmartRenameClassFactory : public IClassFactory +{ +public: + CSmartRenameClassFactory(_In_ REFCLSID clsid) : + m_refCount(1), + m_clsid(clsid) + { + DllAddRef(); + } + + // IUnknown methods + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ void** ppv) + { + static const QITAB qit[] = + { + QITABENT(CSmartRenameClassFactory, IClassFactory), + { 0 } + }; + return QISearch(this, qit, riid, ppv); + } + + IFACEMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&m_refCount); + } + + IFACEMETHODIMP_(ULONG) Release() + { + LONG refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) + { + delete this; + } + return refCount; + } + + // IClassFactory methods + IFACEMETHODIMP CreateInstance(_In_opt_ IUnknown* punkOuter, _In_ REFIID riid, _Outptr_ void** ppv) + { + *ppv = NULL; + HRESULT hr; + if (punkOuter) + { + hr = CLASS_E_NOAGGREGATION; + } + else + { + if (m_clsid == CLSID_PowerRenameMenu) + { + hr = CPowerRenameMenu::s_CreateInstance(punkOuter, riid, ppv); + } + else + { + hr = CLASS_E_CLASSNOTAVAILABLE; + } + } + return hr; + } + + IFACEMETHODIMP LockServer(BOOL bLock) + { + if (bLock) + { + DllAddRef(); + } + else + { + DllRelease(); + } + return S_OK; + } + +private: + ~CSmartRenameClassFactory() + { + DllRelease(); + } + + long m_refCount; + CLSID m_clsid; +}; + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, void*) +{ + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + g_hInst = hInstance; + break; + + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +// +// Checks if there are any external references to this module +// +STDAPI DllCanUnloadNow(void) +{ + return (g_dwModuleRefCount == 0) ? S_OK : S_FALSE; +} + +// +// DLL export for creating COM objects +// +STDAPI DllGetClassObject(_In_ REFCLSID clsid, _In_ REFIID riid, _Outptr_ void **ppv) +{ + *ppv = NULL; + HRESULT hr = E_OUTOFMEMORY; + CSmartRenameClassFactory *pClassFactory = new CSmartRenameClassFactory(clsid); + if (pClassFactory) + { + hr = pClassFactory->QueryInterface(riid, ppv); + pClassFactory->Release(); + } + return hr; +} + +STDAPI DllRegisterServer() +{ + return S_OK; +} + +STDAPI DllUnregisterServer() +{ + return S_OK; +} + +void DllAddRef() +{ + g_dwModuleRefCount++; +} + +void DllRelease() +{ + g_dwModuleRefCount--; +} + + +class PowerRenameModule : public PowertoyModuleIface +{ +private: + // Enabled by default + bool m_enabled = true; + +public: + // Return the display name of the powertoy, this will be cached + virtual PCWSTR get_name() override + { + return L"PowerRename"; + } + + // Enable the powertoy + virtual void enable() + { + m_enabled = true; + save_settings(); + } + + // Disable the powertoy + virtual void disable() + { + m_enabled = false; + save_settings(); + } + + // Returns if the powertoy is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Return array of the names of all events that this powertoy listens for, with + // nullptr as the last element of the array. Nullptr can also be retured for empty list. + virtual PCWSTR* get_events() override + { + return nullptr; + } + + // Return JSON with the configuration options. + // These are the settings shown on the settings page along with their current values. + virtual bool get_config(_Out_ PWSTR buffer, _Out_ int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + settings.set_description(L"A Windows Shell Extension for more advanced bulk renaming using search and replace or regular expressions."); + + // Link to the GitHub PowerRename sub-page + settings.set_overview_link(L"https://github.com/microsoft/PowerToys/tree/master/src/modules/powerrename"); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Passes JSON with the configuration settings for the powertoy. + // This is called when the user hits Save on the settings page. + virtual void set_config(PCWSTR config) override + { + } + + // Signal from the Settings editor to call a custom action. + // This can be used to spawn more complex editors. + virtual void call_custom_action(const wchar_t* action) override + { + } + + // Handle incoming event, data is event-specific + virtual intptr_t signal_event(const wchar_t* name, intptr_t data) override + { + return 0; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + delete this; + } + + void init_settings() + { + m_enabled = CPowerRenameMenu::IsEnabled(); + } + + void save_settings() + { + CPowerRenameMenu::SetEnabled(m_enabled); + } + + PowerRenameModule() + { + init_settings(); + } +}; + + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new PowerRenameModule(); +} diff --git a/src/modules/powerrename/dll/resource.h b/src/modules/powerrename/dll/resource.h new file mode 100644 index 0000000000..1d565af39a --- /dev/null +++ b/src/modules/powerrename/dll/resource.h @@ -0,0 +1,12 @@ +#define IDS_POWERRENAME 801 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/modules/powerrename/dll/stdafx.cpp b/src/modules/powerrename/dll/stdafx.cpp new file mode 100644 index 0000000000..76d63011b5 --- /dev/null +++ b/src/modules/powerrename/dll/stdafx.cpp @@ -0,0 +1,2 @@ +#include "stdafx.h" +#pragma comment(lib, "windowsapp") diff --git a/src/modules/powerrename/dll/stdafx.h b/src/modules/powerrename/dll/stdafx.h new file mode 100644 index 0000000000..84fff93641 --- /dev/null +++ b/src/modules/powerrename/dll/stdafx.h @@ -0,0 +1,26 @@ +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files: +#include +#include +#include +#include +#include +#include +#include +#include "common/common.h" + +void DllAddRef(); +void DllRelease(); + +#define INITGUID +#include + +// {0440049F-D1DC-4E46-B27B-98393D79486B} +DEFINE_GUID(CLSID_PowerRenameMenu, 0x0440049F, 0xD1DC, 0x4E46, 0xB2, 0x7B, 0x98, 0x39, 0x3D, 0x79, 0x48, 0x6B); + +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + diff --git a/src/modules/powerrename/dll/targetver.h b/src/modules/powerrename/dll/targetver.h new file mode 100644 index 0000000000..d3495778cb --- /dev/null +++ b/src/modules/powerrename/dll/targetver.h @@ -0,0 +1,8 @@ +#pragma once +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include + diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp new file mode 100644 index 0000000000..18d1e86a15 --- /dev/null +++ b/src/modules/powerrename/lib/Helpers.cpp @@ -0,0 +1,288 @@ +#include "stdafx.h" +#include "Helpers.h" +#include + +HRESULT GetIconIndexFromPath(_In_ PCWSTR path, _Out_ int* index) +{ + *index = 0; + + HRESULT hr = E_FAIL; + + SHFILEINFO shFileInfo = { 0 }; + + if (!PathIsRelative(path)) + { + DWORD attrib = GetFileAttributes(path); + HIMAGELIST himl = (HIMAGELIST)SHGetFileInfo(path, attrib, &shFileInfo, sizeof(shFileInfo), (SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_USEFILEATTRIBUTES)); + if (himl) + { + *index = shFileInfo.iIcon; + // We shouldn't free the HIMAGELIST. + hr = S_OK; + } + } + + return hr; +} + +HRESULT _ParseEnumItems(_In_ IEnumShellItems* pesi, _In_ IPowerRenameManager* psrm, _In_ int depth = 0) +{ + HRESULT hr = E_INVALIDARG; + + // We shouldn't get this deep since we only enum the contents of + // regular folders but adding just in case + if ((pesi) && (depth < (MAX_PATH / 2))) + { + hr = S_OK; + + ULONG celtFetched; + CComPtr spsi; + while ((S_OK == pesi->Next(1, &spsi, &celtFetched)) && (SUCCEEDED(hr))) + { + CComPtr spsrif; + hr = psrm->get_smartRenameItemFactory(&spsrif); + if (SUCCEEDED(hr)) + { + CComPtr spNewItem; + hr = spsrif->Create(spsi, &spNewItem); + if (SUCCEEDED(hr)) + { + spNewItem->put_depth(depth); + hr = psrm->AddItem(spNewItem); + } + + if (SUCCEEDED(hr)) + { + bool isFolder = false; + if (SUCCEEDED(spNewItem->get_isFolder(&isFolder)) && isFolder) + { + // Bind to the IShellItem for the IEnumShellItems interface + CComPtr spesiNext; + hr = spsi->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&spesiNext)); + if (SUCCEEDED(hr)) + { + // Parse the folder contents recursively + hr = _ParseEnumItems(spesiNext, psrm, depth + 1); + } + } + } + } + + spsi = nullptr; + } + } + + return hr; +} + +// Iterate through the data object and add paths to the rotation manager +HRESULT EnumerateDataObject(_In_ IDataObject* pdo, _In_ IPowerRenameManager* psrm) +{ + CComPtr spsia; + HRESULT hr = SHCreateShellItemArrayFromDataObject(pdo, IID_PPV_ARGS(&spsia)); + if (SUCCEEDED(hr)) + { + CComPtr spesi; + hr = spsia->EnumItems(&spesi); + if (SUCCEEDED(hr)) + { + hr = _ParseEnumItems(spesi, psrm); + } + } + + return hr; +} + +HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p) +{ + WNDCLASS wc = { 0 }; + PWSTR wndClassName = L"MsgWindow"; + + wc.lpfnWndProc = DefWindowProc; + wc.cbWndExtra = sizeof(void*); + wc.hInstance = hInst; + wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1); + wc.lpszClassName = wndClassName; + + RegisterClass(&wc); + + HWND hwnd = CreateWindowEx( + 0, wndClassName, nullptr, 0, + 0, 0, 0, 0, HWND_MESSAGE, + 0, hInst, nullptr); + if (hwnd) + { + SetWindowLongPtr(hwnd, 0, (LONG_PTR)p); + if (pfnWndProc) + { + SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)pfnWndProc); + } + } + + return hwnd; +} + +BOOL GetEnumeratedFileName(__out_ecount(cchMax) PWSTR pszUniqueName, UINT cchMax, + __in PCWSTR pszTemplate, __in_opt PCWSTR pszDir, unsigned long ulMinLong, + __inout unsigned long* pulNumUsed) +{ + PWSTR pszName = nullptr; + HRESULT hr = S_OK; + BOOL fRet = FALSE; + int cchDir = 0; + + if (0 != cchMax && pszUniqueName) + { + *pszUniqueName = 0; + if (pszDir) + { + hr = StringCchCopy(pszUniqueName, cchMax, pszDir); + if (SUCCEEDED(hr)) + { + hr = PathCchAddBackslashEx(pszUniqueName, cchMax, &pszName, nullptr); + if (SUCCEEDED(hr)) + { + cchDir = lstrlen(pszDir); + } + } + } + else + { + cchDir = 0; + pszName = pszUniqueName; + } + } + else + { + hr = E_INVALIDARG; + } + + int cchTmp = 0; + int cchStem = 0; + PCWSTR pszStem = nullptr; + PCWSTR pszRest = nullptr; + wchar_t szFormat[MAX_PATH] = { 0 }; + + if (SUCCEEDED(hr)) + { + pszStem = pszTemplate; + + pszRest = StrChr(pszTemplate, L'('); + while (pszRest) + { + PCWSTR pszEndUniq = CharNext(pszRest); + while (*pszEndUniq && *pszEndUniq >= L'0' && *pszEndUniq <= L'9') + { + pszEndUniq++; + } + + if (*pszEndUniq == L')') + { + break; + } + + pszRest = StrChr(CharNext(pszRest), L'('); + } + + if (!pszRest) + { + pszRest = PathFindExtension(pszTemplate); + cchStem = (int)(pszRest - pszTemplate); + + hr = StringCchCopy(szFormat, ARRAYSIZE(szFormat), L" (%lu)"); + } + else + { + pszRest++; + + cchStem = (int)(pszRest - pszTemplate); + + while (*pszRest && *pszRest >= L'0' && *pszRest <= L'9') + { + pszRest++; + } + + hr = StringCchCopy(szFormat, ARRAYSIZE(szFormat), L"%lu"); + } + } + + unsigned long ulMax = 0; + unsigned long ulMin = 0; + if (SUCCEEDED(hr)) + { + int cchFormat = lstrlen(szFormat); + if (cchFormat < 3) + { + *pszUniqueName = L'\0'; + return FALSE; + } + ulMin = ulMinLong; + cchTmp = cchMax - cchDir - cchStem - (cchFormat - 3); + switch (cchTmp) + { + case 1: + ulMax = 10; + break; + case 2: + ulMax = 100; + break; + case 3: + ulMax = 1000; + break; + case 4: + ulMax = 10000; + break; + case 5: + ulMax = 100000; + break; + default: + if (cchTmp <= 0) + { + ulMax = ulMin; + } + else + { + ulMax = 1000000; + } + break; + } + } + + if (SUCCEEDED(hr)) + { + hr = StringCchCopyN(pszName, pszUniqueName + cchMax - pszName, pszStem, cchStem); + if (SUCCEEDED(hr)) + { + PWSTR pszDigit = pszName + cchStem; + + for (unsigned long ul = ulMin; ((ul < ulMax) && (!fRet)); ul++) + { + wchar_t szTemp[MAX_PATH] = { 0 }; + hr = StringCchPrintf(szTemp, ARRAYSIZE(szTemp), szFormat, ul); + if (SUCCEEDED(hr)) + { + hr = StringCchCat(szTemp, ARRAYSIZE(szTemp), pszRest); + if (SUCCEEDED(hr)) + { + hr = StringCchCopy(pszDigit, pszUniqueName + cchMax - pszDigit, szTemp); + if (SUCCEEDED(hr)) + { + if (!PathFileExists(pszUniqueName)) + { + (*pulNumUsed) = ul; + fRet = TRUE; + } + } + } + } + } + } + } + + if (!fRet) + { + *pszUniqueName = L'\0'; + } + + return fRet; +} diff --git a/src/modules/powerrename/lib/Helpers.h b/src/modules/powerrename/lib/Helpers.h new file mode 100644 index 0000000000..4f6f753e18 --- /dev/null +++ b/src/modules/powerrename/lib/Helpers.h @@ -0,0 +1,10 @@ +#pragma once +#include "stdafx.h" + +HRESULT EnumerateDataObject(_In_ IDataObject* pdo, _In_ IPowerRenameManager* psrm); +HRESULT GetIconIndexFromPath(_In_ PCWSTR path, _Out_ int* index); +HWND CreateMsgWindow(_In_ HINSTANCE hInst, _In_ WNDPROC pfnWndProc, _In_ void* p); +BOOL GetEnumeratedFileName( + __out_ecount(cchMax) PWSTR pszUniqueName, UINT cchMax, + __in PCWSTR pszTemplate, __in_opt PCWSTR pszDir, unsigned long ulMinLong, + __inout unsigned long* pulNumUsed); \ No newline at end of file diff --git a/src/modules/powerrename/lib/PowerRenameInterfaces.h b/src/modules/powerrename/lib/PowerRenameInterfaces.h new file mode 100644 index 0000000000..a69b4fe9f6 --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameInterfaces.h @@ -0,0 +1,109 @@ +#pragma once +#include "stdafx.h" + +enum PowerRenameFlags +{ + CaseSensitive = 0x1, + MatchAllOccurences = 0x2, + UseRegularExpressions = 0x4, + EnumerateItems = 0x8, + ExcludeFiles = 0x10, + ExcludeFolders = 0x20, + ExcludeSubfolders = 0x40, + NameOnly = 0x80, + ExtensionOnly = 0x100 +}; + +interface __declspec(uuid("3ECBA62B-E0F0-4472-AA2E-DEE7A1AA46B9")) IPowerRenameRegExEvents : public IUnknown +{ +public: + IFACEMETHOD(OnSearchTermChanged)(_In_ PCWSTR searchTerm) = 0; + IFACEMETHOD(OnReplaceTermChanged)(_In_ PCWSTR replaceTerm) = 0; + IFACEMETHOD(OnFlagsChanged)(_In_ DWORD flags) = 0; +}; + +interface __declspec(uuid("E3ED45B5-9CE0-47E2-A595-67EB950B9B72")) IPowerRenameRegEx : public IUnknown +{ +public: + IFACEMETHOD(Advise)(_In_ IPowerRenameRegExEvents* regExEvents, _Out_ DWORD* cookie) = 0; + IFACEMETHOD(UnAdvise)(_In_ DWORD cookie) = 0; + IFACEMETHOD(get_searchTerm)(_Outptr_ PWSTR* searchTerm) = 0; + IFACEMETHOD(put_searchTerm)(_In_ PCWSTR searchTerm) = 0; + IFACEMETHOD(get_replaceTerm)(_Outptr_ PWSTR* replaceTerm) = 0; + IFACEMETHOD(put_replaceTerm)(_In_ PCWSTR replaceTerm) = 0; + IFACEMETHOD(get_flags)(_Out_ DWORD* flags) = 0; + IFACEMETHOD(put_flags)(_In_ DWORD flags) = 0; + IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result) = 0; +}; + +interface __declspec(uuid("C7F59201-4DE1-4855-A3A2-26FC3279C8A5")) IPowerRenameItem : public IUnknown +{ +public: + IFACEMETHOD(get_path)(_Outptr_ PWSTR* path) = 0; + IFACEMETHOD(get_shellItem)(_Outptr_ IShellItem** ppsi) = 0; + IFACEMETHOD(get_originalName)(_Outptr_ PWSTR* originalName) = 0; + IFACEMETHOD(get_newName)(_Outptr_ PWSTR* newName) = 0; + IFACEMETHOD(put_newName)(_In_opt_ PCWSTR newName) = 0; + IFACEMETHOD(get_isFolder)(_Out_ bool* isFolder) = 0; + IFACEMETHOD(get_isSubFolderContent)(_Out_ bool* isSubFolderContent) = 0; + IFACEMETHOD(get_selected)(_Out_ bool* selected) = 0; + IFACEMETHOD(put_selected)(_In_ bool selected) = 0; + IFACEMETHOD(get_id)(_Out_ int *id) = 0; + IFACEMETHOD(get_iconIndex)(_Out_ int* iconIndex) = 0; + IFACEMETHOD(get_depth)(_Out_ UINT* depth) = 0; + IFACEMETHOD(put_depth)(_In_ int depth) = 0; + IFACEMETHOD(ShouldRenameItem)(_In_ DWORD flags, _Out_ bool* shouldRename) = 0; + IFACEMETHOD(Reset)() = 0; +}; + +interface __declspec(uuid("{26CBFFD9-13B3-424E-BAC9-D12B0539149C}")) IPowerRenameItemFactory : public IUnknown +{ +public: + IFACEMETHOD(Create)(_In_ IShellItem* psi, _COM_Outptr_ IPowerRenameItem** ppItem) = 0; +}; + +interface __declspec(uuid("87FC43F9-7634-43D9-99A5-20876AFCE4AD")) IPowerRenameManagerEvents : public IUnknown +{ +public: + IFACEMETHOD(OnItemAdded)(_In_ IPowerRenameItem* renameItem) = 0; + IFACEMETHOD(OnUpdate)(_In_ IPowerRenameItem* renameItem) = 0; + IFACEMETHOD(OnError)(_In_ IPowerRenameItem* renameItem) = 0; + IFACEMETHOD(OnRegExStarted)(_In_ DWORD threadId) = 0; + IFACEMETHOD(OnRegExCanceled)(_In_ DWORD threadId) = 0; + IFACEMETHOD(OnRegExCompleted)(_In_ DWORD threadId) = 0; + IFACEMETHOD(OnRenameStarted)() = 0; + IFACEMETHOD(OnRenameCompleted)() = 0; +}; + +interface __declspec(uuid("001BBD88-53D2-4FA6-95D2-F9A9FA4F9F70")) IPowerRenameManager : public IUnknown +{ +public: + IFACEMETHOD(Advise)(_In_ IPowerRenameManagerEvents* renameManagerEvent, _Out_ DWORD* cookie) = 0; + IFACEMETHOD(UnAdvise)(_In_ DWORD cookie) = 0; + IFACEMETHOD(Start)() = 0; + IFACEMETHOD(Stop)() = 0; + IFACEMETHOD(Reset)() = 0; + IFACEMETHOD(Shutdown)() = 0; + IFACEMETHOD(Rename)(_In_ HWND hwndParent) = 0; + IFACEMETHOD(AddItem)(_In_ IPowerRenameItem* pItem) = 0; + IFACEMETHOD(GetItemByIndex)(_In_ UINT index, _COM_Outptr_ IPowerRenameItem** ppItem) = 0; + IFACEMETHOD(GetItemById)(_In_ int id, _COM_Outptr_ IPowerRenameItem** ppItem) = 0; + IFACEMETHOD(GetItemCount)(_Out_ UINT* count) = 0; + IFACEMETHOD(GetSelectedItemCount)(_Out_ UINT* count) = 0; + IFACEMETHOD(GetRenameItemCount)(_Out_ UINT* count) = 0; + IFACEMETHOD(get_flags)(_Out_ DWORD* flags) = 0; + IFACEMETHOD(put_flags)(_In_ DWORD flags) = 0; + IFACEMETHOD(get_smartRenameRegEx)(_COM_Outptr_ IPowerRenameRegEx** ppRegEx) = 0; + IFACEMETHOD(put_smartRenameRegEx)(_In_ IPowerRenameRegEx* pRegEx) = 0; + IFACEMETHOD(get_smartRenameItemFactory)(_COM_Outptr_ IPowerRenameItemFactory** ppItemFactory) = 0; + IFACEMETHOD(put_smartRenameItemFactory)(_In_ IPowerRenameItemFactory* pItemFactory) = 0; +}; + +interface __declspec(uuid("E6679DEB-460D-42C1-A7A8-E25897061C99")) IPowerRenameUI : public IUnknown +{ +public: + IFACEMETHOD(Show)() = 0; + IFACEMETHOD(Close)() = 0; + IFACEMETHOD(Update)() = 0; +}; + diff --git a/src/modules/powerrename/lib/PowerRenameItem.cpp b/src/modules/powerrename/lib/PowerRenameItem.cpp new file mode 100644 index 0000000000..789de11c5b --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameItem.cpp @@ -0,0 +1,221 @@ +#include "stdafx.h" +#include "PowerRenameItem.h" +#include "helpers.h" + +int CPowerRenameItem::s_id = 0; + +IFACEMETHODIMP_(ULONG) CPowerRenameItem::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CPowerRenameItem::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + + if (refCount == 0) + { + delete this; + } + return refCount; +} + +IFACEMETHODIMP CPowerRenameItem::QueryInterface(_In_ REFIID riid, _Outptr_ void** ppv) +{ + static const QITAB qit[] = { + QITABENT(CPowerRenameItem, IPowerRenameItem), + QITABENT(CPowerRenameItem, IPowerRenameItemFactory), + { 0 } + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP CPowerRenameItem::get_path(_Outptr_ PWSTR* path) +{ + *path = nullptr; + CSRWSharedAutoLock lock(&m_lock); + HRESULT hr = m_path ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + hr = SHStrDup(m_path, path); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameItem::get_shellItem(_Outptr_ IShellItem** ppsi) +{ + return SHCreateItemFromParsingName(m_path, nullptr, IID_PPV_ARGS(ppsi)); +} + +IFACEMETHODIMP CPowerRenameItem::get_originalName(_Outptr_ PWSTR* originalName) +{ + CSRWSharedAutoLock lock(&m_lock); + HRESULT hr = m_originalName ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + hr = SHStrDup(m_originalName, originalName); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameItem::put_newName(_In_opt_ PCWSTR newName) +{ + CSRWSharedAutoLock lock(&m_lock); + CoTaskMemFree(m_newName); + m_newName = nullptr; + HRESULT hr = S_OK; + if (newName != nullptr) + { + hr = SHStrDup(newName, &m_newName); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameItem::get_newName(_Outptr_ PWSTR* newName) +{ + CSRWSharedAutoLock lock(&m_lock); + HRESULT hr = m_newName ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + hr = SHStrDup(m_newName, newName); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameItem::get_isFolder(_Out_ bool* isFolder) +{ + CSRWSharedAutoLock lock(&m_lock); + *isFolder = m_isFolder; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::get_isSubFolderContent(_Out_ bool* isSubFolderContent) +{ + CSRWSharedAutoLock lock(&m_lock); + *isSubFolderContent = m_depth > 0; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::get_selected(_Out_ bool* selected) +{ + CSRWSharedAutoLock lock(&m_lock); + *selected = m_selected; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::put_selected(_In_ bool selected) +{ + CSRWSharedAutoLock lock(&m_lock); + m_selected = selected; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::get_id(_Out_ int* id) +{ + CSRWSharedAutoLock lock(&m_lock); + *id = m_id; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::get_iconIndex(_Out_ int* iconIndex) +{ + if (m_iconIndex == -1) + { + GetIconIndexFromPath((PCWSTR)m_path, &m_iconIndex); + } + *iconIndex = m_iconIndex; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::get_depth(_Out_ UINT* depth) +{ + *depth = m_depth; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::put_depth(_In_ int depth) +{ + m_depth = depth; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::ShouldRenameItem(_In_ DWORD flags, _Out_ bool* shouldRename) +{ + // Should we perform a rename on this item given its + // state and the options that were set? + bool hasChanged = m_newName != nullptr && (lstrcmp(m_originalName, m_newName) != 0); + bool excludeBecauseFolder = (m_isFolder && (flags & PowerRenameFlags::ExcludeFolders)); + bool excludeBecauseFile = (!m_isFolder && (flags & PowerRenameFlags::ExcludeFiles)); + bool excludeBecauseSubFolderContent = (m_depth > 0 && (flags & PowerRenameFlags::ExcludeSubfolders)); + *shouldRename = (m_selected && hasChanged && !excludeBecauseFile && + !excludeBecauseFolder && !excludeBecauseSubFolderContent); + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameItem::Reset() +{ + CSRWSharedAutoLock lock(&m_lock); + CoTaskMemFree(m_newName); + m_newName = nullptr; + return S_OK; +} + +HRESULT CPowerRenameItem::s_CreateInstance(_In_opt_ IShellItem* psi, _In_ REFIID iid, _Outptr_ void** resultInterface) +{ + *resultInterface = nullptr; + + CPowerRenameItem *newRenameItem = new CPowerRenameItem(); + HRESULT hr = newRenameItem ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + if (psi != nullptr) + { + hr = newRenameItem->_Init(psi); + } + + if (SUCCEEDED(hr)) + { + hr = newRenameItem->QueryInterface(iid, resultInterface); + } + + newRenameItem->Release(); + } + return hr; +} + +CPowerRenameItem::CPowerRenameItem() : + m_refCount(1), + m_id(++s_id) +{ +} + +CPowerRenameItem::~CPowerRenameItem() +{ + CoTaskMemFree(m_path); + CoTaskMemFree(m_newName); + CoTaskMemFree(m_originalName); +} + +HRESULT CPowerRenameItem::_Init(_In_ IShellItem* psi) +{ + // Get the full filesystem path from the shell item + HRESULT hr = psi->GetDisplayName(SIGDN_FILESYSPATH, &m_path); + if (SUCCEEDED(hr)) + { + hr = psi->GetDisplayName(SIGDN_NORMALDISPLAY, &m_originalName); + if (SUCCEEDED(hr)) + { + // Check if we are a folder now so we can check this attribute quickly later + SFGAOF att = 0; + hr = psi->GetAttributes(SFGAO_STREAM | SFGAO_FOLDER, &att); + if (SUCCEEDED(hr)) + { + // Some items can be both folders and streams (ex: zip folders). + m_isFolder = (att & SFGAO_FOLDER) && !(att & SFGAO_STREAM); + } + } + } + + return hr; +} diff --git a/src/modules/powerrename/lib/PowerRenameItem.h b/src/modules/powerrename/lib/PowerRenameItem.h new file mode 100644 index 0000000000..27be6a55f3 --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameItem.h @@ -0,0 +1,60 @@ +#pragma once +#include "stdafx.h" +#include "PowerRenameInterfaces.h" +#include "srwlock.h" + +class CPowerRenameItem : + public IPowerRenameItem, + public IPowerRenameItemFactory +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameItem + IFACEMETHODIMP get_path(_Outptr_ PWSTR* path); + IFACEMETHODIMP get_shellItem(_Outptr_ IShellItem** ppsi); + IFACEMETHODIMP get_originalName(_Outptr_ PWSTR* originalName); + IFACEMETHODIMP put_newName(_In_opt_ PCWSTR newName); + IFACEMETHODIMP get_newName(_Outptr_ PWSTR* newName); + IFACEMETHODIMP get_isFolder(_Out_ bool* isFolder); + IFACEMETHODIMP get_isSubFolderContent(_Out_ bool* isSubFolderContent); + IFACEMETHODIMP get_selected(_Out_ bool* selected); + IFACEMETHODIMP put_selected(_In_ bool selected); + IFACEMETHODIMP get_id(_Out_ int* id); + IFACEMETHODIMP get_iconIndex(_Out_ int* iconIndex); + IFACEMETHODIMP get_depth(_Out_ UINT* depth); + IFACEMETHODIMP put_depth(_In_ int depth); + IFACEMETHODIMP Reset(); + IFACEMETHODIMP ShouldRenameItem(_In_ DWORD flags, _Out_ bool* shouldRename); + + // IPowerRenameItemFactory + IFACEMETHODIMP Create(_In_ IShellItem* psi, _Outptr_ IPowerRenameItem** ppItem) + { + return CPowerRenameItem::s_CreateInstance(psi, IID_PPV_ARGS(ppItem)); + } + +public: + static HRESULT s_CreateInstance(_In_opt_ IShellItem* psi, _In_ REFIID iid, _Outptr_ void** resultInterface); + +protected: + static int s_id; + CPowerRenameItem(); + virtual ~CPowerRenameItem(); + + HRESULT _Init(_In_ IShellItem* psi); + + bool m_selected = true; + bool m_isFolder = false; + int m_id = -1; + int m_iconIndex = -1; + UINT m_depth = 0; + HRESULT m_error = S_OK; + PWSTR m_path = nullptr; + PWSTR m_originalName = nullptr; + PWSTR m_newName = nullptr; + CSRWLock m_lock; + long m_refCount = 0; +}; \ No newline at end of file diff --git a/src/modules/powerrename/lib/PowerRenameLib.vcxproj b/src/modules/powerrename/lib/PowerRenameLib.vcxproj new file mode 100644 index 0000000000..6c61aa9f26 --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameLib.vcxproj @@ -0,0 +1,167 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {51920F1F-C28C-4ADF-8660-4238766796C2} + Win32Proj + PowerRenameLib + 10.0 + + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\ + + + $(SolutionDir)$(Platform)\$(Configuration)\ + + + + Use + Level4 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + + + + + Use + Level4 + Disabled + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + + + + + Level3 + Use + MaxSpeed + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/src/modules/powerrename/lib/PowerRenameManager.cpp b/src/modules/powerrename/lib/PowerRenameManager.cpp new file mode 100644 index 0000000000..69cd8c4001 --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameManager.cpp @@ -0,0 +1,1000 @@ +#include "stdafx.h" +#include "PowerRenameManager.h" +#include "PowerRenameRegEx.h" // Default RegEx handler +#include +#include +#include "helpers.h" +#include +namespace fs = std::filesystem; + +extern HINSTANCE g_hInst; + +// The default FOF flags to use in the rename operations +#define FOF_DEFAULTFLAGS (FOF_ALLOWUNDO | FOFX_ADDUNDORECORD | FOFX_SHOWELEVATIONPROMPT | FOF_RENAMEONCOLLISION) + +IFACEMETHODIMP_(ULONG) CPowerRenameManager::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CPowerRenameManager::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + + if (refCount == 0) + { + delete this; + } + return refCount; +} + +IFACEMETHODIMP CPowerRenameManager::QueryInterface(_In_ REFIID riid, _Outptr_ void** ppv) +{ + static const QITAB qit[] = { + QITABENT(CPowerRenameManager, IPowerRenameManager), + QITABENT(CPowerRenameManager, IPowerRenameRegExEvents), + { 0 } + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP CPowerRenameManager::Advise(_In_ IPowerRenameManagerEvents* renameOpEvents, _Out_ DWORD* cookie) +{ + CSRWExclusiveAutoLock lock(&m_lockEvents); + m_cookie++; + SMART_RENAME_MGR_EVENT srme; + srme.cookie = m_cookie; + srme.pEvents = renameOpEvents; + renameOpEvents->AddRef(); + m_PowerRenameManagerEvents.push_back(srme); + + *cookie = m_cookie; + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::UnAdvise(_In_ DWORD cookie) +{ + HRESULT hr = E_FAIL; + CSRWExclusiveAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.cookie == cookie) + { + hr = S_OK; + it.cookie = 0; + if (it.pEvents) + { + it.pEvents->Release(); + it.pEvents = nullptr; + } + break; + } + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::Start() +{ + return E_NOTIMPL; +} + +IFACEMETHODIMP CPowerRenameManager::Stop() +{ + return E_NOTIMPL; +} + +IFACEMETHODIMP CPowerRenameManager::Rename(_In_ HWND hwndParent) +{ + m_hwndParent = hwndParent; + return _PerformFileOperation(); +} + +IFACEMETHODIMP CPowerRenameManager::Reset() +{ + // Stop all threads and wait + // Reset all rename items + return E_NOTIMPL; +} + +IFACEMETHODIMP CPowerRenameManager::Shutdown() +{ + _ClearRegEx(); + _Cleanup(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::AddItem(_In_ IPowerRenameItem* pItem) +{ + HRESULT hr = E_FAIL; + // Scope lock + { + CSRWExclusiveAutoLock lock(&m_lockItems); + int id = 0; + pItem->get_id(&id); + // Verify the item isn't already added + if (m_smartRenameItems.find(id) == m_smartRenameItems.end()) + { + m_smartRenameItems[id] = pItem; + pItem->AddRef(); + hr = S_OK; + } + } + + if (SUCCEEDED(hr)) + { + _OnItemAdded(pItem); + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::GetItemByIndex(_In_ UINT index, _COM_Outptr_ IPowerRenameItem** ppItem) +{ + *ppItem = nullptr; + CSRWSharedAutoLock lock(&m_lockItems); + HRESULT hr = E_FAIL; + if (index < m_smartRenameItems.size()) + { + std::map::iterator it = m_smartRenameItems.begin(); + std::advance(it, index); + *ppItem = it->second; + (*ppItem)->AddRef(); + hr = S_OK; + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::GetItemById(_In_ int id, _COM_Outptr_ IPowerRenameItem** ppItem) +{ + *ppItem = nullptr; + + CSRWSharedAutoLock lock(&m_lockItems); + HRESULT hr = E_FAIL; + std::map::iterator it; + it = m_smartRenameItems.find(id); + if (it != m_smartRenameItems.end()) + { + *ppItem = m_smartRenameItems[id]; + (*ppItem)->AddRef(); + hr = S_OK; + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::GetItemCount(_Out_ UINT* count) +{ + CSRWSharedAutoLock lock(&m_lockItems); + *count = static_cast(m_smartRenameItems.size()); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::GetSelectedItemCount(_Out_ UINT* count) +{ + *count = 0; + CSRWSharedAutoLock lock(&m_lockItems); + + for (auto it : m_smartRenameItems) + { + IPowerRenameItem* pItem = it.second; + bool selected = false; + if (SUCCEEDED(pItem->get_selected(&selected)) && selected) + { + (*count)++; + } + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::GetRenameItemCount(_Out_ UINT* count) +{ + *count = 0; + CSRWSharedAutoLock lock(&m_lockItems); + + for (auto it : m_smartRenameItems) + { + IPowerRenameItem* pItem = it.second; + bool shouldRename = false; + if (SUCCEEDED(pItem->ShouldRenameItem(m_flags, &shouldRename)) && shouldRename) + { + (*count)++; + } + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::get_flags(_Out_ DWORD* flags) +{ + _EnsureRegEx(); + *flags = m_flags; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::put_flags(_In_ DWORD flags) +{ + if (flags != m_flags) + { + m_flags = flags; + _EnsureRegEx(); + m_spRegEx->put_flags(flags); + } + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::get_smartRenameRegEx(_COM_Outptr_ IPowerRenameRegEx** ppRegEx) +{ + *ppRegEx = nullptr; + HRESULT hr = _EnsureRegEx(); + if (SUCCEEDED(hr)) + { + *ppRegEx = m_spRegEx; + (*ppRegEx)->AddRef(); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::put_smartRenameRegEx(_In_ IPowerRenameRegEx* pRegEx) +{ + _ClearRegEx(); + m_spRegEx = pRegEx; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::get_smartRenameItemFactory(_COM_Outptr_ IPowerRenameItemFactory** ppItemFactory) +{ + *ppItemFactory = nullptr; + HRESULT hr = E_FAIL; + if (m_spItemFactory) + { + hr = S_OK; + *ppItemFactory = m_spItemFactory; + (*ppItemFactory)->AddRef(); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameManager::put_smartRenameItemFactory(_In_ IPowerRenameItemFactory* pItemFactory) +{ + m_spItemFactory = pItemFactory; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::OnSearchTermChanged(_In_ PCWSTR /*searchTerm*/) +{ + _PerformRegExRename(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::OnReplaceTermChanged(_In_ PCWSTR /*replaceTerm*/) +{ + _PerformRegExRename(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameManager::OnFlagsChanged(_In_ DWORD flags) +{ + // Flags were updated in the smart rename regex. Update our preview. + m_flags = flags; + _PerformRegExRename(); + return S_OK; +} + +HRESULT CPowerRenameManager::s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm) +{ + *ppsrm = nullptr; + CPowerRenameManager *psrm = new CPowerRenameManager(); + HRESULT hr = psrm ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + hr = psrm->_Init(); + if (SUCCEEDED(hr)) + { + hr = psrm->QueryInterface(IID_PPV_ARGS(ppsrm)); + } + psrm->Release(); + } + return hr; +} + +CPowerRenameManager::CPowerRenameManager() : + m_refCount(1) +{ + InitializeCriticalSection(&m_critsecReentrancy); +} + +CPowerRenameManager::~CPowerRenameManager() +{ + DeleteCriticalSection(&m_critsecReentrancy); +} + +HRESULT CPowerRenameManager::_Init() +{ + // Guaranteed to succeed + m_startFileOpWorkerEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + m_startRegExWorkerEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + m_cancelRegExWorkerEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + + m_hwndMessage = CreateMsgWindow(g_hInst, s_msgWndProc, this); + + return S_OK; +} + +// Custom messages for worker threads +enum +{ + SRM_REGEX_ITEM_UPDATED = (WM_APP + 1), // Single smart rename item processed by regex worker thread + SRM_REGEX_STARTED, // RegEx operation was started + SRM_REGEX_CANCELED, // Regex operation was canceled + SRM_REGEX_COMPLETE, // Regex worker thread completed + SRM_FILEOP_COMPLETE // File Operation worker thread completed +}; + +struct WorkerThreadData +{ + HWND hwndManager = nullptr; + HANDLE startEvent = nullptr; + HANDLE cancelEvent = nullptr; + HWND hwndParent = nullptr; + CComPtr spsrm; +}; + +// Msg-only worker window proc for communication from our worker threads +LRESULT CALLBACK CPowerRenameManager::s_msgWndProc(_In_ HWND hwnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam) +{ + LRESULT lRes = 0; + + CPowerRenameManager* pThis = (CPowerRenameManager*)GetWindowLongPtr(hwnd, 0); + if (pThis != nullptr) + { + lRes = pThis->_WndProc(hwnd, uMsg, wParam, lParam); + if (uMsg == WM_NCDESTROY) + { + SetWindowLongPtr(hwnd, 0, NULL); + pThis->m_hwndMessage = nullptr; + } + } + else + { + lRes = DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + return lRes; +} + +LRESULT CPowerRenameManager::_WndProc(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam) +{ + LRESULT lRes = 0; + + AddRef(); + + switch (msg) + { + case SRM_REGEX_ITEM_UPDATED: + { + int id = static_cast(lParam); + CComPtr spItem; + if (SUCCEEDED(GetItemById(id, &spItem))) + { + _OnUpdate(spItem); + } + break; + } + case SRM_REGEX_STARTED: + _OnRegExStarted(static_cast(wParam)); + break; + + case SRM_REGEX_CANCELED: + _OnRegExCanceled(static_cast(wParam)); + break; + + case SRM_REGEX_COMPLETE: + _OnRegExCompleted(static_cast(wParam)); + break; + + default: + lRes = DefWindowProc(hwnd, msg, wParam, lParam); + break; + } + + Release(); + + return lRes; +} + +HRESULT CPowerRenameManager::_PerformFileOperation() +{ + // Do we have items to rename? + UINT renameItemCount = 0; + if (FAILED(GetRenameItemCount(&renameItemCount)) || renameItemCount == 0) + { + return E_FAIL; + } + + // Wait for existing regex thread to finish + _WaitForRegExWorkerThread(); + + // Create worker thread which will perform the actual rename + HRESULT hr = _CreateFileOpWorkerThread(); + if (SUCCEEDED(hr)) + { + _OnRenameStarted(); + + // Signal the worker thread that they can start working. We needed to wait until we + // were ready to process thread messages. + SetEvent(m_startFileOpWorkerEvent); + + while (true) + { + // Check if worker thread has exited + if (WaitForSingleObject(m_fileOpWorkerThreadHandle, 0) == WAIT_OBJECT_0) + { + break; + } + + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + if (msg.message == SRM_FILEOP_COMPLETE) + { + // Worker thread completed + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } + + _OnRenameCompleted(); + } + + return 0; +} + +HRESULT CPowerRenameManager::_CreateFileOpWorkerThread() +{ + WorkerThreadData* pwtd = new WorkerThreadData; + HRESULT hr = pwtd ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + pwtd->hwndManager = m_hwndMessage; + pwtd->startEvent = m_startRegExWorkerEvent; + pwtd->cancelEvent = nullptr; + pwtd->spsrm = this; + m_fileOpWorkerThreadHandle = CreateThread(nullptr, 0, s_fileOpWorkerThread, pwtd, 0, nullptr); + hr = (m_fileOpWorkerThreadHandle) ? S_OK : E_FAIL; + if (FAILED(hr)) + { + delete pwtd; + } + } + + return hr; +} + +DWORD WINAPI CPowerRenameManager::s_fileOpWorkerThread(_In_ void* pv) +{ + if (SUCCEEDED(CoInitializeEx(NULL, 0))) + { + WorkerThreadData* pwtd = reinterpret_cast(pv); + if (pwtd) + { + // Wait to be told we can begin + if (WaitForSingleObject(pwtd->startEvent, INFINITE) == WAIT_OBJECT_0) + { + CComPtr spRenameRegEx; + if (SUCCEEDED(pwtd->spsrm->get_smartRenameRegEx(&spRenameRegEx))) + { + // Create IFileOperation interface + CComPtr spFileOp; + if (SUCCEEDED(CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spFileOp)))) + { + DWORD flags = 0; + spRenameRegEx->get_flags(&flags); + + UINT itemCount = 0; + pwtd->spsrm->GetItemCount(&itemCount); + // Add each rename operation + for (UINT u = 0; u <= itemCount; u++) + { + CComPtr spItem; + if (SUCCEEDED(pwtd->spsrm->GetItemByIndex(u, &spItem))) + { + bool shouldRename = false; + if (SUCCEEDED(spItem->ShouldRenameItem(flags, &shouldRename)) && shouldRename) + { + PWSTR newName = nullptr; + if (SUCCEEDED(spItem->get_newName(&newName))) + { + CComPtr spShellItem; + if (SUCCEEDED(spItem->get_shellItem(&spShellItem))) + { + spFileOp->RenameItem(spShellItem, newName, nullptr); + } + CoTaskMemFree(newName); + } + } + } + } + + // Set the operation flags + if (SUCCEEDED(spFileOp->SetOperationFlags(FOF_DEFAULTFLAGS))) + { + // Set the parent window + if (pwtd->hwndParent) + { + spFileOp->SetOwnerWindow(pwtd->hwndParent); + } + + // Perform the operation + // We don't care about the return code here. We would rather + // return control back to explorer so the user can cleanly + // undo the operation if it failed halfway through. + spFileOp->PerformOperations(); + } + } + } + } + + // Send the manager thread the completion message + PostMessage(pwtd->hwndManager, SRM_FILEOP_COMPLETE, GetCurrentThreadId(), 0); + + delete pwtd; + } + CoUninitialize(); + } + + return 0; +} + +HRESULT CPowerRenameManager::_PerformRegExRename() +{ + HRESULT hr = E_FAIL; + + if (!TryEnterCriticalSection(&m_critsecReentrancy)) + { + // Ensure we do not re-enter since we pump messages here. + // TODO: If we do, post a message back to ourselves + } + else + { + // Ensure previous thread is canceled + _CancelRegExWorkerThread(); + + // Create worker thread which will message us progress and completion. + hr = _CreateRegExWorkerThread(); + if (SUCCEEDED(hr)) + { + ResetEvent(m_cancelRegExWorkerEvent); + + // Signal the worker thread that they can start working. We needed to wait until we + // were ready to process thread messages. + SetEvent(m_startRegExWorkerEvent); + } + } + + return hr; +} + +HRESULT CPowerRenameManager::_CreateRegExWorkerThread() +{ + WorkerThreadData* pwtd = new WorkerThreadData; + HRESULT hr = pwtd ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + pwtd->hwndManager = m_hwndMessage; + pwtd->startEvent = m_startRegExWorkerEvent; + pwtd->cancelEvent = m_cancelRegExWorkerEvent; + pwtd->hwndParent = m_hwndParent; + pwtd->spsrm = this; + m_regExWorkerThreadHandle = CreateThread(nullptr, 0, s_regexWorkerThread, pwtd, 0, nullptr); + hr = (m_regExWorkerThreadHandle) ? S_OK : E_FAIL; + if (FAILED(hr)) + { + delete pwtd; + } + } + + return hr; +} + +DWORD WINAPI CPowerRenameManager::s_regexWorkerThread(_In_ void* pv) +{ + if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) + { + WorkerThreadData* pwtd = reinterpret_cast(pv); + if (pwtd) + { + PostMessage(pwtd->hwndManager, SRM_REGEX_STARTED, GetCurrentThreadId(), 0); + + // Wait to be told we can begin + if (WaitForSingleObject(pwtd->startEvent, INFINITE) == WAIT_OBJECT_0) + { + CComPtr spRenameRegEx; + if (SUCCEEDED(pwtd->spsrm->get_smartRenameRegEx(&spRenameRegEx))) + { + DWORD flags = 0; + spRenameRegEx->get_flags(&flags); + + UINT itemCount = 0; + unsigned long itemEnumIndex = 1; + pwtd->spsrm->GetItemCount(&itemCount); + for (UINT u = 0; u <= itemCount; u++) + { + // Check if cancel event is signaled + if (WaitForSingleObject(pwtd->cancelEvent, 0) == WAIT_OBJECT_0) + { + // Canceled from manager + // Send the manager thread the canceled message + PostMessage(pwtd->hwndManager, SRM_REGEX_CANCELED, GetCurrentThreadId(), 0); + break; + } + + CComPtr spItem; + if (SUCCEEDED(pwtd->spsrm->GetItemByIndex(u, &spItem))) + { + int id = -1; + spItem->get_id(&id); + + bool isFolder = false; + bool isSubFolderContent = false; + spItem->get_isFolder(&isFolder); + spItem->get_isSubFolderContent(&isSubFolderContent); + if ((isFolder && (flags & PowerRenameFlags::ExcludeFolders)) || + (!isFolder && (flags & PowerRenameFlags::ExcludeFiles)) || + (isSubFolderContent && (flags & PowerRenameFlags::ExcludeSubfolders))) + { + // Exclude this item from renaming. Ensure new name is cleared. + spItem->put_newName(nullptr); + + // Send the manager thread the item processed message + PostMessage(pwtd->hwndManager, SRM_REGEX_ITEM_UPDATED, GetCurrentThreadId(), id); + + continue; + } + + PWSTR originalName = nullptr; + if (SUCCEEDED(spItem->get_originalName(&originalName))) + { + PWSTR currentNewName = nullptr; + spItem->get_newName(¤tNewName); + + wchar_t sourceName[MAX_PATH] = { 0 }; + if (flags & NameOnly) + { + StringCchCopy(sourceName, ARRAYSIZE(sourceName), fs::path(originalName).stem().c_str()); + } + else if (flags & ExtensionOnly) + { + std::wstring extension = fs::path(originalName).extension().wstring(); + if (!extension.empty() && extension.front() == '.') + { + extension = extension.erase(0, 1); + } + StringCchCopy(sourceName, ARRAYSIZE(sourceName), extension.c_str()); + } + else + { + StringCchCopy(sourceName, ARRAYSIZE(sourceName), originalName); + } + + + PWSTR newName = nullptr; + // Failure here means we didn't match anything or had nothing to match + // Call put_newName with null in that case to reset it + spRenameRegEx->Replace(sourceName, &newName); + + wchar_t resultName[MAX_PATH] = { 0 }; + + PWSTR newNameToUse = nullptr; + + // newName == nullptr likely means we have an empty search string. We should leave newNameToUse + // as nullptr so we clear the renamed column + if (newName != nullptr) + { + newNameToUse = resultName; + if (flags & NameOnly) + { + StringCchPrintf(resultName, ARRAYSIZE(resultName), L"%s%s", newName, fs::path(originalName).extension().c_str()); + } + else if (flags & ExtensionOnly) + { + std::wstring extension = fs::path(originalName).extension().wstring(); + if (!extension.empty()) + { + StringCchPrintf(resultName, ARRAYSIZE(resultName), L"%s.%s", fs::path(originalName).stem().c_str(), newName); + } + else + { + StringCchCopy(resultName, ARRAYSIZE(resultName), originalName); + } + } + else + { + StringCchCopy(resultName, ARRAYSIZE(resultName), newName); + } + } + + // No change from originalName so set newName to + // null so we clear it from our UI as well. + if (lstrcmp(originalName, newNameToUse) == 0) + { + newNameToUse = nullptr; + } + + wchar_t uniqueName[MAX_PATH] = { 0 }; + if (newNameToUse != nullptr && (flags & EnumerateItems)) + { + unsigned long countUsed = 0; + if (GetEnumeratedFileName(uniqueName, ARRAYSIZE(uniqueName), newNameToUse, nullptr, itemEnumIndex, &countUsed)) + { + newNameToUse = uniqueName; + } + itemEnumIndex++; + } + + spItem->put_newName(newNameToUse); + + // Was there a change? + if (lstrcmp(currentNewName, newNameToUse) != 0) + { + // Send the manager thread the item processed message + PostMessage(pwtd->hwndManager, SRM_REGEX_ITEM_UPDATED, GetCurrentThreadId(), id); + } + + CoTaskMemFree(newName); + CoTaskMemFree(currentNewName); + CoTaskMemFree(originalName); + } + } + } + } + } + + // Send the manager thread the completion message + PostMessage(pwtd->hwndManager, SRM_REGEX_COMPLETE, GetCurrentThreadId(), 0); + + delete pwtd; + } + CoUninitialize(); + } + + return 0; +} + +void CPowerRenameManager::_CancelRegExWorkerThread() +{ + if (m_startRegExWorkerEvent) + { + SetEvent(m_startRegExWorkerEvent); + } + + if (m_cancelRegExWorkerEvent) + { + SetEvent(m_cancelRegExWorkerEvent); + } + + _WaitForRegExWorkerThread(); +} + +void CPowerRenameManager::_WaitForRegExWorkerThread() +{ + if (m_regExWorkerThreadHandle) + { + WaitForSingleObject(m_regExWorkerThreadHandle, INFINITE); + CloseHandle(m_regExWorkerThreadHandle); + m_regExWorkerThreadHandle = nullptr; + } +} + +void CPowerRenameManager::_Cancel() +{ + SetEvent(m_startFileOpWorkerEvent); + _CancelRegExWorkerThread(); +} + +HRESULT CPowerRenameManager::_EnsureRegEx() +{ + HRESULT hr = S_OK; + if (!m_spRegEx) + { + // Create the default regex handler + hr = CPowerRenameRegEx::s_CreateInstance(&m_spRegEx); + if (SUCCEEDED(hr)) + { + hr = _InitRegEx(); + // Get the flags + if (SUCCEEDED(hr)) + { + m_spRegEx->get_flags(&m_flags); + } + } + } + return hr; +} + +HRESULT CPowerRenameManager::_InitRegEx() +{ + HRESULT hr = E_FAIL; + if (m_spRegEx) + { + hr = m_spRegEx->Advise(this, &m_regExAdviseCookie); + } + + return hr; +} + +void CPowerRenameManager::_ClearRegEx() +{ + if (m_spRegEx) + { + m_spRegEx->UnAdvise(m_regExAdviseCookie); + m_regExAdviseCookie = 0; + } +} + +void CPowerRenameManager::_OnItemAdded(_In_ IPowerRenameItem* renameItem) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnItemAdded(renameItem); + } + } +} + +void CPowerRenameManager::_OnUpdate(_In_ IPowerRenameItem* renameItem) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnUpdate(renameItem); + } + } +} + +void CPowerRenameManager::_OnError(_In_ IPowerRenameItem* renameItem) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnError(renameItem); + } + } +} + +void CPowerRenameManager::_OnRegExStarted(_In_ DWORD threadId) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnRegExStarted(threadId); + } + } +} + +void CPowerRenameManager::_OnRegExCanceled(_In_ DWORD threadId) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnRegExCanceled(threadId); + } + } +} + +void CPowerRenameManager::_OnRegExCompleted(_In_ DWORD threadId) +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnRegExCompleted(threadId); + } + } +} + +void CPowerRenameManager::_OnRenameStarted() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnRenameStarted(); + } + } +} + +void CPowerRenameManager::_OnRenameCompleted() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_PowerRenameManagerEvents) + { + if (it.pEvents) + { + it.pEvents->OnRenameCompleted(); + } + } +} + +void CPowerRenameManager::_ClearEventHandlers() +{ + CSRWExclusiveAutoLock lock(&m_lockEvents); + + // Cleanup event handlers + for (auto it : m_PowerRenameManagerEvents) + { + it.cookie = 0; + if (it.pEvents) + { + it.pEvents->Release(); + it.pEvents = nullptr; + } + } + + m_PowerRenameManagerEvents.clear(); +} + +void CPowerRenameManager::_ClearPowerRenameItems() +{ + CSRWExclusiveAutoLock lock(&m_lockItems); + + // Cleanup smart rename items + for (auto it : m_smartRenameItems) + { + IPowerRenameItem* pItem = it.second; + pItem->Release(); + } + + m_smartRenameItems.clear(); +} + +void CPowerRenameManager::_Cleanup() +{ + if (m_hwndMessage) + { + DestroyWindow(m_hwndMessage); + m_hwndMessage = nullptr; + } + + CloseHandle(m_startFileOpWorkerEvent); + m_startFileOpWorkerEvent = nullptr; + + CloseHandle(m_startRegExWorkerEvent); + m_startRegExWorkerEvent = nullptr; + + CloseHandle(m_cancelRegExWorkerEvent); + m_cancelRegExWorkerEvent = nullptr; + + _ClearRegEx(); + _ClearEventHandlers(); + _ClearPowerRenameItems(); +} diff --git a/src/modules/powerrename/lib/PowerRenameManager.h b/src/modules/powerrename/lib/PowerRenameManager.h new file mode 100644 index 0000000000..0179e3b4fe --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameManager.h @@ -0,0 +1,120 @@ +#pragma once +#include +#include +#include "srwlock.h" + +class CPowerRenameManager : + public IPowerRenameManager, + public IPowerRenameRegExEvents +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameManager + IFACEMETHODIMP Advise(_In_ IPowerRenameManagerEvents* renameOpEvent, _Out_ DWORD *cookie); + IFACEMETHODIMP UnAdvise(_In_ DWORD cookie); + IFACEMETHODIMP Start(); + IFACEMETHODIMP Stop(); + IFACEMETHODIMP Reset(); + IFACEMETHODIMP Shutdown(); + IFACEMETHODIMP Rename(_In_ HWND hwndParent); + IFACEMETHODIMP AddItem(_In_ IPowerRenameItem* pItem); + IFACEMETHODIMP GetItemByIndex(_In_ UINT index, _COM_Outptr_ IPowerRenameItem** ppItem); + IFACEMETHODIMP GetItemById(_In_ int id, _COM_Outptr_ IPowerRenameItem** ppItem); + IFACEMETHODIMP GetItemCount(_Out_ UINT* count); + IFACEMETHODIMP GetSelectedItemCount(_Out_ UINT* count); + IFACEMETHODIMP GetRenameItemCount(_Out_ UINT* count); + IFACEMETHODIMP get_flags(_Out_ DWORD* flags); + IFACEMETHODIMP put_flags(_In_ DWORD flags); + IFACEMETHODIMP get_smartRenameRegEx(_COM_Outptr_ IPowerRenameRegEx** ppRegEx); + IFACEMETHODIMP put_smartRenameRegEx(_In_ IPowerRenameRegEx* pRegEx); + IFACEMETHODIMP get_smartRenameItemFactory(_COM_Outptr_ IPowerRenameItemFactory** ppItemFactory); + IFACEMETHODIMP put_smartRenameItemFactory(_In_ IPowerRenameItemFactory* pItemFactory); + + // IPowerRenameRegExEvents + IFACEMETHODIMP OnSearchTermChanged(_In_ PCWSTR searchTerm); + IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm); + IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags); + + static HRESULT s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm); + +protected: + CPowerRenameManager(); + virtual ~CPowerRenameManager(); + + HRESULT _Init(); + void _Cleanup(); + + void _Cancel(); + + void _OnItemAdded(_In_ IPowerRenameItem* renameItem); + void _OnUpdate(_In_ IPowerRenameItem* renameItem); + void _OnError(_In_ IPowerRenameItem* renameItem); + void _OnRegExStarted(_In_ DWORD threadId); + void _OnRegExCanceled(_In_ DWORD threadId); + void _OnRegExCompleted(_In_ DWORD threadId); + void _OnRenameStarted(); + void _OnRenameCompleted(); + + void _ClearEventHandlers(); + void _ClearPowerRenameItems(); + + HRESULT _PerformRegExRename(); + HRESULT _PerformFileOperation(); + + HRESULT _CreateRegExWorkerThread(); + void _CancelRegExWorkerThread(); + void _WaitForRegExWorkerThread(); + HRESULT _CreateFileOpWorkerThread(); + + HRESULT _EnsureRegEx(); + HRESULT _InitRegEx(); + void _ClearRegEx(); + + // Thread proc for performing the regex rename of each item + static DWORD WINAPI s_regexWorkerThread(_In_ void* pv); + // Thread proc for performing the actual file operation that does the file rename + static DWORD WINAPI s_fileOpWorkerThread(_In_ void* pv); + + static LRESULT CALLBACK s_msgWndProc(_In_ HWND hwnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam); + LRESULT _WndProc(_In_ HWND hwnd, _In_ UINT msg, _In_ WPARAM wParam, _In_ LPARAM lParam); + + HANDLE m_regExWorkerThreadHandle = nullptr; + HANDLE m_startRegExWorkerEvent = nullptr; + HANDLE m_cancelRegExWorkerEvent = nullptr; + + HANDLE m_fileOpWorkerThreadHandle = nullptr; + HANDLE m_startFileOpWorkerEvent = nullptr; + + CSRWLock m_lockEvents; + CSRWLock m_lockItems; + + DWORD m_flags = 0; + + DWORD m_cookie = 0; + DWORD m_regExAdviseCookie = 0; + + struct SMART_RENAME_MGR_EVENT + { + IPowerRenameManagerEvents* pEvents; + DWORD cookie; + }; + + CComPtr m_spItemFactory; + CComPtr m_spRegEx; + + _Guarded_by_(m_lockEvents) std::vector m_PowerRenameManagerEvents; + _Guarded_by_(m_lockItems) std::map m_smartRenameItems; + + // Parent HWND used by IFileOperation + HWND m_hwndParent = nullptr; + + HWND m_hwndMessage = nullptr; + + CRITICAL_SECTION m_critsecReentrancy; + + long m_refCount; +}; \ No newline at end of file diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.cpp b/src/modules/powerrename/lib/PowerRenameRegEx.cpp new file mode 100644 index 0000000000..59cf1fd36b --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameRegEx.cpp @@ -0,0 +1,301 @@ +#include "stdafx.h" +#include "PowerRenameRegEx.h" +#include +#include +#include + + +using namespace std; +using std::regex_error; + +IFACEMETHODIMP_(ULONG) CPowerRenameRegEx::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CPowerRenameRegEx::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + + if (refCount == 0) + { + delete this; + } + return refCount; +} + +IFACEMETHODIMP CPowerRenameRegEx::QueryInterface(_In_ REFIID riid, _Outptr_ void** ppv) +{ + static const QITAB qit[] = { + QITABENT(CPowerRenameRegEx, IPowerRenameRegEx), + { 0 } + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP CPowerRenameRegEx::Advise(_In_ IPowerRenameRegExEvents* regExEvents, _Out_ DWORD* cookie) +{ + CSRWExclusiveAutoLock lock(&m_lockEvents); + m_cookie++; + SMART_RENAME_REGEX_EVENT srre; + srre.cookie = m_cookie; + srre.pEvents = regExEvents; + regExEvents->AddRef(); + m_smartRenameRegExEvents.push_back(srre); + + *cookie = m_cookie; + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameRegEx::UnAdvise(_In_ DWORD cookie) +{ + HRESULT hr = E_FAIL; + CSRWExclusiveAutoLock lock(&m_lockEvents); + + for (auto it : m_smartRenameRegExEvents) + { + if (it.cookie == cookie) + { + hr = S_OK; + it.cookie = 0; + if (it.pEvents) + { + it.pEvents->Release(); + it.pEvents = nullptr; + } + break; + } + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameRegEx::get_searchTerm(_Outptr_ PWSTR* searchTerm) +{ + *searchTerm = nullptr; + HRESULT hr = m_searchTerm ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + CSRWSharedAutoLock lock(&m_lock); + hr = SHStrDup(m_searchTerm, searchTerm); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameRegEx::put_searchTerm(_In_ PCWSTR searchTerm) +{ + bool changed = false; + HRESULT hr = searchTerm ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) + { + CSRWExclusiveAutoLock lock(&m_lock); + if (m_searchTerm == nullptr || lstrcmp(searchTerm, m_searchTerm) != 0) + { + changed = true; + CoTaskMemFree(m_searchTerm); + hr = SHStrDup(searchTerm, &m_searchTerm); + } + } + + if (SUCCEEDED(hr) && changed) + { + _OnSearchTermChanged(); + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameRegEx::get_replaceTerm(_Outptr_ PWSTR* replaceTerm) +{ + *replaceTerm = nullptr; + HRESULT hr = m_replaceTerm ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + CSRWSharedAutoLock lock(&m_lock); + hr = SHStrDup(m_replaceTerm, replaceTerm); + } + return hr; +} + +IFACEMETHODIMP CPowerRenameRegEx::put_replaceTerm(_In_ PCWSTR replaceTerm) +{ + bool changed = false; + HRESULT hr = replaceTerm ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) + { + CSRWExclusiveAutoLock lock(&m_lock); + if (m_replaceTerm == nullptr || lstrcmp(replaceTerm, m_replaceTerm) != 0) + { + changed = true; + CoTaskMemFree(m_replaceTerm); + hr = SHStrDup(replaceTerm, &m_replaceTerm); + } + } + + if (SUCCEEDED(hr) && changed) + { + _OnReplaceTermChanged(); + } + + return hr; +} + +IFACEMETHODIMP CPowerRenameRegEx::get_flags(_Out_ DWORD* flags) +{ + *flags = m_flags; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameRegEx::put_flags(_In_ DWORD flags) +{ + if (m_flags != flags) + { + m_flags = flags; + _OnFlagsChanged(); + } + return S_OK; +} + +HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx) +{ + *renameRegEx = nullptr; + + CPowerRenameRegEx *newRenameRegEx = new CPowerRenameRegEx(); + HRESULT hr = newRenameRegEx ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + hr = newRenameRegEx->QueryInterface(IID_PPV_ARGS(renameRegEx)); + newRenameRegEx->Release(); + } + return hr; +} + +CPowerRenameRegEx::CPowerRenameRegEx() : + m_refCount(1) +{ + // Init to empty strings + SHStrDup(L"", &m_searchTerm); + SHStrDup(L"", &m_replaceTerm); +} + +CPowerRenameRegEx::~CPowerRenameRegEx() +{ + CoTaskMemFree(m_searchTerm); + CoTaskMemFree(m_replaceTerm); +} + +HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result) +{ + *result = nullptr; + + CSRWSharedAutoLock lock(&m_lock); + HRESULT hr = (source && wcslen(source) > 0 && m_searchTerm && wcslen(m_searchTerm) > 0) ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) + { + wstring res = source; + try + { + // TODO: creating the regex could be costly. May want to cache this. + std::wstring sourceToUse(source); + std::wstring searchTerm(m_searchTerm); + std::wstring replaceTerm(m_replaceTerm ? wstring(m_replaceTerm) : wstring(L"")); + + if (m_flags & UseRegularExpressions) + { + std::wregex pattern(m_searchTerm, (!(m_flags & CaseSensitive)) ? regex_constants::icase | regex_constants::ECMAScript : regex_constants::ECMAScript); + if (m_flags & MatchAllOccurences) + { + res = regex_replace(wstring(source), pattern, replaceTerm); + } + else + { + std::wsmatch m; + if (std::regex_search(sourceToUse, m, pattern)) + { + res = sourceToUse.replace(m.prefix().length(), searchTerm.length(), replaceTerm); + } + } + } + else + { + // Simple search and replace + size_t pos = 0; + do + { + pos = _Find(sourceToUse, searchTerm, (!(m_flags & CaseSensitive)), pos); + if (pos != std::string::npos) + { + res = sourceToUse.replace(pos, searchTerm.length(), replaceTerm); + pos += replaceTerm.length(); + } + + if (!(m_flags & MatchAllOccurences)) + { + break; + } + } while (pos != std::string::npos); + } + + *result = StrDup(res.c_str()); + hr = (*result) ? S_OK : E_OUTOFMEMORY; + } + catch (regex_error e) + { + hr = E_FAIL; + } + } + return hr; +} + +size_t CPowerRenameRegEx::_Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos) +{ + if (caseInsensitive) + { + // Convert to lower + std::transform(data.begin(), data.end(), data.begin(), ::towlower); + std::transform(toSearch.begin(), toSearch.end(), toSearch.begin(), ::towlower); + } + + // Find sub string position in given string starting at position pos + return data.find(toSearch, pos); +} + +void CPowerRenameRegEx::_OnSearchTermChanged() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_smartRenameRegExEvents) + { + if (it.pEvents) + { + it.pEvents->OnSearchTermChanged(m_searchTerm); + } + } +} + +void CPowerRenameRegEx::_OnReplaceTermChanged() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_smartRenameRegExEvents) + { + if (it.pEvents) + { + it.pEvents->OnReplaceTermChanged(m_replaceTerm); + } + } +} + +void CPowerRenameRegEx::_OnFlagsChanged() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_smartRenameRegExEvents) + { + if (it.pEvents) + { + it.pEvents->OnFlagsChanged(m_flags); + } + } +} diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.h b/src/modules/powerrename/lib/PowerRenameRegEx.h new file mode 100644 index 0000000000..54834c9433 --- /dev/null +++ b/src/modules/powerrename/lib/PowerRenameRegEx.h @@ -0,0 +1,58 @@ +#pragma once +#include "stdafx.h" +#include +#include +#include "srwlock.h" + +#define DEFAULT_FLAGS MatchAllOccurences + +class CPowerRenameRegEx : public IPowerRenameRegEx +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameRegEx + IFACEMETHODIMP Advise(_In_ IPowerRenameRegExEvents* regExEvents, _Out_ DWORD* cookie); + IFACEMETHODIMP UnAdvise(_In_ DWORD cookie); + IFACEMETHODIMP get_searchTerm(_Outptr_ PWSTR* searchTerm); + IFACEMETHODIMP put_searchTerm(_In_ PCWSTR searchTerm); + IFACEMETHODIMP get_replaceTerm(_Outptr_ PWSTR* replaceTerm); + IFACEMETHODIMP put_replaceTerm(_In_ PCWSTR replaceTerm); + IFACEMETHODIMP get_flags(_Out_ DWORD* flags); + IFACEMETHODIMP put_flags(_In_ DWORD flags); + IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result); + + static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx **renameRegEx); + +protected: + CPowerRenameRegEx(); + virtual ~CPowerRenameRegEx(); + + void _OnSearchTermChanged(); + void _OnReplaceTermChanged(); + void _OnFlagsChanged(); + + size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos); + + DWORD m_flags = DEFAULT_FLAGS; + PWSTR m_searchTerm = nullptr; + PWSTR m_replaceTerm = nullptr; + + CSRWLock m_lock; + CSRWLock m_lockEvents; + + DWORD m_cookie = 0; + + struct SMART_RENAME_REGEX_EVENT + { + IPowerRenameRegExEvents* pEvents; + DWORD cookie; + }; + + _Guarded_by_(m_lockEvents) std::vector m_smartRenameRegExEvents; + + long m_refCount = 0; +}; \ No newline at end of file diff --git a/src/modules/powerrename/lib/srwlock.h b/src/modules/powerrename/lib/srwlock.h new file mode 100644 index 0000000000..72f2372e30 --- /dev/null +++ b/src/modules/powerrename/lib/srwlock.h @@ -0,0 +1,79 @@ +#pragma once +#include "stdafx.h" + +// Wrapper around SRWLOCK +class CSRWLock +{ +public: + CSRWLock() + { + InitializeSRWLock(&m_lock); + } + + _Acquires_shared_lock_(this->m_lock) + void LockShared() + { + AcquireSRWLockShared(&m_lock); + } + + _Acquires_exclusive_lock_(this->m_lock) + void LockExclusive() + { + AcquireSRWLockExclusive(&m_lock); + } + + _Releases_shared_lock_(this->m_lock) + void ReleaseShared() + { + ReleaseSRWLockShared(&m_lock); + } + + _Releases_exclusive_lock_(this->m_lock) + void ReleaseExclusive() + { + ReleaseSRWLockExclusive(&m_lock); + } + + virtual ~CSRWLock() + { + } + +private: + SRWLOCK m_lock; +}; + +// RAII over an SRWLock (write) +class CSRWExclusiveAutoLock +{ +public: + CSRWExclusiveAutoLock(CSRWLock *srwLock) + { + m_pSRWLock = srwLock; + srwLock->LockExclusive(); + } + + virtual ~CSRWExclusiveAutoLock() + { + m_pSRWLock->ReleaseExclusive(); + } +protected: + CSRWLock *m_pSRWLock; +}; + +// RAII over an SRWLock (read) +class CSRWSharedAutoLock +{ +public: + CSRWSharedAutoLock(CSRWLock *srwLock) + { + m_pSRWLock = srwLock; + srwLock->LockShared(); + } + + virtual ~CSRWSharedAutoLock() + { + m_pSRWLock->ReleaseShared(); + } +protected: + CSRWLock *m_pSRWLock; +}; \ No newline at end of file diff --git a/src/modules/powerrename/lib/stdafx.cpp b/src/modules/powerrename/lib/stdafx.cpp new file mode 100644 index 0000000000..804d0881d3 --- /dev/null +++ b/src/modules/powerrename/lib/stdafx.cpp @@ -0,0 +1,2 @@ +#include "stdafx.h" + diff --git a/src/modules/powerrename/lib/stdafx.h b/src/modules/powerrename/lib/stdafx.h new file mode 100644 index 0000000000..c15286029b --- /dev/null +++ b/src/modules/powerrename/lib/stdafx.h @@ -0,0 +1,20 @@ +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files: +#include + +// C RunTime Header Files +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PowerRenameInterfaces.h" \ No newline at end of file diff --git a/src/modules/powerrename/lib/targetver.h b/src/modules/powerrename/lib/targetver.h new file mode 100644 index 0000000000..e8a3a95245 --- /dev/null +++ b/src/modules/powerrename/lib/targetver.h @@ -0,0 +1,7 @@ +#pragma once +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/src/modules/powerrename/testapp/PowerRenameTest.cpp b/src/modules/powerrename/testapp/PowerRenameTest.cpp new file mode 100644 index 0000000000..a852521180 --- /dev/null +++ b/src/modules/powerrename/testapp/PowerRenameTest.cpp @@ -0,0 +1,62 @@ +// PowerRenameTest.cpp : Defines the entry point for the application. +// + +#include "stdafx.h" +#include "PowerRenameTest.h" +#include +#include +#include +#include +#include + +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +HINSTANCE g_hInst; + +// {0440049F-D1DC-4E46-B27B-98393D79486B} +//DEFINE_GUID(CLSID_PowerRenameMenu, 0x0440049F, 0xD1DC, 0x4E46, 0xB2, 0x7B, 0x98, 0x39, 0x3D, 0x79, 0x48, 0x6B); + +class __declspec(uuid("{0440049F-D1DC-4E46-B27B-98393D79486B}")) Foo; +static const CLSID CLSID_PowerRenameMenu = __uuidof(Foo); + +DEFINE_GUID(BHID_DataObject, 0xb8c0bd9f, 0xed24, 0x455c, 0x83, 0xe6, 0xd5, 0x39, 0xc, 0x4f, 0xe8, 0xc4); + +int APIENTRY wWinMain( + _In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ PWSTR lpCmdLine, + _In_ int nCmdShow) +{ + g_hInst = hInstance; + HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (SUCCEEDED(hr)) + { + // Create the smart rename manager + CComPtr spsrm; + if (SUCCEEDED(CPowerRenameManager::s_CreateInstance(&spsrm))) + { + // Create the factory for our items + CComPtr spsrif; + if (SUCCEEDED(CPowerRenameItem::s_CreateInstance(nullptr, IID_PPV_ARGS(&spsrif)))) + { + // Pass the factory to the manager + if (SUCCEEDED(spsrm->put_smartRenameItemFactory(spsrif))) + { + // Create the smart rename UI instance and pass the manager + CComPtr spsrui; + if (SUCCEEDED(CPowerRenameUI::s_CreateInstance(spsrm, nullptr, true, &spsrui))) + { + // Call blocks until we are done + spsrui->Show(); + spsrui->Close(); + + // Need to call shutdown to break circular dependencies + spsrm->Shutdown(); + } + } + } + } + CoUninitialize(); + } + return 0; +} \ No newline at end of file diff --git a/src/modules/powerrename/testapp/PowerRenameTest.h b/src/modules/powerrename/testapp/PowerRenameTest.h new file mode 100644 index 0000000000..d00d47e788 --- /dev/null +++ b/src/modules/powerrename/testapp/PowerRenameTest.h @@ -0,0 +1,3 @@ +#pragma once + +#include "resource.h" diff --git a/src/modules/powerrename/testapp/PowerRenameTest.ico b/src/modules/powerrename/testapp/PowerRenameTest.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3ec03bd617f32e58128fa977fd6ac9605124f4b GIT binary patch literal 46227 zcmeG_3s@7^(i=en%FAlCDneRC>$M_k6<<8GwYF8!R&T*-0nuNr4^Sy8A`n5bmRqT{ zK5o_G(b(u^yZQ8UkW5(>;x9{lDqk(~eD_5>eNlDqb zapUaSv*o2vfswy>543gya=eTKJ}bJsb08RyLkrbzg~EDF)&yx{%~3lMOmjI z2r>fq&!#BLn;*SDdg=``Ge%vn(_ zHtGJ!s?^=xQ)VolXES2J@MURR$8V^WUk}@~H&O9u;)XhDr?A*8NV1jpnGS9@R3zjJlMS^bL*v(^3?X@it_xf^eOAIF1)HHQBqYfeohaonv$Cm)jId+ zOVxIDS1y%GYM&OxMbuR%tEwZv6c&U_detcl+-(L0I+vtX6%TS(6-esN{F)w7bMOD| zOWW0^33nGuWA6=U_k~Z`_8H2%Xi~K^>vZ`oLJj;+dof+Rb*dtUE!B9(#yAE zinCMDvqwpLLl>`DVqzVqn&SNSS4zywZ(O!oQ5+P}ZqDo*iQywp2?H;6m*1FM+v(ik zKuPue2llH<lpzzQC0ZQ&fW!@2| zCA+sBFDXoZ&s`OJt!UeG*-;nSw@IqwS!bgXV{4brPy0l^ru(7V((LEr;MieH9$eol ztF#|gWOnaxM#TNAhX?ycZV#28>t6U2vUhev*6X=!y^Cyctm@*mSw&||2b89k2T12S zs5WPQGwMKAfV2p*(!)o6B2$E!rv#ZHO0PlduB^0pWIyVm*{I^DzUzC8eCW8? z=BFT&pQ;pzy=-=tzc!;ZH7GzD1dQ^-Q+y&NpT{jR`AMZnyl1oX>1)aw`%wjE%C9pb z{^#7`jy{pUx+;`bicdg?AKvS8+Eg+s!X*4ofn?BwTUi5A9Wt#IhcW`Cp;u~zX&I+$ z6~0HjCOi(CTN{<%GdDz;c&lIU&Wcl8MG?v_mEWu%n^Nd_qUfnFly0f|W~(eABVuOa zHt$DAeIrLYsMenG_dlE&X7MD9CeFz(_lc}g7e80TZeW2VbJE?B}+N|#LT|(2( zeRDEXggcomlAM-B22c?h3dcL19#xL@1NIL`g0pN}geW^Eq)M@ob3!R1?5(+j=DA*LC zV3UM`T@niRQ7G6ap=dbWwdHjEVHYQI*zzS;6X*qvTp*H2$8BZXM#u$!2E9%Fh1%6;Y%r%wA8iWl z98b^o;Ggdw>_>fXfwbF2~>0cDCW+zQ((`ySCnlYPFH$mt-V0+ra+gMv`S)y(N zzHo($)~+2>oIqd!0<=ro(PThQOSiSPHaGc$z!WPPc@uMMn%q|1f`-LXNOZ8o+V&d$ zHbOdUt0AU!(s0v=VVEv*Gjf(>GO3|6{Q{Q)GvqyDTfmceS{Wq=e`Gh$eZU|X;za!?7xDpmeE6|Pgz zO(KB$bqcOc$ko6)h3u!3J#_Z|c~w;vk-}r%1H1=XsRz{S6idd1hFIc6slF`L`S$H$ z_Qem5dBRTU+4*M5v$Vv$1lR_!RO^Ee{bum6-?p7PZwYA&3)o0e=P64|GczkIGcz?g zm}G@1OG_)XP72S0O#vA^OFoTl;6%6?2%oWZ{~SOKoe0-?^3!~m`s8OxPXB*&n$|r! zzi?BOFg7FVyr(F+_`6=-k&dIk_p|sgGQA|=!w(|Opl0qnzSh@!9ZyqEy{Yv2tco;$!c%1qB5Tm(zT#t*z(Oo{29hzP~WMW9N6j>acU@%{>PyiVK%J zDchX)@#r((N^0@uwz&3goBq}L@|RNv?D=_=P56?Hecrw4KYY=F^rOd%qNoY}|604$ ze}Q1wo2CUpqsJY2c6ZpK$LU8Zind-HYv;EpX3wHx!Mu)9bu&)b-#Goo@8>^%ZpR_-A8pm9le*fP%dwWrZ#%gZ4hgNPEP0ZX zygWHODX{cO?wRD|B?TXp_YA&WcENAcr1zm*!sT*wSXgN+4}`x4Onbu4m9C6a zDyzzKE^l|)9veNfwvB!H=Ueu>hE~Q`J@CK3rl9l8;eQX$AL67e-=O$nb3yrbm%txm zqqqN!a-0`y@A|0LF6XUF2Y(!J;{4dWim&tj-qp-=psii`?^{xRtLDC)WM1xF(Pdh} zo&nW%Pm{OJ7Y(}+?6yGe^278sU;bRy{@{{)8`rzbhg5njp0L%bE_!K#u_ZcwBlk$-$@-sFG|l`h!> z9(?Vda99`_HgTY$d(`wb0ljO-+CANOJbJb4dX!}MowsHz{C?8ouifJug^@uv*qA)| zn%nN4b%VBaGj|$J^Z1&Dy*5r6?Cmc)u?6HlOfo+czNcs1sY|Z5Gm2$_`_D~ZbHzQi zLqtxYoq0l-+$9=+>Cc4_j1I6{ufgKK5d;F(^ zrbsZ(sxx=S^C}5{PdVE zm-o*6c#W?lJZIJWUXDMG-#PX9w8YRegRkD{@b+^r2vFt8?VAf;&)M81?+ugWvh(%< zCo8AS5e)E6nQ_nkX72KDD}Am8<#qmH=l;{Xer^AKK(w`~Rb6G$Ip1HMsspY>EqmrT z$K?L9U3P&bALm$hHSeYj_F7h(5$iCZtdHP5&%&r&yJO0;C?NH-;Xa$6Un*F7-{)B7 zTTg1rU)$V6a=Lesk8)PLhQxqS#@r7j3u_WR0Zr+Ju!br1- ztp`JH25z67I>IV`(#_SoQuES(IaHi9@zkuEO_9M52id->80ovHW1Z6n$!&-IdMC-W zE?1iF)ctW+<<6fUR~}cMtV@|QeV3<6@#0*MtFqFC)9+Md_jVN=8*UY!7Gg3wN}~F` zEFo`b@t#rn?;eWJQkPUGSC+ZEZSejj+6WKYdb$m>lF4(fJmOSk2 z+y1oAmSMHUzSY6m|3RL91@9hmLOV?T*6uL7G4o(@_;xCOTb6XtFDb=I7SfButuFPO ziR>Q_vzpNFOH6$Osh*24)o!@eKY9k=42-ds=I75WH-8lL)mPU?Jqo-?U8;;|Yj$HC zCE7-LI19vnZKzaJD$;^7?MRvTrfeq|P!SX1D~_nEOA48~&s|l$H{_V*%~Jo|E|how z=E*f&lSVime_UQNdqZq&#Je`3!$*x;Xg@k^!-fq%j;rlqXE)&&&z%O?+)zuMRVlEc zTN_xu-!r1FVqE#Wt_gYRrw34nK5vGT8*0$N{;C&sYja`t1v>`^)ja#kr7Kq48WmY> z*Q3Xf*y@qPhHYE8bA+I|k)dvBVMS?s>LED5*}{N;SddiX9^_pn9DA;hD=wj!N4Pv7 zF9yIL-O(5P(2mOm$Fe*CRDUJlVmG1T?dSXduN3=e3yEzmSXcbRF;7)%0(Sp#v76BF z_P;p(TT|bou6+M%-@i$0bHRN4^YPCfKl;W$9FI^L0{Y~TazkVxE#YHhw*Fk=p3oQ) z|Hjgn=x;1}y!|g{{xep8@%^t}UmDAweEjqA&x`>ww{yY#{Lg*;W32JY&wu>nr2>?Sn4{e1tk-_H_k;%Iys-b(kZe*1uaPmj-E4nh8>Br$FtLpb2Dt{=-%@?fww>gg5(`}HCNzfF z|1$cV*v-aarWl zjMeAxN@Nwh)}dMU6JIqF3up_zfuhk1=vuVTiN5e!i~5*?*G3z~2hE8E^bbIb_c_`R zugg}!Ydq@h$29SaF|eVr&`_U49jzz4##?2qe$u6%vBnhYh`JKJ^X30dIm@%cR4NV!^h_-sLCj%(MG2jOv0nn)@vmECyc-1={ z&s^gcd6+VoX+!2h97EW4L-LriA&oYnZCvL;5zvYO@&NSejCI&|T*e1;&eJEeu`x#C z8{5<;gHevUqYWZ@%bcbT(*wux*4qys$-mVVYTwvHddRo9NM047zh39~wJx z9M#W5mix!+@has( zPZ59^AP<0PmqeeQK!-LmX^|IYi1hI^w_Nk*EABj|J^82mp-$bQ5t{yRkgM}HQZ>fc z3*sdZ(};f6Af|-$E0f`+$@t1-s8*?Dh=nSZ5^3Gx?P6kq7>c37L<+@FA(XkR=vNau z1En7Tc~6Ac5i%SuR;)7P_Rmgxa8RG(_1BtfjM--f`=9IcLrc-IVu9EHCBN^1_rLc0 zHMpJwVULHV@)_IzP1U2Re7ydA{NPyNnvh=mXDmQrl zgvC#v#cJ#<57EsKj50Z#^J8#ivG&ywlWS6_Jpec?yx zxj<(;>ygOTy{SG&Uy}1OnAWGOzVZh80(I0nYXN!m`3vV%3^}*Q)`NLg6Mew0=bA?y z*gnBizg*Y9cYJY_@nqfC^oix4Qmc+gMvaf#%Wl+G8F*R8j$Df>NMHP`dl6Do;zmXf zBMwMBvTwC zx39j>7!rS6{Q6h+KReEwlW$7=HK#o`Z)qBF5hqHnq=@mnn;+b+r$5xQ~!YXt>yn zzw>PDchx$4fo*6#2|*s8mGem3Ty4g^FRpu;EMH(-9_R;6+stQlgMS;`*!Kpwm&M#S z)!2z`5*>8z;ozPO>dp2s?lm#@YcS1@5#+)BD<++$T?t@60IfbiU*HAhA^jo~Ren=!kukg)&8SBOE_~-UA>GK&yWsuhIb4Bal23BMSwUQPd=3>6gt zkl&Mem_kO+1$GfTIbpUK + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2} + Win32Proj + PowerRenameTest + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + ..\ui\;..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + + + true + ..\ui\;..\lib\;$(IncludePath) + + + false + ..\ui\;..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + + + false + ..\ui\;..\lib\;$(IncludePath) + + + + Use + Level3 + Disabled + true + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + true + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + Use + Level3 + Disabled + true + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + true + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + Use + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + true + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + Use + Level3 + MaxSpeed + true + true + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + true + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;Pathcch.lib;comctl32.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + \ No newline at end of file diff --git a/src/modules/powerrename/testapp/PowerRenameTest.vcxproj.filters b/src/modules/powerrename/testapp/PowerRenameTest.vcxproj.filters new file mode 100644 index 0000000000..c3b11225d1 --- /dev/null +++ b/src/modules/powerrename/testapp/PowerRenameTest.vcxproj.filters @@ -0,0 +1,44 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/powerrename/testapp/resource.h b/src/modules/powerrename/testapp/resource.h new file mode 100644 index 0000000000..7b01b8faea --- /dev/null +++ b/src/modules/powerrename/testapp/resource.h @@ -0,0 +1,19 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by SmartRenameTest.rc +// +#define IDD_POWERRENAMETEST_DIALOG 402 +#define IDR_MAINFRAME 428 +#define IDC_STATIC -1 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NO_MFC 1 +#define _APS_NEXT_RESOURCE_VALUE 429 +#define _APS_NEXT_COMMAND_VALUE 32771 +#define _APS_NEXT_CONTROL_VALUE 1000 +#define _APS_NEXT_SYMED_VALUE 410 +#endif +#endif diff --git a/src/modules/powerrename/testapp/small.ico b/src/modules/powerrename/testapp/small.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3ec03bd617f32e58128fa977fd6ac9605124f4b GIT binary patch literal 46227 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 + +// C RunTime Header Files +#include +#include +#include +#include +#include +#include +#include diff --git a/src/modules/powerrename/testapp/targetver.h b/src/modules/powerrename/testapp/targetver.h new file mode 100644 index 0000000000..87c0086de7 --- /dev/null +++ b/src/modules/powerrename/testapp/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/src/modules/powerrename/ui/PowerRenameUI.cpp b/src/modules/powerrename/ui/PowerRenameUI.cpp new file mode 100644 index 0000000000..a3ebe154b4 --- /dev/null +++ b/src/modules/powerrename/ui/PowerRenameUI.cpp @@ -0,0 +1,930 @@ +#include "stdafx.h" +#include "resource.h" +#include "PowerRenameUI.h" +#include +#include +#include +#include + +extern HINSTANCE g_hInst; + + +int g_rgnMatchModeResIDs[] = +{ + IDS_ENTIREITEMNAME, + IDS_NAMEONLY, + IDS_EXTENSIONONLY +}; + +enum +{ + MATCHMODE_FULLNAME = 0, + MATCHMODE_NAMEONLY, + MATCHMODE_EXTENIONONLY +}; + +struct FlagCheckboxMap +{ + DWORD flag; + DWORD id; +}; + +FlagCheckboxMap g_flagCheckboxMap[] = +{ + { UseRegularExpressions, IDC_CHECK_USEREGEX }, + { ExcludeSubfolders, IDC_CHECK_EXCLUDESUBFOLDERS }, + { EnumerateItems, IDC_CHECK_ENUMITEMS }, + { ExcludeFiles, IDC_CHECK_EXCLUDEFILES }, + { CaseSensitive, IDC_CHECK_CASESENSITIVE }, + { MatchAllOccurences, IDC_CHECK_MATCHALLOCCRENCES }, + { ExcludeFolders, IDC_CHECK_EXCLUDEFOLDERS }, + { NameOnly, IDC_CHECK_NAMEONLY }, + { ExtensionOnly, IDC_CHECK_EXTENSIONONLY } +}; + +// IUnknown +IFACEMETHODIMP CPowerRenameUI::QueryInterface(__in REFIID riid, __deref_out void** ppv) +{ + static const QITAB qit[] = + { + QITABENT(CPowerRenameUI, IPowerRenameUI), + QITABENT(CPowerRenameUI, IPowerRenameManagerEvents), + QITABENT(CPowerRenameUI, IDropTarget), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP_(ULONG) CPowerRenameUI::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CPowerRenameUI::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) + { + delete this; + } + return refCount; +} + +HRESULT CPowerRenameUI::s_CreateInstance(_In_ IPowerRenameManager* psrm, _In_opt_ IDataObject* pdo, _In_ bool enableDragDrop, _Outptr_ IPowerRenameUI** ppsrui) +{ + *ppsrui = nullptr; + CPowerRenameUI *prui = new CPowerRenameUI(); + HRESULT hr = prui ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + // Pass the IPowerRenameManager to the IPowerRenameUI so it can subscribe to events + hr = prui->_Initialize(psrm, pdo, enableDragDrop); + if (SUCCEEDED(hr)) + { + hr = prui->QueryInterface(IID_PPV_ARGS(ppsrui)); + } + prui->Release(); + } + return hr; +} + +// IPowerRenameUI +IFACEMETHODIMP CPowerRenameUI::Show() +{ + return _DoModal(NULL); +} + +IFACEMETHODIMP CPowerRenameUI::Close() +{ + _OnCloseDlg(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::Update() +{ + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::get_hwnd(_Out_ HWND* hwnd) +{ + *hwnd = m_hwnd; + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::get_showUI(_Out_ bool* showUI) +{ + // Let callers know that it is OK to show UI (ex: progress dialog, error dialog and conflict dialog UI) + *showUI = true; + return S_OK; +} + +// IPowerRenameManagerEvents +IFACEMETHODIMP CPowerRenameUI::OnItemAdded(_In_ IPowerRenameItem*) +{ + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnUpdate(_In_ IPowerRenameItem*) +{ + UINT itemCount = 0; + if (m_spsrm) + { + m_spsrm->GetItemCount(&itemCount); + } + m_listview.RedrawItems(0, itemCount); + _UpdateCounts(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnError(_In_ IPowerRenameItem*) +{ + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnRegExStarted(_In_ DWORD threadId) +{ + m_disableCountUpdate = true; + m_currentRegExId = threadId; + _UpdateCounts(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnRegExCanceled(_In_ DWORD threadId) +{ + if (m_currentRegExId == threadId) + { + m_disableCountUpdate = false; + _UpdateCounts(); + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnRegExCompleted(_In_ DWORD threadId) +{ + // Enable list view + if (m_currentRegExId == threadId) + { + m_disableCountUpdate = false; + _UpdateCounts(); + } + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnRenameStarted() +{ + // Disable controls + EnableWindow(m_hwnd, FALSE); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::OnRenameCompleted() +{ + // Enable controls + EnableWindow(m_hwnd, TRUE); + + // Close the window + _OnCloseDlg(); + return S_OK; +} + +// IDropTarget +IFACEMETHODIMP CPowerRenameUI::DragEnter(_In_ IDataObject* pdtobj, DWORD /* grfKeyState */, POINTL pt, _Inout_ DWORD* pdwEffect) +{ + if (m_spdth) + { + POINT ptT = { pt.x, pt.y }; + m_spdth->DragEnter(m_hwnd, pdtobj, &ptT, *pdwEffect); + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::DragOver(DWORD /* grfKeyState */, POINTL pt, _Inout_ DWORD* pdwEffect) +{ + if (m_spdth) + { + POINT ptT = { pt.x, pt.y }; + m_spdth->DragOver(&ptT, *pdwEffect); + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::DragLeave() +{ + if (m_spdth) + { + m_spdth->DragLeave(); + } + + return S_OK; +} + +IFACEMETHODIMP CPowerRenameUI::Drop(_In_ IDataObject* pdtobj, DWORD, POINTL pt, _Inout_ DWORD* pdwEffect) +{ + if (m_spdth) + { + POINT ptT = { pt.x, pt.y }; + m_spdth->Drop(pdtobj, &ptT, *pdwEffect); + } + + _OnClear(); + + EnableWindow(GetDlgItem(m_hwnd, ID_RENAME), TRUE); + EnableWindow(m_hwndLV, TRUE); + + // Populate the manager from the data object + if (m_spsrm) + { + _EnumerateItems(pdtobj); + } + + return S_OK; +} + +HRESULT CPowerRenameUI::_Initialize(_In_ IPowerRenameManager* psrm, _In_opt_ IDataObject* pdo, _In_ bool enableDragDrop) +{ + // Cache the smart rename manager + m_spsrm = psrm; + + // Cache the data object for enumeration later + m_spdo = pdo; + + m_enableDragDrop = enableDragDrop; + + HRESULT hr = CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC, IID_PPV_ARGS(&m_spdth)); + if (SUCCEEDED(hr)) + { + // Subscribe to smart rename manager events + hr = m_spsrm->Advise(this, &m_cookie); + } + + if (FAILED(hr)) + { + _Cleanup(); + } + + return hr; +} + +void CPowerRenameUI::_Cleanup() +{ + if (m_spsrm && m_cookie != 0) + { + m_spsrm->UnAdvise(m_cookie); + m_cookie = 0; + m_spsrm = nullptr; + } + + m_spdo = nullptr; + m_spdth = nullptr; + + if (m_enableDragDrop) + { + RevokeDragDrop(m_hwnd); + } +} + +void CPowerRenameUI::_EnumerateItems(_In_ IDataObject* pdtobj) +{ + // Enumerate the data object and popuplate the manager + if (m_spsrm) + { + m_disableCountUpdate = true; + EnumerateDataObject(pdtobj, m_spsrm); + m_disableCountUpdate = false; + + UINT itemCount = 0; + m_spsrm->GetItemCount(&itemCount); + m_listview.SetItemCount(itemCount); + + _UpdateCounts(); + } +} + +// TODO: persist settings made in the UI +HRESULT CPowerRenameUI::_ReadSettings() +{ + return S_OK; +} + +HRESULT CPowerRenameUI::_WriteSettings() +{ + return S_OK; +} + +void CPowerRenameUI::_OnClear() +{ +} + +void CPowerRenameUI::_OnCloseDlg() +{ + // Persist the current settings + _WriteSettings(); + EndDialog(m_hwnd, 1); +} + +void CPowerRenameUI::_OnDestroyDlg() +{ + _Cleanup(); +} + +void CPowerRenameUI::_OnRename() +{ + if (m_spsrm) + { + m_spsrm->Rename(m_hwnd); + } +} + +void CPowerRenameUI::_OnAbout() +{ + // Launch github page + SHELLEXECUTEINFO info = {0}; + info.cbSize = sizeof(SHELLEXECUTEINFO); + info.lpVerb = L"open"; + info.lpFile = L"https://github.com/microsoft/PowerToys/tree/master/src/modules/powerrename"; + info.nShow = SW_SHOWDEFAULT; + + ShellExecuteEx(&info); +} + +HRESULT CPowerRenameUI::_DoModal(__in_opt HWND hwnd) +{ + HRESULT hr = S_OK; + INT_PTR ret = DialogBoxParam(g_hInst, MAKEINTRESOURCE(IDD_MAIN), hwnd, s_DlgProc, (LPARAM)this); + if (ret < 0) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + return hr; +} + +INT_PTR CPowerRenameUI::_DlgProc(UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + INT_PTR bRet = TRUE; // default for all handled cases in switch below + + switch (uMsg) + { + case WM_INITDIALOG: + _OnInitDlg(); + break; + + case WM_COMMAND: + _OnCommand(wParam, lParam); + break; + + case WM_NOTIFY: + bRet = _OnNotify(wParam, lParam); + break; + + case WM_CLOSE: + _OnCloseDlg(); + break; + + case WM_DESTROY: + _OnDestroyDlg(); + break; + + default: + bRet = FALSE; + } + return bRet; +} + +void CPowerRenameUI::_OnInitDlg() +{ + // Initialize from stored settings + _ReadSettings(); + + m_hwndLV = GetDlgItem(m_hwnd, IDC_LIST_PREVIEW); + + m_listview.Init(m_hwndLV); + + // Initialize checkboxes from flags + if (m_spsrm) + { + DWORD flags = 0; + m_spsrm->get_flags(&flags); + _SetCheckboxesFromFlags(flags); + } + + if (m_spdo) + { + // Populate the manager from the data object + _EnumerateItems(m_spdo); + } + + // Load the main icon + LoadIconWithScaleDown(g_hInst, MAKEINTRESOURCE(IDI_RENAME), 32, 32, &m_iconMain); + + // Update the icon associated with our main app window + SendMessage(m_hwnd, WM_SETICON, (WPARAM)ICON_SMALL, (LPARAM)m_iconMain); + SendMessage(m_hwnd, WM_SETICON, (WPARAM)ICON_BIG, (LPARAM)m_iconMain); + + // TODO: put this behind a setting? + if (m_enableDragDrop) + { + RegisterDragDrop(m_hwnd, this); + } + + // Disable rename button by default. It will be enabled in _UpdateCounts if + // there are tiems to be renamed + EnableWindow(GetDlgItem(m_hwnd, ID_RENAME), FALSE); + + // Update UI elements that depend on number of items selected or to be renamed + _UpdateCounts(); + + m_initialized = true; +} + +void CPowerRenameUI::_OnCommand(_In_ WPARAM wParam, _In_ LPARAM lParam) +{ + switch (LOWORD(wParam)) + { + case IDOK: + case IDCANCEL: + _OnCloseDlg(); + break; + + case ID_RENAME: + _OnRename(); + break; + + case ID_ABOUT: + _OnAbout(); + break; + + case IDC_EDIT_REPLACEWITH: + case IDC_EDIT_SEARCHFOR: + if (GET_WM_COMMAND_CMD(wParam, lParam) == EN_CHANGE) + { + _OnSearchReplaceChanged(); + } + break; + + case IDC_CHECK_CASESENSITIVE: + case IDC_CHECK_ENUMITEMS: + case IDC_CHECK_EXCLUDEFILES: + case IDC_CHECK_EXCLUDEFOLDERS: + case IDC_CHECK_EXCLUDESUBFOLDERS: + case IDC_CHECK_MATCHALLOCCRENCES: + case IDC_CHECK_USEREGEX: + case IDC_CHECK_EXTENSIONONLY: + case IDC_CHECK_NAMEONLY: + if (BN_CLICKED == HIWORD(wParam)) + { + _ValidateFlagCheckbox(LOWORD(wParam)); + _GetFlagsFromCheckboxes(); + } + break; + } +} + +BOOL CPowerRenameUI::_OnNotify(_In_ WPARAM wParam, _In_ LPARAM lParam) +{ + bool ret = FALSE; + LPNMHDR pnmdr = (LPNMHDR)lParam; + LPNMLISTVIEW pnmlv = (LPNMLISTVIEW)pnmdr; + NMLVEMPTYMARKUP* pnmMarkup = NULL; + + if (pnmdr) + { + BOOL checked = FALSE; + switch (pnmdr->code) + { + case HDN_ITEMSTATEICONCLICK: + if (m_spsrm) + { + m_listview.ToggleAll(m_spsrm, (!(((LPNMHEADER)lParam)->pitem->fmt & HDF_CHECKED))); + _UpdateCounts(); + } + break; + + case LVN_GETEMPTYMARKUP: + pnmMarkup = (NMLVEMPTYMARKUP*)lParam; + pnmMarkup->dwFlags = EMF_CENTERED; + LoadString(g_hInst, IDS_LISTVIEW_EMPTY, pnmMarkup->szMarkup, ARRAYSIZE(pnmMarkup->szMarkup)); + ret = TRUE; + break; + + case LVN_BEGINLABELEDIT: + ret = TRUE; + break; + + case LVN_KEYDOWN: + if (m_spsrm) + { + m_listview.OnKeyDown(m_spsrm, (LV_KEYDOWN*)pnmdr); + _UpdateCounts(); + } + break; + + case LVN_GETDISPINFO: + if (m_spsrm) + { + m_listview.GetDisplayInfo(m_spsrm, (LV_DISPINFO*)pnmlv); + } + break; + + case NM_CLICK: + { + if (m_spsrm) + { + m_listview.OnClickList(m_spsrm, (NM_LISTVIEW*)pnmdr); + _UpdateCounts(); + } + break; + } + } + } + + return ret; +} + +void CPowerRenameUI::_OnSearchReplaceChanged() +{ + // Pass updated search and replace terms to the IPowerRenameRegEx handler + CComPtr spRegEx; + if (m_spsrm && SUCCEEDED(m_spsrm->get_smartRenameRegEx(&spRegEx))) + { + wchar_t buffer[MAX_PATH] = { 0 }; + GetDlgItemText(m_hwnd, IDC_EDIT_SEARCHFOR, buffer, ARRAYSIZE(buffer)); + spRegEx->put_searchTerm(buffer); + + buffer[0] = L'\0'; + GetDlgItemText(m_hwnd, IDC_EDIT_REPLACEWITH, buffer, ARRAYSIZE(buffer)); + spRegEx->put_replaceTerm(buffer); + } +} + +DWORD CPowerRenameUI::_GetFlagsFromCheckboxes() +{ + DWORD flags = 0; + for (int i = 0; i < ARRAYSIZE(g_flagCheckboxMap); i++) + { + if (Button_GetCheck(GetDlgItem(m_hwnd, g_flagCheckboxMap[i].id)) == BST_CHECKED) + { + flags |= g_flagCheckboxMap[i].flag; + } + } + + // Ensure we update flags + if (m_spsrm) + { + m_spsrm->put_flags(flags); + } + + return flags; +} + +void CPowerRenameUI::_SetCheckboxesFromFlags(_In_ DWORD flags) +{ + for (int i = 0; i < ARRAYSIZE(g_flagCheckboxMap); i++) + { + Button_SetCheck(GetDlgItem(m_hwnd, g_flagCheckboxMap[i].id), flags & g_flagCheckboxMap[i].flag); + } +} + +void CPowerRenameUI::_ValidateFlagCheckbox(_In_ DWORD checkBoxId) +{ + if (checkBoxId == IDC_CHECK_NAMEONLY) + { + if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_CHECK_NAMEONLY)) == BST_CHECKED) + { + Button_SetCheck(GetDlgItem(m_hwnd, IDC_CHECK_EXTENSIONONLY), FALSE); + } + } + else if (checkBoxId == IDC_CHECK_EXTENSIONONLY) + { + if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_CHECK_EXTENSIONONLY)) == BST_CHECKED) + { + Button_SetCheck(GetDlgItem(m_hwnd, IDC_CHECK_NAMEONLY), FALSE); + } + } +} + +void CPowerRenameUI::_UpdateCounts() +{ + // This method is CPU intensive. We disable it during certain operations + // for performance reasons. + if (m_disableCountUpdate) + { + return; + } + + UINT selectedCount = 0; + UINT renamingCount = 0; + if (m_spsrm) + { + m_spsrm->GetSelectedItemCount(&selectedCount); + m_spsrm->GetRenameItemCount(&renamingCount); + } + + if (m_selectedCount != selectedCount || + m_renamingCount != renamingCount) + { + m_selectedCount = selectedCount; + m_renamingCount = renamingCount; + + // Update selected and rename count label + wchar_t countsLabelFormat[100] = { 0 }; + LoadString(g_hInst, IDS_COUNTSLABELFMT, countsLabelFormat, ARRAYSIZE(countsLabelFormat)); + + wchar_t countsLabel[100] = { 0 }; + StringCchPrintf(countsLabel, ARRAYSIZE(countsLabel), countsLabelFormat, selectedCount, renamingCount); + SetDlgItemText(m_hwnd, IDC_STATUS_MESSAGE, countsLabel); + + // Update Rename button state + EnableWindow(GetDlgItem(m_hwnd, ID_RENAME), (renamingCount > 0)); + } +} + +void CPowerRenameListView::Init(_In_ HWND hwndLV) +{ + if (hwndLV) + { + m_hwndLV = hwndLV; + + EnableWindow(m_hwndLV, TRUE); + + // Set the standard styles + DWORD dwLVStyle = (DWORD)GetWindowLongPtr(m_hwndLV, GWL_STYLE); + dwLVStyle |= LVS_ALIGNLEFT | LVS_REPORT | LVS_SHAREIMAGELISTS | LVS_SINGLESEL; + SetWindowLongPtr(m_hwndLV, GWL_STYLE, dwLVStyle); + + // Set the extended view styles + ListView_SetExtendedListViewStyle(m_hwndLV, LVS_EX_CHECKBOXES | LVS_EX_DOUBLEBUFFER); + + // Get the system image lists. Our list view is setup to not destroy + // these since the image list belongs to the entire explorer process + HIMAGELIST himlLarge; + HIMAGELIST himlSmall; + if (Shell_GetImageLists(&himlLarge, &himlSmall)) + { + ListView_SetImageList(m_hwndLV, himlSmall, LVSIL_SMALL); + ListView_SetImageList(m_hwndLV, himlLarge, LVSIL_NORMAL); + } + + _UpdateColumns(); + } +} + +void CPowerRenameListView::ToggleAll(_In_ IPowerRenameManager* psrm, _In_ bool selected) +{ + if (m_hwndLV) + { + UINT itemCount = 0; + psrm->GetItemCount(&itemCount); + for (UINT i = 0; i < itemCount; i++) + { + CComPtr spItem; + if (SUCCEEDED(psrm->GetItemByIndex(i, &spItem))) + { + spItem->put_selected(selected); + } + } + + RedrawItems(0, itemCount); + } +} + +void CPowerRenameListView::ToggleItem(_In_ IPowerRenameManager* psrm, _In_ int item) +{ + CComPtr spItem; + if (SUCCEEDED(psrm->GetItemByIndex(item, &spItem))) + { + bool selected = false; + spItem->get_selected(&selected); + spItem->put_selected(!selected); + + RedrawItems(item, item); + } +} + +void CPowerRenameListView::OnKeyDown(_In_ IPowerRenameManager* psrm, _In_ LV_KEYDOWN* lvKeyDown) +{ + if (lvKeyDown->wVKey == VK_SPACE) + { + int selectionMark = ListView_GetSelectionMark(m_hwndLV); + if (selectionMark != -1) + { + ToggleItem(psrm, selectionMark); + } + } +} + +void CPowerRenameListView::OnClickList(_In_ IPowerRenameManager* psrm, NM_LISTVIEW* pnmListView) +{ + LVHITTESTINFO hitinfo; + //Copy click point + hitinfo.pt = pnmListView->ptAction; + + //Make the hit test... + int item = ListView_HitTest(m_hwndLV, &hitinfo); + if (item != -1) + { + if ((hitinfo.flags & LVHT_ONITEM) != 0) + { + ToggleItem(psrm, item); + } + } +} + +void CPowerRenameListView::UpdateItemCheckState(_In_ IPowerRenameManager* psrm, _In_ int iItem) +{ + if (psrm && m_hwndLV && (iItem > -1)) + { + CComPtr spItem; + if (SUCCEEDED(psrm->GetItemByIndex(iItem, &spItem))) + { + bool checked = ListView_GetCheckState(m_hwndLV, iItem); + spItem->put_selected(checked); + + UINT uSelected = (checked) ? LVIS_SELECTED : 0; + ListView_SetItemState(m_hwndLV, iItem, uSelected, LVIS_SELECTED); + + // Update the rename column if necessary + int id = 0; + spItem->get_id(&id); + RedrawItems(id, id); + } + + // Get the total number of list items and compare it to what is selected + // We need to update the column checkbox if all items are selected or if + // not all of the items are selected. + bool checkHeader = (ListView_GetSelectedCount(m_hwndLV) == ListView_GetItemCount(m_hwndLV)); + _UpdateHeaderCheckState(checkHeader); + } +} + +#define COL_ORIGINAL_NAME 0 +#define COL_NEW_NAME 1 + +void CPowerRenameListView::GetDisplayInfo(_In_ IPowerRenameManager* psrm, _Inout_ LV_DISPINFO* plvdi) +{ + UINT count = 0; + psrm->GetItemCount(&count); + if (plvdi->item.iItem < 0 || plvdi->item.iItem > static_cast(count)) + { + // Invalid index + return; + } + + CComPtr renameItem; + if (SUCCEEDED(psrm->GetItemByIndex((int)plvdi->item.iItem, &renameItem))) + { + if (plvdi->item.mask & LVIF_IMAGE) + { + renameItem->get_iconIndex(&plvdi->item.iImage); + } + + if (plvdi->item.mask & LVIF_STATE) + { + plvdi->item.stateMask = LVIS_STATEIMAGEMASK; + + bool isSelected = false; + renameItem->get_selected(&isSelected); + if (isSelected) + { + // Turn check box on + plvdi->item.state = INDEXTOSTATEIMAGEMASK(2); + } + else + { + // Turn check box off + plvdi->item.state = INDEXTOSTATEIMAGEMASK(1); + } + } + + if (plvdi->item.mask & LVIF_PARAM) + { + int id = 0; + renameItem->get_id(&id); + plvdi->item.lParam = static_cast(id); + } + + if (plvdi->item.mask & LVIF_INDENT) + { + UINT depth = 0; + renameItem->get_depth(&depth); + plvdi->item.iIndent = static_cast(depth); + } + + if (plvdi->item.mask & LVIF_TEXT) + { + PWSTR subItemText = nullptr; + if (plvdi->item.iSubItem == COL_ORIGINAL_NAME) + { + renameItem->get_originalName(&subItemText); + } + else if (plvdi->item.iSubItem == COL_NEW_NAME) + { + DWORD flags = 0; + psrm->get_flags(&flags); + bool shouldRename = false; + if (SUCCEEDED(renameItem->ShouldRenameItem(flags, &shouldRename)) && shouldRename) + { + renameItem->get_newName(&subItemText); + } + } + + StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, subItemText ? subItemText : L""); + CoTaskMemFree(subItemText); + subItemText = nullptr; + } + } +} + +void CPowerRenameListView::RedrawItems(_In_ int first, _In_ int last) +{ + ListView_RedrawItems(m_hwndLV, first, last); +} + +void CPowerRenameListView::SetItemCount(_In_ UINT itemCount) +{ + ListView_SetItemCount(m_hwndLV, itemCount); +} + +void CPowerRenameListView::_UpdateColumns() +{ + if (m_hwndLV) + { + // And the list view columns + int iInsertPoint = 0; + + LV_COLUMN lvc = { 0 }; + lvc.mask = LVCF_FMT | LVCF_ORDER | LVCF_WIDTH | LVCF_TEXT; + lvc.fmt = LVCFMT_LEFT; + lvc.iOrder = iInsertPoint; + + wchar_t buffer[64] = { 0 }; + LoadString(g_hInst, IDS_ORIGINAL, buffer, ARRAYSIZE(buffer)); + lvc.pszText = buffer; + + ListView_InsertColumn(m_hwndLV, iInsertPoint, &lvc); + + iInsertPoint++; + + lvc.iOrder = iInsertPoint; + LoadString(g_hInst, IDS_RENAMED, buffer, ARRAYSIZE(buffer)); + lvc.pszText = buffer; + + ListView_InsertColumn(m_hwndLV, iInsertPoint, &lvc); + + // Get a handle to the header of the columns + HWND hwndHeader = ListView_GetHeader(m_hwndLV); + + if (hwndHeader) + { + // Update the header style to allow checkboxes + DWORD dwHeaderStyle = (DWORD)GetWindowLongPtr(hwndHeader, GWL_STYLE); + dwHeaderStyle |= HDS_CHECKBOXES; + SetWindowLongPtr(hwndHeader, GWL_STYLE, dwHeaderStyle); + + _UpdateHeaderCheckState(TRUE); + } + + _UpdateColumnSizes(); + } +} + +void CPowerRenameListView::_UpdateColumnSizes() +{ + if (m_hwndLV) + { + RECT rc; + GetClientRect(m_hwndLV, &rc); + + ListView_SetColumnWidth(m_hwndLV, 0, (rc.right - rc.left) / 2); + ListView_SetColumnWidth(m_hwndLV, 1, (rc.right - rc.left) / 2); + } +} + +void CPowerRenameListView::_UpdateHeaderCheckState(_In_ bool check) +{ + // Get a handle to the header of the columns + HWND hwndHeader = ListView_GetHeader(m_hwndLV); + if (hwndHeader) + { + wchar_t szBuff[MAX_PATH] = { 0 }; + + // Retrieve the existing header first so we + // don't trash the text already there + HDITEM hdi = { 0 }; + hdi.mask = HDI_FORMAT | HDI_TEXT; + hdi.pszText = szBuff; + hdi.cchTextMax = ARRAYSIZE(szBuff); + + Header_GetItem(hwndHeader, 0, &hdi); + + // Set the first column to contain a checkbox + hdi.fmt |= HDF_CHECKBOX; + hdi.fmt |= (check) ? HDF_CHECKED : 0; + + Header_SetItem(hwndHeader, 0, &hdi); + } +} + + diff --git a/src/modules/powerrename/ui/PowerRenameUI.h b/src/modules/powerrename/ui/PowerRenameUI.h new file mode 100644 index 0000000000..7ba31d93cd --- /dev/null +++ b/src/modules/powerrename/ui/PowerRenameUI.h @@ -0,0 +1,133 @@ +#pragma once +#include + +class CPowerRenameListView +{ + +public: + CPowerRenameListView() = default; + ~CPowerRenameListView() = default; + + void Init(_In_ HWND hwndLV); + void ToggleAll(_In_ IPowerRenameManager* psrm, _In_ bool selected); + void ToggleItem(_In_ IPowerRenameManager* psrm, _In_ int item); + void UpdateItemCheckState(_In_ IPowerRenameManager* psrm, _In_ int iItem); + void RedrawItems(_In_ int first, _In_ int last); + void SetItemCount(_In_ UINT itemCount); + void OnKeyDown(_In_ IPowerRenameManager* psrm, _In_ LV_KEYDOWN* lvKeyDown); + void OnClickList(_In_ IPowerRenameManager* psrm, NM_LISTVIEW* pnmListView); + void GetDisplayInfo(_In_ IPowerRenameManager* psrm, _Inout_ LV_DISPINFO* plvdi); + HWND GetHWND() { return m_hwndLV; } + +private: + void _UpdateColumns(); + void _UpdateColumnSizes(); + void _UpdateHeaderCheckState(_In_ bool check); + + HWND m_hwndLV = nullptr; +}; + +class CPowerRenameUI : + public IDropTarget, + public IPowerRenameUI, + public IPowerRenameManagerEvents +{ +public: + CPowerRenameUI() : + m_refCount(1) + { + (void)OleInitialize(nullptr); + } + + // IUnknown + IFACEMETHODIMP QueryInterface(__in REFIID riid, __deref_out void** ppv); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameUI + IFACEMETHODIMP Show(); + IFACEMETHODIMP Close(); + IFACEMETHODIMP Update(); + IFACEMETHODIMP get_hwnd(_Out_ HWND* hwnd); + IFACEMETHODIMP get_showUI(_Out_ bool* showUI); + + // IPowerRenameManagerEvents + IFACEMETHODIMP OnItemAdded(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnUpdate(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnError(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnRegExStarted(_In_ DWORD threadId); + IFACEMETHODIMP OnRegExCanceled(_In_ DWORD threadId); + IFACEMETHODIMP OnRegExCompleted(_In_ DWORD threadId); + IFACEMETHODIMP OnRenameStarted(); + IFACEMETHODIMP OnRenameCompleted(); + + // IDropTarget + IFACEMETHODIMP DragEnter(_In_ IDataObject* pdtobj, DWORD grfKeyState, POINTL pt, _Inout_ DWORD* pdwEffect); + IFACEMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, _Inout_ DWORD* pdwEffect); + IFACEMETHODIMP DragLeave(); + IFACEMETHODIMP Drop(_In_ IDataObject* pdtobj, DWORD grfKeyState, POINTL pt, _Inout_ DWORD* pdwEffect); + + static HRESULT s_CreateInstance(_In_ IPowerRenameManager* psrm, _In_opt_ IDataObject* pdo, _In_ bool enableDragDrop, _Outptr_ IPowerRenameUI** ppsrui); + +private: + ~CPowerRenameUI() + { + DeleteObject(m_iconMain); + OleUninitialize(); + } + + HRESULT _DoModal(__in_opt HWND hwnd); + + static INT_PTR CALLBACK s_DlgProc(HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + CPowerRenameUI* pDlg = reinterpret_cast(GetWindowLongPtr(hdlg, DWLP_USER)); + if (uMsg == WM_INITDIALOG) + { + pDlg = reinterpret_cast(lParam); + pDlg->m_hwnd = hdlg; + SetWindowLongPtr(hdlg, DWLP_USER, reinterpret_cast(pDlg)); + } + return pDlg ? pDlg->_DlgProc(uMsg, wParam, lParam) : FALSE; + } + + INT_PTR _DlgProc(UINT uMsg, WPARAM wParam, LPARAM lParam); + void _OnCommand(_In_ WPARAM wParam, _In_ LPARAM lParam); + BOOL _OnNotify(_In_ WPARAM wParam, _In_ LPARAM lParam); + + HRESULT _Initialize(_In_ IPowerRenameManager* psrm, _In_opt_ IDataObject* pdo, _In_ bool enableDragDrop); + void _Cleanup(); + + void _OnInitDlg(); + void _OnRename(); + void _OnAbout(); + void _OnCloseDlg(); + void _OnDestroyDlg(); + void _OnClear(); + void _OnSearchReplaceChanged(); + + HRESULT _ReadSettings(); + HRESULT _WriteSettings(); + + DWORD _GetFlagsFromCheckboxes(); + void _SetCheckboxesFromFlags(_In_ DWORD flags); + void _ValidateFlagCheckbox(_In_ DWORD checkBoxId); + + void _EnumerateItems(_In_ IDataObject* pdtobj); + void _UpdateCounts(); + + long m_refCount = 0; + bool m_initialized = false; + bool m_enableDragDrop = false; + bool m_disableCountUpdate = false; + HWND m_hwnd = nullptr; + HWND m_hwndLV = nullptr; + HICON m_iconMain = nullptr; + DWORD m_cookie = 0; + DWORD m_currentRegExId = 0; + UINT m_selectedCount = 0; + UINT m_renamingCount = 0; + CComPtr m_spsrm; + CComPtr m_spdo; + CComPtr m_spdth; + CPowerRenameListView m_listview; +}; \ No newline at end of file diff --git a/src/modules/powerrename/ui/PowerRenameUI.rc b/src/modules/powerrename/ui/PowerRenameUI.rc new file mode 100644 index 0000000000..19531cf993 --- /dev/null +++ b/src/modules/powerrename/ui/PowerRenameUI.rc @@ -0,0 +1,165 @@ +ÿþ// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#ifndef APSTUDIO_INVOKED +#include "targetver.h" +#endif +#define APSTUDIO_HIDDEN_SYMBOLS +#include "windows.h" +#undef APSTUDIO_HIDDEN_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +///////////////////////////////////////////////////////////////////////////// +// +// Accelerator +// + +IDC_SMARTRENAME ACCELERATORS +BEGIN + "?", IDM_ABOUT, ASCII, ALT + "/", IDM_ABOUT, ASCII, ALT +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_MAIN DIALOGEX 0, 0, 351, 304 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Power Rename" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_EDIT_SEARCHFOR,71,20,259,13,ES_AUTOHSCROLL + EDITTEXT IDC_EDIT_REPLACEWITH,71,38,259,13,ES_AUTOHSCROLL + CONTROL "Use Regular Expressions",IDC_CHECK_USEREGEX,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,21,83,93,10 + CONTROL "Case Sensitive",IDC_CHECK_CASESENSITIVE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,21,95,61,10 + CONTROL "Match All Occurences",IDC_CHECK_MATCHALLOCCRENCES, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,21,107,85,10 + CONTROL "Exclude Files",IDC_CHECK_EXCLUDEFILES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,132,83,58,10 + CONTROL "Exclude Folders",IDC_CHECK_EXCLUDEFOLDERS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,132,95,68,10 + CONTROL "Exclude Subfolder Items",IDC_CHECK_EXCLUDESUBFOLDERS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,132,107,92,10 + CONTROL "Enumerate Items",IDC_CHECK_ENUMITEMS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,241,83,72,10 + CONTROL "Item Name Only",IDC_CHECK_NAMEONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,241,95,69,10 + CONTROL "Item Extension Only",IDC_CHECK_EXTENSIONONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,241,107,82,10 + CONTROL "",IDC_LIST_PREVIEW,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_BORDER | WS_TABSTOP,22,148,308,116 + PUSHBUTTON "&Rename",ID_RENAME,178,283,50,14 + PUSHBUTTON "&Help",ID_ABOUT,234,283,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,290,283,50,14 + RTEXT "Search for:",IDC_STATIC,25,23,39,8 + LTEXT "Replace with:",IDC_STATIC,21,40,43,8 + LTEXT "Items Selected: 0 | Renaming: 0",IDC_STATUS_MESSAGE,11,284,137,13 + GROUPBOX "Options",IDC_STATIC,11,68,329,58 + GROUPBOX "Preview",IDC_STATIC,11,133,329,142 + GROUPBOX "Enter the criteria below to rename the items",IDC_STATIC,11,7,329,55 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_MAIN, DIALOG + BEGIN + LEFTMARGIN, 11 + RIGHTMARGIN, 340 + TOPMARGIN, 7 + BOTTOMMARGIN, 297 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#ifndef APSTUDIO_INVOKED\r\n" + "#include ""targetver.h""\r\n" + "#endif\r\n" + "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" + "#include ""windows.h""\r\n" + "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_RENAME ICON "Rename.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_APP_TITLE "PowerRename" + IDS_RENAMED "Renamed" + IDS_ORIGINAL "Original" + IDS_LISTVIEW_EMPTY "All items have been filtered out.\nPlease select from the options above to show items." + IDS_ENTIREITEMNAME "Item Name and Extension" + IDC_SMARTRENAME "SMARTRENAME" + IDS_COUNTSLABELFMT "Items Selected: %u | Renaming: %u" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + + \ No newline at end of file diff --git a/src/modules/powerrename/ui/PowerRenameUI.vcxproj b/src/modules/powerrename/ui/PowerRenameUI.vcxproj new file mode 100644 index 0000000000..4ee7ffa188 --- /dev/null +++ b/src/modules/powerrename/ui/PowerRenameUI.vcxproj @@ -0,0 +1,187 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {0E072714-D127-460B-AFAD-B4C40B412798} + Win32Proj + PowerRename + 10.0 + PowerRenameUI + + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + ..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + true + ..\lib\;$(IncludePath) + + + false + ..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + false + ..\lib\;$(IncludePath) + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + true + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;$(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameLib.lib;%(AdditionalDependencies) + + + + + Use + Level3 + Disabled + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + true + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;$(OutDir)PowerRenameLib.lib;%(AdditionalDependencies) + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + true + + + + + Level3 + Use + MaxSpeed + true + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + true + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/powerrename/ui/Rename.ico b/src/modules/powerrename/ui/Rename.ico new file mode 100644 index 0000000000000000000000000000000000000000..d96be9175d8248fc7f7eaf0c44d5eb3acbd4f75d GIT binary patch literal 108583 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≤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

C> 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 literal 0 HcmV?d00001 diff --git a/src/modules/powerrename/ui/resource.h b/src/modules/powerrename/ui/resource.h new file mode 100644 index 0000000000000000000000000000000000000000..3c4301afcbf03a746c67fdd7c2b9669f11e7cf25 GIT binary patch literal 3888 zcmbW4TW{Jx5Ju;@QvU-|AFC!My`+64#z|0sAPgjVLJbL3s$8U8dQsKC-u8UEp%~cR zn94F%v#(P_KV}WJ>%I;t69e`Y+|=| zV|ScWyQgnN-<>_!C%fb>vzHt-`)ucS&3Q}zJ=Zh(F6f!?)B|_t+)wGbq_t5RVF=4V zeQG~=rm!mrw=nwjU(>$gI>fpSdME$hl#MSOS5%E$8*ZjJ9>yZ z#u{nWfSs>F)Yu;?Qn%LPeoRmsw!^m%ZLk(mw#KPVbnQVs;`{pxYRH#bDAVTOz2Jy- zdu>}$dke@7*gdWTNCQY!H5U0zl-zOT|Il5=W*@I zY?0q3ZEnLXPE2|Bi)&*)P z(sQo6jF*mfk?T=%AF?c#$L(VCI(e>6Y{zw0-k6cY-yH81Ij(aH@+`~Olis^JLY=G; z&qyMdqkHxJcu$%v-^g9Q9MCFg%S$rbM|DMgoLh6Ak;VvJdWBtXUB7%*vfYePI2Xns&Wc? zL{Hi7?T%-?WxHi-hDt+MQ59Tw+^n|UeV;Msqj`2b@2#@C@H%c5m08QIIn}p3J06j% z?6TdvjG^^6Jc4J(V{)0Dp|iYPr_ZulBSt~x#nu_ie#>lSm4Wjd)gG!oG7cZN^7--? zcy>JhEwdNrQn4AD#ktXFc}o>D9PP`-kY4G%{5>Wm)A>vd;wrPiE+1IbW zR?)Z};(i^y!hTt%`bx#GN(Gh--Kvo@)(C5rb5?ViR-&-ygvhEZIvfo&)0!RU*4)~; tP1RQ2Zod~(H6#_&bophhCJb5b=SMS~=E`5o^_6y)XY3uPk8`?s{}*^u&0qik literal 0 HcmV?d00001 diff --git a/src/modules/powerrename/ui/stdafx.cpp b/src/modules/powerrename/ui/stdafx.cpp new file mode 100644 index 0000000000..1577c4e3bc --- /dev/null +++ b/src/modules/powerrename/ui/stdafx.cpp @@ -0,0 +1 @@ +#include "stdafx.h" \ No newline at end of file diff --git a/src/modules/powerrename/ui/stdafx.h b/src/modules/powerrename/ui/stdafx.h new file mode 100644 index 0000000000..91db2143c1 --- /dev/null +++ b/src/modules/powerrename/ui/stdafx.h @@ -0,0 +1,14 @@ +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files: +#include + +// C RunTime Header Files +#include +#include +#include +#include + diff --git a/src/modules/powerrename/ui/targetver.h b/src/modules/powerrename/ui/targetver.h new file mode 100644 index 0000000000..87c0086de7 --- /dev/null +++ b/src/modules/powerrename/ui/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/src/modules/powerrename/unittests/MockPowerRenameItem.cpp b/src/modules/powerrename/unittests/MockPowerRenameItem.cpp new file mode 100644 index 0000000000..185d3fc600 --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameItem.cpp @@ -0,0 +1,34 @@ +#include "stdafx.h" +#include "MockPowerRenameItem.h" + +HRESULT CMockPowerRenameItem::CreateInstance(_In_opt_ PCWSTR path, _In_opt_ PCWSTR originalName, _In_ UINT depth, _In_ bool isFolder, _Outptr_ IPowerRenameItem** ppItem) +{ + *ppItem = nullptr; + CMockPowerRenameItem* newItem = new CMockPowerRenameItem(); + HRESULT hr = newItem ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + newItem->Init(path, originalName, depth, isFolder); + hr = newItem->QueryInterface(IID_PPV_ARGS(ppItem)); + newItem->Release(); + } + + return hr; +} + +void CMockPowerRenameItem::Init(_In_opt_ PCWSTR path, _In_opt_ PCWSTR originalName, _In_ UINT depth, _In_ bool isFolder) +{ + if (path != nullptr) + { + SHStrDup(path, &m_path); + } + + if (originalName != nullptr) + { + SHStrDup(originalName, &m_originalName); + } + + m_depth = depth; + m_isFolder = isFolder; +} + diff --git a/src/modules/powerrename/unittests/MockPowerRenameItem.h b/src/modules/powerrename/unittests/MockPowerRenameItem.h new file mode 100644 index 0000000000..2b41e4321d --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameItem.h @@ -0,0 +1,12 @@ +#pragma once +#include "stdafx.h" +#include +#include "srwlock.h" + +class CMockPowerRenameItem : + public CPowerRenameItem +{ +public: + static HRESULT CreateInstance(_In_opt_ PCWSTR path, _In_opt_ PCWSTR originalName, _In_ UINT depth, _In_ bool isFolder, _Outptr_ IPowerRenameItem** ppItem); + void Init(_In_opt_ PCWSTR path, _In_opt_ PCWSTR originalName, _In_ UINT depth, _In_ bool isFolder); +}; \ No newline at end of file diff --git a/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.cpp b/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.cpp new file mode 100644 index 0000000000..fc9f826f8b --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.cpp @@ -0,0 +1,92 @@ +#include "stdafx.h" +#include "MockPowerRenameManagerEvents.h" + +// IUnknown +IFACEMETHODIMP CMockPowerRenameManagerEvents::QueryInterface(__in REFIID riid, __deref_out void** ppv) +{ + static const QITAB qit[] = + { + QITABENT(CMockPowerRenameManagerEvents, IPowerRenameManagerEvents), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP_(ULONG) CMockPowerRenameManagerEvents::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CMockPowerRenameManagerEvents::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) + { + delete this; + } + return refCount; +} + +// IPowerRenameManagerEvents +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnItemAdded(_In_ IPowerRenameItem* pItem) +{ + m_itemAdded = pItem; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnUpdate(_In_ IPowerRenameItem* pItem) +{ + m_itemUpdated = pItem; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnError(_In_ IPowerRenameItem* pItem) +{ + m_itemError = pItem; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnRegExStarted(_In_ DWORD threadId) +{ + m_regExStarted = true; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnRegExCanceled(_In_ DWORD threadId) +{ + m_regExCanceled = true; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnRegExCompleted(_In_ DWORD threadId) +{ + m_regExCompleted = true; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnRenameStarted() +{ + m_renameStarted = true; + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameManagerEvents::OnRenameCompleted() +{ + m_renameCompleted = true; + return S_OK; +} + +HRESULT CMockPowerRenameManagerEvents::s_CreateInstance(_In_ IPowerRenameManager* psrm, _Outptr_ IPowerRenameUI** ppsrui) +{ + *ppsrui = nullptr; + CMockPowerRenameManagerEvents* events = new CMockPowerRenameManagerEvents(); + HRESULT hr = events != nullptr ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + hr = events->QueryInterface(IID_PPV_ARGS(ppsrui)); + events->Release(); + } + + return hr; +} + diff --git a/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.h b/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.h new file mode 100644 index 0000000000..9cd8a09d53 --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameManagerEvents.h @@ -0,0 +1,43 @@ +#pragma once +#include + +class CMockPowerRenameManagerEvents : + public IPowerRenameManagerEvents +{ +public: + CMockPowerRenameManagerEvents() : + m_refCount(1) + { + } + + // IUnknown + IFACEMETHODIMP QueryInterface(__in REFIID riid, __deref_out void** ppv); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameManagerEvents + IFACEMETHODIMP OnItemAdded(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnUpdate(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnError(_In_ IPowerRenameItem* renameItem); + IFACEMETHODIMP OnRegExStarted(_In_ DWORD threadId); + IFACEMETHODIMP OnRegExCanceled(_In_ DWORD threadId); + IFACEMETHODIMP OnRegExCompleted(_In_ DWORD threadId); + IFACEMETHODIMP OnRenameStarted(); + IFACEMETHODIMP OnRenameCompleted(); + + static HRESULT s_CreateInstance(_In_ IPowerRenameManager* psrm, _Outptr_ IPowerRenameUI** ppsrui); + + ~CMockPowerRenameManagerEvents() + { + } + + CComPtr m_itemAdded; + CComPtr m_itemUpdated; + CComPtr m_itemError; + bool m_regExStarted = false; + bool m_regExCanceled = false; + bool m_regExCompleted = false; + bool m_renameStarted = false; + bool m_renameCompleted = false; + long m_refCount = 0; +}; \ No newline at end of file diff --git a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp new file mode 100644 index 0000000000..8a6c00c30d --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp @@ -0,0 +1,69 @@ +#include "stdafx.h" +#include "MockPowerRenameRegExEvents.h" + +IFACEMETHODIMP_(ULONG) CMockPowerRenameRegExEvents::AddRef() +{ + return InterlockedIncrement(&m_refCount); +} + +IFACEMETHODIMP_(ULONG) CMockPowerRenameRegExEvents::Release() +{ + long refCount = InterlockedDecrement(&m_refCount); + + if (refCount == 0) + { + delete this; + } + return refCount; +} + +IFACEMETHODIMP CMockPowerRenameRegExEvents::QueryInterface(_In_ REFIID riid, _Outptr_ void** ppv) +{ + static const QITAB qit[] = { + QITABENT(CMockPowerRenameRegExEvents, IPowerRenameRegExEvents), + { 0 } + }; + return QISearch(this, qit, riid, ppv); +} + +IFACEMETHODIMP CMockPowerRenameRegExEvents::OnSearchTermChanged(_In_ PCWSTR searchTerm) +{ + CoTaskMemFree(m_searchTerm); + m_searchTerm = nullptr; + if (searchTerm != nullptr) + { + SHStrDup(searchTerm, &m_searchTerm); + } + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameRegExEvents::OnReplaceTermChanged(_In_ PCWSTR replaceTerm) +{ + CoTaskMemFree(m_replaceTerm); + m_replaceTerm = nullptr; + if (replaceTerm != nullptr) + { + SHStrDup(replaceTerm, &m_replaceTerm); + } + return S_OK; +} + +IFACEMETHODIMP CMockPowerRenameRegExEvents::OnFlagsChanged(_In_ DWORD flags) +{ + m_flags = flags; + return S_OK; +} + +HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree) +{ + *ppsrree = nullptr; + CMockPowerRenameRegExEvents* psrree = new CMockPowerRenameRegExEvents(); + HRESULT hr = psrree ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + hr = psrree->QueryInterface(IID_PPV_ARGS(ppsrree)); + psrree->Release(); + } + return hr; +} + diff --git a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h new file mode 100644 index 0000000000..2c019ad5e2 --- /dev/null +++ b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include "srwlock.h" + +class CMockPowerRenameRegExEvents: + public IPowerRenameRegExEvents +{ +public: + // IUnknown + IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + + // IPowerRenameRegExEvents + IFACEMETHODIMP OnSearchTermChanged(_In_ PCWSTR searchTerm); + IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm); + IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags); + + static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree); + + CMockPowerRenameRegExEvents() : + m_refCount(1) + { + } + + ~CMockPowerRenameRegExEvents() + { + CoTaskMemFree(m_searchTerm); + CoTaskMemFree(m_replaceTerm); + } + + PWSTR m_searchTerm = nullptr; + PWSTR m_replaceTerm = nullptr; + DWORD m_flags = 0; + long m_refCount; +}; diff --git a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj new file mode 100644 index 0000000000..0e938a5406 --- /dev/null +++ b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj @@ -0,0 +1,198 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {2151F984-E006-4A9F-92EF-C6DDE3DC8413} + Win32Proj + PowerRenameLibUnitTests + 10.0 + PowerRenameUnitTests + + + + DynamicLibrary + true + v142 + Unicode + false + + + DynamicLibrary + false + v142 + true + Unicode + false + + + DynamicLibrary + true + v142 + Unicode + false + + + DynamicLibrary + false + v142 + true + Unicode + false + + + + + + + + + + + + + + + + + + + + + false + ..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + false + ..\lib\;$(IncludePath) + + + false + ..\lib\;$(IncludePath) + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + false + ..\lib\;$(IncludePath) + + + + Use + Level3 + Disabled + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;$(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameLib.lib;%(AdditionalDependencies); + + + + + Use + Level3 + Disabled + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + _DEBUG;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;$(OutDir)PowerRenameLib.lib;%(AdditionalDependencies) + + + + + Level3 + Use + MaxSpeed + true + true + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + Level3 + Use + MaxSpeed + true + true + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + NDEBUG;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + $(OutDir)PowerRenameLib.lib;$(OutDir)PowerRenameUI.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;$(OutDir)..\..\src\modules\powerrename\UI\$(Platform)\$(Configuration)\PowerRenameUI.res;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + \ No newline at end of file diff --git a/src/modules/powerrename/unittests/PowerRenameManagerTests.cpp b/src/modules/powerrename/unittests/PowerRenameManagerTests.cpp new file mode 100644 index 0000000000..5c39e17f5b --- /dev/null +++ b/src/modules/powerrename/unittests/PowerRenameManagerTests.cpp @@ -0,0 +1,241 @@ +#include "stdafx.h" +#include "CppUnitTest.h" +#include +#include +#include +#include "MockPowerRenameItem.h" +#include "MockPowerRenameManagerEvents.h" +#include "TestFileHelper.h" + +#define DEFAULT_FLAGS MatchAllOccurences + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +EXTERN_C IMAGE_DOS_HEADER __ImageBase; + +#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase) + +HINSTANCE g_hInst = HINST_THISCOMPONENT; + +namespace PowerRenameManagerTests +{ + TEST_CLASS(SimpleTests) + { + public: + + struct rename_pairs + { + std::wstring originalName; + std::wstring newName; + bool isFile; + bool shouldRename; + int depth; + }; + + void RenameHelper(_In_ rename_pairs* renamePairs, _In_ int numPairs, _In_ std::wstring searchTerm, _In_ std::wstring replaceTerm, _In_ DWORD flags) + { + // Create a single item (in a temp directory) and verify rename works as expected + CTestFileHelper testFileHelper; + for (int i = 0; i < numPairs; i++) + { + if (renamePairs[i].isFile) + { + Assert::IsTrue(testFileHelper.AddFile(renamePairs[i].originalName)); + } + else + { + Assert::IsTrue(testFileHelper.AddFolder(renamePairs[i].originalName)); + } + } + + CComPtr mgr; + Assert::IsTrue(CPowerRenameManager::s_CreateInstance(&mgr) == S_OK); + CMockPowerRenameManagerEvents* mockMgrEvents = new CMockPowerRenameManagerEvents(); + CComPtr mgrEvents; + Assert::IsTrue(mockMgrEvents->QueryInterface(IID_PPV_ARGS(&mgrEvents)) == S_OK); + DWORD cookie = 0; + Assert::IsTrue(mgr->Advise(mgrEvents, &cookie) == S_OK); + + for (int i = 0; i < numPairs; i++) + { + CComPtr item; + CMockPowerRenameItem::CreateInstance(testFileHelper.GetFullPath( + renamePairs[i].originalName).c_str(), + renamePairs[i].originalName.c_str(), + renamePairs[i].depth, + !renamePairs[i].isFile, + &item); + + int itemId = 0; + Assert::IsTrue(item->get_id(&itemId) == S_OK); + mgr->AddItem(item); + + // Verify the item we added is the same from the event + Assert::IsTrue(mockMgrEvents->m_itemAdded != nullptr && mockMgrEvents->m_itemAdded == item); + int eventItemId = 0; + Assert::IsTrue(mockMgrEvents->m_itemAdded->get_id(&eventItemId) == S_OK); + Assert::IsTrue(itemId == eventItemId); + } + + // TODO: Setup match and replace parameters + CComPtr renRegEx; + Assert::IsTrue(mgr->get_smartRenameRegEx(&renRegEx) == S_OK); + renRegEx->put_flags(flags); + renRegEx->put_searchTerm(searchTerm.c_str()); + renRegEx->put_replaceTerm(replaceTerm.c_str()); + + Sleep(1000); + + // Perform the rename + Assert::IsTrue(mgr->Rename(0) == S_OK); + + Sleep(1000); + + // Verify the rename occurred + for (int i = 0; i < numPairs; i++) + { + Assert::IsTrue(testFileHelper.PathExists(renamePairs[i].originalName) == !renamePairs[i].shouldRename); + Assert::IsTrue(testFileHelper.PathExists(renamePairs[i].newName) == renamePairs[i].shouldRename); + } + + Assert::IsTrue(mgr->Shutdown() == S_OK); + + mockMgrEvents->Release(); + } + TEST_METHOD(CreateTest) + { + CComPtr mgr; + Assert::IsTrue(CPowerRenameManager::s_CreateInstance(&mgr) == S_OK); + } + + TEST_METHOD(CreateAndShutdownTest) + { + CComPtr mgr; + Assert::IsTrue(CPowerRenameManager::s_CreateInstance(&mgr) == S_OK); + Assert::IsTrue(mgr->Shutdown() == S_OK); + } + + TEST_METHOD(AddItemTest) + { + CComPtr mgr; + Assert::IsTrue(CPowerRenameManager::s_CreateInstance(&mgr) == S_OK); + CComPtr item; + CMockPowerRenameItem::CreateInstance(L"foo", L"foo", 0, false, &item); + mgr->AddItem(item); + Assert::IsTrue(mgr->Shutdown() == S_OK); + } + + TEST_METHOD(VerifySmartManagerEvents) + { + CComPtr mgr; + Assert::IsTrue(CPowerRenameManager::s_CreateInstance(&mgr) == S_OK); + CMockPowerRenameManagerEvents* mockMgrEvents = new CMockPowerRenameManagerEvents(); + CComPtr mgrEvents; + Assert::IsTrue(mockMgrEvents->QueryInterface(IID_PPV_ARGS(&mgrEvents)) == S_OK); + DWORD cookie = 0; + Assert::IsTrue(mgr->Advise(mgrEvents, &cookie) == S_OK); + CComPtr item; + CMockPowerRenameItem::CreateInstance(L"foo", L"foo", 0, false, &item); + int itemId = 0; + Assert::IsTrue(item->get_id(&itemId) == S_OK); + mgr->AddItem(item); + + // Verify the item we added is the same from the event + Assert::IsTrue(mockMgrEvents->m_itemAdded != nullptr && mockMgrEvents->m_itemAdded == item); + int eventItemId = 0; + Assert::IsTrue(mockMgrEvents->m_itemAdded->get_id(&eventItemId) == S_OK); + Assert::IsTrue(itemId == eventItemId); + Assert::IsTrue(mgr->Shutdown() == S_OK); + + mockMgrEvents->Release(); + } + + TEST_METHOD(VerifySingleRename) + { + // Create a single item and verify rename works as expected + rename_pairs renamePairs[] = + { + {L"foo.txt", L"bar.txt", true, true} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS); + } + + TEST_METHOD(VerifyMultiRename) + { + // Create a single item and verify rename works as expected + rename_pairs renamePairs[] = + { + {L"foo1.txt", L"bar1.txt", true, true, 0}, + {L"foo2.txt", L"bar2.txt", true, true, 0}, + {L"foo3.txt", L"bar3.txt", true, true, 0}, + {L"foo4.txt", L"bar4.txt", true, true, 0}, + {L"foo5.txt", L"bar5.txt", true, true, 0}, + {L"baa.txt", L"baa_norename.txt", true, false, 0} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS); + } + + TEST_METHOD(VerifyFilesOnlyRename) + { + // Verify only files are renamed when folders match too + rename_pairs renamePairs[] = + { + {L"foo.txt", L"bar.txt", true, true, 0}, + {L"foo", L"foo_norename", false, false, 0} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | ExcludeFolders); + } + + TEST_METHOD(VerifyFoldersOnlyRename) + { + // Verify only folders are renamed when files match too + rename_pairs renamePairs[] = + { + {L"foo.txt", L"foo_norename.txt", true, false, 0}, + {L"foo", L"bar", false, true, 0} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | ExcludeFiles); + } + + TEST_METHOD(VerifyFileNameOnlyRename) + { + // Verify only file name is renamed, not extension + rename_pairs renamePairs[] = + { + {L"foo.foo", L"bar.foo", true, true, 0}, + {L"test.foo", L"test.foo_norename", true, false, 0} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | NameOnly); + } + + TEST_METHOD(VerifyFileExtensionOnlyRename) + { + // Verify only file extension is renamed, not name + rename_pairs renamePairs[] = + { + {L"foo.foo", L"foo.bar", true, true, 0}, + {L"test.foo", L"test.bar", true, true, 0} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | ExtensionOnly); + } + + TEST_METHOD(VerifySubFoldersRename) + { + // Verify subfolders do not get renamed + rename_pairs renamePairs[] = + { + {L"foo1", L"bar1", false, true, 0}, + {L"foo2", L"foo2_norename", false, false, 1} + }; + + RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | ExcludeSubfolders); + } + + }; +} \ No newline at end of file diff --git a/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp new file mode 100644 index 0000000000..82bb022466 --- /dev/null +++ b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp @@ -0,0 +1,331 @@ +#include "stdafx.h" +#include "CppUnitTest.h" +#include +#include +#include "MockPowerRenameRegExEvents.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace PowerRenameRegExTests +{ + struct SearchReplaceExpected + { + PCWSTR search; + PCWSTR replace; + PCWSTR test; + PCWSTR expected; + }; + + TEST_CLASS(SimpleTests) + { + public: + TEST_METHOD(GeneralReplaceTest) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(L"foo") == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(L"big") == S_OK); + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); + Assert::IsTrue(wcscmp(result, L"bigbar") == 0); + CoTaskMemFree(result); + } + + TEST_METHOD(ReplaceNoMatch) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(L"notfound") == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(L"big") == S_OK); + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); + Assert::IsTrue(wcscmp(result, L"foobar") == 0); + CoTaskMemFree(result); + } + + TEST_METHOD(ReplaceNoSearchOrReplaceTerm) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) != S_OK); + Assert::IsTrue(result == nullptr); + CoTaskMemFree(result); + } + + TEST_METHOD(ReplaceNoReplaceTerm) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(L"foo") == S_OK); + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); + Assert::IsTrue(wcscmp(result, L"bar") == 0); + CoTaskMemFree(result); + } + + TEST_METHOD(ReplaceEmptyStringReplaceTerm) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(L"foo") == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(L"") == S_OK); + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); + Assert::IsTrue(wcscmp(result, L"bar") == 0); + CoTaskMemFree(result); + } + + TEST_METHOD(VerifyDefaultFlags) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = 0; + Assert::IsTrue(renameRegEx->get_flags(&flags) == S_OK); + Assert::IsTrue(flags == MatchAllOccurences); + } + + TEST_METHOD(VerifyCaseSensitiveSearch) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = CaseSensitive; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"Foo", L"Foo", L"FooBar", L"FooBar" }, + { L"Foo", L"boo", L"FooBar", L"booBar" }, + { L"Foo", L"boo", L"foobar", L"foobar" }, + { L"123", L"654", L"123456", L"654456" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceFirstOnly) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = 0; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AABBA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceAll) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurences; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceAllCaseInsensitive) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurences | CaseSensitive; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceFirstOnlyUseRegEx) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = UseRegularExpressions; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AABBA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceAllUseRegEx) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurences | UseRegularExpressions; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceAllUseRegExCaseSensitive) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurences | UseRegularExpressions | CaseSensitive; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyMatchAllWildcardUseRegEx) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurences | UseRegularExpressions; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L".*", L"Foo", L"AAAAAA", L"Foo" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyReplaceFirstWildcardUseRegEx) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = UseRegularExpressions; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = + { + { L".*", L"Foo", L"AAAAAA", L"FooAAAA" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->put_searchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(sreTable[i].replace) == S_OK); + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } + } + + TEST_METHOD(VerifyEventsFire) + { + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + CMockPowerRenameRegExEvents* mockEvents = new CMockPowerRenameRegExEvents(); + CComPtr regExEvents; + Assert::IsTrue(mockEvents->QueryInterface(IID_PPV_ARGS(®ExEvents)) == S_OK); + DWORD cookie = 0; + Assert::IsTrue(renameRegEx->Advise(regExEvents, &cookie) == S_OK); + DWORD flags = MatchAllOccurences | UseRegularExpressions | CaseSensitive; + Assert::IsTrue(renameRegEx->put_flags(flags) == S_OK); + Assert::IsTrue(renameRegEx->put_searchTerm(L"FOO") == S_OK); + Assert::IsTrue(renameRegEx->put_replaceTerm(L"BAR") == S_OK); + Assert::IsTrue(lstrcmpi(L"FOO", mockEvents->m_searchTerm) == 0); + Assert::IsTrue(lstrcmpi(L"BAR", mockEvents->m_replaceTerm) == 0); + Assert::IsTrue(flags == mockEvents->m_flags); + Assert::IsTrue(renameRegEx->UnAdvise(cookie) == S_OK); + mockEvents->Release(); + } + }; +} \ No newline at end of file diff --git a/src/modules/powerrename/unittests/TestFileHelper.cpp b/src/modules/powerrename/unittests/TestFileHelper.cpp new file mode 100644 index 0000000000..3cc814e9f2 --- /dev/null +++ b/src/modules/powerrename/unittests/TestFileHelper.cpp @@ -0,0 +1,73 @@ +#include "stdafx.h" +#include "TestFileHelper.h" +#include +#include +#include + +namespace fs = std::filesystem; + + + +CTestFileHelper::CTestFileHelper() +{ + _CreateTempDirectory(); +} +CTestFileHelper::~CTestFileHelper() +{ + _DeleteTempDirectory(); +} + +// Pass a relative path which will be appended to the temp directory path +bool CTestFileHelper::AddFile(_In_ const std::wstring path) +{ + fs::path newFilePath = _tempDirectory; + newFilePath.append(path); + std::ofstream ofs(newFilePath); + ofs.close(); + return true; +} + +// Pass a relative path which will be appended to the temp directory path +bool CTestFileHelper::AddFolder(_In_ const std::wstring path) +{ + fs::path newFolderPath = _tempDirectory; + newFolderPath.append(path); + return fs::create_directory(fs::path(newFolderPath)); +} + +fs::path CTestFileHelper::GetFullPath(_In_ const std::wstring path) +{ + fs::path fullPath = _tempDirectory; + fullPath.append(path); + return fullPath; +} + +bool CTestFileHelper::PathExists(_In_ const std::wstring path) +{ + fs::path fullPath = _tempDirectory; + fullPath.append(path); + return fs::exists(fullPath); +} + +bool CTestFileHelper::_CreateTempDirectory() +{ + // Initialize to the temp directory + _tempDirectory = fs::temp_directory_path(); + + // Create a unique folder name + GUID guid = { 0 }; + CoCreateGuid(&guid); + + wchar_t uniqueName[MAX_PATH] = { 0 }; + StringFromGUID2(guid, uniqueName, ARRAYSIZE(uniqueName)); + + _tempDirectory.append(uniqueName); + + return fs::create_directory(_tempDirectory); +} + +void CTestFileHelper::_DeleteTempDirectory() +{ + fs::remove_all(_tempDirectory); +} + diff --git a/src/modules/powerrename/unittests/TestFileHelper.h b/src/modules/powerrename/unittests/TestFileHelper.h new file mode 100644 index 0000000000..50b0283055 --- /dev/null +++ b/src/modules/powerrename/unittests/TestFileHelper.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + + +class CTestFileHelper +{ +public: + CTestFileHelper(); + ~CTestFileHelper(); + + bool AddFile(_In_ const std::wstring path); + bool AddFolder(_In_ const std::wstring path); + const std::filesystem::path GetTempDirectory() { return _tempDirectory; } + bool PathExists(_In_ const std::wstring path); + std::filesystem::path GetFullPath(_In_ const std::wstring path); + +private: + bool _CreateTempDirectory(); + void _DeleteTempDirectory(); + + std::filesystem::path _tempDirectory; +}; \ No newline at end of file diff --git a/src/modules/powerrename/unittests/stdafx.cpp b/src/modules/powerrename/unittests/stdafx.cpp new file mode 100644 index 0000000000..fd4f341c7b --- /dev/null +++ b/src/modules/powerrename/unittests/stdafx.cpp @@ -0,0 +1 @@ +#include "stdafx.h" diff --git a/src/modules/powerrename/unittests/stdafx.h b/src/modules/powerrename/unittests/stdafx.h new file mode 100644 index 0000000000..e15fd148b2 --- /dev/null +++ b/src/modules/powerrename/unittests/stdafx.h @@ -0,0 +1,9 @@ +#pragma once + +#include "targetver.h" + +#include + +// Headers for CppUnitTest +#include "CppUnitTest.h" + diff --git a/src/modules/powerrename/unittests/targetver.h b/src/modules/powerrename/unittests/targetver.h new file mode 100644 index 0000000000..4dac0b05c6 --- /dev/null +++ b/src/modules/powerrename/unittests/targetver.h @@ -0,0 +1,10 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include + + diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 84d2e57f5e..d1b591cdf3 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -53,7 +53,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // For now only load known DLLs std::unordered_set known_dlls = { L"shortcut_guide.dll", - L"fancyzones.dll" + L"fancyzones.dll", + L"PowerRenameExt.dll" }; for (auto& file : std::filesystem::directory_iterator(L"modules/")) { if (file.path().extension() != L".dll")