[New+]Support for variables in template filenames (#37074)

* Add variable support - initial version without UI

* Add variable in template filename support in New+

* Fix XAML style

* Addressed code review feedback
This commit is contained in:
Christian Gaarden Gaardmark
2025-03-18 04:52:51 -07:00
committed by GitHub
parent 1d358af600
commit 1f81d14000
28 changed files with 592 additions and 96 deletions

View File

@@ -232,6 +232,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowDataDiagnosticsValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusReplaceVariablesValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredRunAtStartupValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());

View File

@@ -64,6 +64,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
};
}

View File

@@ -68,6 +68,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
}
}
}

View File

@@ -86,6 +86,7 @@ namespace powertoys_gpo {
const std::wstring POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES = L"MwbDisableUserDefinedIpMappingRules";
const std::wstring POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES = L"MwbPolicyDefinedIpMappingRules";
const std::wstring POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION = L"NewPlusHideTemplateFilenameExtension";
const std::wstring POLICY_NEW_PLUS_REPLACE_VARIABLES = L"NewPlusReplaceVariablesInTemplateFilenames";
// Methods used for reading the registry
#pragma region ReadRegistryMethods
@@ -609,5 +610,11 @@ namespace powertoys_gpo {
{
return getConfiguredValue(POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION);
}
inline gpo_rule_configured_t getConfiguredNewPlusReplaceVariablesValue()
{
return getConfiguredValue(POLICY_NEW_PLUS_REPLACE_VARIABLES);
}
#pragma endregion IndividualModuleSettingPolicies
}

View File

@@ -671,5 +671,15 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="NewPlusReplaceVariablesInTemplateFilenames" class="Both" displayName="$(string.NewPlusReplaceVariablesInTemplateFilenames)" explainText="$(string.NewPlusReplaceVariablesInTemplateFilenamesDescription)" key="Software\Policies\PowerToys" valueName="NewPlusReplaceVariablesInTemplateFilenames">
<parentCategory ref="NewPlus" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_89_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -226,6 +226,14 @@ If you enable this policy, the setting is enabled and the extension is hidden.
If you disable this policy, the setting is disabled and the extension is shown.
If you don't configure this policy, the user takes control over the setting and can enable or disable it.
</string>
<string id="NewPlusReplaceVariablesInTemplateFilenamesDescription">This policy configures if supported variables will get replaced in template filenames.
If you enable this policy, the setting is enabled and supported variables in filenames will get replaced.
If you disable this policy, the setting is disabled and variables in filenames will not get replaced.
If you don't configure this policy, the user will be able to control the setting and can enable or disable it.
</string>
<string id="ConfigureAllUtilityGlobalEnabledState">Configure global utility enabled state</string>
@@ -289,7 +297,7 @@ If you don't configure this policy, the user takes control over the setting and
<string id="MwbPolicyDefinedIpMappingRules">Predefined IP Address mapping rules</string>
<string id="NewPlusHideTemplateFilenameExtension">Hide template filename extension</string>
<string id="AllowDiagnosticData">Allow sending diagnostic data</string>
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
</stringTable>
<presentationTable>

View File

@@ -83,6 +83,8 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\NewShellExtensionContextMenu\constants.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_filesystem.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\new_utilities.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\settings.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\shell_context_sub_menu.h" />
@@ -97,6 +99,7 @@
<ClInclude Include="shell_context_menu_win10.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\powertoys_module.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\settings.cpp" />

View File

@@ -57,6 +57,12 @@
<ClInclude Include="..\NewShellExtensionContextMenu\settings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_filesystem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -92,6 +98,9 @@
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files\new.rc">

View File

@@ -32,7 +32,7 @@ IFACEMETHODIMP shell_context_menu_win10::Initialize(PCIDLIST_ABSOLUTE, IDataObje
IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UINT menu_index, UINT menu_first_cmd_id, UINT, UINT menu_flags)
{
if (!NewSettingsInstance().GetEnabled()
|| package::IsWin11OrGreater()
|| package::IsWin11OrGreater()
)
{
return E_FAIL;
@@ -47,7 +47,7 @@ IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UIN
{
// Create the initial context popup menu containing the list of templates and open templates action
int menu_id = menu_first_cmd_id;
MENUITEMINFO newplus_main_context_menu_item;
MENUITEMINFO newplus_main_context_menu_item = { 0 };
HMENU sub_menu_of_templates = CreatePopupMenu();
int sub_menu_index = 0;
@@ -142,7 +142,7 @@ void shell_context_menu_win10::add_open_templates_to_context_menu(HMENU sub_menu
wchar_t menu_name_open[256] = { 0 };
wcscpy_s(menu_name_open, ARRAYSIZE(menu_name_open), localized_context_menu_item_open_templates.c_str());
const auto open_folder_item = Make<template_folder_context_menu_item>(template_folder_root);
MENUITEMINFO newplus_menu_item_open_templates;
MENUITEMINFO newplus_menu_item_open_templates = { 0 };
newplus_menu_item_open_templates.cbSize = sizeof(MENUITEMINFO);
newplus_menu_item_open_templates.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
newplus_menu_item_open_templates.wID = menu_id;
@@ -174,7 +174,7 @@ void shell_context_menu_win10::add_open_templates_to_context_menu(HMENU sub_menu
void shell_context_menu_win10::add_separator_to_context_menu(HMENU sub_menu_of_templates, int sub_menu_index)
{
MENUITEMINFO menu_item_separator;
MENUITEMINFO menu_item_separator = { 0 };
menu_item_separator.cbSize = sizeof(MENUITEMINFO);
menu_item_separator.fMask = MIIM_FTYPE;
menu_item_separator.fType = MFT_SEPARATOR;
@@ -184,8 +184,11 @@ void shell_context_menu_win10::add_separator_to_context_menu(HMENU sub_menu_of_t
void shell_context_menu_win10::add_template_item_to_context_menu(HMENU sub_menu_of_templates, int sub_menu_index, newplus::template_item* const template_item, int menu_id, int index)
{
wchar_t menu_name[256] = { 0 };
wcscpy_s(menu_name, ARRAYSIZE(menu_name), template_item->get_menu_title(!utilities::get_newplus_setting_hide_extension(), !utilities::get_newplus_setting_hide_starting_digits()).c_str());
MENUITEMINFO newplus_menu_item_template;
wcscpy_s(menu_name, ARRAYSIZE(menu_name), template_item->get_menu_title(
!utilities::get_newplus_setting_hide_extension(),
!utilities::get_newplus_setting_hide_starting_digits(),
utilities::get_newplus_setting_resolve_variables()).c_str());
MENUITEMINFO newplus_menu_item_template = { 0 };
newplus_menu_item_template.cbSize = sizeof(MENUITEMINFO);
newplus_menu_item_template.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_DATA;
newplus_menu_item_template.wID = menu_id;

View File

@@ -114,6 +114,8 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="dll_main.h" />
<ClInclude Include="helpers_filesystem.h" />
<ClInclude Include="helpers_variables.h" />
<ClInclude Include="shell_context_menu.h" />
<ClInclude Include="shell_context_sub_menu.h" />
<ClInclude Include="shell_context_sub_menu_item.h" />
@@ -128,6 +130,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ClInclude Include="template_item.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="new_utilities.cpp" />
<ClCompile Include="shell_context_menu.cpp" />
<ClCompile Include="shell_context_sub_menu.cpp" />

View File

@@ -34,6 +34,9 @@
<ClCompile Include="new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="template_folder.h">
@@ -75,6 +78,12 @@
<ClInclude Include="resource.base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="helpers_filesystem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="helpers_variables.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -14,6 +14,8 @@ namespace newplus::constants::non_localizable
constexpr WCHAR settings_json_key_hide_starting_digits[] = L"HideStartingDigits";
constexpr WCHAR settings_json_key_replace_variables[] = L"ReplaceVariables";
constexpr WCHAR settings_json_key_template_location[] = L"TemplateLocation";
constexpr WCHAR context_menu_package_name[] = L"NewPlusContextMenu";
@@ -30,5 +32,5 @@ namespace newplus::constants::non_localizable
constexpr WCHAR open_templates_icon_dark_resource_relative_path[] = L"\\Assets\\NewPlus\\Open_templates_dark.ico";
constexpr WCHAR desktop_ini_filename[] = L"desktop.ini";
constexpr WCHAR parent_folder_name_variable[] = L"$PARENT_FOLDER_NAME";
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include "helpers_variables.h"
namespace newplus::helpers::filesystem
{
namespace constants::non_localizable
{
constexpr WCHAR desktop_ini_filename[] = L"desktop.ini";
}
inline bool is_hidden(const std::filesystem::path path)
{
const std::filesystem::path::string_type name = path.filename();
if (name == constants::non_localizable::desktop_ini_filename)
{
return true;
}
return false;
}
inline bool is_directory(const std::filesystem::path path)
{
const auto entry = std::filesystem::directory_entry(path);
return entry.is_directory();
}
inline std::wstring make_valid_filename(const std::wstring& string, const wchar_t replace_with = L' ')
{
// replace all non-filename-valid chars with replace_with wchar
std::wstring valid_filename = string;
std::replace_if(valid_filename.begin(), valid_filename.end(), [](wchar_t c) { return c == L'/' || c == L'\\' || c == L':' || c == L'*' || c == L'?' || c == L'"' || c == L'<' || c == L'>' || c == L'|'; }, replace_with);
return valid_filename;
}
inline std::wstring make_unique_path_name(const std::wstring& initial_path)
{
std::filesystem::path folder_path(initial_path);
std::filesystem::path path_based_on(initial_path);
int counter = 1;
while (std::filesystem::exists(folder_path))
{
std::wstring new_filename = path_based_on.stem().wstring() + L" (" + std::to_wstring(counter) + L")";
if (path_based_on.has_extension())
{
new_filename += path_based_on.extension().wstring();
}
folder_path = path_based_on.parent_path() / new_filename;
counter++;
}
return folder_path.wstring();
}
}

View File

@@ -0,0 +1,169 @@
#pragma once
#include <regex>
#include "..\..\powerrename\lib\Helpers.h"
#include "helpers_filesystem.h"
#pragma comment(lib, "Pathcch.lib")
namespace newplus::helpers::variables
{
inline std::wstring resolve_an_environment_variable(const std::wstring& string)
{
std::wstring return_string = string;
wchar_t* env_variable = nullptr;
_wdupenv_s(&env_variable, nullptr, return_string.c_str());
if (env_variable != nullptr)
{
return_string = env_variable;
free(env_variable);
}
return return_string;
}
inline std::wstring resolve_date_time_variables(const std::wstring& string)
{
SYSTEMTIME local_now = { 0 };
GetLocalTime(&local_now);
wchar_t resolved_filename[MAX_PATH] = { 0 };
GetDatedFileName(resolved_filename, ARRAYSIZE(resolved_filename), string.c_str(), local_now);
return resolved_filename;
}
inline std::wstring replace_all_occurrences(const std::wstring& string, const std::wstring& search_for, const std::wstring& replacement)
{
std::wstring return_string = string;
size_t pos = 0;
while ((pos = return_string.find(search_for, pos)) != std::wstring::npos)
{
return_string.replace(pos, search_for.length(), replacement);
pos += replacement.length();
}
return return_string;
}
inline std::wstring resolve_environment_variables(const std::wstring& string)
{
// Do case-insensitive string replacement of environment variables being consistent with normal %eNV_VaR% behavior
std::wstring return_string = string;
const std::wregex reg_expression(L"%([^%]+)%");
std::wsmatch match;
size_t start = 0;
while (std::regex_search(return_string.cbegin() + start, return_string.cend(), match, reg_expression))
{
std::wstring env_var_name = match[1].str();
std::wstring env_var_value = resolve_an_environment_variable(env_var_name);
if (!env_var_value.empty())
{
size_t match_position = match.position(0) + start;
return_string.replace(match_position, match.length(0), env_var_value);
start = match_position + env_var_value.length();
}
else
{
start += match.position(0) + match.length(0);
}
}
return return_string;
}
inline std::wstring resolve_parent_folder(const std::wstring& string, const std::wstring& parent_folder_name)
{
// Do case-sensitive string replacement, for consistency on variables designated with $
std::wstring result = replace_all_occurrences(string, constants::non_localizable::parent_folder_name_variable, parent_folder_name);
return result;
}
inline std::filesystem::path resolve_variables_in_filename(const std::wstring& filename, const std::wstring& parent_folder_name)
{
std::wstring result;
result = resolve_date_time_variables(filename);
result = resolve_environment_variables(result);
if (!parent_folder_name.empty())
{
result = resolve_parent_folder(result, parent_folder_name);
}
result = newplus::helpers::filesystem::make_valid_filename(result);
return result;
}
inline std::filesystem::path resolve_variables_in_path(const std::filesystem::path& path)
{
// Need to resolve the whole path top-down (root to leaf), because of the support for $PARENT_FOLDER_NAME
std::filesystem::path result;
std::wstring previous_section;
std::wstring current_section;
auto path_section = path.begin();
int level = 0;
while (path_section != path.end())
{
previous_section = current_section;
current_section = path_section->wstring();
if (level <= 1)
{
// Up to and including L"x:\\"
result /= current_section;
}
else
{
// Past L"x:\\", e.g. L"x:\\level1" and beyond
result /= resolve_variables_in_filename(current_section, previous_section);
}
path_section++;
level++;
}
return result;
}
inline void resolve_variables_in_filename_and_rename_files(const std::filesystem::path& path, const bool do_rename = true)
{
// Depth first recursion, so that we start renaming the leaves, and avoid having to rescan
for (const auto& entry : std::filesystem::directory_iterator(path))
{
if (std::filesystem::is_directory(entry.status()))
{
resolve_variables_in_filename_and_rename_files(entry.path(), do_rename);
}
}
// Perform the actual rename
for (const auto& current : std::filesystem::directory_iterator(path))
{
if (!newplus::helpers::filesystem::is_hidden(current))
{
const std::filesystem::path resolved_path = resolve_variables_in_path(current.path());
// Only rename if the filename is actually different
const std::wstring non_resolved_leaf = current.path().filename();
const std::wstring resolved_leaf = resolved_path.filename();
if (StrCmpIW(non_resolved_leaf.c_str(), resolved_leaf.c_str()) != 0)
{
const std::wstring org_name = current.path();
const std::wstring new_name = current.path().parent_path() / resolved_leaf;
const std::wstring really_new_name = helpers::filesystem::make_unique_path_name(new_name);
// To aid with testing, only conditionally rename
if (do_rename)
{
std::filesystem::rename(org_name, really_new_name);
}
}
}
}
}
}

View File

@@ -9,6 +9,7 @@
#include "settings.h"
#include "template_item.h"
#include "trace.h"
#include "helpers_variables.h"
#pragma comment(lib, "Shlwapi.lib")
@@ -72,23 +73,6 @@ namespace newplus::utilities
return hIcon;
}
inline bool is_hidden(const std::filesystem::path path)
{
const std::filesystem::path::string_type name = path.filename();
if (name == constants::non_localizable::desktop_ini_filename)
{
return true;
}
return false;
}
inline bool is_directory(const std::filesystem::path path)
{
const auto entry = std::filesystem::directory_entry(path);
return entry.is_directory();
}
inline bool wstring_same_when_comparing_ignore_case(std::wstring stringA, std::wstring stringB)
{
transform(stringA.begin(), stringA.end(), stringA.begin(), towupper);
@@ -97,20 +81,6 @@ namespace newplus::utilities
return (stringA == stringB);
}
inline void process_pending_window_messages(HWND window_handle = NULL)
{
if (window_handle == NULL)
{
window_handle = GetActiveWindow();
}
MSG current_message;
while (PeekMessage(&current_message, window_handle, NULL, NULL, PM_REMOVE))
{
DispatchMessage(&current_message);
}
}
inline std::wstring get_new_template_folder_location()
{
return NewSettingsInstance().GetTemplateLocation();
@@ -126,6 +96,11 @@ namespace newplus::utilities
return NewSettingsInstance().GetHideStartingDigits();
}
inline bool get_newplus_setting_resolve_variables()
{
return NewSettingsInstance().GetReplaceVariables();
}
inline void create_folder_if_not_exist(const std::filesystem::path path)
{
std::filesystem::create_directory(path);
@@ -259,6 +234,7 @@ namespace newplus::utilities
{
ComPtr<IWebBrowserApp> web_browser_app;
VARIANT v;
VariantInit(&v);
V_VT(&v) = VT_I4;
V_I4(&v) = i;
hr = shell_windows->Item(v, &shell_window);
@@ -382,15 +358,30 @@ namespace newplus::utilities
std::filesystem::path source_fullpath = template_entry->path;
std::filesystem::path target_fullpath = std::wstring(target_path_name);
// Only append name to target if source is not a directory
if (!utilities::is_directory(source_fullpath))
// Get target name without starting digits as appropriate
const std::wstring target_name = template_entry->get_target_filename(!utilities::get_newplus_setting_hide_starting_digits());
// Get initial resolved name
target_fullpath /= target_name;
// Expand variables in name of the target path
if (utilities::get_newplus_setting_resolve_variables())
{
target_fullpath.append(template_entry->get_target_filename(!utilities::get_newplus_setting_hide_starting_digits()));
target_fullpath = helpers::variables::resolve_variables_in_path(target_fullpath);
}
// Copy file and determine final filename
// See if our target already exist, and if so then generate a unique name
target_fullpath = helpers::filesystem::make_unique_path_name(target_fullpath);
// Finally copy file/folder/subfolders
std::filesystem::path target_final_fullpath = template_entry->copy_object_to(GetActiveWindow(), target_fullpath);
// Resolve variables and rename files in newly copied folders and subfolders and files
if (utilities::get_newplus_setting_resolve_variables() && helpers::filesystem::is_directory(target_final_fullpath))
{
helpers::variables::resolve_variables_in_filename_and_rename_files(target_final_fullpath);
}
// Touch all files and set last modified to "now"
update_last_write_time(target_final_fullpath);

View File

@@ -43,6 +43,7 @@ void NewSettings::Save()
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_file_extension, new_settings.hide_file_extension);
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_starting_digits, new_settings.hide_starting_digits);
values.add_property(newplus::constants::non_localizable::settings_json_key_replace_variables, new_settings.replace_variables);
values.add_property(newplus::constants::non_localizable::settings_json_key_template_location, new_settings.template_location);
values.save_to_settings_file();
@@ -70,6 +71,9 @@ void NewSettings::InitializeWithDefaultSettings()
// Currently a similar defaulting logic is also in InitializeWithDefaultSettings in NewViewModel.cs
SetHideFileExtension(true);
// By default Replace Variables is turned off
SetReplaceVariables(false);
SetTemplateLocation(GetTemplateLocationDefaultPath());
}
@@ -139,6 +143,12 @@ void NewSettings::ParseJson()
new_settings.hide_starting_digits = hideStartingDigitsValue.value();
}
auto resolveVariables = settings.get_bool_value(newplus::constants::non_localizable::settings_json_key_replace_variables);
if (resolveVariables.has_value())
{
new_settings.replace_variables = resolveVariables.value();
}
GetSystemTimeAsFileTime(&new_settings_last_loaded_timestamp);
}
@@ -163,11 +173,8 @@ bool NewSettings::GetEnabled()
bool NewSettings::GetHideFileExtension() const
{
auto gpoSetting = powertoys_gpo::getConfiguredNewPlusHideTemplateFilenameExtensionValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_enabled)
{
return true;
}
const auto gpoSetting = powertoys_gpo::getConfiguredNewPlusHideTemplateFilenameExtensionValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled)
{
return false;
@@ -191,6 +198,23 @@ void NewSettings::SetHideStartingDigits(const bool hide_starting_digits)
new_settings.hide_starting_digits = hide_starting_digits;
}
bool NewSettings::GetReplaceVariables() const
{
const auto gpoSetting = powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled)
{
return false;
}
return new_settings.replace_variables;
}
void NewSettings::SetReplaceVariables(const bool replace_variables)
{
new_settings.replace_variables = replace_variables;
}
std::wstring NewSettings::GetTemplateLocation() const
{
return new_settings.template_location;
@@ -201,7 +225,7 @@ void NewSettings::SetTemplateLocation(const std::wstring template_location)
new_settings.template_location = template_location;
}
std::wstring NewSettings::GetTemplateLocationDefaultPath()
std::wstring NewSettings::GetTemplateLocationDefaultPath() const
{
static const std::wstring default_template_sub_folder_name =
GET_RESOURCE_STRING_FALLBACK(

View File

@@ -12,6 +12,8 @@ public:
void SetHideFileExtension(const bool hide_file_extension);
bool GetHideStartingDigits() const;
void SetHideStartingDigits(const bool hide_starting_digits);
bool GetReplaceVariables() const;
void SetReplaceVariables(const bool resolve_variables);
std::wstring GetTemplateLocation() const;
void SetTemplateLocation(const std::wstring template_location);
@@ -25,12 +27,13 @@ private:
bool enabled{ false };
bool hide_file_extension{ true };
bool hide_starting_digits{ true };
bool replace_variables{ true };
std::wstring template_location;
};
void RefreshEnabledState();
void InitializeWithDefaultSettings();
std::wstring GetTemplateLocationDefaultPath();
std::wstring GetTemplateLocationDefaultPath() const;
void Reload();
void ParseJson();

View File

@@ -69,8 +69,21 @@ IFACEMETHODIMP shell_context_menu::GetFlags(_Out_ EXPCMDFLAGS* returned_menu_ite
IFACEMETHODIMP shell_context_menu::EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** returned_enum_commands)
{
auto e = Make<shell_context_sub_menu>(site_of_folder);
return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands));
try
{
auto e = Make<shell_context_sub_menu>(site_of_folder);
return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands));
}
catch (const std::exception& ex)
{
Logger::error("New+ create submenu error: {}", ex.what());
return E_FAIL;
}
catch (...)
{
Logger::error("New+ create submenu error");
return E_FAIL;
}
}
#pragma endregion
@@ -80,8 +93,8 @@ IFACEMETHODIMP shell_context_menu::SetSite(_In_ IUnknown* site) noexcept
this->site_of_folder = site;
return S_OK;
}
IFACEMETHODIMP shell_context_menu::GetSite(_In_ REFIID riid, _COM_Outptr_ void** returned_site) noexcept
IFACEMETHODIMP shell_context_menu::GetSite(_In_ REFIID interface_type, _COM_Outptr_ void** returned_site) noexcept
{
return this->site_of_folder.CopyTo(riid, returned_site);
return this->site_of_folder.CopyTo(interface_type, returned_site);
}
#pragma endregion

