[New+] Hide existing new - remake#44979
Conversation
POPUPWINDOW POSITIONITEM POWERRENAMECONTEXTMENU
|
Hi @niels9001 and @yeelam-gordon, do you have feedback, suggestions, ideas on how I'm hiding/restoring the built-in New? The approach? Things missing? GPO policy changes (Am I using the correct version numbers etc.) Thank you! |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Super n00b - is there somewhere I can see why the "azp run" failed so that I can help address any outstanding issues? And other things that you want me to address? Thank you! cc: @niels9001 |
There was a problem hiding this comment.
Pull request overview
This pull request adds functionality to New+ that allows users and admins to hide the Windows built-in "New" context menu when New+ is enabled. This addresses user requests to avoid duplicate "New" entries in the context menu. The implementation uses a registry manipulation approach to disable the built-in handler by writing an invalid value to its registry key.
Changes:
- Added a toggle setting in New+ Settings UI to hide/show the built-in "New" context menu
- Implemented GPO policy to allow administrators to control this behavior
- Added installer custom action to restore the built-in "New" menu on uninstall
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs | Adds HideBuiltInNew property with registry manipulation methods to enable/disable built-in New context menu |
| src/settings-ui/Settings.UI/Strings/en-us/Resources.resw | Adds localized string for the new toggle setting |
| src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml | Adds UI control for hiding built-in New with GPO support |
| src/settings-ui/Settings.UI.Library/NewPlusProperties.cs | Adds BuiltInNewHidePreference property to persist user setting |
| src/modules/NewPlus/NewShellExtensionContextMenu/settings.h | Declares methods for getting/setting hide built-in New preference |
| src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp | Implements settings serialization for hide built-in New preference with GPO support |
| src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp | Applies hide built-in New setting when module initializes |
| src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h | Implements C++ registry manipulation functions to disable/enable built-in New |
| src/modules/NewPlus/NewShellExtensionContextMenu/constants.h | Adds constant for settings JSON key |
| src/gpo/assets/en-US/PowerToys.adml | Adds GPO policy descriptions for hiding built-in New |
| src/gpo/assets/PowerToys.admx | Adds GPO policy definition for hiding built-in New |
| src/common/utils/gpo.h | Adds GPO policy constant and getter method |
| src/common/GPOWrapper/GPOWrapper.idl | Adds WinRT method declaration for GPO wrapper |
| src/common/GPOWrapper/GPOWrapper.h | Adds GPO wrapper method declaration |
| src/common/GPOWrapper/GPOWrapper.cpp | Implements GPO wrapper method |
| installer/PowerToysSetupVNext/Product.wxs | Adds custom action to restore built-in New on uninstall |
| installer/PowerToysSetupCustomActionsVNext/CustomAction.def | Exports new custom action function |
| installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp | Implements custom action to restore built-in New registry value |
| doc/devdocs/modules/newplus.md | Adds documentation on manually restoring built-in New context menu |
| using (var newKey = Registry.CurrentUser.OpenSubKey(BuiltInNewRegistryPath, writable: true)) | ||
| { | ||
| string builtInNewHandlerValue = newKey.GetValue(null, null) as string; | ||
|
|
||
| if (!string.IsNullOrEmpty(builtInNewHandlerValue) && builtInNewHandlerValue.StartsWith(NewDisabledValuePrefix, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| // Already disabled | ||
| return; | ||
| } | ||
|
|
||
| // Any string value will disable the built-in New handler | ||
| string newDisabledValue = NewDisabledValuePrefix + builtInNewHandlerValue; | ||
| newKey.SetValue(string.Empty, newDisabledValue); | ||
| } |
There was a problem hiding this comment.
OpenSubKey can return null if the registry key doesn't exist, but the code doesn't check for this before calling GetValue and SetValue on newKey. This will cause a NullReferenceException if the registry key doesn't exist. Add a null check after OpenSubKey.
| void init_settings() | ||
| { | ||
| powertoy_new_enabled = NewSettingsInstance().GetEnabled(); | ||
|
|
||
| UpdateRegistration(powertoy_new_enabled); | ||
|
|
||
| if (powertoy_new_enabled) | ||
| { | ||
| // NOTE: This requires that the runner is running and have loaded the new plus module. | ||
| // It's not enough for user to just invoke the context menu. | ||
| if (NewSettingsInstance().GetHideBuiltInNew()) | ||
| { | ||
| newplus::utilities::disable_built_in_new_via_registry(); | ||
| } | ||
| else | ||
| { | ||
| newplus::utilities::enable_built_in_new_via_registry(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
When New+ is enabled (enable() method at line 111-124), the built-in New context menu state is not updated according to user preference. The enable() method registers New+ but doesn't call the logic to hide/show the built-in New based on settings. This means if a user previously set HideBuiltInNew=true, then disabled New+, then re-enabled New+, the built-in New won't be hidden until init_settings() is called again. Consider checking NewSettingsInstance().GetHideBuiltInNew() and calling the appropriate registry function in the enable() method.
| } | ||
|
|
||
| // Null key default value enables built-in New handler | ||
| newKey.DeleteValue(null, true); |
There was a problem hiding this comment.
The DeleteValue call passes 'true' for throwOnMissingValue, which will throw an exception if the value doesn't exist. However, you already check if the value is null at line 452 and return early, so the value should exist. The 'true' parameter seems unnecessary and could be changed to 'false' for more defensive coding, or removed entirely since false is the default.
| newKey.DeleteValue(null, true); | |
| newKey.DeleteValue(null); |
| private const string ModuleName = NewPlusSettings.ModuleName; | ||
|
|
||
| private const string BuiltInNewRegistryPath = @"Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers\New"; | ||
| private const string BuiltNewCOMGuid = "{D969A300-E7FF-11d0-A93B-00A0C90F2719}"; |
There was a problem hiding this comment.
The constant name "BuiltNewCOMGuid" should be "BuiltInNewCOMGuid" to match the naming convention used elsewhere (BuiltInNewRegistryPath, BuiltInNewHidePreference, etc.) and for consistency with "built-in" terminology.
| private const string BuiltNewCOMGuid = "{D969A300-E7FF-11d0-A93B-00A0C90F2719}"; | |
| private const string BuiltInNewCOMGuid = "{D969A300-E7FF-11d0-A93B-00A0C90F2719}"; |
| if (_disableBuiltInNew) | ||
| { | ||
| EnableBuiltInNewViaRegistry(); | ||
| } | ||
| else | ||
| { | ||
| DisableBuiltInNewViaRegistry(); |
There was a problem hiding this comment.
The logic for updating registry based on the toggle is inverted. The code checks the OLD value (_disableBuiltInNew) to decide which registry function to call, but it should check the NEW value (value parameter).
Currently:
- If _disableBuiltInNew is true (currently hidden), it calls EnableBuiltInNewViaRegistry()
- If _disableBuiltInNew is false (currently shown), it calls DisableBuiltInNewViaRegistry()
This is backwards. It should be:
- If value is true (user wants to hide), call DisableBuiltInNewViaRegistry()
- If value is false (user wants to show), call EnableBuiltInNewViaRegistry()
Change line 256 from "if (_disableBuiltInNew)" to "if (value)" and swap the method calls.
| if (_disableBuiltInNew) | |
| { | |
| EnableBuiltInNewViaRegistry(); | |
| } | |
| else | |
| { | |
| DisableBuiltInNewViaRegistry(); | |
| if (value) | |
| { | |
| DisableBuiltInNewViaRegistry(); | |
| } | |
| else | |
| { | |
| EnableBuiltInNewViaRegistry(); |
|
|
||
| If you enable this policy, then the built-in New context menu will be hidden, and user can only create new files and folders using New+ and the explorer toolbar New button. | ||
|
|
||
| If you disable this policy, then the build-in New context menu will be displayed as normal in Windows. |
There was a problem hiding this comment.
Typo in comment: "build-in" should be "built-in".
| If you disable this policy, then the build-in New context menu will be displayed as normal in Windows. | |
| If you disable this policy, then the built-in New context menu will be displayed as normal in Windows. |
| inline bool disable_built_in_new_via_registry() | ||
| { | ||
| // This is implemented to support where New+ GPO is configured to | ||
| // hide the built-in New context menu but Settings UI hasn't been launched | ||
| // Mirrors the logic in DisableBuiltInNewViaRegistry in .cs | ||
|
|
||
| HKEY key{}; | ||
|
|
||
| if (RegCreateKeyExW(HKEY_CURRENT_USER, | ||
| built_in_new_registry_path, | ||
| 0, | ||
| nullptr, | ||
| REG_OPTION_NON_VOLATILE, | ||
| KEY_ALL_ACCESS, | ||
| nullptr, | ||
| &key, | ||
| nullptr) != ERROR_SUCCESS) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| const auto built_in_new_registry_disabled_value_prefix_len = lstrlenW(built_in_new_registry_disabled_value_prefix); | ||
|
|
||
| if (RegSetValueExW(key, nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(&built_in_new_registry_disabled_value_prefix), built_in_new_registry_disabled_value_prefix_len) != ERROR_SUCCESS) | ||
| { | ||
| RegCloseKey(key); | ||
| return true; | ||
| } | ||
|
|
||
| RegCloseKey(key); | ||
| return false; | ||
|
|
||
| } |
There was a problem hiding this comment.
The return value logic is inverted. The function returns true on RegSetValueExW failure (line 479) and false on success (line 483). This contradicts the boolean return type semantics where true typically indicates success. This should return false on RegSetValueExW failure and true on success to be consistent with standard C++ conventions and the enable_built_in_new_via_registry function which also has inverted return logic.
| using (var newKey = Registry.CurrentUser.OpenSubKey(BuiltInNewRegistryPath, writable: true)) | ||
| { | ||
| string builtInNewHandlerValue = newKey.GetValue(null, null) as string; | ||
|
|
||
| if (builtInNewHandlerValue is null) | ||
| { | ||
| // Already enabled | ||
| return; | ||
| } | ||
|
|
||
| // Null key default value enables built-in New handler | ||
| newKey.DeleteValue(null, true); | ||
| } |
There was a problem hiding this comment.
OpenSubKey can return null if the registry key doesn't exist, but the code doesn't check for this before calling GetValue and DeleteValue on newKey. This will cause a NullReferenceException if the registry key doesn't exist. Add a null check after OpenSubKey.
| InitializeEnabledValue(); | ||
| InitializeGpoValues(); | ||
|
|
||
| _disableBuiltInNew = !IsBuiltInNewEnabled(); |
There was a problem hiding this comment.
The initialization at line 59 reads the current state from the registry via IsBuiltInNewEnabled(), but doesn't consider the saved user preference in Settings.Properties.BuiltInNewHidePreference. This could cause a mismatch where the UI shows a different state than what the user last saved if the registry was modified externally or during uninstall/reinstall. Consider reading from Settings.Properties.BuiltInNewHidePreference.Value first, or reconciling the two sources during initialization. Note that the C++ module (powertoys_module.cpp line 182) uses GetHideBuiltInNew() which reads from settings, not registry.
| _disableBuiltInNew = !IsBuiltInNewEnabled(); | |
| _disableBuiltInNew = Settings.Properties.BuiltInNewHidePreference.Value; |
| using (var newKey = Registry.CurrentUser.OpenSubKey(BuiltInNewRegistryPath, writable: false)) | ||
| { | ||
| string builtInNewHandlerValue = newKey.GetValue(null, null) as string; | ||
|
|
||
| if (builtInNewHandlerValue is null || string.Equals(builtInNewHandlerValue, BuiltNewCOMGuid, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| // If no default value for key, or GUID is BuiltNewCOMGuid then built-in New is enabled | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
OpenSubKey can return null if the registry key doesn't exist, but the code doesn't check for this before calling methods on newKey. This will cause a NullReferenceException if the registry key "Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers\New" doesn't exist. Add a null check after OpenSubKey, similar to the pattern used in KeyboardManagerViewModel.cs line 322-329.
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@cgaarden can you run xaml styler and push the updated files? That should make the ci pass |
cgaarden
left a comment
There was a problem hiding this comment.
Updated XAML style using (note had to rename the branch as the script didn't like "(remake)"):
..pipelines\applyXamlStyling.ps1 -Main
Ran ..pipelines\applyXamlStyling.ps1 -Main Thanks for the pointer Niels! |
|
|
||
| - For development and testing, using the Windows 10 handler can be easier since it doesn't require signing. | ||
|
|
||
| ## Restoring Built-in Windows New context menu |
There was a problem hiding this comment.
@cgaarden Would you mind opening a PR in the MS Learn docs too with this troubleshooting paragraph?
https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/hub/powertoys/newplus.md
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Summary of the Pull Request
PR Checklist
Note: Supersedes #39843
New+ Feature - Doc Update - Option to hide the built-in New context m… MicrosoftDocs/windows-dev-docs#5473)
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed
Windows 11 x64
Settings UI
New+ enabled
New+ disabled
GPO setting enabled
GPO settings disabled
Manually updating newplus/settings.json
Windows 11 ARM64
I tested the reg hack manually, but didn't go through a full pass.
Windows 10 x64
NOT tested.
Windows 11, Settings, New+ Disabled and no GPO

Windows 11, Settings, New+ Enabled and no GPO

Hide built-in New: Off (the default)

Hide built-in New: On

Modern

Classic

Disabling New+ also unhide New

Windows 11, Settings, New+ Enabled and with GPO
Hide built-in New: GPO enabled

Hide built-in New: GPO disabled
