/
githubmirror
/
PowerToys
Обзор
Документация
Войти
/
githubmirror
/
PowerToys
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs
550 строк
21 KB
Mike Griese
CmdPal: Only list available docks when pinning (#48723)
19 июн 2026, 03:12
Не верифицирован
19 июн 2026, 03:12
6dcda8a
Код
Авторство
О чём код?
// 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 System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Ext.Apps; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Services; 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 ISettingsService _settingsService; private readonly TopLevelCommandManager _topLevelCommandManager; private readonly IMonitorService? _monitorService; public CommandPaletteContextMenuFactory(ISettingsService settingsService, TopLevelCommandManager topLevelCommandManager, IMonitorService? monitorService = null) { _settingsService = settingsService; _topLevelCommandManager = topLevelCommandManager; _monitorService = monitorService; } /// <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> public List<IContextItemViewModel> 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<IContextItem> moreCommands = []; var itemId = commandItem.Command.Id; var providerContext = page.ProviderContext; // AppListItems can be surfaced on the main page even though they still // belong to the All Apps provider. // MainListPage only returns our in-proc wrappers/items. if (providerContext.ProviderId != AllAppsCommandProvider.WellKnownId && page is ListViewModel { IsMainPage: true } && commandItem.Model.Unsafe is AppListItem) { providerContext = _topLevelCommandManager.LookupProvider(AllAppsCommandProvider.WellKnownId)?.GetProviderContext() ?? providerContext; } var supportsPinning = providerContext.SupportsPinning; // Also bail early for ListItemViewModels that wrap a TopLevelViewModel. // For those items, TopLevelViewModel.BuildContextMenu() already includes // the correct pin commands by calling AddMoreCommandsToTopLevel with the // item's own provider context. Adding them again here (using the page's // potentially incorrect provider context) would produce duplicate pin // entries such as two "Pin to Dock" buttons. // Check SupportsPinning first to avoid the .Unsafe type-check in the // common non-pinning case. if (supportsPinning && commandItem.Model.Unsafe is TopLevelViewModel) { return results; } 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) { var alreadyPinnedToTopLevel = _settingsService.Settings.IsCommandPinned(providerId, 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, _settingsService, _topLevelCommandManager); var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem); moreCommands.Add(contextItem); } TryAddPinToDockCommand(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; } /// <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; 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) { TryAddMovePinnedCommands(itemId, providerId, commandItem, moreCommands); TryAddUnpinFromHomeCommand(itemId, providerId, commandItem, moreCommands); TryAddPinToHomeCommand(itemId, providerId, commandItem, moreCommands); TryAddPinToDockCommand(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 TryAddPinToHomeCommand( string itemId, string providerId, CommandItemViewModel commandItem, List<IContextItem> moreCommands) { if (_settingsService.Settings.IsCommandPinned(providerId, itemId)) { return; } var pinToTopLevelCommand = new PinToCommand( commandId: itemId, providerId: providerId, pin: true, PinLocation.TopLevel, _settingsService, _topLevelCommandManager); var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem); moreCommands.Add(contextItem); } private void TryAddUnpinFromHomeCommand( string itemId, string providerId, CommandItemViewModel commandItem, List<IContextItem> moreCommands) { var isPinnedSubCommand = _settingsService.Settings.IsCommandPinned(providerId, itemId); if (isPinnedSubCommand) { var pinToTopLevelCommand = new PinToCommand( commandId: itemId, providerId: providerId, pin: !isPinnedSubCommand, PinLocation.TopLevel, _settingsService, _topLevelCommandManager); var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem); moreCommands.Add(contextItem); } } private void TryAddMovePinnedCommands( string itemId, string providerId, CommandItemViewModel commandItem, List<IContextItem> moreCommands) { if (!_settingsService.Settings.IsCommandPinned(providerId, itemId)) { return; } var moveToTopCommand = new MovePinnedCommand(providerId, itemId, MovePinnedDirection.ToTop, _settingsService, _topLevelCommandManager); moreCommands.Add(new MovePinnedContextItem(moveToTopCommand, commandItem)); var moveUpCommand = new MovePinnedCommand(providerId, itemId, MovePinnedDirection.Up, _settingsService, _topLevelCommandManager); moreCommands.Add(new MovePinnedContextItem(moveUpCommand, commandItem)); var moveDownCommand = new MovePinnedCommand(providerId, itemId, MovePinnedDirection.Down, _settingsService, _topLevelCommandManager); moreCommands.Add(new MovePinnedContextItem(moveDownCommand, commandItem)); } private void TryAddPinToDockCommand( string itemId, string providerId, List<IContextItem> moreCommands, CommandItemViewModel commandItem) { if (!_settingsService.Settings.EnableDock) { return; } var inStartBands = _settingsService.Settings.DockSettings.StartBands.Any(band => MatchesBand(band, itemId, providerId)); var inCenterBands = _settingsService.Settings.DockSettings.CenterBands.Any(band => MatchesBand(band, itemId, providerId)); var inEndBands = _settingsService.Settings.DockSettings.EndBands.Any(band => MatchesBand(band, itemId, providerId)); var alreadyPinned = inStartBands || inCenterBands || inEndBands; /** && _settingsService.Settings.DockSettings.PinnedCommands.Contains(this.Id)**/ var pinToTopLevelCommand = new PinToCommand( commandId: itemId, providerId: providerId, pin: !alreadyPinned, PinLocation.Dock, _settingsService, _topLevelCommandManager, commandItemViewModel: commandItem, monitorService: _monitorService); 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 MovePinnedContextItem : CommandContextItem { private readonly MovePinnedCommand _command; private readonly CommandItemViewModel _commandItem; public MovePinnedContextItem(MovePinnedCommand command, CommandItemViewModel commandItem) : base(command) { _command = command; _commandItem = commandItem; command.MoveStateChanged += this.OnMoveStateChanged; } private void OnMoveStateChanged(object? sender, EventArgs e) { _commandItem.RefreshMoreCommands(); } ~MovePinnedContextItem() { _command.MoveStateChanged -= this.OnMoveStateChanged; } } private sealed partial class PinToCommand : InvokableCommand { private readonly string _commandId; private readonly string _providerId; private readonly ISettingsService _settingsService; private readonly TopLevelCommandManager _topLevelCommandManager; private readonly IMonitorService? _monitorService; private readonly bool _pin; private readonly PinLocation _pinLocation; private readonly CommandItemViewModel? _commandItemViewModel; 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, ISettingsService settingsService, TopLevelCommandManager topLevelCommandManager, CommandItemViewModel? commandItemViewModel = null, IMonitorService? monitorService = null) { _commandId = commandId; _providerId = providerId; _pinLocation = pinLocation; _settingsService = settingsService; _topLevelCommandManager = topLevelCommandManager; _pin = pin; _commandItemViewModel = commandItemViewModel; _monitorService = monitorService; } 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() { var title = _commandItemViewModel?.Title ?? string.Empty; var subtitle = _commandItemViewModel?.Subtitle ?? string.Empty; var icon = _commandItemViewModel?.Icon; var dockSettings = _settingsService.Settings.DockSettings; var dockSide = dockSettings.Side; IReadOnlyList<MonitorInfo>? monitors = GetDockEnabledMonitors(_monitorService, dockSettings); ShowPinToDockDialogMessage message = new(_providerId, _commandId, title, subtitle, icon, dockSide, monitors); WeakReferenceMessenger.Default.Send(message); } // Only list monitors where the dock is currently enabled, so users can't // pin a command to a display that has no dock visible. private static IReadOnlyList<MonitorInfo>? GetDockEnabledMonitors(IMonitorService? monitorService, DockSettings dockSettings) { var monitors = monitorService?.GetMonitors(); if (monitors is null) { return null; } var configs = dockSettings.MonitorConfigs; // When there are no per-monitor configs (legacy / first-run), the dock // is only shown on the primary monitor. if (configs.Count == 0) { return monitors.Where(m => m.IsPrimary).ToList(); } return monitors .Where(m => configs.Any(c => string.Equals(c.MonitorDeviceId, m.StableId, System.StringComparison.OrdinalIgnoreCase) && c.Enabled)) .ToList(); } private void UnpinFromDock() { PinToDockMessage message = new(_providerId, _commandId, false); WeakReferenceMessenger.Default.Send(message); } } private sealed partial class MovePinnedCommand : InvokableCommand { private readonly string _providerId; private readonly string _commandId; private readonly MovePinnedDirection _moveDirection; private readonly ISettingsService _settingsService; private readonly TopLevelCommandManager _topLevelCommandManager; public override IconInfo Icon => _moveDirection switch { MovePinnedDirection.ToTop => Icons.MoveToTopIcon, MovePinnedDirection.Up => Icons.MoveUpIcon, _ => Icons.MoveDownIcon, }; public override string Name => _moveDirection switch { MovePinnedDirection.ToTop => RS_.GetString("top_level_move_to_top_command_name"), MovePinnedDirection.Up => RS_.GetString("top_level_move_up_command_name"), _ => RS_.GetString("top_level_move_down_command_name"), }; internal event EventHandler? MoveStateChanged; public MovePinnedCommand( string providerId, string commandId, MovePinnedDirection moveDirection, ISettingsService settingsService, TopLevelCommandManager topLevelCommandManager) { _providerId = providerId; _commandId = commandId; _moveDirection = moveDirection; _settingsService = settingsService; _topLevelCommandManager = topLevelCommandManager; } public override CommandResult Invoke() { var moved = false; _settingsService.UpdateSettings( s => { var updated = _moveDirection switch { MovePinnedDirection.ToTop => s.TryMovePinnedCommandToTop(_providerId, _commandId), MovePinnedDirection.Up => s.TryMovePinnedCommand(_providerId, _commandId, true, IsLoaded), _ => s.TryMovePinnedCommand(_providerId, _commandId, false, IsLoaded), }; moved = !ReferenceEquals(updated, s); return updated; }, hotReload: false); if (moved) { WeakReferenceMessenger.Default.Send<UpdateFallbackItemsMessage>(); MoveStateChanged?.Invoke(this, EventArgs.Empty); } return CommandResult.KeepOpen(); // Pass a visibility check so moves skip stale pinned entries // (removed/disabled/failed extensions) that aren't shown on home. bool IsLoaded(PinnedCommandSettings pin) { return _topLevelCommandManager.LookupCommand(pin.CommandId) is TopLevelViewModel cmd && cmd.CommandProviderId == pin.ProviderId; } } } private enum MovePinnedDirection { ToTop, Up, Down, } }