View File

@@ -27,7 +27,7 @@ public:
#pragma region IObjectWithSite
IFACEMETHODIMP SetSite(_In_ IUnknown* site) noexcept;
IFACEMETHODIMP GetSite(_In_ REFIID riid, _COM_Outptr_ void** site) noexcept;
IFACEMETHODIMP GetSite(_In_ REFIID interface_type, _COM_Outptr_ void** site) noexcept;
#pragma endregion
protected:

View File

@@ -22,7 +22,8 @@ IFACEMETHODIMP shell_context_sub_menu_item::GetTitle(_In_opt_ IShellItemArray* i
{
return SHStrDup(this->template_entry->get_menu_title(
!utilities::get_newplus_setting_hide_extension(),
!utilities::get_newplus_setting_hide_starting_digits()
!utilities::get_newplus_setting_hide_starting_digits(),
utilities::get_newplus_setting_resolve_variables()
).c_str(), title);
}
@@ -95,6 +96,7 @@ IFACEMETHODIMP separator_context_menu_item::GetIcon(_In_opt_ IShellItemArray*, _
IFACEMETHODIMP separator_context_menu_item::GetFlags(_Out_ EXPCMDFLAGS* returned_flags)
{
// Separators no longer work on Windows 11 regular context menu. They do still work on the extended context menu.
*returned_flags = ECF_ISSEPARATOR;
return S_OK;
}

View File

@@ -35,7 +35,7 @@ void template_folder::rescan_template_folder()
}
else
{
if (!utilities::is_hidden(entry.path()))
if (!helpers::filesystem::is_hidden(entry.path()))
{
files.push_back({ entry.path().wstring(), new template_item(entry) });
}

View File

@@ -6,7 +6,6 @@
#include "new_utilities.h"
#include <cassert>
#include <thread>
#include <shellapi.h>
#include <shlobj_core.h>
using namespace Microsoft::WRL;
@@ -17,7 +16,7 @@ template_item::template_item(const std::filesystem::path entry)
path = entry;
}
std::wstring template_item::get_menu_title(const bool show_extension, const bool show_starting_digits) const
std::wstring template_item::get_menu_title(const bool show_extension, const bool show_starting_digits, const bool show_resolved_variables) const
{
std::wstring title = path.filename();
@@ -27,13 +26,21 @@ std::wstring template_item::get_menu_title(const bool show_extension, const bool
title = remove_starting_digits_from_filename(title);
}
if (show_resolved_variables)
{
title = helpers::variables::resolve_variables_in_filename(title, constants::non_localizable::parent_folder_name_variable);
}
if (show_extension || !path.has_extension())
{
return title;
}
std::wstring ext = path.extension();
title = title.substr(0, title.length() - ext.length());
if (!helpers::filesystem::is_directory(path))
{
std::wstring ext = path.extension();
title = title.substr(0, title.length() - ext.length());
}
return title;
}
@@ -53,7 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
{
filename.erase(0, min(filename.find_first_not_of(L"0123456789 ."), filename.size()));
filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size()));
filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size()));
return filename;
}
@@ -70,7 +78,7 @@ HICON template_item::get_explorer_icon_handle() const
std::filesystem::path template_item::copy_object_to(const HWND window_handle, const std::filesystem::path destination) const
{
// SHFILEOPSTRUCT wants the from and to paths to be terminated with two NULLs,
// SHFILEOPSTRUCT wants the from and to paths to be terminated with two NULLs.
wchar_t double_terminated_path_from[MAX_PATH + 1] = { 0 };
wcsncpy_s(double_terminated_path_from, this->path.c_str(), this->path.wstring().length());
double_terminated_path_from[this->path.wstring().length() + 1] = 0;
@@ -84,37 +92,16 @@ std::filesystem::path template_item::copy_object_to(const HWND window_handle, co
file_operation_params.hwnd = window_handle;
file_operation_params.pFrom = double_terminated_path_from;
file_operation_params.pTo = double_terminated_path_to;
file_operation_params.fFlags = FOF_RENAMEONCOLLISION | FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS | FOF_WANTMAPPINGHANDLE;
file_operation_params.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;
const int result = SHFileOperation(&file_operation_params);
if (!file_operation_params.hNameMappings)
if (result != 0)
{
// No file name collision on copy
if (utilities::is_directory(this->path))
{
// Append dir for consistency on directory naming inclusion for with and without collision
std::filesystem::path with_dir = destination;
with_dir /= this->path.filename();
return with_dir;
}
return destination;
throw std::runtime_error("Failed to copy template");
}
struct file_operation_collision_mapping
{
int index;
SHNAMEMAPPING* mapping;
};
file_operation_collision_mapping* mapping = static_cast<file_operation_collision_mapping*>(file_operation_params.hNameMappings);
SHNAMEMAPPING* map = &mapping->mapping[0];
std::wstring final_path(map->pszNewPath);
SHFreeNameMappings(file_operation_params.hNameMappings);
return final_path;
return destination;
}
void template_item::refresh_target(const std::filesystem::path target_final_fullpath) const

View File

@@ -15,7 +15,7 @@ namespace newplus
public:
template_item(const std::filesystem::path entry);
std::wstring get_menu_title(const bool show_extension, const bool show_starting_digits) const;
std::wstring get_menu_title(const bool show_extension, const bool show_starting_digits, const bool show_resolved_variables) const;
std::wstring get_target_filename(const bool include_starting_digits) const;

View File

@@ -18,6 +18,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
HideFileExtension = new BoolProperty(true);
HideStartingDigits = new BoolProperty(true);
TemplateLocation = new StringProperty(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "NewPlus", "Templates"));
ReplaceVariables = new BoolProperty(false);
}
[JsonPropertyName("HideFileExtension")]
@@ -29,6 +30,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("TemplateLocation")]
public StringProperty TemplateLocation { get; set; }
[JsonPropertyName("ReplaceVariables")]
public BoolProperty ReplaceVariables { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
}
}

