mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-01-18 06:29:44 +08:00
[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:
parent
e8685de7f7
commit
30f442d774
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
||||
|