mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-09 12:46:47 +02:00
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.
This commit is contained in:
@@ -29,6 +29,15 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
|
|
||||||
public IconInfoViewModel Icon { get; private set; }
|
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<string, ExtensionObject<object>>? _properties;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, ExtensionObject<object>>? Properties => _properties?.AsReadOnly();
|
||||||
|
|
||||||
public CommandViewModel(ICommand? command, WeakReference<IPageContext> pageContext)
|
public CommandViewModel(ICommand? command, WeakReference<IPageContext> pageContext)
|
||||||
: base(pageContext)
|
: base(pageContext)
|
||||||
{
|
{
|
||||||
@@ -80,6 +89,11 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
UpdateProperty(nameof(Icon));
|
UpdateProperty(nameof(Icon));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model is IExtendedAttributesProvider command2)
|
||||||
|
{
|
||||||
|
UpdatePropertiesFromExtension(command2);
|
||||||
|
}
|
||||||
|
|
||||||
model.PropChanged += Model_PropChanged;
|
model.PropChanged += Model_PropChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,4 +144,26 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
model.PropChanged -= Model_PropChanged;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
|||||||
|
|
||||||
IRandomAccessStreamReference? IIconData.Data => Data.Unsafe;
|
IRandomAccessStreamReference? IIconData.Data => Data.Unsafe;
|
||||||
|
|
||||||
|
public string? FontFamily { get; private set; }
|
||||||
|
|
||||||
public IconDataViewModel(IIconData? icon)
|
public IconDataViewModel(IIconData? icon)
|
||||||
{
|
{
|
||||||
_model = new(icon);
|
_model = new(icon);
|
||||||
@@ -43,5 +45,22 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
|||||||
|
|
||||||
Icon = model.Icon;
|
Icon = model.Icon;
|
||||||
Data = new(model.Data);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,11 @@ public sealed class CommandProviderWrapper
|
|||||||
// On a BG thread here
|
// On a BG thread here
|
||||||
fallbacks = model.FallbackCommands();
|
fallbacks = model.FallbackCommands();
|
||||||
|
|
||||||
|
if (model is ICommandProvider2 two)
|
||||||
|
{
|
||||||
|
UnsafePreCacheApiAdditions(two);
|
||||||
|
}
|
||||||
|
|
||||||
Id = model.Id;
|
Id = model.Id;
|
||||||
DisplayName = model.DisplayName;
|
DisplayName = model.DisplayName;
|
||||||
Icon = new(model.Icon);
|
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 bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||||
|
|
||||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(icon.Icon))
|
if (!string.IsNullOrEmpty(icon.Icon))
|
||||||
{
|
{
|
||||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
else if (icon.Data is not null)
|
else if (icon.Data is not null)
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
// Return Value:
|
// Return Value:
|
||||||
// - An IconElement with its IconSource set, if possible.
|
// - An IconElement with its IconSource set, if possible.
|
||||||
template<typename TIconSource>
|
template<typename TIconSource>
|
||||||
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 };
|
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" });
|
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
|
else
|
||||||
{
|
{
|
||||||
// Note: you _do_ need to manually set the font here.
|
// Note: you _do_ need to manually set the font here.
|
||||||
@@ -225,9 +230,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(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<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, targetSize);
|
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||||
@@ -343,13 +348,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
|
|
||||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||||
const bool monochrome,
|
const bool monochrome,
|
||||||
|
const winrt::hstring& fontFamily,
|
||||||
const int targetSize)
|
const int targetSize)
|
||||||
{
|
{
|
||||||
std::wstring_view iconPathWithoutIndex;
|
std::wstring_view iconPathWithoutIndex;
|
||||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||||
if (!indexOpt.has_value())
|
if (!indexOpt.has_value())
|
||||||
{
|
{
|
||||||
return _IconSourceMUX(iconPath, monochrome, targetSize);
|
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), 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);
|
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||||
if (!indexOpt.has_value())
|
if (!indexOpt.has_value())
|
||||||
{
|
{
|
||||||
auto source = IconSourceMUX(iconPath, false, targetSize);
|
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
|
||||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||||
icon.IconSource(source);
|
icon.IconSource(source);
|
||||||
return icon;
|
return icon;
|
||||||
|
|||||||
@@ -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::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(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);
|
||||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
|||||||
{
|
{
|
||||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(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);
|
||||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
author: Mike Griese
|
author: Mike Griese
|
||||||
created on: 2024-07-19
|
created on: 2024-07-19
|
||||||
last updated: 2025-03-10
|
last updated: 2025-08-08
|
||||||
issue id: n/a
|
issue id: n/a
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1410,8 +1410,8 @@ interface IDetailsLink requires IDetailsData {
|
|||||||
Windows.Foundation.Uri Link { get; };
|
Windows.Foundation.Uri Link { get; };
|
||||||
String Text { get; };
|
String Text { get; };
|
||||||
}
|
}
|
||||||
interface IDetailsCommand requires IDetailsData {
|
interface IDetailsCommands requires IDetailsData {
|
||||||
ICommand Command { get; };
|
ICommand[] Commands { get; };
|
||||||
}
|
}
|
||||||
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
||||||
interface IDetailsSeparator requires IDetailsData {}
|
interface IDetailsSeparator requires IDetailsData {}
|
||||||
@@ -1936,6 +1936,115 @@ When displaying a page:
|
|||||||
* The title will be `IPage.Title ?? ICommand.Name`
|
* The title will be `IPage.Title ?? ICommand.Name`
|
||||||
* The icon will be `ICommand.Icon`
|
* 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<String, Object> 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<string, object>? 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
|
## 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
|
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`.
|
`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
|
[^1]: In this example, as in other places, I've referenced a
|
||||||
`Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action.
|
`Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action.
|
||||||
Our SDK will include partial class implementations for interfaces like
|
Our SDK will include partial class implementations for interfaces like
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Windows.Foundation;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
|
|
||||||
@@ -169,6 +172,61 @@ internal sealed partial class SampleListPage : ListPage
|
|||||||
{
|
{
|
||||||
Title = "Get the name of the Foreground window",
|
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<string, object>`
|
||||||
|
// will not work across the ABI
|
||||||
|
public IDictionary<string, object> 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<object, IPropChangedEventArgs> 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<string, object>`
|
||||||
|
// will not work across the ABI
|
||||||
|
public IDictionary<string, object> GetProperties() => new Windows.Foundation.Collections.ValueSet()
|
||||||
|
{
|
||||||
|
{ "yo", "dog" },
|
||||||
|
{ "Secret", 12345 },
|
||||||
|
{ "hmm?", null },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"SamplePagesExtension (Package)": {
|
"SamplePagesExtension (Package)": {
|
||||||
"commandName": "MsixPackage",
|
"commandName": "MsixPackage",
|
||||||
"doNotLaunchApp": true,
|
"doNotLaunchApp": true,
|
||||||
"nativeDebugging": true
|
"nativeDebugging": false
|
||||||
},
|
},
|
||||||
"SamplePagesExtension (Unpackaged)": {
|
"SamplePagesExtension (Unpackaged)": {
|
||||||
"commandName": "Project"
|
"commandName": "Project"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Windows.Foundation;
|
|||||||
|
|
||||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
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;
|
public virtual string Id { get; protected set; } = string.Empty;
|
||||||
|
|
||||||
@@ -47,4 +47,26 @@ public abstract partial class CommandProvider : ICommandProvider
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>an array of objects that implement all the leaf interfaces we support</returns>
|
||||||
|
public object[] GetApiExtensionStubs()
|
||||||
|
{
|
||||||
|
return [new SupportCommandsWithProperties()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A stub class which implements IExtendedAttributesProvider. Just marshalling this
|
||||||
|
/// across the ABI will be enough for CmdPal to store IExtendedAttributesProvider in
|
||||||
|
/// its type cache.
|
||||||
|
/// </summary>
|
||||||
|
private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider
|
||||||
|
{
|
||||||
|
public IDictionary<string, object>? GetProperties() => null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public partial class FontIconData : IconData, IExtendedAttributesProvider
|
||||||
|
{
|
||||||
|
public string FontFamily { get; set; }
|
||||||
|
|
||||||
|
public FontIconData(string glyph, string fontFamily)
|
||||||
|
: base(glyph)
|
||||||
|
{
|
||||||
|
FontFamily = fontFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||||
|
{
|
||||||
|
{ "FontFamily", FontFamily },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Windows.Storage.Streams;
|
using Windows.Storage.Streams;
|
||||||
|
|
||||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ public partial class IconInfo : IIconInfo
|
|||||||
Dark = dark;
|
Dark = dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IconInfo(IconData icon)
|
||||||
|
{
|
||||||
|
Light = icon;
|
||||||
|
Dark = icon;
|
||||||
|
}
|
||||||
|
|
||||||
internal IconInfo()
|
internal IconInfo()
|
||||||
: this(string.Empty)
|
: this(string.Empty)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -364,5 +364,17 @@ namespace Microsoft.CommandPalette.Extensions
|
|||||||
void InitializeWithHost(IExtensionHost host);
|
void InitializeWithHost(IExtensionHost host);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||||
|
interface IExtendedAttributesProvider
|
||||||
|
{
|
||||||
|
Windows.Foundation.Collections.IMap<String, Object> GetProperties();
|
||||||
|
};
|
||||||
|
|
||||||
|
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||||
|
interface ICommandProvider2 requires ICommandProvider
|
||||||
|
{
|
||||||
|
Object[] GetApiExtensionStubs();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user