View File

@@ -1,9 +1,10 @@
<Page
<Page
x:Class="Microsoft.PowerToys.Settings.UI.Views.NewPlusPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Microsoft.PowerToys.Settings.UI.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
@@ -84,6 +85,116 @@
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="NewPlus_behavior" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="NewPlus_Behaviour_Replace_Variables_Toggle" IsEnabled="{x:Bind ViewModel.IsReplaceVariablesSettingsCardEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="4">
<ToggleSwitch x:Uid="ReplaceVariablesToggle" IsOn="{x:Bind ViewModel.ReplaceVariables, Mode=TwoWay}" />
<Button
x:Uid="FileCreationButton"
Width="28"
Height="40"
Margin="0,0,-4,0"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="Transparent"
BorderBrush="Transparent"
Content="&#xE946;"
FontFamily="{ThemeResource SymbolThemeFontFamily}">
<Button.Flyout>
<Flyout x:Name="VariableExamplesFlyout" ShouldConstrainToRootBounds="False">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="62" />
<ColumnDefinition Width="300" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Text="$YYYY" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="0,0,0,5"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Year_YYYY_Variable_Description" /></TextBlock>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Text="$MM" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="0,0,0,5"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Month_MM_Variable_Description" /></TextBlock>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Text="$DD" />
<TextBlock
Grid.Row="2"
Grid.Column="1"
Margin="0,0,0,5"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Day_DD_Variable_Description" /></TextBlock>
<TextBlock
Grid.Row="3"
Grid.Column="0"
Text="$hh" />
<TextBlock
Grid.Row="3"
Grid.Column="1"
Margin="0,0,0,5"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Hour_hh_Variable_Description" /></TextBlock>
<TextBlock
Grid.Row="4"
Grid.Column="0"
Text="$mm" />
<TextBlock
Grid.Row="4"
Grid.Column="1"
Margin="0,0,0,5"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Minute_mm_Variable_Description" /></TextBlock>
<TextBlock
Grid.Row="5"
Grid.Column="0"
Text="$ss" />
<TextBlock
Grid.Row="5"
Grid.Column="1"
Margin="0,0,0,0"
TextWrapping="Wrap"><Run x:Uid="NewPlus_Second_ss_Variable_Description" /></TextBlock>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
<tkcontrols:SettingsCard.Description>
<StackPanel>
<HyperlinkButton x:Uid="NewPlus_Behaviour_Replace_Variables_Learn_More" NavigateUri="https://aka.ms/PowerToysOverview_NewPlus" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>
<InfoBar
x:Uid="GPO_SettingIsManaged"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsReplaceVariablesSettingGPOConfigured, Mode=OneWay}"
IsTabStop="{x:Bind ViewModel.IsReplaceVariablesSettingGPOConfigured, Mode=OneWay}"
Severity="Informational" />
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>

