[PowerRename] upper/lower/titlecase transform feature (#4183)

* Add basic transform functionality

* Add basic transform functionality

* Change toupper/tolower/isspace to towupper/towlower/towisspace. For loops omitted if possible.

* Avoid wcslen() in for statement

* Avoid wcslen() in for statement

* Add basic transform functionality

* Change toupper/tolower/isspace to towupper/towlower/towisspace. For loops omitted if possible.

* Avoid wcslen() in for statement

* Avoid wcslen() in for statement

* Add basic transform functionality

* Change toupper/tolower/isspace to towupper/towlower/towisspace. For loops omitted if possible.

* Avoid wcslen() in for statement

* Adjust Powerrename Interface

* Add trimming rename string

* Remove leading and trailing spaces from rename string

* Add support for transforming only item name or extension. Temporarily remove trimming to refactor. Change CAPITALIZED to TITLECASE

* Fix bug when search for area is empty

* Add trimming back with refactor(leading spaces, trailing spaces, trailing dots)

* Now supports transforming when search area is empty

* Add smarter titlecase

Transformation breaks when new filename contains an unusable character (\/?:*?"<>|)
These characters need to be removed from new name anyway.

* minor bugfix

* Add unittests, contains failing tests

* Remove unnecessary/failing tests

* remove generated file

* some code formatting and fix memory leak issues

* Use proper allocation, change int to size_t

* Refactor. Move transforming to Helpers.cpp

* Refactor. Move trimming to Helpers.cpp

* Change StrDup to SHStrDup. Some refactoring.

* Fix memery leak, add proper result controls, use newNameToUse in functon calls becaause it is where the final form of the string is tracked

* Change declarations of strings, add proper result controls

* Slightly widen the labels to cover the whole text

* Add extended characters support

* Rename a variable

* Correctly identify the last word for titlecase

* Add empty line to last line of resource.h
This commit is contained in:
Mehmet Murat Akburak 2020-07-02 11:52:01 +03:00 committed by GitHub
parent e8685de7f7
commit 30f442d774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 296 additions and 18 deletions

View file

@ -1,6 +1,172 @@
#include "pch.h"
#include "Helpers.h"
#include <ShlGuid.h>
#include <cstring>
#include <filesystem>
namespace fs = std::filesystem;
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source)
{
HRESULT hr = (source && wcslen(source) > 0) ? S_OK : E_INVALIDARG;
if (SUCCEEDED(hr))
{
PWSTR newName = nullptr;
hr = SHStrDup(source, &newName);
if (SUCCEEDED(hr))
{
size_t firstValidIndex = 0, lastValidIndex = wcslen(newName) - 1;
while (firstValidIndex <= lastValidIndex && iswspace(newName[firstValidIndex]))
{
firstValidIndex++;
}
while (firstValidIndex <= lastValidIndex && (iswspace(newName[lastValidIndex]) || newName[lastValidIndex] == L'.'))
{
lastValidIndex--;
}
newName[lastValidIndex + 1] = '\0';
hr = StringCchCopy(result, cchMax, newName + firstValidIndex);
}
CoTaskMemFree(newName);
}
return hr;
}
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags)
{
std::locale::global(std::locale(""));
HRESULT hr = (source && wcslen(source) > 0 && flags) ? S_OK : E_INVALIDARG;
if (SUCCEEDED(hr))
{
if (flags & Uppercase)
{
if (flags & NameOnly)
{
std::wstring stem = fs::path(source).stem().wstring();
std::transform(stem.begin(), stem.end(), stem.begin(), ::towupper);
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), fs::path(source).extension().c_str());
}
else if (flags & ExtensionOnly)
{
std::wstring extension = fs::path(source).extension().wstring();
if (!extension.empty())
{
std::transform(extension.begin(), extension.end(), extension.begin(), ::towupper);
hr = StringCchPrintf(result, cchMax, L"%s%s", fs::path(source).stem().c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towupper);
}
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towupper);
}
}
}
else if (flags & Lowercase)
{
if (flags & NameOnly)
{
std::wstring stem = fs::path(source).stem().wstring();
std::transform(stem.begin(), stem.end(), stem.begin(), ::towlower);
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), fs::path(source).extension().c_str());
}
else if (flags & ExtensionOnly)
{
std::wstring extension = fs::path(source).extension().wstring();
if (!extension.empty())
{
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
hr = StringCchPrintf(result, cchMax, L"%s%s", fs::path(source).stem().c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towlower);
}
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
if (SUCCEEDED(hr))
{
std::transform(result, result + wcslen(result), result, ::towlower);
}
}
}
else if (flags & Titlecase)
{
if (!(flags & ExtensionOnly))
{
std::vector<std::wstring> exceptions = { L"a", L"an", L"to", L"the", L"at", L"by", L"for", L"in", L"of", L"on", L"up", L"and", L"as", L"but", L"or", L"nor" };
std::wstring stem = fs::path(source).stem().wstring();
std::wstring extension = fs::path(source).extension().wstring();
size_t stemLength = stem.length();
bool isFirstWord = true;
while (stemLength > 0 && (iswspace(stem[stemLength - 1]) || iswpunct(stem[stemLength - 1])))
{
stemLength--;
}
for (size_t i = 0; i < stemLength; i++)
{
if (!i || iswspace(stem[i - 1]) || iswpunct(stem[i - 1]))
{
if (iswspace(stem[i]) || iswpunct(stem[i]))
{
continue;
}
size_t wordLength = 0;
while (i + wordLength < stemLength && !iswspace(stem[i + wordLength]) && !iswpunct(stem[i + wordLength]))
{
wordLength++;
}
if (isFirstWord || i + wordLength == stemLength || std::find(exceptions.begin(), exceptions.end(), stem.substr(i, wordLength)) == exceptions.end())
{
stem[i] = towupper(stem[i]);
isFirstWord = false;
}
else
{
stem[i] = towlower(stem[i]);
}
}
else
{
stem[i] = towlower(stem[i]);
}
}
hr = StringCchPrintf(result, cchMax, L"%s%s", stem.c_str(), extension.c_str());
}
else
{
hr = StringCchCopy(result, cchMax, source);
}
}
else
{
hr = StringCchCopy(result, cchMax, source);
}
}
return hr;
}
HRESULT _ParseEnumItems(_In_ IEnumShellItems* pesi, _In_ IPowerRenameManager* psrm, _In_ int depth = 0)
{

View file

@ -3,6 +3,8 @@
#include <common.h>
#include <lib/PowerRenameInterfaces.h>
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source);
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags);
HRESULT EnumerateDataObject(_In_ IUnknown* pdo, _In_ IPowerRenameManager* psrm);
BOOL GetEnumeratedFileName(
__out_ecount(cchMax) PWSTR pszUniqueName,

View file

@ -11,7 +11,10 @@ enum PowerRenameFlags
ExcludeFolders = 0x20,
ExcludeSubfolders = 0x40,
NameOnly = 0x80,
ExtensionOnly = 0x100
ExtensionOnly = 0x100,
Uppercase = 0x200,
Lowercase = 0x400,
Titlecase = 0x800
};
interface __declspec(uuid("3ECBA62B-E0F0-4472-AA2E-DEE7A1AA46B9")) IPowerRenameRegExEvents : public IUnknown

