// 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. using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; 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; } /// /// 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 /// public List UnsafeBuildAndInitMoreCommands( IContextItem[] items, CommandItemViewModel commandItem) { var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem); 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; } List moreCommands = []; var itemId = commandItem.Command.Id; var providerContext = page.ProviderContext; var supportsPinning = providerContext.SupportsPinning; if (supportsPinning && !string.IsNullOrEmpty(itemId)) { // 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 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); } TryAddPinToDockCommand(providerSettings, itemId, providerId, moreCommands, commandItem); } } if (moreCommands.Count > 0) { moreCommands.Insert(0, new Separator()); var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem); results.AddRange(moreResults); } return results; } /// /// 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. /// public void AddMoreCommandsToTopLevel( TopLevelViewModel topLevelItem, ICommandProviderContext providerContext, List contextItems) { var itemId = topLevelItem.Id; var supportsPinning = providerContext.SupportsPinning; List 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 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; } 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; private bool IsPinToDock => _pinLocation == PinLocation.Dock; public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon; 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")); 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; 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); } 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); } } }