View File

@@ -2,9 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using System.Windows;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;

View File

@@ -4445,6 +4445,46 @@ Activate by holding the key for the character you want to add an accent to, then
<value>This option is useful when using digits, spaces and dots at the beginning of filenames to control the display order of templates</value>
<comment>Template filename starting digits settings toggle</comment>
</data>
<data name="NewPlus_behavior.Header" xml:space="preserve">
<value>Behavior</value>
<comment>New+ behavior related settings label</comment>
</data>
<data name="NewPlus_Behaviour_Replace_Variables_Toggle.Header" xml:space="preserve">
<value>Replace variables in template filename</value>
<comment>New+ replace variables in template filename behavior toggle</comment>
</data>
<data name="NewPlus_Behaviour_Replace_Variables_Learn_More.Content" xml:space="preserve">
<value>Learn more about supported variables and see examples</value>
<comment>New+ help link to learn more about supported variables and see examples</comment>
</data>
<data name="NewPlus_Behaviour_Replace_Variables_Info_Card_Title.Text" xml:space="preserve">
<value>Commonly used variables</value>
<comment>New+ commonly used variables header in the flyout info card</comment>
</data>
<data name="NewPlus_Year_YYYY_Variable_Description.Text" xml:space="preserve">
<value>Year, represented by a full four or five digits, depending on the calendar used.</value>
<comment>New+ description of the year $YYYY variable - casing of $YYYY is important</comment>
</data>
<data name="NewPlus_Month_MM_Variable_Description.Text" xml:space="preserve">
<value>Month, as digits with leading zeros for single-digit months.</value>
<comment>New+ description of the month $MM variable - casing of $MM is important</comment>
</data>
<data name="NewPlus_Day_DD_Variable_Description.Text" xml:space="preserve">
<value>Day of the month, as digits with leading zeros for single-digit days.</value>
<comment>New+ description of the day $DD variable - casing of $DD is important</comment>
</data>
<data name="NewPlus_Hour_hh_Variable_Description.Text" xml:space="preserve">
<value>Hours, with leading zeros for single-digit hours.</value>
<comment>New+ description of the hour $hh variable - casing of $hh is important</comment>
</data>
<data name="NewPlus_Minute_mm_Variable_Description.Text" xml:space="preserve">
<value>Minutes, with leading zeros for single-digit minutes.</value>
<comment>New+ description of the minute $mm variable - casing of $mm is important</comment>
</data>
<data name="NewPlus_Second_ss_Variable_Description.Text" xml:space="preserve">
<value>Seconds, with leading zeros for single-digit seconds.</value>
<comment>New+ description of the second $ss variable - casing of $ss is important</comment>
</data>
<data name="NewPlus.SecondaryLinksHeader" xml:space="preserve">
<value>Attribution</value>
<comment>giving credit</comment>

