From bd175a207ff17d90175fd3e823235f3997bf2581 Mon Sep 17 00:00:00 2001 From: Christian Gaarden Gaardmark Date: Sat, 21 Dec 2024 17:38:30 -0800 Subject: [PATCH 1/3] Add variable support - initial version without UI --- src/common/GPOWrapper/GPOWrapper.cpp | 4 + src/common/GPOWrapper/GPOWrapper.h | 1 + src/common/GPOWrapper/GPOWrapper.idl | 1 + src/common/utils/gpo.h | 7 + .../NewPlus.ShellExtension.win10.vcxproj | 3 + ...wPlus.ShellExtension.win10.vcxproj.filters | 9 + .../shell_context_menu_win10.cpp | 7 +- .../NewShellExtensionContextMenu.vcxproj | 3 + ...wShellExtensionContextMenu.vcxproj.filters | 9 + .../NewShellExtensionContextMenu/constants.h | 4 +- .../helpers_filesystem.h | 53 ++++++ .../helpers_variables.h | 169 ++++++++++++++++++ .../new_utilities.h | 55 +++--- .../NewShellExtensionContextMenu/settings.cpp | 30 ++++ .../NewShellExtensionContextMenu/settings.h | 3 + .../shell_context_sub_menu_item.cpp | 4 +- .../template_folder.cpp | 2 +- .../template_item.cpp | 49 ++--- .../template_item.h | 2 +- .../Settings.UI.Library/NewPlusProperties.cs | 4 + .../SettingsXAML/Views/NewPlusPage.xaml | 69 ++++++- .../SettingsXAML/Views/NewPlusPage.xaml.cs | 3 - .../Settings.UI/Strings/en-us/Resources.resw | 36 ++++ .../ViewModels/NewPlusViewModel.cs | 43 ++++- 24 files changed, 485 insertions(+), 85 deletions(-) create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index a9ecb43818e6..694b600f369d 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -228,4 +228,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getAllowDataDiagnosticsValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusReplaceVariablesValue() + { + return static_cast(powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue()); + } } diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index 34c1e3646be2..b465c3f5bfc1 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -62,6 +62,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static winrt::hstring GPOWrapper::GetConfiguredMwbPolicyDefinedIpMappingRules(); static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); }; } diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index af58834a0c13..c042fedfaf33 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -66,6 +66,7 @@ namespace PowerToys static String GetConfiguredMwbPolicyDefinedIpMappingRules(); static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); } } } diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 04e03b476764..845c6731d9a7 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -84,6 +84,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"NewPlusReplaceVariables"; // Methods used for reading the registry #pragma region ReadRegistryMethods @@ -597,5 +598,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/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 c54631df0925..f8eace913326 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() +//cgaarden || package::IsWin11OrGreater() ) { return E_FAIL; @@ -184,7 +184,10 @@ 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()); + 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; newplus_menu_item_template.cbSize = sizeof(MENUITEMINFO); newplus_menu_item_template.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_DATA; 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..f918fdc9442f --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h @@ -0,0 +1,53 @@ +#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); + + int counter = 1; + + while (std::filesystem::exists(folder_path)) + { + folder_path = initial_path + L" (" + std::to_wstring(counter) + L")"; + 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..cffa4098fd78 --- /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 now = { 0 }; + GetSystemTime(&now); + wchar_t resolved_filename[MAX_PATH] = { 0 }; + GetDatedFileName(resolved_filename, ARRAYSIZE(resolved_filename), string.c_str(), 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 66ac6deccc85..c32a39e0b5b5 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); @@ -367,13 +343,22 @@ 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 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); // Consider copy completed. If we do tracing after enter_rename_mode, then rename mode won't consistently work diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp index 7dd5042c4da2..5a025e1c4ada 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); } @@ -191,6 +200,27 @@ 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_enabled) + { + return true; + } + 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; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h index 545ba7b2cca8..5490c8f53185 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,6 +27,7 @@ class NewSettings bool enabled{ false }; bool hide_file_extension{ true }; bool hide_starting_digits{ true }; + bool replace_variables{ true }; std::wstring template_location; }; 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 7f4e427d6531..146484e7cfb7 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -4,6 +4,7 @@ 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" @@ -70,9 +71,6 @@ IsOpen="{x:Bind ViewModel.IsHideFileExtSettingGPOConfigured, Mode=OneWay}" IsTabStop="{x:Bind ViewModel.IsHideFileExtSettingGPOConfigured, Mode=OneWay}" Severity="Informational"> - @@ -81,6 +79,71 @@ + + + + + + + + + + + + + + + + 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 7f3b5fdabab7..da0d94f9daa7 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -4555,6 +4555,42 @@ 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 name behavior toggle + + + Learn more about supported variables + New+ learn more about supported variables link + + + Commonly used variables + New+ commonly used variables header in the flyout info card + + + COPY + New+ description of the $DATE variable + + + COPY + New+ description of the $TIME variable + + + COPY + New+ description of the $YYYY variable + + + COPY + New+ description of the $MM variable + + + COPY + New+ description of the $DD variable + Attribution giving credit diff --git a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs index 11e3d3ee4620..b9e6d3acac15 100644 --- a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs @@ -7,22 +7,15 @@ using System.Globalization; using System.IO; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; -using System.Windows; - using Common.UI; using global::PowerToys.GPOWrapper; using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; -using Windows.ApplicationModel.VoiceCommands; -using Windows.System; - using static Microsoft.PowerToys.Settings.UI.Helpers.ShellGetFolder; namespace Microsoft.PowerToys.Settings.UI.ViewModels @@ -52,6 +45,7 @@ public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository _isNewPlusEnabled && _hideFileExtensionIsGPOConfigured; + public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured; + public bool HideStartingDigits { get => _hideStartingDigits; @@ -174,6 +175,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; @@ -238,11 +265,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() { From 7e36a2375a7a9d05c94960d37d9d4ac1ee503131 Mon Sep 17 00:00:00 2001 From: Christian Gaarden Gaardmark Date: Sat, 21 Dec 2024 17:38:30 -0800 Subject: [PATCH 2/3] Add variable in template filename support in New+ --- src/common/GPOWrapper/GPOWrapper.cpp | 4 + src/common/GPOWrapper/GPOWrapper.h | 1 + src/common/GPOWrapper/GPOWrapper.idl | 1 + src/common/utils/gpo.h | 7 + src/gpo/assets/PowerToys.admx | 11 ++ src/gpo/assets/en-US/PowerToys.adml | 10 ++ .../NewPlus.ShellExtension.win10.vcxproj | 3 + ...wPlus.ShellExtension.win10.vcxproj.filters | 9 + .../shell_context_menu_win10.cpp | 7 +- .../NewShellExtensionContextMenu.vcxproj | 3 + ...wShellExtensionContextMenu.vcxproj.filters | 9 + .../NewShellExtensionContextMenu/constants.h | 4 +- .../helpers_filesystem.h | 53 ++++++ .../helpers_variables.h | 169 ++++++++++++++++++ .../new_utilities.h | 55 +++--- .../NewShellExtensionContextMenu/settings.cpp | 30 ++++ .../NewShellExtensionContextMenu/settings.h | 3 + .../shell_context_sub_menu_item.cpp | 4 +- .../template_folder.cpp | 2 +- .../template_item.cpp | 49 ++--- .../template_item.h | 2 +- .../Settings.UI.Library/NewPlusProperties.cs | 4 + .../SettingsXAML/Views/NewPlusPage.xaml | 73 +++++++- .../SettingsXAML/Views/NewPlusPage.xaml.cs | 3 - .../Settings.UI/Strings/en-us/Resources.resw | 40 +++++ .../ViewModels/NewPlusViewModel.cs | 46 ++++- 26 files changed, 517 insertions(+), 85 deletions(-) create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h create mode 100644 src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index a9ecb43818e6..694b600f369d 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -228,4 +228,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getAllowDataDiagnosticsValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusReplaceVariablesValue() + { + return static_cast(powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue()); + } } diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index 34c1e3646be2..b465c3f5bfc1 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -62,6 +62,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static winrt::hstring GPOWrapper::GetConfiguredMwbPolicyDefinedIpMappingRules(); static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); }; } diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index af58834a0c13..c042fedfaf33 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -66,6 +66,7 @@ namespace PowerToys static String GetConfiguredMwbPolicyDefinedIpMappingRules(); static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue(); static GpoRuleConfigured GetAllowDataDiagnosticsValue(); + static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue(); } } } diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 04e03b476764..a4cc3f9e2830 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -84,6 +84,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 @@ -597,5 +598,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 799b1f20f36d..09d3ad5ea3bd 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -23,6 +23,7 @@ + @@ -635,5 +636,15 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index 7a8f037eee5c..1ad861f201f6 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -29,6 +29,7 @@ PowerToys version 0.84.0 or later PowerToys version 0.85.0 or later PowerToys version 0.86.0 or later + PowerToys version 0.89.0 or later This policy configures the enabled state for all PowerToys utilities. @@ -205,6 +206,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 @@ -266,6 +275,7 @@ 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 + Replace variables in template filenames 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 c54631df0925..f8eace913326 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() +//cgaarden || package::IsWin11OrGreater() ) { return E_FAIL; @@ -184,7 +184,10 @@ 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()); + 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; newplus_menu_item_template.cbSize = sizeof(MENUITEMINFO); newplus_menu_item_template.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_DATA; 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..f918fdc9442f --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_filesystem.h @@ -0,0 +1,53 @@ +#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); + + int counter = 1; + + while (std::filesystem::exists(folder_path)) + { + folder_path = initial_path + L" (" + std::to_wstring(counter) + L")"; + 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..cffa4098fd78 --- /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 now = { 0 }; + GetSystemTime(&now); + wchar_t resolved_filename[MAX_PATH] = { 0 }; + GetDatedFileName(resolved_filename, ARRAYSIZE(resolved_filename), string.c_str(), 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 66ac6deccc85..c32a39e0b5b5 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); @@ -367,13 +343,22 @@ 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 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); // Consider copy completed. If we do tracing after enter_rename_mode, then rename mode won't consistently work diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp index 7dd5042c4da2..5a025e1c4ada 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); } @@ -191,6 +200,27 @@ 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_enabled) + { + return true; + } + 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; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h index 545ba7b2cca8..5490c8f53185 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,6 +27,7 @@ class NewSettings bool enabled{ false }; bool hide_file_extension{ true }; bool hide_starting_digits{ true }; + bool replace_variables{ true }; std::wstring template_location; }; 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 7f4e427d6531..322845ca0dbd 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -4,6 +4,7 @@ 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" @@ -70,9 +71,6 @@ IsOpen="{x:Bind ViewModel.IsHideFileExtSettingGPOConfigured, Mode=OneWay}" IsTabStop="{x:Bind ViewModel.IsHideFileExtSettingGPOConfigured, Mode=OneWay}" Severity="Informational"> - @@ -81,6 +79,75 @@ + + + + + + + + + + + + + + + + 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 7f3b5fdabab7..90fb608b1549 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -4555,6 +4555,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 + + + Discover variables and examples + New+ discover variables and examples link + + + 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 11e3d3ee4620..845640e39de3 100644 --- a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs @@ -7,22 +7,15 @@ using System.Globalization; using System.IO; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; -using System.Windows; - using Common.UI; using global::PowerToys.GPOWrapper; using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; -using Windows.ApplicationModel.VoiceCommands; -using Windows.System; - using static Microsoft.PowerToys.Settings.UI.Helpers.ShellGetFolder; namespace Microsoft.PowerToys.Settings.UI.ViewModels @@ -52,6 +45,7 @@ public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository _isNewPlusEnabled && _hideFileExtensionIsGPOConfigured; + public bool IsReplaceVariablesSettingsCardEnabled => _isNewPlusEnabled && !_replaceVariablesIsGPOConfigured; + + public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured; + public bool HideStartingDigits { get => _hideStartingDigits; @@ -174,6 +178,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; @@ -238,11 +268,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() { From 643bec6b2428c97c4ba34fb5e26d400495f9fad2 Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Tue, 25 Feb 2025 11:24:43 +0000 Subject: [PATCH 3/3] Fix XAML style --- .../SettingsXAML/Views/NewPlusPage.xaml | 91 ++++++++++++++----- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml index 396e86d60a46..45c1856982c6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -1,4 +1,4 @@ - - - - - - - + + + + + + - - + + - - - - - + + - - + + - - - - - + + - - + + + + + + + + @@ -151,8 +193,7 @@ IsClosable="False" IsOpen="{x:Bind ViewModel.IsReplaceVariablesSettingGPOConfigured, Mode=OneWay}" IsTabStop="{x:Bind ViewModel.IsReplaceVariablesSettingGPOConfigured, Mode=OneWay}" - Severity="Informational"> - + Severity="Informational" />