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 0000000000..b3ec03bd61 Binary files /dev/null and b/src/modules/powerrename/testapp/PowerRenameTest.ico differ diff --git a/src/modules/powerrename/testapp/PowerRenameTest.rc b/src/modules/powerrename/testapp/PowerRenameTest.rc new file mode 100644 index 0000000000..051800940c --- /dev/null +++ b/src/modules/powerrename/testapp/PowerRenameTest.rc @@ -0,0 +1,71 @@ +ÿþ// 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 + +#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 + +#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/testapp/PowerRenameTest.vcxproj b/src/modules/powerrename/testapp/PowerRenameTest.vcxproj new file mode 100644 index 0000000000..62a34eb08e --- /dev/null +++ b/src/modules/powerrename/testapp/PowerRenameTest.vcxproj @@ -0,0 +1,188 @@ + + + + + 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 0000000000..b3ec03bd61 Binary files /dev/null and b/src/modules/powerrename/testapp/small.ico differ diff --git a/src/modules/powerrename/testapp/stdafx.cpp b/src/modules/powerrename/testapp/stdafx.cpp new file mode 100644 index 0000000000..fd4f341c7b --- /dev/null +++ b/src/modules/powerrename/testapp/stdafx.cpp @@ -0,0 +1 @@ +#include "stdafx.h" diff --git a/src/modules/powerrename/testapp/stdafx.h b/src/modules/powerrename/testapp/stdafx.h new file mode 100644 index 0000000000..6b1cfaefc9 --- /dev/null +++ b/src/modules/powerrename/testapp/stdafx.h @@ -0,0 +1,16 @@ +#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 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 0000000000..d96be9175d Binary files /dev/null and b/src/modules/powerrename/ui/Rename.ico differ diff --git a/src/modules/powerrename/ui/resource.h b/src/modules/powerrename/ui/resource.h new file mode 100644 index 0000000000..3c4301afcb Binary files /dev/null and b/src/modules/powerrename/ui/resource.h differ 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")