mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
CmdPal: Load pinned command items from anywhere (#45566)
This doesn't actually have a UX to expose this yet - we need to stack a
couple of PRs up to get to that.
But this adds plumbing such that we can now stash away a command ID, and
retrieve it later as a top-level command. Kinda like pinning for apps,
but for _anything_.
It works off of a new command provider interface `ICommandProvider4`,
which lets us look up Command**Item**s by ID. If we see a command ID
stored in that command provider's settings, we will try to look it up,
and then load it from the command provider.
e.g.
```json
"com.microsoft.cmdpal.builtin.system": {
"IsEnabled": true,
"FallbackCommands": {
"com.microsoft.cmdpal.builtin.system.fallback": {
"IsEnabled": true,
"IncludeInGlobalResults": true
}
},
"PinnedCommandIds": [
"com.microsoft.cmdpal.builtin.system.lock",
"com.microsoft.cmdpal.builtin.system.restart_shell"
]
},
```
will get us
<img width="840" height="197" alt="image"
src="https://github.com/user-attachments/assets/9ed19003-8361-4318-8dc9-055414456a51"
/>
Then it's just a matter of plumbing the command provider ID through the
layers, so that the command item knows who it is from. We'll need that
later for actually wiring this to the command's context menu.
related to #45191
related to #45201
This commit is contained in:
@@ -165,4 +165,6 @@ public interface IAppHostService
|
||||
AppExtensionHost GetDefaultHost();
|
||||
|
||||
AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost);
|
||||
|
||||
CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext);
|
||||
}
|
||||
|
||||
@@ -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 = "<EMPTY>" };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
/// <param name="nested">Indicates whether the page is not the top-level page.</param>
|
||||
/// <param name="host">The command palette host that will host the page (for status messages)</param>
|
||||
/// <returns>A new instance of the page view model.</returns>
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host);
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext);
|
||||
}
|
||||
|
||||
@@ -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<TelemetryExtensionInvokedMessage>(
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<IPageContext> pageContext)
|
||||
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
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<TopLevelViewModel>();
|
||||
|
||||
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<ICommandItem>();
|
||||
|
||||
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<SettingsModel>()!;
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public class ProviderSettings
|
||||
|
||||
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
|
||||
|
||||
public List<string> PinnedCommandIds { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public string ProviderDisplayName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class TopLevelCommandManager : ObservableObject,
|
||||
IRecipient<ReloadCommandsMessage>,
|
||||
IRecipient<PinCommandItemMessage>,
|
||||
IPageContext,
|
||||
IDisposable
|
||||
{
|
||||
@@ -42,6 +43,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_commandProviderCache = commandProviderCache;
|
||||
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
|
||||
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<IListItem>();
|
||||
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<IListItem>();
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -411,5 +411,11 @@ namespace Microsoft.CommandPalette.Extensions
|
||||
{
|
||||
ICommandItem[] GetDockBands();
|
||||
};
|
||||
|
||||
|
||||
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||
interface ICommandProvider4 requires ICommandProvider3
|
||||
{
|
||||
ICommandItem GetCommandItem(String id);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user