From efc3c5e5c83675c98eddfa93f9dbfe80ea46654b Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 12 Feb 2026 12:59:15 -0600 Subject: [PATCH] CmdPal: Add Dock API (#45432) This doesn't actually add the dock. It just adds the API for it. Extension authors can use this to create their own dock bands. re: #45201 --- .github/actions/spell-check/allow/names.txt | 1 + .../doc/initial-sdk-spec/initial-sdk-spec.md | 85 +++++++++++++++++- .../CommandProvider.cs | 20 ++++- .../Dock/WrappedDockItem.cs | 62 +++++++++++++ .../Dock/WrappedDockList.cs | 86 +++++++++++++++++++ .../Microsoft.CommandPalette.Extensions.idl | 7 +- 6 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index 4df6c5c3e1..bea601b3d1 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -207,6 +207,7 @@ Bilibili BVID capturevideosample cmdow +Contoso Controlz cortana devhints 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 131129bd2d..57b5c4bd42 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 @@ -1,7 +1,7 @@ --- author: Mike Griese created on: 2024-07-19 -last updated: 2025-08-08 +last updated: 2026-02-05 issue id: n/a --- @@ -75,6 +75,8 @@ functionality. - [Advanced scenarios](#advanced-scenarios) - [Status messages](#status-messages) - [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) - [Class diagram](#class-diagram) - [Future considerations](#future-considerations) - [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments) @@ -2045,6 +2047,87 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`, developers won't have to do anything. The toolkit will just do the right thing for them. +## Addenda IV: Dock bands + +The "dock" is another way to surface commands to the user. This is a +toolbar-like window that can be docked to the side of the screen, or floated as +its own window. It enables another surface for extensions to display real-time +information and shortcuts to users. + +Bands are powered by the same interfaces as DevPal itself. Extensions can provide +bands via the new `DockBand` property on `ICommandProvider3`. + +```csharp +interface ICommandProvider3 requires ICommandProvider2 +{ + ICommandItem[] GetDockBands(); +}; +``` + +A **Dock Band** is one "strip of items" in the dock. Each band can have multiple +items. This allows an extension to create a strip of buttons that should all be +treated as a single unit. For example, a media player band will want probably +four items: +* one for the previous track +* one for play/pause +* one for next track +* and one to display the album art and track title + +`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem` +represents one band in the dock. These represent all of the bands that an +extension would allow the user to add to their dock. + +All of the `ICommandItem`s returned from `GetDockBands` **must** have a +`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will +ignore that band. + +Bands are not automatically added to the dock. Instead, the user must choose +which bands they want to add. This is done via the DevPal settings page. +Furthermore, bands are not displayed in the list of commands in DevPal itself. +This allows extension authors to create objects that are only intended for the +dock, without cluttering up the main DevPal UI, and vice versa. + +DevPal will then create UI in the dock for each band the user has chosen to add. +What that looks like will depend on the `Command` in the `ICommandItem`: +* A `IInvokableCommand` will be rendered as a single button. Think "the + time/date" button on the taskbar, that opens the notification center. +* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem` + in the list. Think "media controls" for a music player. +* A `IContentPage` will be rendered as a single button. Clicking that button + will open a flyout with that content rendered in it. Think "weather" or "news" + flyouts. + +If the `Command` in the `IListItem`s of a band are pages, then clicking those +buttons will open DevPal to that page, as if it were a flyout from the dock. + +The `.Title` property of the top-level `ICommandItem` representing the band will +be used as the name of the band in the settings. So a media player band might +want to set the `Title` to "Contoso Music Player", even if the individual +buttons in the band don't show that title. + +Users may also "pin" a top-level command from DevPal into the dock. DevPal will +take care of creating a new band (owned by devpal) with that command in it. This +allows users to add quick shortcuts to their favorite commands in the dock. +Think: pinning an app, or pinning a particular GitHub query. + +Bands are added via ID. An extension may choose to have a TopLevelCommand and a +DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand +to the dock, DevPal will pin the band from `GetDockBands`, rather than creating +a simple pinned command. This allows extension authors to seamlessly have a +top-level command present a palette-specific experience, while also having a +dock-specific experience. In our ongoing media player example, the top-level +command might open DevPal to a full-featured music control page, while the dock +band has simpler buttons on it (without a title/subtitle). + +Users may choose to have: +* the orientation of the dock: vertical or horizontal +* the size of the dock +* which bands are shown in the dock +* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are + shown or hidden. + - 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. + ## 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/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs index ca64c87b23..3ad4671263 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandProvider.cs @@ -6,7 +6,10 @@ using Windows.Foundation; namespace Microsoft.CommandPalette.Extensions.Toolkit; -public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2 +public abstract partial class CommandProvider : + ICommandProvider, + ICommandProvider2, + ICommandProvider3 { public virtual string Id { get; protected set; } = string.Empty; @@ -48,6 +51,21 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid } } + /// + /// Get the dock bands provided by this command provider. Dock bands are + /// strips of items that appear on various UI surfaces in CmdPal, such as a + /// toolbar. Each ICommandItem returned from this method will be treated as + /// one atomic band by cmdpal. + /// + /// If the command on an item here is a + /// IListPage, then cmdpal will render all of the items on that page as one + /// band. You can use this to create complex bands with multiple buttons. + /// + public virtual ICommandItem[]? GetDockBands() + { + return null; + } + /// /// This is used to manually populate the WinRT type cache in CmdPal with /// any interfaces that might not follow a straight linear path of requires. diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs new file mode 100644 index 0000000000..5cdfa5403b --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockItem.cs @@ -0,0 +1,62 @@ +// 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.CommandPalette.Extensions.Toolkit; + +/// +/// Helper class for creating a band out of a set of items. This allows you to +/// simply just instantiate a set of buttons as ListItems, then pass them in to +/// this class to create a band from those items. For example: +/// +/// ```cs +/// var foo = new MyFooListItem(); +/// var bar = new MyBarListItem(); +/// var band = new WrappedDockItem([foo, bar], "com.me.myBand", "My cool desk band"); +/// ``` +/// +public partial class WrappedDockItem : CommandItem +{ + public override string Title => _itemTitle; + + public override ICommand? Command => _backingList; + + private readonly string _itemTitle; + private readonly WrappedDockList _backingList; + + public IListItem[] Items { get => _backingList.GetItems(); set => _backingList.SetItems(value); } + + public WrappedDockItem( + ICommand command, + string displayTitle) + { + _backingList = new WrappedDockList(command); + _itemTitle = string.IsNullOrEmpty(displayTitle) ? command.Name : displayTitle; + Icon = command.Icon; + } + + // This was too much of a foot gun - we'd internally create a ListItem that + // didn't bubble the prop change events back up. That was bad. + // public WrappedDockItem( + // ICommandItem item, + // string id, + // string displayTitle) + // { + // _backingList = new WrappedDockList(item, id); + // _itemTitle = string.IsNullOrEmpty(displayTitle) ? item.Title : displayTitle; + // _icon = item.Icon; + // } + + /// + /// Initializes a new instance of the class. + /// Create a new dock band for a set of list items + /// + public WrappedDockItem( + IListItem[] items, + string id, + string displayTitle) + { + _backingList = new WrappedDockList(items, id, displayTitle); + _itemTitle = displayTitle; + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs new file mode 100644 index 0000000000..fbb30f0b17 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Dock/WrappedDockList.cs @@ -0,0 +1,86 @@ +// 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.CommandPalette.Extensions.Toolkit; + +/// +/// Helper class for a list page that just holds a set of items as a band. +/// The page itself doesn't do anything interesting. +/// +internal sealed partial class WrappedDockList : ListPage +{ + private string _id; + + public override string Id => _id; + + private List _items; + + internal WrappedDockList(ICommand command) + { + _items = new() { new ListItem(command) }; + Name = command.Name; + _id = command.Id; + } + + // Maybe revisit sometime. + // The hard problem is that the wrapping item will not + // listen for property changes on the inner item. + // public WrappedDockList(ICommandItem item, string id) + // { + // var command = item.Command; + // _items = new() + // { + // new ListItem(command) + // { + // Title = item.Title, + // Subtitle = item.Subtitle, + // Icon = item.Icon, + // MoreCommands = item.MoreCommands, + // }, + // }; + // Name = command.Name; + // _id = string.IsNullOrEmpty(id) ? command.Id : id; + // } + + /// + /// Initializes a new instance of the class. + /// Create a new list page for the set of items provided. + /// + internal WrappedDockList(IListItem[] items, string id, string name) + { + _items = new(items); + Name = name; + _id = id; + } + + internal WrappedDockList(ICommand[] items, string id, string name) + { + _items = new(); + foreach (var item in items) + { + _items.Add(new ListItem(item)); + } + + Name = name; + _id = id; + } + + public override IListItem[] GetItems() + { + return _items.ToArray(); + } + + internal void SetItems(IListItem[]? newItems) + { + if (newItems == null) + { + _items = []; + RaiseItemsChanged(0); + return; + } + + ListHelpers.InPlaceUpdateList(_items, newItems); + RaiseItemsChanged(_items.Count); + } +} 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 d7eed3bc15..dff98f6516 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 @@ -405,6 +405,11 @@ namespace Microsoft.CommandPalette.Extensions { Object[] GetApiExtensionStubs(); }; - + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandProvider3 requires ICommandProvider2 + { + ICommandItem[] GetDockBands(); + }; + }