Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions installer/PowerToysSetup/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@
<!-- Clean Video Conference Mute registry keys that might be around from previous installations. We've deprecated this utility since then. -->
<Custom Action="CleanVideoConferenceRegistry" Before="InstallFinalize">NOT Installed</Custom>

<!-- User may have disabled the built-in New context menu via New+ -->
<Custom Action="RestoreBuiltInNewContextMenu" Before="RemoveFiles">
Installed AND (REMOVE="ALL")
</Custom>

</InstallExecuteSequence>

<CustomAction Id="SetLaunchPowerToysParam"
Expand Down Expand Up @@ -462,6 +467,14 @@
DllEntry="InstallCmdPalPackageCA"
/>

<CustomAction Id="RestoreBuiltInNewContextMenu"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="RestoreBuiltInNewContextMenuCA"
/>

<!-- Close 'PowerToys.exe' before uninstall-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="DisableShutdown" />
<Property Id="MSIFASTINSTALL" Value="DisableShutdown" />
Expand Down
59 changes: 59 additions & 0 deletions installer/PowerToysSetupCustomActions/CustomAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,65 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(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<HKEY, decltype(regDeleter)> regKeyHandle(new HKEY(nullptr), regDeleter);

const LONG openStatus = RegOpenKeyExW(HKEY_CLASSES_ROOT, builtInNewRegistryPath.c_str(), 0, KEY_READ | KEY_WRITE, regKeyHandle.get());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we separate the machine-level and per-user installers? I confirmed that the built-in New context menu is only in HKLM, but HKCU can override it. So the machine-level installer only needs to write to HKLM, and the per-user installer only needs to write to HKCU — both will work and can stay clean when uninstalled.

Copy link
Contributor

@yeelam-gordon yeelam-gordon Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have code example how to know it is per-machine vs per-user installation here to make the right decision?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could check the msi InstallScope, in installer, we could do like this
hr = WcaGetProperty(L"InstallScope", &currentScope);
if (std::wstring{currentScope} == L"perUser")

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<LPBYTE>(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<const BYTE*>(builtInNewEnabledValue.c_str()), static_cast<DWORD>((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;
Expand Down
1 change: 1 addition & 0 deletions installer/PowerToysSetupCustomActions/CustomAction.def
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ EXPORTS
UninstallCommandNotFoundModuleCA
UpgradeCommandNotFoundModuleCA
UnsetAdvancedPasteAPIKeyCA
RestoreBuiltInNewContextMenuCA
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@
<TextBlock x:Uid="NewPlus_Hide_Starting_Digits_Description" />
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>

<tkcontrols:SettingsCard x:Uid="NewPlus_Hide_BuiltIn_New_Toggle" IsEnabled="{x:Bind ViewModel.IsDisableBuiltInNewSettingsCardEnabled, Mode=OneWay}">
<ToggleSwitch x:Uid="DisableBuiltInNewToggle" IsOn="{x:Bind ViewModel.HideBuiltInNew, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<InfoBar
x:Uid="Elevation_Required"
IsClosable="True"
IsOpen="{x:Bind ViewModel.IsEnabledAndNotElevated, Mode=OneWay}"
Severity="Informational" />
</controls:SettingsGroup>

<controls:SettingsGroup x:Uid="NewPlus_behavior" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
Expand Down
7 changes: 7 additions & 0 deletions src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -4451,6 +4451,10 @@ Activate by holding the key for the character you want to add an accent to, then
<value>This option is useful when using digits, spaces and dots at the beginning of filenames to control the display order of templates</value>
<comment>Template filename starting digits settings toggle</comment>
</data>
<data name="NewPlus_Hide_BuiltIn_New_Toggle.Header" xml:space="preserve">
<value>Hide the built-in New context menu</value>
<comment>Localize New in accordance with Windows New</comment>
</data>
<data name="NewPlus_behavior.Header" xml:space="preserve">
<value>Behavior</value>
<comment>New+ behavior related settings label</comment>
Expand Down Expand Up @@ -5047,4 +5051,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="BugReportUnderConstruction" xml:space="preserve">
<value>Bug report package is being created</value>
</data>
<data name="Elevation_Required.Title" xml:space="preserve">
<value>To change this setting you'll need to run PowerToys as administrator. You can restart PowerToys as administrator on the General page.</value>
</data>
</root>
117 changes: 116 additions & 1 deletion src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have code example how to know it is per-machine vs per-user installation here to make the right decision?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting-ui, we can use GetCurrentInstallScope()

Copy link
Contributor Author

@cgaarden cgaarden Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @lei9444 and thanks for the feedback

I'm happy to make these changes but I don't see any "New" registry key under HKEY_CURRENT_USER\Software\Classes\Directory\Background\shellex\ContextMenuHandlers

Question:
Do any of you see a key there (HKEY_CURRENT_USER\Software\Classes\Directory\Background\shellex\ContextMenuHandlers\New)?

Alternative implementation:
Based on GetCurrentInstallScope() I could check

If PerMachine
HKEY_CURRENT_USER\Software\Classes\Directory\Background\shellex\ContextMenuHandlers\New AND
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shellex\ContextMenuHandlers\New

If PerUser
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shellex\ContextMenuHandlers\New

But not sure if this is an actual scenario? Any hints on how to reproduce so that I can verify?

Kind regards,
Christian

private const string NewDisabledValuePrefix = "0_";
private const string BuiltNewCOMGuid = "{D969A300-E7FF-11d0-A93B-00A0C90F2719}";

public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
Expand All @@ -51,6 +55,8 @@ public NewPlusViewModel(ISettingsUtils settingsUtils, ISettingsRepository<Genera
InitializeEnabledValue();
InitializeGpoValues();

_disableBuiltInNew = !IsBuiltInNewEnabled();

// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
}
Expand Down Expand Up @@ -96,6 +102,8 @@ public bool IsEnabled
OnPropertyChanged(nameof(IsHideFileExtSettingGPOConfigured));
OnPropertyChanged(nameof(IsReplaceVariablesSettingGPOConfigured));
OnPropertyChanged(nameof(IsReplaceVariablesSettingsCardEnabled));
OnPropertyChanged(nameof(IsDisableBuiltInNewSettingsCardEnabled));
OnPropertyChanged(nameof(IsEnabledAndNotElevated));

OutGoingGeneralSettings outgoingMessage = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoingMessage.ToString());
Expand All @@ -110,6 +118,13 @@ public bool IsEnabled
}
}

private bool IsElevated()
{
WindowsIdentity identity = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}

public bool IsWin10OrLower
{
get => !OSVersionHelper.IsWindows11();
Expand Down Expand Up @@ -164,6 +179,8 @@ public bool HideFileExtension

public bool IsReplaceVariablesSettingGPOConfigured => _isNewPlusEnabled && _replaceVariablesIsGPOConfigured;

public bool IsDisableBuiltInNewSettingsCardEnabled => _isNewPlusEnabled && IsElevated();

public bool HideStartingDigits
{
get => _hideStartingDigits;
Expand Down Expand Up @@ -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(DisableBuiltInNew));

NotifySettingsChanged();
}
}
}

public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}

public bool IsEnabledAndNotElevated
{
get => _isNewPlusEnabled && !IsElevated();
}

public ButtonClickCommand OpenCurrentNewTemplateFolder => new ButtonClickCommand(OpenNewTemplateFolder);

public ButtonClickCommand PickAnotherNewTemplateFolder => new ButtonClickCommand(PickNewTemplateFolder);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -317,5 +368,69 @@ private async Task<string> 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);
}
}
}
}
Loading