From e8426210362576c95f2db8431d7117a6e3dca437 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 21 Aug 2025 16:53:00 -0500 Subject: [PATCH] CmdPal: Make it easier to add APIs in the future (#41056) We learned a lot about adding interfaces in WinRT this week. I figured I'd send a PR to write it all down. --- .../CommandViewModel.cs | 36 ++++++ .../IconDataViewModel.cs | 19 +++ .../CommandProviderWrapper.cs | 18 +++ .../Helpers/IconCacheService.cs | 2 +- .../IconPathConverter.cpp | 16 ++- .../Microsoft.Terminal.UI/IconPathConverter.h | 2 +- .../IconPathConverter.idl | 2 +- .../doc/initial-sdk-spec/initial-sdk-spec.md | 117 +++++++++++++++++- .../Pages/SampleListPage.cs | 58 +++++++++ .../Properties/launchSettings.json | 2 +- .../CommandProvider.cs | 24 +++- .../FontIconData.cs | 32 +++++ .../IconData.cs | 1 + .../IconInfo.cs | 6 + .../Microsoft.CommandPalette.Extensions.idl | 12 ++ 15 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs index 30a85045d3..b2c03411d1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs @@ -29,6 +29,15 @@ public partial class CommandViewModel : ExtensionObjectViewModel public IconInfoViewModel Icon { get; private set; } + // UNDER NO CIRCUMSTANCES MAY SOMEONE WRITE TO THIS DICTIONARY. + // This is our copy of the data from the extension. + // Adding values to it does not add to the extension. + // Modifying it will not modify the extension + // (except it might, if the dictionary was passed by ref) + private Dictionary>? _properties; + + public IReadOnlyDictionary>? Properties => _properties?.AsReadOnly(); + public CommandViewModel(ICommand? command, WeakReference pageContext) : base(pageContext) { @@ -80,6 +89,11 @@ public partial class CommandViewModel : ExtensionObjectViewModel UpdateProperty(nameof(Icon)); } + if (model is IExtendedAttributesProvider command2) + { + UpdatePropertiesFromExtension(command2); + } + model.PropChanged += Model_PropChanged; } @@ -130,4 +144,26 @@ public partial class CommandViewModel : ExtensionObjectViewModel model.PropChanged -= Model_PropChanged; } } + + private void UpdatePropertiesFromExtension(IExtendedAttributesProvider? model) + { + var propertiesFromExtension = model?.GetProperties(); + if (propertiesFromExtension == null) + { + _properties = null; + return; + } + + _properties = []; + + // COPY the properties into us. + // The IDictionary that was passed to us may be marshalled by-ref or by-value, we _don't know_. + // + // If it's by-ref, the values are arbitrary objects that are out-of-proc. + // If it's bu-value, then everything is in-proc, and we can't mutate the data. + foreach (var property in propertiesFromExtension) + { + _properties.Add(property.Key, new(property.Value)); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs index 5f4b4436f2..76b5f786c8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs @@ -27,6 +27,8 @@ public partial class IconDataViewModel : ObservableObject, IIconData IRandomAccessStreamReference? IIconData.Data => Data.Unsafe; + public string? FontFamily { get; private set; } + public IconDataViewModel(IIconData? icon) { _model = new(icon); @@ -43,5 +45,22 @@ public partial class IconDataViewModel : ObservableObject, IIconData Icon = model.Icon; Data = new(model.Data); + + if (model is IExtendedAttributesProvider icon2) + { + var props = icon2.GetProperties(); + + // From Raymond Chen: + // Make sure you don't try do do something like + // icon2.GetProperties().TryGetValue("awesomeKey", out var awesomeValue); + // icon2.GetProperties().TryGetValue("slackerKey", out var slackerValue); + // because each call to GetProperties() is a cross process hop, and if you + // marshal-by-value the property set, then you don't want to throw it away and + // re-marshal it for every property. MAKE SURE YOU CACHE IT. + if (props?.TryGetValue("FontFamily", out var family) ?? false) + { + FontFamily = family as string; + } + } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 59903d7ed8..00cbe03d48 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -153,6 +153,11 @@ public sealed class CommandProviderWrapper // On a BG thread here fallbacks = model.FallbackCommands(); + if (model is ICommandProvider2 two) + { + UnsafePreCacheApiAdditions(two); + } + Id = model.Id; DisplayName = model.DisplayName; Icon = new(model.Icon); @@ -203,6 +208,19 @@ public sealed class CommandProviderWrapper } } + private void UnsafePreCacheApiAdditions(ICommandProvider2 provider) + { + var apiExtensions = provider.GetApiExtensionStubs(); + Logger.LogDebug($"Provider supports {apiExtensions.Length} extensions"); + foreach (var a in apiExtensions) + { + if (a is IExtendedAttributesProvider command2) + { + Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider"); + } + } + } + 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/Helpers/IconCacheService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs index 5d058166ff..a506344f61 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/IconCacheService.cs @@ -25,7 +25,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue) { if (!string.IsNullOrEmpty(icon.Icon)) { - var source = IconPathConverter.IconSourceMUX(icon.Icon, false); + var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily); return source; } else if (icon.Data is not null) diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp index 935bcb936f..62a704f390 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp @@ -158,7 +158,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation // Return Value: // - An IconElement with its IconSource set, if possible. template - TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const int targetSize) + TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize) { TIconSource iconSource{ nullptr }; @@ -187,6 +187,11 @@ namespace winrt::Microsoft::Terminal::UI::implementation { icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); } + else if (!fontFamily.empty()) + { + icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily }); + + } else { // Note: you _do_ need to manually set the font here. @@ -225,9 +230,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation // return _getIconSource(path, false); // } - static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const int targetSize) + static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize) { - return _getIconSource(path, monochrome, targetSize); + return _getIconSource(path, monochrome, fontFamily, targetSize); } static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon, @@ -343,13 +348,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath, const bool monochrome, + const winrt::hstring& fontFamily, const int targetSize) { std::wstring_view iconPathWithoutIndex; const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); if (!indexOpt.has_value()) { - return _IconSourceMUX(iconPath, monochrome, targetSize); + return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize); } const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize); @@ -369,7 +375,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); if (!indexOpt.has_value()) { - auto source = IconSourceMUX(iconPath, false, targetSize); + auto source = IconSourceMUX(iconPath, false, L"", targetSize); Microsoft::UI::Xaml::Controls::IconSourceElement icon; icon.IconSource(source); return icon; diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h index 74f6311a23..8f504eb8c7 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h @@ -10,7 +10,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation //static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath); //static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath); - static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24); + static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24); static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath); static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize); diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl index 5b6f677003..63f2aa93f4 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl @@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI { // static Windows.UI.Xaml.Controls.IconElement IconWUX(String path); // static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path); - static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale); + static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily); static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path); static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize); }; 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 421ba6b50f..b64c2f09f3 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-03-10 +last updated: 2025-08-08 issue id: n/a --- @@ -1410,8 +1410,8 @@ interface IDetailsLink requires IDetailsData { Windows.Foundation.Uri Link { get; }; String Text { get; }; } -interface IDetailsCommand requires IDetailsData { - ICommand Command { get; }; +interface IDetailsCommands requires IDetailsData { + ICommand[] Commands { get; }; } [uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")] interface IDetailsSeparator requires IDetailsData {} @@ -1936,6 +1936,115 @@ When displaying a page: * The title will be `IPage.Title ?? ICommand.Name` * The icon will be `ICommand.Icon` +## Addenda I: API additions (ICommandProvider2) + +In experiments with extending our API, we've found some quirks with the way +that we use WinRT's metadata-based marshalling (MBM). Typically, you'd add +another contract version, add the new runtimeclass under the new contract +version, and then have the client app just check if that contract is available. + +However, we're not using `runtimeclass`es that are exposed from the extensions. +Everything is being transferred over MBM, based on the +`Microsoft.CommandPalette.Extensions.winmd`. And out-of-proc MBM has some +limitations. You can essentially only have a linear chain of requires for +extension interfaces. + +> E.g. if it implements `IWidget2` and `IWidget2 requires IWidget`, and the object's `GetRuntimeClassName` gives `IWidget2`, we know to look at `IWidget2` directly and `IWidget` due to requires. +> +> The unfortunate thing for the developer experience when authoring an extension with cppwinrt/CsWinRT implementations of interfaces, is they implement each interface separately. So the `IInspectable::GetRuntimeClassName` method inherited by `Interface1` gives `"Interface1"` and the method inherited by `Interface2` gives `"Interface2"`. +> +> Only one of these interfaces can be what the object responds to with a QI for `IInspectable`, and that's the implementation that MBM calls. + +That means we can't just add another interface easily. But what we can do: + +> It might be possible to prefill the cache with the interfaces in question by +> marshaling objects that implement each of the interfaces in a way that +> registration-free MBM can work with. +> +> E.g. to keep it simple, marshal an +> instance of a separate implementation class per interface that "implements" +> each interface + +So that's exactly what we're going to do, because it works. As an example, +we're going to add the following interface to our API: + +```csharp +interface IExtendedAttributesProvider +{ + Windows.Foundation.Collections.IMap GetProperties(); +}; + +interface ICommandProvider2 requires ICommandProvider +{ + Object[] GetApiExtensionStubs(); +}; +``` + +`IExtendedAttributesProvider` is just a simple interface, indicating that there's some +property bag of additional values that the host could read. We're starting with +this, because it's a helpful tool for us to add arbitrary properties to object +in an experimental fashion. We can continue to add more things we read from +this property set, without breaking the ABI. + +As an example, `ICommand` proves uniquely challenging to extend, because it has +both the `IInvokableCommand` and `IPage` family trees of interfaces which +extend from it. Typically, it would be impossible for a class to be defined as + +```cs +class MyCommandWithProperties : IInvokableCommand, IExtendedAttributesProvider { ... } +``` + +because Command Palette would only ever see the _first_ interface +(`IInvokableCommand`) via MBM, and would never be able to check if an extension +object was an `IExtendedAttributesProvider`. But a class defined like + +```cs +class CommandWithOnlyProperties : IExtendedAttributesProvider { ... } +``` + +will populate the WinRT type cache in Command Palette with the type information +for `ICommandWithProperties`. In fact, if Command Palette has the +`IExtendedAttributesProvider` type info in it's cache, and then later receives a new +`MyCommandWithProperties` object, it'll actually be able to know that +`MyCommandWithProperties` is an `IExtendedAttributesProvider`. WinRT is just weird +like that some times. + +`ICommandProvider2` is where the magic happens. This is a _linear_ addition to +`ICommandProvider`, which merely adds a method to return a set of objects. +Extensions can implement that method, by returning out stub implementations of +all the future additions to the API that we may add. In so doing, CmdPal will +be able to ask each extension for these stubs, pre-load the type cache for each +extension, and then never have to worry in the future. + +As an example: + +```cs +public partial class SamplePagesCommandsProvider : CommandProvider, ICommandProvider2 { + public SamplePagesCommandsProvider() { + DisplayName = "Sample Pages Commands"; + Icon = new IconInfo("\uE82D"); + } + public override ICommandItem[] TopLevelCommands() { + return [ + new CommandItem(new SamplesListPage()) { Title = "Sample Pages", Subtitle = "View example commands" }, + ]; + } + + // Here is where we enable support for future additions to the API + public object[] GetApiExtensionStubs() { + return [new SupportCommandsWithProperties()]; + } + private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider { + public IDictionary? GetProperties() => null; + } +} + +``` + +Fortunately, we can put all of that (`GetApiExtensionStubs`, +`SupportCommandsWithProperties`) directly in `Toolkit.CommandProvider`, so +developers won't have to do anything. The toolkit will just do the right thing +for them. ## Class diagram @@ -2210,6 +2319,8 @@ this prevents us from being able to use `[contract]` attributes to add to the interfaces. We'll instead need to rely on the tried-and-true method of adding a `IFoo2` when we want to add methods to `IFoo`. +[Addenda I](#addenda-i-api-additions-icommandprovider2) talks a little more on some of the challenges with adding more APIs. + [^1]: In this example, as in other places, I've referenced a `Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action. Our SDK will include partial class implementations for interfaces like diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs index 4d0cd8b6f4..85e9cc8083 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs @@ -2,8 +2,11 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; using Windows.System; using Windows.Win32; @@ -169,6 +172,61 @@ internal sealed partial class SampleListPage : ListPage { Title = "Get the name of the Foreground window", }, + + new ListItem(new CommandWithProperties()) + { + Title = "I have properties", + }, + new ListItem(new OtherCommandWithProperties()) + { + Title = "I also have properties", + }, ]; } + + internal sealed partial class CommandWithProperties : InvokableCommand, IExtendedAttributesProvider + { + private FontIconData _icon = new("\u0026", "Wingdings"); + + public override IconInfo Icon => new(_icon, _icon); + + public override string Name => "Whatever"; + + // LOAD-BEARING: Use a Windows.Foundation.Collections.ValueSet as the + // backing store for Properties. A regular `Dictionary` + // will not work across the ABI + public IDictionary GetProperties() => new Windows.Foundation.Collections.ValueSet() + { + { "Foo", "bar" }, + { "Secret", 42 }, + { "hmm?", null }, + }; + } + + internal sealed partial class OtherCommandWithProperties : IExtendedAttributesProvider, IInvokableCommand + { + public string Name => "Whatever 2"; + + public IIconInfo Icon => new IconInfo("\uF146"); + + public string Id => string.Empty; + + public event TypedEventHandler PropChanged; + + public ICommandResult Invoke(object sender) + { + PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(Name))); + return CommandResult.ShowToast("whoop"); + } + + // LOAD-BEARING: Use a Windows.Foundation.Collections.ValueSet as the + // backing store for Properties. A regular `Dictionary` + // will not work across the ABI + public IDictionary GetProperties() => new Windows.Foundation.Collections.ValueSet() + { + { "yo", "dog" }, + { "Secret", 12345 }, + { "hmm?", null }, + }; + } } diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Properties/launchSettings.json b/src/modules/cmdpal/ext/SamplePagesExtension/Properties/launchSettings.json index b62e4e316c..bb3120a64a 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Properties/launchSettings.json +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Properties/launchSettings.json @@ -3,7 +3,7 @@ "SamplePagesExtension (Package)": { "commandName": "MsixPackage", "doNotLaunchApp": true, - "nativeDebugging": true + "nativeDebugging": false }, "SamplePagesExtension (Unpackaged)": { "commandName": "Project" 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 308265f7c0..ca64c87b23 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,7 @@ using Windows.Foundation; namespace Microsoft.CommandPalette.Extensions.Toolkit; -public abstract partial class CommandProvider : ICommandProvider +public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2 { public virtual string Id { get; protected set; } = string.Empty; @@ -47,4 +47,26 @@ public abstract partial class CommandProvider : ICommandProvider { } } + + /// + /// 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. + /// + /// You don't need to call this as an extension author. + /// + /// an array of objects that implement all the leaf interfaces we support + public object[] GetApiExtensionStubs() + { + return [new SupportCommandsWithProperties()]; + } + + /// + /// A stub class which implements IExtendedAttributesProvider. Just marshalling this + /// across the ABI will be enough for CmdPal to store IExtendedAttributesProvider in + /// its type cache. + /// + private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider + { + public IDictionary? GetProperties() => null; + } } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs new file mode 100644 index 0000000000..7e12d38d0c --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs @@ -0,0 +1,32 @@ +// 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 Windows.Foundation.Collections; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +/// +/// Represents an icon that is a font glyph. +/// This is used for icons that are defined by a specific font face, +/// such as Wingdings. +/// +/// Note that Command Palette will default to using the Segoe Fluent Icons, +/// Segoe MDL2 Assets font for glyphs in the Segoe UI Symbol range, or Segoe +/// UI for any other glyphs. This class is only needed if you want a non-Segoe +/// font icon. +/// +public partial class FontIconData : IconData, IExtendedAttributesProvider +{ + public string FontFamily { get; set; } + + public FontIconData(string glyph, string fontFamily) + : base(glyph) + { + FontFamily = fontFamily; + } + + public IDictionary? GetProperties() => new ValueSet() + { + { "FontFamily", FontFamily }, + }; +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs index b36f7242ae..35fdf7b8e7 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconData.cs @@ -2,6 +2,7 @@ // 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.Diagnostics.CodeAnalysis; using Windows.Storage.Streams; namespace Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs index 674a96ec4b..731b529903 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/IconInfo.cs @@ -27,6 +27,12 @@ public partial class IconInfo : IIconInfo Dark = dark; } + public IconInfo(IconData icon) + { + Light = icon; + Dark = icon; + } + internal IconInfo() : this(string.Empty) { 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 51ddbdc572..e45cc97d0f 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 @@ -364,5 +364,17 @@ namespace Microsoft.CommandPalette.Extensions void InitializeWithHost(IExtensionHost host); }; + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface IExtendedAttributesProvider + { + Windows.Foundation.Collections.IMap GetProperties(); + }; + + [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] + interface ICommandProvider2 requires ICommandProvider + { + Object[] GetApiExtensionStubs(); + }; + }