View File

@@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using global::PowerToys.GPOWrapper;
@@ -18,8 +17,6 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Windows.ApplicationModel.VoiceCommands;
using Windows.System;
using static Microsoft.PowerToys.Settings.UI.Helpers.ShellGetFolder;
@@ -50,6 +47,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_hideFileExtension = Settings.Properties.HideFileExtension.Value;
_hideStartingDigits = Settings.Properties.HideStartingDigits.Value;
_templateLocation = Settings.Properties.TemplateLocation.Value;
_replaceVariables = Settings.Properties.ReplaceVariables.Value;
InitializeEnabledValue();
InitializeGpoValues();
@@ -77,6 +75,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Policy for hide file extension setting
_hideFileExtensionGpoRuleConfiguration = GPOWrapper.GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
_hideFileExtensionIsGPOConfigured = _hideFileExtensionGpoRuleConfiguration == GpoRuleConfigured.Disabled || _hideFileExtensionGpoRuleConfiguration == GpoRuleConfigured.Enabled;
// Same for Replace Variables
_replaceVariablesIsGPOConfigured = GPOWrapper.GetConfiguredNewPlusReplaceVariablesValue() == GpoRuleConfigured.Enabled
|| GPOWrapper.GetConfiguredNewPlusReplaceVariablesValue() == GpoRuleConfigured.Disabled;
}
public bool IsEnabled
@@ -92,6 +94,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(IsHideFileExtSettingsCardEnabled));
OnPropertyChanged(nameof(IsHideFileExtSettingGPOConfigured));
OnPropertyChanged(nameof(IsReplaceVariablesSettingGPOConfigured));
OnPropertyChanged(nameof(IsReplaceVariablesSettingsCardEnabled));
OutGoingGeneralSettings outgoingMessage = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoingMessage.ToString());
@@ -156,6 +160,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsHideFileExtSettingGPOConfigured => _isNewPlusEnabled && _hideFileExtensionIsGPOConfigured;
public bool IsReplaceVariablesSettingsCardEnabled => _isNewPlusEnabled && !_replaceVariablesIsGPOConfigured;
public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured;
public bool HideStartingDigits
{
get => _hideStartingDigits;
@@ -172,6 +180,32 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool ReplaceVariables
{
get
{
// Check to see if setting has been enabled or disabled via GPO, and if so, use that value
if (IsReplaceVariablesSettingGPOConfigured)
{
return GPOWrapper.GetConfiguredNewPlusReplaceVariablesValue() == GpoRuleConfigured.Enabled;
}
return _replaceVariables;
}
set
{
if (_replaceVariables != value)
{
_replaceVariables = value;
Settings.Properties.ReplaceVariables.Value = value;
OnPropertyChanged(nameof(ReplaceVariables));
NotifySettingsChanged();
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
@@ -236,11 +270,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private string _templateLocation;
private bool _hideFileExtension;
private bool _hideStartingDigits;
private bool _replaceVariables;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private GpoRuleConfigured _hideFileExtensionGpoRuleConfiguration;
private bool _hideFileExtensionIsGPOConfigured;
private bool _replaceVariablesIsGPOConfigured;
public void RefreshEnabledState()
{