diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 25d60a7d10a3..691a66b43f71 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -232,6 +232,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getAllowDataDiagnosticsValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusReplaceVariablesValue() + { + return static_cast(powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredRunAtStartupValue() { return static_cast(powertoys_gpo::getConfiguredRunAtStartupValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index 4b0cb7af0001..f3eb07680be3 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -64,6 +64,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); static GpoRuleConfigured GetConfiguredRunAtStartupValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); }; } diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 247e2a090088..b7aa8e22aa0e 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -68,6 +68,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); static GpoRuleConfigured GetConfiguredRunAtStartupValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); } } } diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 93f20692082c..479fb0ef2370 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -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 } diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 82707dbd3e97..7702b99b9849 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -671,5 +671,15 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index bc8afc9d2b4b..145e39d4858c 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -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. + + 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. Configure global utility enabled state @@ -289,7 +297,6 @@ If you don't configure this policy, the user takes control over the setting and Predefined IP Address mapping rules Hide template filename extension Allow sending diagnostic data - Configure the run at startup setting diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj index 6d975b33262d..38d8f640f04e 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj @@ -83,6 +83,8 @@ + + @@ -97,6 +99,7 @@ + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters index 25399a81dc3d..a883117179eb 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj.filters @@ -57,6 +57,12 @@ Header Files + + Header Files + + + Header Files + @@ -92,6 +98,9 @@ Source Files + + Source Files + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp index 09e948d9b2f4..8801ec6c4853 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp @@ -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_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; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj index 68b74d6ae14a..9ac251c8ec54 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj @@ -114,6 +114,8 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + + @@ -128,6 +130,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters index de0cea201749..7d014eb00f7b 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters @@ -34,6 +34,9 @@ Source Files + + Source Files + @@ -75,6 +78,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h b/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h index 48c7054d3612..aa9fcee88ca8 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h @@ -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"; } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h new file mode 100644 index 000000000000..fac956194419 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h @@ -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(); + } +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h new file mode 100644 index 000000000000..63f23c3e8601 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h @@ -0,0 +1,169 @@ +#pragma once + +#include +#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); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h index 5df9dda1fea1..580555f8a65a 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h @@ -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(¤t_message, window_handle, NULL, NULL, PM_REMOVE)) - { - DispatchMessage(¤t_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 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); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp index 7dd5042c4da2..da870942c742 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp @@ -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,8 @@ void NewSettings::InitializeWithDefaultSettings() // Currently a similar defaulting logic is also in InitializeWithDefaultSettings in NewViewModel.cs SetHideFileExtension(true); + SetReplaceVariables(true); + SetTemplateLocation(GetTemplateLocationDefaultPath()); } @@ -139,6 +142,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,17 +172,14 @@ 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; } - return new_settings.hide_file_extension; + return true; } void NewSettings::SetHideFileExtension(const bool hide_file_extension) @@ -191,6 +197,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 true; +} + +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 +224,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( diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h index 545ba7b2cca8..10ebd96a4d44 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h @@ -12,6 +12,8 @@ class NewSettings 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 @@ class NewSettings 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(); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp index b3bf2992d5f1..887701b0aca1 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp @@ -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(site_of_folder); - return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands)); + try + { + auto e = Make(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 diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h index 6e74d9730de6..1b173a49578d 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h @@ -27,7 +27,7 @@ class __declspec(uuid(NEW_SHELL_EXTENSION_EXPLORER_COMMAND_UUID_STR)) shell_cont #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: diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp index 2b1b940d65e6..a4c4e092b479 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp @@ -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; } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp index e52a871e806b..7f50dc035216 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp @@ -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) }); } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp index 5de461beceee..a7ddfe835f42 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp @@ -6,7 +6,6 @@ #include "new_utilities.h" #include #include -#include #include 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_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 diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h index 7b8ba78aeefe..813d7aa1a726 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h @@ -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; diff --git a/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs b/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs index 2a4970c69057..7c92fe285b3e 100644 --- a/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs +++ b/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs @@ -18,6 +18,7 @@ public NewPlusProperties() 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(true); } [JsonPropertyName("HideFileExtension")] @@ -29,6 +30,9 @@ public NewPlusProperties() [JsonPropertyName("TemplateLocation")] public StringProperty TemplateLocation { get; set; } + [JsonPropertyName("ReplaceVariables")] + public BoolProperty ReplaceVariables { get; set; } + public override string ToString() => JsonSerializer.Serialize(this); } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml index 85bfd8fc5e7b..45c1856982c6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -1,9 +1,10 @@ - + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs index 78e2fa948fc5..2df68743c413 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs @@ -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; diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 7f62fdcaa996..300482f8a10c 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -4430,6 +4430,46 @@ Activate by holding the key for the character you want to add an accent to, then This option is useful when using digits, spaces and dots at the beginning of filenames to control the display order of templates Template filename starting digits settings toggle + + Behavior + New+ behavior related settings label + + + Replace variables in template filename + New+ replace variables in template filename behavior toggle + + + Learn more about supported variables and see examples + New+ help link to learn more about supported variables and see examples + + + Commonly used variables + New+ commonly used variables header in the flyout info card + + + Year, represented by a full four or five digits, depending on the calendar used. + New+ description of the year $YYYY variable - casing of $YYYY is important + + + Month, as digits with leading zeros for single-digit months. + New+ description of the month $MM variable - casing of $MM is important + + + Day of the month, as digits with leading zeros for single-digit days. + New+ description of the day $DD variable - casing of $DD is important + + + Hours, with leading zeros for single-digit hours. + New+ description of the hour $hh variable - casing of $hh is important + + + Minutes, with leading zeros for single-digit minutes. + New+ description of the minute $mm variable - casing of $mm is important + + + Seconds, with leading zeros for single-digit seconds. + New+ description of the second $ss variable - casing of $ss is important + Attribution giving credit diff --git a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs index 36c356ce57f5..0bd44b4fbfc2 100644 --- a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs @@ -7,7 +7,6 @@ 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.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 @@ public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository _isNewPlusEnabled && _hideFileExtensionIsGPOConfigured; + public bool IsReplaceVariablesSettingsCardEnabled => _isNewPlusEnabled && !_replaceVariablesIsGPOConfigured; + + public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured; + public bool HideStartingDigits { get => _hideStartingDigits; @@ -172,6 +180,32 @@ public bool HideStartingDigits } } + 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 @@ public static void CopyTemplateExamples(string templateLocation) 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() {