View file

@ -3,6 +3,7 @@
#include "PowerRenameRegEx.h" // Default RegEx handler
#include <algorithm>
#include <shlobj.h>
#include <cstring>
#include "helpers.h"
#include "window_helpers.h"
#include <filesystem>
@ -763,7 +764,6 @@ DWORD WINAPI CPowerRenameManager::s_regexWorkerThread(_In_ void* pv)
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
@ -775,6 +775,13 @@ DWORD WINAPI CPowerRenameManager::s_regexWorkerThread(_In_ void* pv)
// newName == nullptr likely means we have an empty search string. We should leave newNameToUse
// as nullptr so we clear the renamed column
// Except string transformation is selected.
if (newName == nullptr && (flags & Uppercase || flags & Lowercase || flags & Titlecase))
{
SHStrDup(sourceName, &newName);
}
if (newName != nullptr)
{
newNameToUse = resultName;
@ -800,6 +807,22 @@ DWORD WINAPI CPowerRenameManager::s_regexWorkerThread(_In_ void* pv)
}
}
wchar_t trimmedName[MAX_PATH] = { 0 };
if (newNameToUse != nullptr && SUCCEEDED(GetTrimmedFileName(trimmedName, ARRAYSIZE(trimmedName), newNameToUse)))
{
newNameToUse = trimmedName;
}
wchar_t transformedName[MAX_PATH] = { 0 };
if (newNameToUse != nullptr && (flags & Uppercase || flags & Lowercase || flags & Titlecase))
{
if (SUCCEEDED(GetTransformedFileName(transformedName, ARRAYSIZE(transformedName), newNameToUse, flags)))
{
newNameToUse = transformedName;
}
}
// No change from originalName so set newName to
// null so we clear it from our UI as well.
if (lstrcmp(originalName, newNameToUse) == 0)

