Compare commits

...

25 Commits

Author SHA1 Message Date
Mike Griese
323c9a7b23 The motherfuckin WinRT analyzers were disabled 2025-08-19 15:22:24 -05:00
Mike Griese
29e13df9cb what the hell is going on here 2025-08-19 11:00:19 -05:00
Mike Griese
13b7ff1315 Revert "a property definitely works, wtf"
This reverts commit 9e20d014aa.
2025-08-19 09:12:36 -05:00
Mike Griese
9e20d014aa a property definitely works, wtf 2025-08-19 09:07:04 -05:00
Mike Griese
8268c58ab5 nits 2025-08-19 09:06:49 -05:00
Mike Griese
37c7e3bbb8 Merge remote-tracking branch 'origin/main' into dev/migrie/f/properties-test 2025-08-19 06:40:07 -05:00
Mike Griese
cc4fd8f8ae why does trimming & AOT this break this API? 2025-08-13 11:24:38 -05:00
Mike Griese
e7dd3acf51 Reapply "more updates"
This reverts commit a78ce3843a.
2025-08-13 08:42:48 -05:00
Mike Griese
a78ce3843a Revert "more updates"
This reverts commit b1432c7f54.
2025-08-13 08:42:11 -05:00
Mike Griese
b1432c7f54 more updates 2025-08-13 08:41:51 -05:00
Mike Griese
62c2c60654 Merge remote-tracking branch 'origin/main' into dev/migrie/f/properties-test 2025-08-12 13:22:03 -05:00
Mike Griese
c5db9d2bd6 rename; use IDictionary instead of IPropertySet 2025-08-11 06:34:15 -05:00
Mike Griese
9ecd374574 spel 2025-08-08 06:50:12 -05:00
Mike Griese
dd1a60c9f6 really VS really 2025-08-08 06:48:03 -05:00
Mike Griese
f3588e7f70 more spec cleanup 2025-08-08 06:12:51 -05:00
Mike Griese
940e71f2a8 stupid levels returning to nominal values 2025-08-08 05:57:58 -05:00
Mike Griese
36f85218e1 :shipit: 2025-08-07 17:06:55 -05:00
Mike Griese
3ed3bb6a81 this can be shipped 2025-08-07 17:00:11 -05:00
Mike Griese
906602090d warning - extreme stupid levels detected
(98%)  ■■■■■■■■■□
2025-08-07 16:09:57 -05:00
Mike Griese
73bc438042 I keep thinking it can't get any stupider and they keep bringing me back 2025-08-07 15:27:38 -05:00
Mike Griese
88111e2fbd Revert "not this"
This reverts commit 7eaa701920.
2025-08-07 14:19:11 -05:00
Mike Griese
7eaa701920 not this 2025-08-07 14:19:06 -05:00
Mike Griese
95cfdbb93d oh god oh god what have i done 2025-08-07 10:51:14 -05:00
Mike Griese
c198ceaa1e Merge remote-tracking branch 'origin/main' into dev/migrie/f/properties-test 2025-08-07 09:24:37 -05:00
Mike Griese
2990aad9fd this didn't work and no one knows why 2025-08-06 06:08:01 -05:00
22 changed files with 552 additions and 35 deletions

View File

@@ -56,7 +56,7 @@
TODO: in Common.Dotnet.CsWinRT.props, on upgrade, verify RemoveCsWinRTPackageAnalyzer is no longer needed.
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.250720.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
@@ -75,6 +75,7 @@
<PackageVersion Include="SharpCompress" Version="0.37.2" />
<!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. -->
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->

View File

@@ -1553,6 +1553,7 @@ SOFTWARE.
- ScipBe.Common.Office.OneNote 3.0.1
- SharpCompress 0.37.2
- SkiaSharp.Views.WinUI 2.88.9
- Shmuelie.WinRTServer 2.1.1
- StreamJsonRpc 2.21.69
- StyleCop.Analyzers 1.2.0-beta.556
- System.CodeDom 9.0.8

View File

@@ -38,9 +38,9 @@
</ItemGroup>
<!-- this may need to be removed on future CsWinRT upgrades-->
<Target Name="RemoveCsWinRTPackageAnalyzer" BeforeTargets="CoreCompile">
<!-- <Target Name="RemoveCsWinRTPackageAnalyzer" BeforeTargets="CoreCompile">
<ItemGroup>
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
</ItemGroup>
</Target>
</Target> -->
</Project>

View File

@@ -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<string, ExtensionObject<object>>? _properties;
public IReadOnlyDictionary<string, ExtensionObject<object>>? Properties => _properties?.AsReadOnly();
public CommandViewModel(ICommand? command, WeakReference<IPageContext> 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;
}
@@ -112,6 +126,10 @@ public partial class CommandViewModel : ExtensionObjectViewModel
var iconInfo = model.Icon;
Icon = new(iconInfo);
Icon.InitializeProperties();
break;
case nameof(_properties):
UpdatePropertiesFromExtension(model as IExtendedAttributesProvider);
break;
}
@@ -130,4 +148,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));
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();

