diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs index 125d8d78f4..eab0a8522e 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs @@ -165,4 +165,6 @@ public interface IAppHostService AppExtensionHost GetDefaultHost(); AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost); + + CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext); } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandProviderContext.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandProviderContext.cs new file mode 100644 index 0000000000..948a911dd9 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandProviderContext.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.CmdPal.Core.ViewModels; + +public sealed class CommandProviderContext +{ + public required string ProviderId { get; init; } + + public static CommandProviderContext Empty { get; } = new() { ProviderId = "" }; +} diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs index b30d02ce83..0163868f9b 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs @@ -47,8 +47,8 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] - public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host) - : base(model, scheduler, host) + public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) + : base(model, scheduler, host, providerContext) { _model = new(model); } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs index bbfbcadcea..d1f59e268b 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs @@ -89,8 +89,8 @@ public partial class ListViewModel : PageViewModel, IDisposable } } - public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host) - : base(model, scheduler, host) + public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) + : base(model, scheduler, host, providerContext) { _model = new(model); EmptyContent = new(new(null), PageContext); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/LoadingPageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/LoadingPageViewModel.cs index 3e2cd420c2..ffd405c884 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/LoadingPageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/LoadingPageViewModel.cs @@ -9,7 +9,7 @@ namespace Microsoft.CmdPal.Core.ViewModels; public partial class LoadingPageViewModel : PageViewModel { public LoadingPageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost host) - : base(model, scheduler, host) + : base(model, scheduler, host, CommandProviderContext.Empty) { ModelIsLoading = true; IsInitialized = false; diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/NullPageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/NullPageViewModel.cs index 504eef6af1..e2427f811c 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/NullPageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/NullPageViewModel.cs @@ -5,4 +5,4 @@ namespace Microsoft.CmdPal.Core.ViewModels; internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost) - : PageViewModel(null, scheduler, extensionHost); + : PageViewModel(null, scheduler, extensionHost, CommandProviderContext.Empty); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 3b08b9266b..27e3129cbb 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -76,13 +76,16 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public IconInfoViewModel Icon { get; protected set; } - public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost) + public CommandProviderContext ProviderContext { get; protected set; } + + public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, CommandProviderContext providerContext) : base(scheduler) { InitializeSelfAsPageContext(); _pageModel = new(model); Scheduler = scheduler; ExtensionHost = extensionHost; + ProviderContext = providerContext; Icon = new(null); ExtensionHost.StatusMessages.CollectionChanged += StatusMessages_CollectionChanged; @@ -275,5 +278,5 @@ public interface IPageViewModelFactoryService /// Indicates whether the page is not the top-level page. /// The command palette host that will host the page (for status messages) /// A new instance of the page view model. - PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host); + PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext); } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index 62c70076ad..ff64e6f6ca 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -258,6 +258,7 @@ public partial class ShellViewModel : ObservableObject, } var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost); + var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext); _rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host); @@ -273,15 +274,15 @@ public partial class ShellViewModel : ObservableObject, // Telemetry: Track extension page navigation for session metrics if (host is not null) { - string extensionId = host.GetExtensionDisplayName() ?? "builtin"; - string commandId = command?.Id ?? "unknown"; - string commandName = command?.Name ?? "unknown"; + var extensionId = host.GetExtensionDisplayName() ?? "builtin"; + var commandId = command?.Id ?? "unknown"; + var commandName = command?.Name ?? "unknown"; WeakReferenceMessenger.Default.Send( new(extensionId, commandId, commandName, true, 0)); } // Construct our ViewModel of the appropriate type and pass it the UI Thread context. - var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!); + var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!, providerContext); if (pageViewModel is null) { CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); @@ -352,10 +353,10 @@ public partial class ShellViewModel : ObservableObject, // Telemetry: Track command execution time and success var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var command = message.Command.Unsafe; - string extensionId = host?.GetExtensionDisplayName() ?? "builtin"; - string commandId = command?.Id ?? "unknown"; - string commandName = command?.Name ?? "unknown"; - bool success = false; + var extensionId = host?.GetExtensionDisplayName() ?? "builtin"; + var commandId = command?.Id ?? "unknown"; + var commandName = command?.Name ?? "unknown"; + var success = false; try { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs index e3cd8a92d7..3312cac2f9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteContentPageViewModel.cs @@ -9,8 +9,8 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandPaletteContentPageViewModel : ContentPageViewModel { - public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host) - : base(model, scheduler, host) + public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) + : base(model, scheduler, host, providerContext) { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs index 90e70d7fef..0c8397b3c4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs @@ -17,12 +17,12 @@ public class CommandPalettePageViewModelFactory _scheduler = scheduler; } - public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host) + public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext) { return page switch { - IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested }, - IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host), + IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext) { IsNested = nested }, + IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext), _ => null, }; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index d0f73f4a12..2e2c009db7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; using Windows.Foundation; @@ -158,6 +159,9 @@ public sealed class CommandProviderWrapper UnsafePreCacheApiAdditions(two); } + // Load pinned commands from saved settings + var pinnedCommands = LoadPinnedCommands(model, providerSettings); + Id = model.Id; DisplayName = model.DisplayName; Icon = new(model.Icon); @@ -175,7 +179,7 @@ public sealed class CommandProviderWrapper Settings = new(model.Settings, this, _taskScheduler); // We do need to explicitly initialize commands though - InitializeCommands(commands, fallbacks, serviceProvider, pageContext); + InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext); Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})"); } @@ -206,27 +210,34 @@ public sealed class CommandProviderWrapper } } - private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference pageContext) + private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference pageContext) { var settings = serviceProvider.GetService()!; var providerSettings = GetProviderSettings(settings); - + var ourContext = GetProviderContext(); var makeAndAdd = (ICommandItem? i, bool fallback) => { CommandItemViewModel commandItemViewModel = new(new(i), pageContext); - TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i); + TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i); topLevelViewModel.InitializeProperties(); return topLevelViewModel; }; + var topLevelList = new List(); + if (commands is not null) { - TopLevelItems = commands - .Select(c => makeAndAdd(c, false)) - .ToArray(); + topLevelList.AddRange(commands.Select(c => makeAndAdd(c, false))); } + if (pinnedCommands is not null) + { + topLevelList.AddRange(pinnedCommands.Select(c => makeAndAdd(c, false))); + } + + TopLevelItems = topLevelList.ToArray(); + if (fallbacks is not null) { FallbackItems = fallbacks @@ -235,6 +246,32 @@ public sealed class CommandProviderWrapper } } + private ICommandItem[] LoadPinnedCommands(ICommandProvider model, ProviderSettings providerSettings) + { + var pinnedItems = new List(); + + if (model is ICommandProvider4 provider4) + { + foreach (var pinnedId in providerSettings.PinnedCommandIds) + { + try + { + var commandItem = provider4.GetCommandItem(pinnedId); + if (commandItem is not null) + { + pinnedItems.Add(commandItem); + } + } + catch (Exception e) + { + Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}"); + } + } + } + + return pinnedItems.ToArray(); + } + private void UnsafePreCacheApiAdditions(ICommandProvider2 provider) { var apiExtensions = provider.GetApiExtensionStubs(); @@ -248,6 +285,26 @@ public sealed class CommandProviderWrapper } } + public void PinCommand(string commandId, IServiceProvider serviceProvider) + { + var settings = serviceProvider.GetService()!; + var providerSettings = GetProviderSettings(settings); + + if (!providerSettings.PinnedCommandIds.Contains(commandId)) + { + providerSettings.PinnedCommandIds.Add(commandId); + SettingsModel.SaveSettings(settings); + + // Raise CommandsChanged so the TopLevelCommandManager reloads our commands + this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1)); + } + } + + public CommandProviderContext GetProviderContext() + { + return new() { ProviderId = ProviderId }; + } + public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid; public override int GetHashCode() => _commandProvider.GetHashCode(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs index fe440cdaa7..7b5d5f06c6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -31,7 +31,7 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, if (model.SettingsPage is not null) { - SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost); + SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost, provider.GetProviderContext()); SettingsPage.InitializeProperties(); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PinCommandItemMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PinCommandItemMessage.cs new file mode 100644 index 0000000000..c9b9f00506 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PinCommandItemMessage.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record PinCommandItemMessage(string ProviderId, string CommandId) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs index 3bb9a43360..419d6fbbe5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -18,6 +18,8 @@ public class ProviderSettings public Dictionary FallbackCommands { get; set; } = new(); + public List PinnedCommandIds { get; set; } = []; + [JsonIgnore] public string ProviderDisplayName { get; set; } = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index 4473a1e144..1de71f59a3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -22,6 +22,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class TopLevelCommandManager : ObservableObject, IRecipient, + IRecipient, IPageContext, IDisposable { @@ -42,6 +43,7 @@ public partial class TopLevelCommandManager : ObservableObject, _commandProviderCache = commandProviderCache; _taskScheduler = _serviceProvider.GetService()!; WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); _reloadCommandsGate = new(ReloadAllCommandsAsyncCore); } @@ -414,6 +416,21 @@ public partial class TopLevelCommandManager : ObservableObject, public void Receive(ReloadCommandsMessage message) => ReloadAllCommandsAsync().ConfigureAwait(false); + public void Receive(PinCommandItemMessage message) + { + var wrapper = LookupProvider(message.ProviderId); + wrapper?.PinCommand(message.CommandId, _serviceProvider); + } + + private CommandProviderWrapper? LookupProvider(string providerId) + { + lock (_commandProvidersLock) + { + return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId) + ?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId); + } + } + void IPageContext.ShowException(Exception ex, string? extensionHint) { var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager"); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index 13b9423119..1fa9682f33 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -27,7 +27,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx private readonly IServiceProvider _serviceProvider; private readonly CommandItemViewModel _commandItemViewModel; - private readonly string _commandProviderId; + public CommandProviderContext ProviderContext { get; private set; } private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id; @@ -57,7 +57,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx public CommandItemViewModel ItemViewModel => _commandItemViewModel; - public string CommandProviderId => _commandProviderId; + public string CommandProviderId => ProviderContext.ProviderId; ////// ICommandItem public string Title => _commandItemViewModel.Title; @@ -190,7 +190,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx CommandItemViewModel item, bool isFallback, CommandPaletteHost extensionHost, - string commandProviderId, + CommandProviderContext commandProviderContext, SettingsModel settings, ProviderSettings providerSettings, IServiceProvider serviceProvider, @@ -199,7 +199,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx _serviceProvider = serviceProvider; _settings = settings; _providerSettings = providerSettings; - _commandProviderId = commandProviderId; + ProviderContext = commandProviderContext; _commandItemViewModel = item; IsFallback = isFallback; @@ -358,8 +358,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx { // Use WyHash64 to generate stable ID hashes. // manually seeding with 0, so that the hash is stable across launches - var result = WyHash64.ComputeHash64(_commandProviderId + DisplayTitle + Title + Subtitle, seed: 0); - _generatedId = $"{_commandProviderId}{result}"; + var result = WyHash64.ComputeHash64(CommandProviderId + DisplayTitle + Title + Subtitle, seed: 0); + _generatedId = $"{CommandProviderId}{result}"; } private void DoOnUiThread(Action action) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs index 7edf0a34c9..95a2c24aa6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/PowerToysAppHostService.cs @@ -26,4 +26,15 @@ internal sealed class PowerToysAppHostService : IAppHostService return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance; } + + public CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext) + { + CommandProviderContext? topLevelId = null; + if (command is TopLevelViewModel topLevelViewModel) + { + topLevelId = topLevelViewModel.ProviderContext; + } + + return topLevelId ?? currentContext ?? throw new InvalidOperationException("No command provider context could be found for the given command, and no current context was provided."); + } } diff --git a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md index 57b5c4bd42..66ecbf34b6 100644 --- a/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md +++ b/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md @@ -77,6 +77,7 @@ functionality. - [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus) - [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2) - [Addenda IV: Dock bands](#addenda-iv-dock-bands) + - [Pinning nested commands to the dock (and top level)](#pinning-nested-commands-to-the-dock-and-top-level) - [Class diagram](#class-diagram) - [Future considerations](#future-considerations) - [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments) @@ -2128,6 +2129,36 @@ Users may choose to have: - Dock bands will still display the `Title` & `Subtitle` of each item in the band as the tooltip on those items, even when the "labels" are hidden. +### Pinning nested commands to the dock (and top level) + +We'll use another command provider method to allow the host to ask extensions +for items based on their ID. + +```csharp +interface ICommandProvider4 requires ICommandProvider3 +{ + ICommandItem GetCommandItem(String id); +}; +``` + +This will allow users to pin not just top-level commands, but also nested +commands which have an ID. The host can store that ID away, and then later ask +the extension for the `ICommandItem` with that ID, to get the full details of +the command to pin. + +This is needed separate from the `GetCommand` method on `ICommandProvider`, +because that method is was designed for two main purposes: + +* Short-circuiting the loading of top-level commands for frozen extensions. In + that case, DevPal would only need to look up the actual `ICommand` to perform + it. It wouldn't need the full `ICommandItem` with all the details. +* Allowing invokable commands to navigate using the GoToPageArgs. In that case, + DevPal would only need the `ICommand` to perform the navigation. + +In neither of those scenarios was the full "display" of the item needed. In +pinning scenarios, however, we need everything that the user would see in the UI +for that item, which is all in the `ICommandItem`. + ## Class diagram This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index 317087847e..a223dcab01 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -184,6 +184,20 @@ public partial class AllAppsCommandProvider : CommandProvider return null; } + public override ICommandItem? GetCommandItem(string id) + { + var items = _page.GetItems(); + foreach (var item in items) + { + if (item.Command.Id == id) + { + return item; + } + } + + return null; + } + private void OnPinStateChanged(object? sender, System.EventArgs e) { RaiseItemsChanged(0); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs index 9448cc0f2f..fbf086063b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -42,37 +42,61 @@ internal static class Commands var results = new List(); results.AddRange(new[] { - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true))) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_shutdown, confirmCommands, Resources.Microsoft_plugin_sys_shutdown_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/s /hybrid /t 0", runWithHiddenWindow: true)) + { + Id = "com.microsoft.cmdpal.builtin.system.shutdown", + }) { Title = Resources.Microsoft_plugin_sys_shutdown_computer, Subtitle = Resources.Microsoft_plugin_sys_shutdown_computer_description, Icon = Icons.ShutdownIcon, }, - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true))) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_restart, confirmCommands, Resources.Microsoft_plugin_sys_restart_computer_confirmation, () => OpenInShellHelper.OpenInShell("shutdown", "/g /t 0", runWithHiddenWindow: true)) + { + Id = "com.microsoft.cmdpal.builtin.system.restart", + }) { Title = Resources.Microsoft_plugin_sys_restart_computer, Subtitle = Resources.Microsoft_plugin_sys_restart_computer_description, Icon = Icons.RestartIcon, }, - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0))) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_signout, confirmCommands, Resources.Microsoft_plugin_sys_sign_out_confirmation, () => NativeMethods.ExitWindowsEx(EWXLOGOFF, 0)) + { + Id = "com.microsoft.cmdpal.builtin.system.signout", + }) { Title = Resources.Microsoft_plugin_sys_sign_out, Subtitle = Resources.Microsoft_plugin_sys_sign_out_description, Icon = Icons.LogoffIcon, }, - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation())) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_lock, confirmCommands, Resources.Microsoft_plugin_sys_lock_confirmation, () => NativeMethods.LockWorkStation()) + { + Id = "com.microsoft.cmdpal.builtin.system.lock", + }) { Title = Resources.Microsoft_plugin_sys_lock, Subtitle = Resources.Microsoft_plugin_sys_lock_description, Icon = Icons.LockIcon, }, - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true))) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_sleep, confirmCommands, Resources.Microsoft_plugin_sys_sleep_confirmation, () => NativeMethods.SetSuspendState(false, true, true)) + { + Id = "com.microsoft.cmdpal.builtin.system.sleep", + }) { Title = Resources.Microsoft_plugin_sys_sleep, Subtitle = Resources.Microsoft_plugin_sys_sleep_description, Icon = Icons.SleepIcon, }, - new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true))) + new ListItem( + new ExecuteCommandConfirmation(Resources.Microsoft_plugin_command_name_hibernate, confirmCommands, Resources.Microsoft_plugin_sys_hibernate_confirmation, () => NativeMethods.SetSuspendState(true, true, true)) + { + Id = "com.microsoft.cmdpal.builtin.system.hibernate", + }) { Title = Resources.Microsoft_plugin_sys_hibernate, Subtitle = Resources.Microsoft_plugin_sys_hibernate_description, @@ -85,13 +109,19 @@ internal static class Commands { results.AddRange(new[] { - new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")) + new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder") + { + Id = "com.microsoft.cmdpal.builtin.system.recycle_bin", + }) { Title = Resources.Microsoft_plugin_sys_RecycleBinOpen, Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description, Icon = Icons.RecycleBinIcon, }, - new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage)) + new ListItem(new EmptyRecycleBinConfirmation(emptyRBSuccessMessage) + { + Id = "com.microsoft.cmdpal.builtin.system.empty_recycle_bin", + }) { Title = Resources.Microsoft_plugin_sys_RecycleBinEmptyResult, Subtitle = Resources.Microsoft_plugin_sys_RecycleBinEmpty_description, @@ -102,7 +132,10 @@ internal static class Commands else { results.Add( - new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder")) + new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder") + { + Id = "com.microsoft.cmdpal.builtin.system.recycle_bin", + }) { Title = Resources.Microsoft_plugin_sys_RecycleBin, Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description, @@ -110,7 +143,15 @@ internal static class Commands }); } - results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true))) + results.Add(new ListItem( + new ExecuteCommandConfirmation( + Resources.Microsoft_plugin_sys_RestartShell_name!, + confirmCommands, + Resources.Microsoft_plugin_sys_RestartShell_confirmation!, + static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true)) + { + Id = "com.microsoft.cmdpal.builtin.system.restart_shell", + }) { Title = Resources.Microsoft_plugin_sys_RestartShell!, Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!, @@ -141,19 +182,19 @@ internal static class Commands var results = new List(); // We update the cache only if the last query is older than 'updateCacheIntervalSeconds' seconds - DateTime timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery; + var timeOfLastNetworkQueryBefore = timeOfLastNetworkQuery; timeOfLastNetworkQuery = DateTime.Now; // Set time of last query to this query if ((timeOfLastNetworkQuery - timeOfLastNetworkQueryBefore).TotalSeconds >= UpdateCacheIntervalSeconds) { networkPropertiesCache = NetworkConnectionProperties.GetList(); } - CompositeFormat sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description); - CompositeFormat sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description); - CompositeFormat sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description); + var sysIpv4DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip4_description); + var sysIpv6DescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_ip6_description); + var sysMacDescriptionCompositeFormate = CompositeFormat.Parse(Resources.Microsoft_plugin_sys_mac_description); var hideDisconnectedNetworkInfo = manager.HideDisconnectedNetworkInfo(); - foreach (NetworkConnectionProperties intInfo in networkPropertiesCache) + foreach (var intInfo in networkPropertiesCache) { if (hideDisconnectedNetworkInfo) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs index 4bc86c209d..bf3a0f79e2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -39,4 +39,18 @@ public sealed partial class SystemCommandExtensionProvider : CommandProvider } public override IFallbackCommandItem[] FallbackCommands() => [_fallbackSystemItem]; + + public override ICommandItem? GetCommandItem(string id) + { + var everything = Page.GetItems(); + foreach (var item in everything) + { + if (item.Command.Id == id) + { + return item; + } + } + + return null; + } } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs index 3ad4671263..769d7010f3 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs @@ -9,7 +9,8 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit; public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2, - ICommandProvider3 + ICommandProvider3, + ICommandProvider4 { public virtual string Id { get; protected set; } = string.Empty; @@ -25,6 +26,8 @@ public abstract partial class CommandProvider : public virtual ICommand? GetCommand(string id) => null; + public virtual ICommandItem? GetCommandItem(string id) => null; + public virtual ICommandSettings? Settings { get; protected set; } public virtual bool Frozen { get; protected set; } = true; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl index dff98f6516..7836a3aac3 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl @@ -411,5 +411,11 @@ namespace Microsoft.CommandPalette.Extensions { ICommandItem[] GetDockBands(); }; - + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandProvider4 requires ICommandProvider3 + { + ICommandItem GetCommandItem(String id); + }; + }