View file

@ -233,8 +233,7 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result)
} while (pos != std::string::npos);
}
*result = StrDup(res.c_str());
hr = (*result) ? S_OK : E_OUTOFMEMORY;
hr = SHStrDup(res.c_str(), result);
}
catch (regex_error e)
{

View file

@ -39,7 +39,10 @@ FlagCheckboxMap g_flagCheckboxMap[] = {
{ MatchAllOccurences, IDC_CHECK_MATCHALLOCCURENCES },
{ ExcludeFolders, IDC_CHECK_EXCLUDEFOLDERS },
{ NameOnly, IDC_CHECK_NAMEONLY },
{ ExtensionOnly, IDC_CHECK_EXTENSIONONLY }
{ ExtensionOnly, IDC_CHECK_EXTENSIONONLY },
{ Uppercase, IDC_TRANSFORM_UPPERCASE },
{ Lowercase, IDC_TRANSFORM_LOWERCASE },
{ Titlecase, IDC_TRANSFORM_TITLECASE }
};
struct RepositionMap
@ -683,6 +686,9 @@ void CPowerRenameUI::_OnCommand(_In_ WPARAM wParam, _In_ LPARAM lParam)
case IDC_CHECK_USEREGEX:
case IDC_CHECK_EXTENSIONONLY:
case IDC_CHECK_NAMEONLY:
case IDC_TRANSFORM_UPPERCASE:
case IDC_TRANSFORM_LOWERCASE:
case IDC_TRANSFORM_TITLECASE:
if (BN_CLICKED == HIWORD(wParam))
{
_ValidateFlagCheckbox(LOWORD(wParam));
@ -891,7 +897,31 @@ void CPowerRenameUI::_SetCheckboxesFromFlags(_In_ DWORD flags)
void CPowerRenameUI::_ValidateFlagCheckbox(_In_ DWORD checkBoxId)
{
if (checkBoxId == IDC_CHECK_NAMEONLY)
if (checkBoxId == IDC_TRANSFORM_UPPERCASE )
{
if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_UPPERCASE)) == BST_CHECKED)
{
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_LOWERCASE), FALSE);
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_TITLECASE), FALSE);
}
}
else if (checkBoxId == IDC_TRANSFORM_LOWERCASE)
{
if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_LOWERCASE)) == BST_CHECKED)
{
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_UPPERCASE), FALSE);
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_TITLECASE), FALSE);
}
}
else if (checkBoxId == IDC_TRANSFORM_TITLECASE)
{
if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_TITLECASE)) == BST_CHECKED)
{
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_UPPERCASE), FALSE);
Button_SetCheck(GetDlgItem(m_hwnd, IDC_TRANSFORM_LOWERCASE), FALSE);
}
}
else if (checkBoxId == IDC_CHECK_NAMEONLY)
{
if (Button_GetCheck(GetDlgItem(m_hwnd, IDC_CHECK_NAMEONLY)) == BST_CHECKED)
{

View file

@ -28,7 +28,7 @@
// Dialog
//
IDD_MAIN DIALOGEX 0, 0, 351, 304
IDD_MAIN DIALOGEX 0, 0, 364, 317
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CENTER | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
CAPTION "PowerRename"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
@ -37,24 +37,27 @@
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_MATCHALLOCCURENCES,
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,21,107,85,10
CONTROL "Match All Occurrences",IDC_CHECK_MATCHALLOCCURENCES,
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,21,107,85,10
CONTROL "Make Uppercase", IDC_TRANSFORM_UPPERCASE, "Button", BS_AUTOCHECKBOX | WS_TABSTOP, 21, 119, 64, 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
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,132,107,92,10
CONTROL "Make Lowercase", IDC_TRANSFORM_LOWERCASE, "Button", BS_AUTOCHECKBOX | WS_TABSTOP, 132, 119, 64, 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
DEFPUSHBUTTON "&Rename",ID_RENAME,178,283,50,14
PUSHBUTTON "&Help",ID_ABOUT,234,283,50,14
PUSHBUTTON "&Cancel",IDCANCEL,290,283,50,14
CONTROL "Make Titlecase", IDC_TRANSFORM_TITLECASE, "Button", BS_AUTOCHECKBOX | WS_TABSTOP, 241, 119, 62, 10
CONTROL "",IDC_LIST_PREVIEW,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_BORDER | WS_TABSTOP,22,160,308,116
DEFPUSHBUTTON "&Rename",ID_RENAME,178,295,50,14
PUSHBUTTON "&Help",ID_ABOUT,234,295,50,14
PUSHBUTTON "&Cancel",IDCANCEL,290,295,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_OPTIONSGROUP,11,68,329,58
GROUPBOX "Preview",IDC_PREVIEWGROUP,11,133,329,142
LTEXT "Items Selected: 0 | Renaming: 0",IDC_STATUS_MESSAGE,11,295,137,13
GROUPBOX "Options",IDC_OPTIONSGROUP,11,68,329,70
GROUPBOX "Preview",IDC_PREVIEWGROUP,11,145,329,142
GROUPBOX "Enter the criteria below to rename the items",IDC_SEARCHREPLACEGROUP,11,7,329,55
END

View file

@ -229,5 +229,57 @@ namespace PowerRenameManagerTests
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | ExcludeSubfolders);
}
TEST_METHOD (VerifyUppercaseTransform)
{
rename_pairs renamePairs[] = {
{ L"foo", L"BAR", true, true, 0 },
{ L"foo.test", L"BAR.TEST", true, true, 0 },
{ L"TEST", L"TEST_norename", true, false, 0 }
};
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | Uppercase);
}
TEST_METHOD (VerifyLowercaseTransform)
{
rename_pairs renamePairs[] = {
{ L"Foo", L"bar", false, true, 0 },
{ L"Foo.teST", L"bar.test", false, true, 0 },
{ L"test", L"test_norename", false, false, 0 }
};
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | Lowercase);
}
TEST_METHOD (VerifyTitlecaseTransform)
{
rename_pairs renamePairs[] = {
{ L"foo and the to", L"Bar and the To", false, true, 0 },
{ L"Test", L"Test_norename", false, false, 0 }
};
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | Titlecase);
}
TEST_METHOD (VerifyNameOnlyTransform)
{
rename_pairs renamePairs[] = {
{ L"foo.txt", L"BAR.txt", false, true, 0 },
{ L"TEST", L"TEST_norename", false, false, 1 }
};
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | Uppercase | NameOnly);
}
TEST_METHOD (VerifyExtensionOnlyTransform)
{
rename_pairs renamePairs[] = {
{ L"foo.FOO", L"foo.bar", false, true, 0 },
{ L"foo.bar", L"foo.bar_rename", false, false, 0 }
};
RenameHelper(renamePairs, ARRAYSIZE(renamePairs), L"foo", L"bar", DEFAULT_FLAGS | Lowercase | ExtensionOnly);
}
};
}
}