View File

@@ -126,6 +126,10 @@ public class ExtensionWrapper : IExtensionWrapper
// We'll just return out nothing.
return;
}
else if (hr.Value != 0)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value} ({hr.Value:X8})");
}
// Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);

View File

@@ -315,7 +315,7 @@ public partial class TopLevelCommandManager : ObservableObject,
Logger.LogDebug($"Starting {extension.PackageFullName}");
try
{
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10));
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(30));
return new CommandProviderWrapper(extension, _taskScheduler);
}
catch (Exception ex)
@@ -329,7 +329,7 @@ public partial class TopLevelCommandManager : ObservableObject,
{
try
{
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(10));
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(30));
}
catch (TimeoutException)
{

View File

@@ -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)

View File

@@ -158,7 +158,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// Return Value:
// - An IconElement with its IconSource set, if possible.
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 };
@@ -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<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,
@@ -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;

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -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<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
@@ -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

View File

@@ -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,131 @@ 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",
},
// new ListItem(new NoOpCommand() { Icon = new(new OneFontIconData("\u0027", "Wingdings")), Name = "one" }) { Title = "OneFontIconData", Tags = [new Tag("DynamicallyAccessedMembers"), new Tag("internal sealed"), new Tag("in samples")] },
// new ListItem(new NoOpCommand() { Icon = new(new TwoFontIconData("\u0028", "Wingdings")), Name = "two" }) { Title = "TwoFontIconData", Tags = [new Tag(" "), new Tag("internal sealed"), new Tag("in samples")] },
// new ListItem(new NoOpCommand() { Icon = new(new ThreeFontIconData("\u0029", "Wingdings")), Name = "three" }) { Title = "ThreeFontIconData", Tags = [new Tag("DynamicallyAccessedMembers"), new Tag("PUBLIC sealed"), new Tag("in samples")] },
];
}
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
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";
public IDictionary<string, object> GetProperties() => new Dictionary<string, object>()
{
{ "Foo", "bar" },
{ "Secret", 42 },
{ "hmm?", null },
};
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.CommandPalette.Extensions.Toolkit.FontIconData))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IDictionary<string, object>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<string, object>))]
public static void PreserveTypes()
{
}
public CommandWithProperties()
{
PreserveTypes();
}
}
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");
}
public IDictionary<string, object> GetProperties() => new Dictionary<string, object>()
{
{ "yo", "dog" },
{ "Secret", 12345 },
{ "hmm?", null },
};
}
// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
// internal sealed partial class OneFontIconData : IconData, IExtendedAttributesProvider
// {
// private string FontFamily { get; set; }
// private readonly IDictionary<string, object> _properties;
// public IDictionary<string, object> GetProperties() => _properties;
// public OneFontIconData(string glyph, string fontFamily)
// : base(glyph)
// {
// FontFamily = fontFamily;
// _properties = new Dictionary<string, object>()
// {
// { "FontFamily", FontFamily },
// };
// }
// }
// internal sealed partial class TwoFontIconData : IconData, IExtendedAttributesProvider
// {
// private string FontFamily { get; set; }
// private readonly IDictionary<string, object> _properties;
// public IDictionary<string, object> GetProperties() => _properties;
// public TwoFontIconData(string glyph, string fontFamily)
// : base(glyph)
// {
// FontFamily = fontFamily;
// _properties = new Dictionary<string, object>()
// {
// { "FontFamily", FontFamily },
// };
// }
// }
// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
// public sealed partial class ThreeFontIconData : IconData, IExtendedAttributesProvider
// {
// private string FontFamily { get; set; }
// private readonly IDictionary<string, object> _properties;
// public IDictionary<string, object> GetProperties() => _properties;
// public ThreeFontIconData(string glyph, string fontFamily)
// : base(glyph)
// {
// FontFamily = fontFamily;
// _properties = new Dictionary<string, object>()
// {
// { "FontFamily", FontFamily },
// };
// }
// }
}

View File

