diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 77ffad8483f9..1fc595adcbb9 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -218,6 +218,11 @@ NOT Installed + + + Installed AND (REMOVE="ALL") + + + + diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 74304c416323..1bd23e65a766 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1260,6 +1260,65 @@ UINT __stdcall CleanNewPlusRuntimeRegistryCA(MSIHANDLE hInstall) return WcaFinalize(er); } +UINT __stdcall RestoreBuiltInNewContextMenuCA(MSIHANDLE hInstall) +{ + // Must be run as administrator to open and modify the registry. + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "RestoreBuiltInNewContextMenuCA"); + + try + { + const std::wstring builtInNewRegistryPath = LR"(Directory\Background\shellex\ContextMenuHandlers\New)"; + const std::wstring newDisabledValuePrefix = L"0_"; + + auto regDeleter = [](HKEY* regKeyHandle) { if (regKeyHandle && *regKeyHandle) RegCloseKey(*regKeyHandle); delete regKeyHandle; }; + std::unique_ptr regKeyHandle(new HKEY(nullptr), regDeleter); + + const LONG openStatus = RegOpenKeyExW(HKEY_CLASSES_ROOT, builtInNewRegistryPath.c_str(), 0, KEY_READ | KEY_WRITE, regKeyHandle.get()); + if (openStatus != ERROR_SUCCESS) + { + throw std::runtime_error("Failed to open New context menu registry key."); + } + + wchar_t buffer[256]; + DWORD bufferSize = sizeof(buffer); + const LONG queryStatus = RegQueryValueExW(*regKeyHandle, nullptr, nullptr, nullptr, reinterpret_cast(buffer), &bufferSize); + if (queryStatus != ERROR_SUCCESS) + { + throw std::runtime_error("Failed to read New context menu registry key."); + } + + const std::wstring builtInNewHandlerValue(buffer); + const bool startsWithPrefix = builtInNewHandlerValue.find(newDisabledValuePrefix) == 0; + + if (!startsWithPrefix) + { + return ERROR_SUCCESS; + } + + const std::wstring builtInNewEnabledValue = builtInNewHandlerValue.substr(newDisabledValuePrefix.length()); + const LONG setStatus = RegSetValueExW(*regKeyHandle, nullptr, 0, REG_SZ, reinterpret_cast(builtInNewEnabledValue.c_str()), static_cast((builtInNewEnabledValue.length() + 1)) * sizeof(wchar_t)); + if (setStatus != ERROR_SUCCESS) + { + throw std::runtime_error("Failed to update/restore the New context menu shell extension in the registry."); + } + } + catch (const std::exception& e) + { + std::string errorMessage{ "Exception thrown while trying to restore built-in New: " }; + errorMessage += e.what(); + Logger::error(errorMessage); + + er = ERROR_INSTALL_FAILURE; + } + + er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er; + return WcaFinalize(er); +} + UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) { HRESULT hr = S_OK; diff --git a/installer/PowerToysSetupCustomActions/CustomAction.def b/installer/PowerToysSetupCustomActions/CustomAction.def index 9467ca22047e..726d1d5fe250 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.def +++ b/installer/PowerToysSetupCustomActions/CustomAction.def @@ -32,3 +32,4 @@ EXPORTS CleanFileLocksmithRuntimeRegistryCA CleanPowerRenameRuntimeRegistryCA CleanNewPlusRuntimeRegistryCA + RestoreBuiltInNewContextMenuCA diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml index 6a597a43bcca..56d341961372 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -70,6 +70,15 @@ + + + + + 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 d02ecafc2105..cb5cb4081f0c 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -4515,6 +4515,10 @@ Activate by holding the key for the character you want to add an accent to, then Ignores digits, spaces, and dots at the start of filenames—useful for sorting templates without showing those characters Template filename starting digits settings toggle + + Hide the built-in New context menu + Localize New in accordance with Windows New + Behavior New+ behavior related settings label @@ -5299,4 +5303,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Utilities + + To change this setting you'll need to run PowerToys as administrator. You can restart PowerToys as administrator on the General page. + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs index 0bd44b4fbfc2..99a2bc33dd06 100644 --- a/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Security.Principal; using System.Text.Json; using System.Threading.Tasks; using System.Windows; @@ -17,7 +18,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; using Microsoft.PowerToys.Settings.UI.SerializationContext; - +using Microsoft.Win32; using static Microsoft.PowerToys.Settings.UI.Helpers.ShellGetFolder; namespace Microsoft.PowerToys.Settings.UI.ViewModels @@ -31,6 +32,9 @@ public partial class NewPlusViewModel : Observable private NewPlusSettings Settings { get; set; } private const string ModuleName = NewPlusSettings.ModuleName; + private const string BuiltInNewRegistryPath = @"HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\New"; + private const string NewDisabledValuePrefix = "0_"; + private const string BuiltNewCOMGuid = "{D969A300-E7FF-11d0-A93B-00A0C90F2719}"; public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc) { @@ -51,6 +55,8 @@ public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository !OSVersionHelper.IsWindows11(); @@ -164,6 +179,8 @@ public bool HideFileExtension public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured; + public bool IsDisableBuiltInNewSettingsCardEnabled => _isNewPlusEnabled && IsElevated(); + public bool HideStartingDigits { get => _hideStartingDigits; @@ -206,11 +223,44 @@ public bool ReplaceVariables } } + public bool HideBuiltInNew + { + get + { + return _disableBuiltInNew; + } + + set + { + if (_disableBuiltInNew != value) + { + if (_disableBuiltInNew) + { + EnableBuiltInNew(); + } + else + { + DisableBuiltInNew(); + } + + _disableBuiltInNew = value; + OnPropertyChanged(nameof(HideBuiltInNew)); + + NotifySettingsChanged(); + } + } + } + public bool IsEnabledGpoConfigured { get => _enabledStateIsGPOConfigured; } + public bool IsEnabledAndNotElevated + { + get => _isNewPlusEnabled && !IsElevated(); + } + public ButtonClickCommand OpenCurrentNewTemplateFolder => new ButtonClickCommand(OpenNewTemplateFolder); public ButtonClickCommand PickAnotherNewTemplateFolder => new ButtonClickCommand(PickNewTemplateFolder); @@ -271,6 +321,7 @@ public static void CopyTemplateExamples(string templateLocation) private bool _hideFileExtension; private bool _hideStartingDigits; private bool _replaceVariables; + private bool _disableBuiltInNew; private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; @@ -317,5 +368,69 @@ private async Task PickFolderDialog() var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.GetSettingsWindow()); return await Task.FromResult(GetFolderDialogWithFlags(hwnd, FolderDialogFlags._BIF_NEWDIALOGSTYLE)); } + + private bool IsBuiltInNewEnabled() + { + try + { + string builtInNewHandlerValue = Registry.GetValue(BuiltInNewRegistryPath, string.Empty, null) as string; + + return IsValidRegistryCOMFormatGuid(builtInNewHandlerValue); + } + catch (Exception ex) + { + Logger.LogError("Failed to determine built-in New enablement status.", ex); + } + + return false; + } + + private static bool IsValidRegistryCOMFormatGuid(string input) + { + return Guid.TryParseExact(input, "B", out _); + } + + private void DisableBuiltInNew() + { + try + { + string builtInNewHandlerValue = Registry.GetValue(BuiltInNewRegistryPath, string.Empty, null) as string; + + if (builtInNewHandlerValue.StartsWith(NewDisabledValuePrefix, StringComparison.OrdinalIgnoreCase)) + { + // Already disabled + return; + } + + Debug.Assert(builtInNewHandlerValue == BuiltNewCOMGuid, "Unexpected GUID encountered while disabling built-in New"); + string newDisabledValue = NewDisabledValuePrefix + builtInNewHandlerValue; + Registry.SetValue(BuiltInNewRegistryPath, string.Empty, newDisabledValue); + } + catch (Exception ex) + { + Logger.LogError("Failed to disable built-in New in the registry.", ex); + MessageBox.Show(ex.Message); + } + } + + private void EnableBuiltInNew() + { + try + { + string builtInNewHandlerValue = Registry.GetValue(BuiltInNewRegistryPath, string.Empty, null) as string; + + if (builtInNewHandlerValue.StartsWith(NewDisabledValuePrefix, StringComparison.OrdinalIgnoreCase)) + { + string newEnabledValue = builtInNewHandlerValue.Substring(NewDisabledValuePrefix.Length); + Debug.Assert(newEnabledValue == BuiltNewCOMGuid, "Unexpected GUID encountered while reenabling built-in New"); + Registry.SetValue(BuiltInNewRegistryPath, string.Empty, newEnabledValue); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to enable built-in New in the registry.", ex); + MessageBox.Show(ex.Message); + } + } } }