CmdPal: Add context commands for pinning nested commands (#45673)

_targets #45572_

This change allows our contact menu factory to actually create and add
additional context menu commands for pinning commands to the top level.
Now for any command provider built with the latest SDK that return
subcommands with an ID, we will add additional context menu commands
that allows you to pin that command to the top level.

<img width="540" height="181" alt="image"
src="https://github.com/user-attachments/assets/6c2cfe3c-4143-44d1-9308-bfc71db4c842"
/>
<img width="729" height="317" alt="image"
src="https://github.com/user-attachments/assets/4ff75c9f-1f35-4c1e-a03e-6fab5cbab423"
/>

related to https://github.com/microsoft/PowerToys/issues/45191
related to https://github.com/microsoft/PowerToys/issues/45201


This PR notably does not remove pinning from the apps extension. I
thought that made sense to do as a follow-up PR for the sake of
reviewability.

--- 

description from #45676 which was merged into this

Removes the code that the apps provider was using to support pinning
apps to the top level list of commands. Now the all apps provider just
uses the global support for pinning commands to the top level.

This does have the side effect of removing the separation of pinned apps
from unpinned apps on the All Apps page. However, we all pretty much
agree that wasn't a particularly widely used feature, and it's safe to
remove.

With this, we can finally call this issue done 🎉
closes https://github.com/microsoft/PowerToys/issues/45191
This commit is contained in:
Mike Griese
2026-02-26 10:09:17 -06:00
committed by GitHub
parent cdeae7c854
commit 7a0e4ac891
30 changed files with 389 additions and 434 deletions

View File

@@ -2,8 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI;
@@ -24,8 +29,164 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
{
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
// TODO: #45201 Here, we'll want to add pin/unpin commands for pinning
// items to the top-level or to the dock.
List<IContextItem> moreCommands = [];
var itemId = commandItem.Command.Id;
if (commandItem.PageContext.TryGetTarget(out var page) &&
page.ProviderContext.SupportsPinning &&
!string.IsNullOrEmpty(itemId))
{
// Add pin/unpin commands for pinning items to the top-level or to
// the dock.
var providerId = page.ProviderContext.ProviderId;
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
{
var providerSettings = _settingsModel.GetProviderSettings(provider);
var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId);
// Don't add pin/unpin commands for items displayed as
// TopLevelViewModels that aren't already pinned.
//
// We can't look up if this command item is in the top level
// items in the manager, because we are being called _before_ we
// get added to the manager's list of commands.
var isTopLevelItem = page is TopLevelItemPageContext;
if (!isTopLevelItem || alreadyPinnedToTopLevel)
{
var pinToTopLevelCommand = new PinToCommand(
commandId: itemId,
providerId: providerId,
pin: !alreadyPinnedToTopLevel,
PinLocation.TopLevel,
_settingsModel,
_topLevelCommandManager);
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
moreCommands.Add(contextItem);
}
}
}
if (moreCommands.Count > 0)
{
moreCommands.Insert(0, new Separator());
var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem);
results.AddRange(moreResults);
}
return results;
}
internal enum PinLocation
{
TopLevel,
Dock,
}
private sealed partial class PinToContextItem : CommandContextItem
{
private readonly PinToCommand _command;
private readonly CommandItemViewModel _commandItem;
public PinToContextItem(PinToCommand command, CommandItemViewModel commandItem)
: base(command)
{
_command = command;
_commandItem = commandItem;
command.PinStateChanged += this.OnPinStateChanged;
}
private void OnPinStateChanged(object? sender, EventArgs e)
{
// update our MoreCommands
_commandItem.RefreshMoreCommands();
}
~PinToContextItem()
{
_command.PinStateChanged -= this.OnPinStateChanged;
}
}
private sealed partial class PinToCommand : InvokableCommand
{
private readonly string _commandId;
private readonly string _providerId;
private readonly SettingsModel _settings;
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly bool _pin;
private readonly PinLocation _pinLocation;
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
public override string Name => _pin ? RS_.GetString("top_level_pin_command_name") : RS_.GetString("top_level_unpin_command_name");
internal event EventHandler? PinStateChanged;
public PinToCommand(
string commandId,
string providerId,
bool pin,
PinLocation pinLocation,
SettingsModel settings,
TopLevelCommandManager topLevelCommandManager)
{
_commandId = commandId;
_providerId = providerId;
_pinLocation = pinLocation;
_settings = settings;
_topLevelCommandManager = topLevelCommandManager;
_pin = pin;
}
public override CommandResult Invoke()
{
Logger.LogDebug($"PinTo{_pinLocation}Command.Invoke({_pin}): {_providerId}/{_commandId}");
if (_pin)
{
switch (_pinLocation)
{
case PinLocation.TopLevel:
PinToTopLevel();
break;
// TODO: After dock is added:
// case PinLocation.Dock:
// PinToDock();
// break;
}
}
else
{
switch (_pinLocation)
{
case PinLocation.TopLevel:
UnpinFromTopLevel();
break;
// case PinLocation.Dock:
// UnpinFromDock();
// break;
}
}
PinStateChanged?.Invoke(this, EventArgs.Empty);
return CommandResult.KeepOpen();
}
private void PinToTopLevel()
{
PinCommandItemMessage message = new(_providerId, _commandId);
WeakReferenceMessenger.Default.Send(message);
}
private void UnpinFromTopLevel()
{
UnpinCommandItemMessage message = new(_providerId, _commandId);
WeakReferenceMessenger.Default.Send(message);
}
}
}

View File

@@ -26,9 +26,9 @@ internal sealed class PowerToysAppHostService : IAppHostService
return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance;
}
public CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext)
public ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext)
{
CommandProviderContext? topLevelId = null;
ICommandProviderContext? topLevelId = null;
if (command is TopLevelViewModel topLevelViewModel)
{
topLevelId = topLevelViewModel.ProviderContext;

View File

@@ -801,10 +801,26 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>K</value>
<comment>Keyboard key</comment>
</data>
<data name="ConfigureShortcut" xml:space="preserve">
<data name="ConfigureShortcut" xml:space="preserve">
<value>Configure shortcut</value>
</data>
<data name="ConfigureShortcutText.Text" xml:space="preserve">
<value>Assign shortcut</value>
</data>
<data name="top_level_pin_command_name" xml:space="preserve">
<value>Pin to home</value>
<comment>Command name for pinning an item to the top level list of commands</comment>
</data>
<data name="top_level_unpin_command_name" xml:space="preserve">
<value>Unpin from home</value>
<comment>Command name for unpinning an item from the top level list of commands</comment>
</data>
<data name="dock_pin_command_name" xml:space="preserve">
<value>Pin to dock</value>
<comment>Command name for pinning an item to the dock</comment>
</data>
<data name="dock_unpin_command_name" xml:space="preserve">
<value>Unpin from dock</value>
<comment>Command name for unpinning an item from the dock</comment>
</data>
</root>