2026-02-24 06:26:33 -06:00
|
|
|
// Copyright (c) Microsoft Corporation
|
|
|
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
|
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
|
|
2026-02-26 10:09:17 -06:00
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
|
|
|
using ManagedCommon;
|
2026-02-24 06:26:33 -06:00
|
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
2026-02-26 10:09:17 -06:00
|
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
2026-02-27 07:24:23 -06:00
|
|
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
2026-02-24 06:26:33 -06:00
|
|
|
using Microsoft.CommandPalette.Extensions;
|
2026-02-26 10:09:17 -06:00
|
|
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
|
|
|
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
2026-02-24 06:26:33 -06:00
|
|
|
|
|
|
|
|
namespace Microsoft.CmdPal.UI;
|
|
|
|
|
|
|
|
|
|
internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFactory
|
|
|
|
|
{
|
|
|
|
|
private readonly SettingsModel _settingsModel;
|
|
|
|
|
private readonly TopLevelCommandManager _topLevelCommandManager;
|
|
|
|
|
|
|
|
|
|
public CommandPaletteContextMenuFactory(SettingsModel settingsModel, TopLevelCommandManager topLevelCommandManager)
|
|
|
|
|
{
|
|
|
|
|
_settingsModel = settingsModel;
|
|
|
|
|
_topLevelCommandManager = topLevelCommandManager;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
/// <summary>
|
|
|
|
|
/// Constructs the view models for the MoreCommands of a
|
|
|
|
|
/// CommandItemViewModel. In our case, we can use our settings to add a
|
|
|
|
|
/// contextually-relevant pin/unpin command to this item.
|
|
|
|
|
///
|
|
|
|
|
/// This is called on all CommandItemViewModels. There are however some
|
|
|
|
|
/// weird edge cases we need to handle, concerning
|
|
|
|
|
/// </summary>
|
2026-02-24 06:26:33 -06:00
|
|
|
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
|
|
|
|
|
IContextItem[] items,
|
|
|
|
|
CommandItemViewModel commandItem)
|
|
|
|
|
{
|
|
|
|
|
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
IPageContext? page = null;
|
|
|
|
|
var succeeded = commandItem.PageContext.TryGetTarget(out page);
|
|
|
|
|
if (!succeeded || page is null)
|
|
|
|
|
{
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isTopLevelItem = page is TopLevelItemPageContext;
|
|
|
|
|
if (isTopLevelItem)
|
|
|
|
|
{
|
|
|
|
|
// Bail early. We'll handle it below.
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 10:09:17 -06:00
|
|
|
List<IContextItem> moreCommands = [];
|
|
|
|
|
var itemId = commandItem.Command.Id;
|
2026-02-27 07:24:23 -06:00
|
|
|
var providerContext = page.ProviderContext;
|
|
|
|
|
var supportsPinning = providerContext.SupportsPinning;
|
2026-02-26 10:09:17 -06:00
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
if (supportsPinning &&
|
2026-02-26 10:09:17 -06:00
|
|
|
!string.IsNullOrEmpty(itemId))
|
|
|
|
|
{
|
|
|
|
|
// Add pin/unpin commands for pinning items to the top-level or to
|
|
|
|
|
// the dock.
|
2026-02-27 07:24:23 -06:00
|
|
|
var providerId = providerContext.ProviderId;
|
2026-02-26 10:09:17 -06:00
|
|
|
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.
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
TryAddPinToDockCommand(providerSettings, itemId, providerId, moreCommands, commandItem);
|
2026-02-26 10:09:17 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (moreCommands.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
moreCommands.Insert(0, new Separator());
|
|
|
|
|
var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem);
|
|
|
|
|
results.AddRange(moreResults);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 06:26:33 -06:00
|
|
|
return results;
|
|
|
|
|
}
|
2026-02-26 10:09:17 -06:00
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
/// <summary>
|
|
|
|
|
/// Called to create the context menu on TopLevelViewModels.
|
|
|
|
|
///
|
|
|
|
|
/// These are handled differently from everyone else. With
|
|
|
|
|
/// TopLevelViewModels, the ID isn't on the Command, it is on the
|
|
|
|
|
/// TopLevelViewModel itself. Basically, we can't figure out how to add
|
|
|
|
|
/// pin/unpin commands directly attached to the ICommandItems that we get
|
|
|
|
|
/// from the API.
|
|
|
|
|
///
|
|
|
|
|
/// Instead, this method is used to extend the set of IContextItems that are
|
|
|
|
|
/// added to the TopLevelViewModel itself. This lets us pin/unpin the
|
|
|
|
|
/// generated ID of the TopLevelViewModel, even if the command didn't have
|
|
|
|
|
/// one.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void AddMoreCommandsToTopLevel(
|
|
|
|
|
TopLevelViewModel topLevelItem,
|
|
|
|
|
ICommandProviderContext providerContext,
|
|
|
|
|
List<IContextItem?> contextItems)
|
|
|
|
|
{
|
|
|
|
|
var itemId = topLevelItem.Id;
|
|
|
|
|
var supportsPinning = providerContext.SupportsPinning;
|
|
|
|
|
List<IContextItem> moreCommands = [];
|
|
|
|
|
var commandItem = topLevelItem.ItemViewModel;
|
|
|
|
|
|
|
|
|
|
// Add pin/unpin commands for pinning items to the top-level or to
|
|
|
|
|
// the dock.
|
|
|
|
|
var providerId = providerContext.ProviderId;
|
|
|
|
|
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
|
|
|
|
{
|
|
|
|
|
var providerSettings = _settingsModel.GetProviderSettings(provider);
|
|
|
|
|
|
|
|
|
|
var isPinnedSubCommand = providerSettings.PinnedCommandIds.Contains(itemId);
|
|
|
|
|
if (isPinnedSubCommand)
|
|
|
|
|
{
|
|
|
|
|
var pinToTopLevelCommand = new PinToCommand(
|
|
|
|
|
commandId: itemId,
|
|
|
|
|
providerId: providerId,
|
|
|
|
|
pin: !isPinnedSubCommand,
|
|
|
|
|
PinLocation.TopLevel,
|
|
|
|
|
_settingsModel,
|
|
|
|
|
_topLevelCommandManager);
|
|
|
|
|
|
|
|
|
|
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
|
|
|
|
|
moreCommands.Add(contextItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryAddPinToDockCommand(providerSettings, itemId, providerId, moreCommands, commandItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (moreCommands.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
moreCommands.Insert(0, new Separator());
|
|
|
|
|
|
|
|
|
|
// var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem);
|
|
|
|
|
contextItems.AddRange(moreCommands);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryAddPinToDockCommand(
|
|
|
|
|
ProviderSettings providerSettings,
|
|
|
|
|
string itemId,
|
|
|
|
|
string providerId,
|
|
|
|
|
List<IContextItem> moreCommands,
|
|
|
|
|
CommandItemViewModel commandItem)
|
|
|
|
|
{
|
|
|
|
|
if (!_settingsModel.EnableDock)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inStartBands = _settingsModel.DockSettings.StartBands.Any(band => MatchesBand(band, itemId, providerId));
|
|
|
|
|
var inCenterBands = _settingsModel.DockSettings.CenterBands.Any(band => MatchesBand(band, itemId, providerId));
|
|
|
|
|
var inEndBands = _settingsModel.DockSettings.EndBands.Any(band => MatchesBand(band, itemId, providerId));
|
|
|
|
|
var alreadyPinned = inStartBands || inCenterBands || inEndBands; /** &&
|
|
|
|
|
_settingsModel.DockSettings.PinnedCommands.Contains(this.Id)**/
|
|
|
|
|
var pinToTopLevelCommand = new PinToCommand(
|
|
|
|
|
commandId: itemId,
|
|
|
|
|
providerId: providerId,
|
|
|
|
|
pin: !alreadyPinned,
|
|
|
|
|
PinLocation.Dock,
|
|
|
|
|
_settingsModel,
|
|
|
|
|
_topLevelCommandManager);
|
|
|
|
|
|
|
|
|
|
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
|
|
|
|
|
moreCommands.Add(contextItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static bool MatchesBand(DockBandSettings bandSettings, string commandId, string providerId)
|
|
|
|
|
{
|
|
|
|
|
return bandSettings.CommandId == commandId &&
|
|
|
|
|
bandSettings.ProviderId == providerId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 10:09:17 -06:00
|
|
|
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;
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
private bool IsPinToDock => _pinLocation == PinLocation.Dock;
|
|
|
|
|
|
2026-02-26 10:09:17 -06:00
|
|
|
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
public override string Name => _pin ?
|
|
|
|
|
(IsPinToDock ? RS_.GetString("dock_pin_command_name") : RS_.GetString("top_level_pin_command_name")) :
|
|
|
|
|
(IsPinToDock ? RS_.GetString("dock_unpin_command_name") : RS_.GetString("top_level_unpin_command_name"));
|
2026-02-26 10:09:17 -06:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
case PinLocation.Dock:
|
|
|
|
|
PinToDock();
|
|
|
|
|
break;
|
2026-02-26 10:09:17 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
switch (_pinLocation)
|
|
|
|
|
{
|
|
|
|
|
case PinLocation.TopLevel:
|
|
|
|
|
UnpinFromTopLevel();
|
|
|
|
|
break;
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
case PinLocation.Dock:
|
|
|
|
|
UnpinFromDock();
|
|
|
|
|
break;
|
2026-02-26 10:09:17 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
private void PinToDock()
|
|
|
|
|
{
|
|
|
|
|
PinToDockMessage message = new(_providerId, _commandId, true);
|
|
|
|
|
WeakReferenceMessenger.Default.Send(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UnpinFromDock()
|
|
|
|
|
{
|
|
|
|
|
PinToDockMessage message = new(_providerId, _commandId, false);
|
|
|
|
|
WeakReferenceMessenger.Default.Send(message);
|
|
|
|
|
}
|
2026-02-26 10:09:17 -06:00
|
|
|
}
|
2026-02-24 06:26:33 -06:00
|
|
|
}
|