@@ -4,33 +4,92 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Shmuelie.WinRTServer;
using Shmuelie.WinRTServer.CsWinRT;
using WinRT;
namespace SamplePagesExtension;
public class Program
{
private static ManualResetEvent _comServerEventHandle = new(false);
[MTAThread]
[DynamicWindowsRuntimeCast(typeof(IExtension))]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
using ExtensionServer server = new();
var extensionDisposedEvent = new ManualResetEvent(false);
var extensionInstance = new SampleExtension(extensionDisposedEvent);
global::Shmuelie.WinRTServer.ComServer server = new();
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
server.RegisterExtension(() => extensionInstance);
// ManualResetEvent extensionDisposedEvent = new(false);
//// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
//// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
//// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
// SampleExtension extensionInstance = new(extensionDisposedEvent);
// server.RegisterClass<SampleExtension, IExtension>(() => extensionInstance);
RegisterExtension<SampleExtension, IExtension>(
server,
(EventWaitHandle handle) =>
{
SampleExtension extension = new();
extension.Disposed += (s, e) =>
{
handle.Set();
};
return extension;
});
server.Start();
// This will make the main thread wait until the event is signalled by the extension class.
// Since we have a single instance of the extension object, we exit as soon as it is disposed.
extensionDisposedEvent.WaitOne();
_comServerEventHandle.WaitOne();
}
else
{
Console.WriteLine("Not being launched as a Extension... exiting.");
}
}
[DynamicWindowsRuntimeCast(typeof(IExtension))]
public static void RegisterExtension<TExtension, TInterface>(
ComServer server,
Func<EventWaitHandle, TExtension> factory)
where TExtension : class, TInterface, new()
{
TExtension extension = null;
_ = Task.Run(() =>
{
_comServerEventHandle.WaitOne();
extension = null;
});
server.RegisterClass<TExtension, TInterface>(() =>
{
if (extension == null)
{
extension = factory(_comServerEventHandle);
}
return extension!;
});
// Shmuelie.WinRTServer.CsWinRT.ComServerExtensions.RegisterClass<TExtension, TInterface>(
// server,
// () =>
// {
// if (extension == null)
// {
// extension = factory(_comServerEventHandle);
// }
// return extension!;
// });
}
}

View File

@@ -3,7 +3,7 @@
"SamplePagesExtension (Package)": {
"commandName": "MsixPackage",
"doNotLaunchApp": true,
"nativeDebugging": true
"nativeDebugging": false
},
"SamplePagesExtension (Unpackaged)": {
"commandName": "Project"

View File

@@ -4,23 +4,26 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace SamplePagesExtension;
[ComVisible(true)]
// [ComVisible(true)]
// [ComDefaultInterface(typeof(IExtension))]
[Guid("6112D28D-6341-45C8-92C3-83ED55853A9F")]
[ComDefaultInterface(typeof(IExtension))]
public sealed partial class SampleExtension : IExtension, IDisposable
// [global::WinRT.WinRTExposedType]
public sealed partial class SampleExtension : IExtension, IDisposable// , IDynamicInterfaceCastable
{
private readonly ManualResetEvent _extensionDisposedEvent;
private bool disposed;
public event TypedEventHandler<IExtension, object> Disposed;
private readonly SamplePagesCommandsProvider _provider = new();
public SampleExtension(ManualResetEvent extensionDisposedEvent)
public SampleExtension()
{
this._extensionDisposedEvent = extensionDisposedEvent;
}
public object GetProvider(ProviderType providerType)
@@ -36,6 +39,19 @@ public sealed partial class SampleExtension : IExtension, IDisposable
public void Dispose()
{
this._extensionDisposedEvent.Set();
if (!disposed)
{
Disposed?.Invoke(this, null);
_provider.Dispose();
disposed = true;
}
}
// public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented)
// {
// Debug.WriteLine($"{interfaceType}");
// return true;
// }
// public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType) => throw new NotImplementedException();
}

View File

@@ -1,5 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<!-- <Import Project="..\..\..\..\Common.Dotnet.PrepareGeneratedFolder.targets" />
<PropertyGroup>
<WindowsSdkPackageVersion>10.0.26100.68-preview</WindowsSdkPackageVersion>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup>
<WarningLevel>4</WarningLevel>
<NoWarn></NoWarn>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<WarningsNotAsErrors>CA1824;CA1416;CA1720;CA1859;CA2263;CA2022;MVVMTK0045;MVVMTK0049</WarningsNotAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DebugSymbols>true</DebugSymbols>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<ErrorReport>prompt</ErrorReport>
<DefineConstants>RELEASE;TRACE</DefineConstants>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
</PropertyGroup> -->
<!-- ####################################################### -->
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -11,7 +45,6 @@
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
</PropertyGroup>
<ItemGroup>
@@ -38,6 +71,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Shmuelie.WinRTServer" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
@@ -64,6 +98,7 @@
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
<WarningsNotAsErrors>IL2104;$(WarningsNotAsErrors)</WarningsNotAsErrors>
<PublishAot>true</PublishAot>
</PropertyGroup>

View File

@@ -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
{
}
}
/// <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;
}
}

View File

@@ -0,0 +1,39 @@
// 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 System.Diagnostics.CodeAnalysis;
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>
///
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class FontIconData : IconData, IExtendedAttributesProvider
{
private string FontFamily { get; set; }
private readonly IDictionary<string, object> _properties;
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<string, object>))]
public IDictionary<string, object>? GetProperties() => _properties;
public FontIconData(string glyph, string fontFamily)
: base(glyph)
{
FontFamily = fontFamily;
_properties = new Dictionary<string, object>()
{
{ "FontFamily", FontFamily },
};
}
}

View File

@@ -27,6 +27,12 @@ public partial class IconInfo : IIconInfo
Dark = dark;
}
public IconInfo(IconData icon)
{
Light = icon;
Dark = icon;
}
internal IconInfo()
: this(string.Empty)
{

View File

@@ -364,5 +364,17 @@ namespace Microsoft.CommandPalette.Extensions
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();
};
}