mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-11 23:06:45 +01:00
Load extensions into TRA (#176)
Targets both #175 (which targets #171), and #172, so the delta is very sloppy. There's also some sloppy bits of delta here where I tried to see if the single-instance thing fixed the RoOriginate thing. I'm gonna try to tidy all this up a bit. Refs #73
This commit is contained in:
@@ -700,6 +700,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualDesktopExtension", "
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "src\modules\Workspaces\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj", "{89D0E199-B17A-418C-B2F8-7375B6708357}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project Templates", "Project Templates", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -3543,7 +3545,7 @@ Global
|
||||
{C831231F-891C-4572-9694-45062534B42A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{2C2AF4F4-3DB2-416F-9F38-2B710D961E6F} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{BE349BD2-7FF8-47B6-8F74-42B52DDF730A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{8E47BF33-A402-4582-930C-95E35418F653} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
|
||||
@@ -3552,6 +3554,7 @@ Global
|
||||
{8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -16,9 +16,8 @@ public sealed class CommandProviderWrapper
|
||||
private readonly ICommandProvider _commandProvider;
|
||||
|
||||
private readonly IExtensionWrapper? extensionWrapper;
|
||||
private ICommandItem[] _topLevelItems = [];
|
||||
|
||||
public ICommandItem[] TopLevelItems => _topLevelItems;
|
||||
public ICommandItem[] TopLevelItems { get; private set; } = [];
|
||||
|
||||
public CommandProviderWrapper(ICommandProvider provider)
|
||||
{
|
||||
@@ -29,8 +28,14 @@ public sealed class CommandProviderWrapper
|
||||
public CommandProviderWrapper(IExtensionWrapper extension)
|
||||
{
|
||||
extensionWrapper = extension;
|
||||
if (!extensionWrapper.IsRunning())
|
||||
{
|
||||
throw new ArgumentException("You forgot to start the extension. This is a coding error - make sure to call StartExtensionAsync");
|
||||
}
|
||||
|
||||
var extensionImpl = extension.GetExtensionObject();
|
||||
if (extensionImpl?.GetProvider(ProviderType.Commands) is not ICommandProvider provider)
|
||||
var providerObject = extensionImpl?.GetProvider(ProviderType.Commands);
|
||||
if (providerObject is not ICommandProvider provider)
|
||||
{
|
||||
throw new ArgumentException("extension didn't actually implement ICommandProvider");
|
||||
}
|
||||
@@ -46,14 +51,14 @@ public sealed class CommandProviderWrapper
|
||||
return;
|
||||
}
|
||||
|
||||
var t = new Task<ICommandItem[]>(() => _commandProvider.TopLevelCommands());
|
||||
var t = new Task<ICommandItem[]>(_commandProvider.TopLevelCommands);
|
||||
t.Start();
|
||||
var commands = await t.ConfigureAwait(false);
|
||||
|
||||
// On a BG thread here
|
||||
if (commands != null)
|
||||
{
|
||||
_topLevelItems = commands;
|
||||
TopLevelItems = commands;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ListViewModel(IListPage _model) : ObservableObject
|
||||
public partial class ListViewModel : ObservableObject
|
||||
{
|
||||
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
|
||||
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
|
||||
@@ -21,6 +22,39 @@ public partial class ListViewModel(IListPage _model) : ObservableObject
|
||||
[ObservableProperty]
|
||||
public partial bool IsInitialized { get; private set; }
|
||||
|
||||
private readonly ExtensionObject<IListPage> _model;
|
||||
|
||||
public ListViewModel(IListPage model)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchItems();
|
||||
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchItems()
|
||||
{
|
||||
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
|
||||
|
||||
// TEMPORARY: just plop all the items into a single group
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
// TODO unsafe
|
||||
var newItems = _model.Unsafe!.GetItems();
|
||||
|
||||
Items.Clear();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
ListItemViewModel viewModel = new(item);
|
||||
viewModel.InitializeProperties();
|
||||
group.Add(viewModel);
|
||||
}
|
||||
|
||||
// Am I really allowed to modify that observable collection on a BG
|
||||
// thread and have it just work in the UI??
|
||||
Items.AddGroup(group);
|
||||
}
|
||||
|
||||
//// Run on background thread from ListPage.xaml.cs
|
||||
[RelayCommand]
|
||||
private Task<bool> InitializeAsync()
|
||||
@@ -29,19 +63,9 @@ public partial class ListViewModel(IListPage _model) : ObservableObject
|
||||
|
||||
// TODO: We may want to investigate using some sort of AsyncEnumerable or populating these as they come in to the UI layer
|
||||
// Though we have to think about threading here and circling back to the UI thread with a TaskScheduler.
|
||||
FetchItems();
|
||||
|
||||
// TEMPORARY: just plop all the items into a single group
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
|
||||
|
||||
foreach (var item in _model.GetItems())
|
||||
{
|
||||
ListItemViewModel viewModel = new(item);
|
||||
viewModel.InitializeProperties();
|
||||
group.Add(viewModel);
|
||||
}
|
||||
|
||||
Items.AddGroup(group);
|
||||
_model.Unsafe!.ItemsChanged += Model_ItemsChanged;
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// 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.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Pages;
|
||||
|
||||
@@ -14,16 +17,26 @@ namespace Microsoft.CmdPal.UI.Pages;
|
||||
/// </summary>
|
||||
public partial class MainListPage : DynamicListPage
|
||||
{
|
||||
private readonly IListItem[] _items;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
// TODO: Thinking we may want a separate MainViewModel from the ShellViewModel and/or a CommandService/Provider
|
||||
// which holds the TopLevelCommands and anything that needs to access those functions...
|
||||
public MainListPage(ShellViewModel shellViewModel)
|
||||
private readonly ObservableCollection<TopLevelCommandWrapper> _commands;
|
||||
|
||||
public MainListPage(IServiceProvider serviceProvider)
|
||||
{
|
||||
_items = shellViewModel.TopLevelCommands.Select(w => w.Unsafe!).Where(li => li != null).ToArray();
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
|
||||
|
||||
// reference the TLC collection directly... maybe? TODO is this a good idea ot a terrible one?
|
||||
_commands = tlcManager!.TopLevelCommands;
|
||||
_commands.CollectionChanged += Commands_CollectionChanged;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items;
|
||||
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_commands.Count);
|
||||
|
||||
public override IListItem[] GetItems() => _commands
|
||||
.Select(tlc => tlc)
|
||||
.ToArray();
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.AppExtensions;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
public event EventHandler OnExtensionsChanged = (_, _) => { };
|
||||
|
||||
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
|
||||
private static readonly Lock _lock = new();
|
||||
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
|
||||
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
|
||||
|
||||
// private readonly ILocalSettingsService _localSettingsService;
|
||||
private bool _disposedValue;
|
||||
|
||||
private const string CreateInstanceProperty = "CreateInstance";
|
||||
private const string ClassIdProperty = "@ClassId";
|
||||
|
||||
private static readonly List<IExtensionWrapper> _installedExtensions = [];
|
||||
private static readonly List<IExtensionWrapper> _enabledExtensions = [];
|
||||
|
||||
public ExtensionService()
|
||||
{
|
||||
_catalog.PackageInstalling += Catalog_PackageInstalling;
|
||||
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
|
||||
_catalog.PackageUpdating += Catalog_PackageUpdating;
|
||||
|
||||
//// These two were an investigation into getting updates when a package
|
||||
//// gets redeployed from VS. Neither get raised (nor do the above)
|
||||
//// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
|
||||
//// _catalog.PackageStaging += Catalog_PackageStaging;
|
||||
// _localSettingsService = settingsService;
|
||||
}
|
||||
|
||||
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var isCmdPalExtension = Task.Run(() =>
|
||||
{
|
||||
return IsValidCmdPalExtension(args.Package);
|
||||
}).Result;
|
||||
|
||||
if (isCmdPalExtension)
|
||||
{
|
||||
OnPackageChange(args.Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var extension in _installedExtensions)
|
||||
{
|
||||
if (extension.PackageFullName == args.Package.Id.FullName)
|
||||
{
|
||||
OnPackageChange(args.Package);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var isCmdPalExtension = Task.Run(() =>
|
||||
{
|
||||
return IsValidCmdPalExtension(args.TargetPackage);
|
||||
}).Result;
|
||||
|
||||
if (isCmdPalExtension)
|
||||
{
|
||||
OnPackageChange(args.TargetPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPackageChange(Package package)
|
||||
{
|
||||
_installedExtensions.Clear();
|
||||
_enabledExtensions.Clear();
|
||||
OnExtensionsChanged.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsValidCmdPalExtension(Package package)
|
||||
{
|
||||
var extensions = await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
if (package.Id?.FullName == extension.Package?.Id?.FullName)
|
||||
{
|
||||
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||
|
||||
return cmdPalProvider != null && classId.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
|
||||
{
|
||||
var classIds = new List<string>();
|
||||
var properties = await extension.GetExtensionPropertiesAsync();
|
||||
|
||||
if (properties is null)
|
||||
{
|
||||
return (null, classIds);
|
||||
}
|
||||
|
||||
var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
|
||||
if (cmdPalProvider is null)
|
||||
{
|
||||
return (null, classIds);
|
||||
}
|
||||
|
||||
var activation = GetSubPropertySet(cmdPalProvider, "Activation");
|
||||
if (activation is null)
|
||||
{
|
||||
return (cmdPalProvider, classIds);
|
||||
}
|
||||
|
||||
// Handle case where extension creates multiple instances.
|
||||
classIds.AddRange(GetCreateInstanceList(activation));
|
||||
|
||||
return (cmdPalProvider, classIds);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
|
||||
|
||||
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
|
||||
{
|
||||
await _getInstalledExtensionsLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_installedExtensions.Count == 0)
|
||||
{
|
||||
var extensions = await GetInstalledAppExtensionsAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||
|
||||
if (cmdPalProvider == null || classIds.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var classId in classIds)
|
||||
{
|
||||
var extensionWrapper = new ExtensionWrapper(extension, classId);
|
||||
|
||||
var supportedInterfaces = GetSubPropertySet(cmdPalProvider, "SupportedInterfaces");
|
||||
if (supportedInterfaces is not null)
|
||||
{
|
||||
foreach (var supportedInterface in supportedInterfaces)
|
||||
{
|
||||
ProviderType pt;
|
||||
if (Enum.TryParse(supportedInterface.Key, out pt))
|
||||
{
|
||||
extensionWrapper.AddProviderType(pt);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: throw warning or fire notification that extension declared unsupported extension interface
|
||||
// https://github.com/microsoft/DevHome/issues/617
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
|
||||
var extensionUniqueId = extension.AppInfo.AppUserModelId + "!" + extension.Id;
|
||||
var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled");
|
||||
|
||||
_installedExtensions.Add(extensionWrapper);
|
||||
if (!isExtensionDisabled)
|
||||
{
|
||||
_enabledExtensions.Add(extensionWrapper);
|
||||
}
|
||||
|
||||
// TelemetryFactory.Get<ITelemetry>().Log(
|
||||
// "Extension_ReportInstalled",
|
||||
// LogLevel.Critical,
|
||||
// new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return includeDisabledExtensions ? _installedExtensions : _enabledExtensions;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_getInstalledExtensionsLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
return extension.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task SignalStopExtensionsAsync()
|
||||
{
|
||||
var installedExtensions = await GetInstalledExtensionsAsync();
|
||||
foreach (var installedExtension in installedExtensions)
|
||||
{
|
||||
if (installedExtension.IsRunning())
|
||||
{
|
||||
installedExtension.SignalDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
|
||||
{
|
||||
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
|
||||
|
||||
List<IExtensionWrapper> filteredExtensions = [];
|
||||
foreach (var installedExtension in installedExtensions)
|
||||
{
|
||||
if (installedExtension.HasProviderType(providerType))
|
||||
{
|
||||
filteredExtensions.Add(installedExtension);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredExtensions;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_getInstalledExtensionsLock.Dispose();
|
||||
_getInstalledWidgetsLock.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
|
||||
|
||||
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null;
|
||||
|
||||
/// <summary>
|
||||
/// There are cases where the extension creates multiple COM instances.
|
||||
/// </summary>
|
||||
/// <param name="activationPropSet">Activation property set object</param>
|
||||
/// <returns>List of ClassId strings associated with the activation property</returns>
|
||||
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
|
||||
{
|
||||
var propSetList = new List<string>();
|
||||
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
|
||||
if (singlePropertySet != null)
|
||||
{
|
||||
var classId = GetProperty(singlePropertySet, ClassIdProperty);
|
||||
|
||||
// If the instance has a classId as a single string, then it's only supporting a single instance.
|
||||
if (classId != null)
|
||||
{
|
||||
propSetList.Add(classId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
|
||||
if (propertySetArray != null)
|
||||
{
|
||||
foreach (var prop in propertySetArray)
|
||||
{
|
||||
if (prop is not IPropertySet propertySet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var classId = GetProperty(propertySet, ClassIdProperty);
|
||||
if (classId != null)
|
||||
{
|
||||
propSetList.Add(classId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return propSetList;
|
||||
}
|
||||
|
||||
private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string;
|
||||
|
||||
public void EnableExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
_enabledExtensions.Add(extension.First());
|
||||
}
|
||||
|
||||
public void DisableExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
_enabledExtensions.Remove(extension.First());
|
||||
}
|
||||
|
||||
/*
|
||||
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
|
||||
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
|
||||
//{
|
||||
// // Only attempt to disable feature if its available.
|
||||
// if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
|
||||
// // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
|
||||
// // for the rest of its process lifetime.
|
||||
// DisableExtension(extension.ExtensionUniqueId);
|
||||
// // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
|
||||
// await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
|
||||
// return true;
|
||||
//} */
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.AppExtensions;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Com;
|
||||
using WinRT;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionWrapper : IExtensionWrapper
|
||||
{
|
||||
private const int HResultRpcServerNotRunning = -2147023174;
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private readonly List<ProviderType> _providerTypes = [];
|
||||
|
||||
private readonly Dictionary<Type, ProviderType> _providerTypeMap = new()
|
||||
{
|
||||
[typeof(ICommandProvider)] = ProviderType.Commands,
|
||||
};
|
||||
|
||||
private IExtension? _extensionObject;
|
||||
|
||||
public ExtensionWrapper(AppExtension appExtension, string classId)
|
||||
{
|
||||
PackageDisplayName = appExtension.Package.DisplayName;
|
||||
ExtensionDisplayName = appExtension.DisplayName;
|
||||
PackageFullName = appExtension.Package.Id.FullName;
|
||||
PackageFamilyName = appExtension.Package.Id.FamilyName;
|
||||
ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId));
|
||||
Publisher = appExtension.Package.PublisherDisplayName;
|
||||
InstalledDate = appExtension.Package.InstalledDate;
|
||||
Version = appExtension.Package.Id.Version;
|
||||
ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id;
|
||||
}
|
||||
|
||||
public string PackageDisplayName { get; }
|
||||
|
||||
public string ExtensionDisplayName { get; }
|
||||
|
||||
public string PackageFullName { get; }
|
||||
|
||||
public string PackageFamilyName { get; }
|
||||
|
||||
public string ExtensionClassId { get; }
|
||||
|
||||
public string Publisher { get; }
|
||||
|
||||
public DateTimeOffset InstalledDate { get; }
|
||||
|
||||
public PackageVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique id for this Dev Home extension. The unique id is a concatenation of:
|
||||
/// <list type="number">
|
||||
/// <item>The AppUserModelId (AUMID) of the extension's application. The AUMID is the concatenation of the package
|
||||
/// family name and the application id and uniquely identifies the application containing the extension within
|
||||
/// the package.</item>
|
||||
/// <item>The Extension Id. This is the unique identifier of the extension within the application.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public string ExtensionUniqueId { get; }
|
||||
|
||||
public bool IsRunning()
|
||||
{
|
||||
if (_extensionObject is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_extensionObject.As<IInspectable>().GetRuntimeClassName();
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
if (e.ErrorCode == HResultRpcServerNotRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task StartExtensionAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!IsRunning())
|
||||
{
|
||||
var extensionPtr = nint.Zero;
|
||||
try
|
||||
{
|
||||
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, typeof(IExtension).GUID, out var extensionObj);
|
||||
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (extensionPtr != nint.Zero)
|
||||
{
|
||||
Marshal.Release(extensionPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SignalDispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (IsRunning())
|
||||
{
|
||||
_extensionObject?.Dispose();
|
||||
}
|
||||
|
||||
_extensionObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IExtension? GetExtensionObject()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return IsRunning() ? _extensionObject : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T?> GetProviderAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
await StartExtensionAsync();
|
||||
|
||||
return GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]) as T;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
await StartExtensionAsync();
|
||||
|
||||
var supportedProviders = GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]);
|
||||
if (supportedProviders is IEnumerable<T> multipleProvidersSupported)
|
||||
{
|
||||
return multipleProvidersSupported;
|
||||
}
|
||||
else if (supportedProviders is T singleProviderSupported)
|
||||
{
|
||||
return [singleProviderSupported];
|
||||
}
|
||||
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
public void AddProviderType(ProviderType providerType) => _providerTypes.Add(providerType);
|
||||
|
||||
public bool HasProviderType(ProviderType providerType) => _providerTypes.Contains(providerType);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
GetPhysicallyInstalledSystemMemory
|
||||
GlobalMemoryStatusEx
|
||||
GetSystemInfo
|
||||
CoCreateInstance
|
||||
SetForegroundWindow
|
||||
IsIconic
|
||||
RegisterHotKey
|
||||
SetWindowLongPtr
|
||||
CallWindowProc
|
||||
ShowWindow
|
||||
SetForegroundWindow
|
||||
SetFocus
|
||||
SetActiveWindow
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
SHCreateStreamOnFileEx
|
||||
CoAllowSetForegroundWindow
|
||||
SHCreateStreamOnFileEx
|
||||
SHLoadIndirectString
|
||||
@@ -2,15 +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.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.Pages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -20,39 +16,28 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider) : Observa
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
public ObservableCollection<CommandProviderWrapper> ActionsProvider { get; set; } = [];
|
||||
|
||||
public ObservableCollection<ExtensionObject<IListItem>> TopLevelCommands { get; set; } = [];
|
||||
|
||||
private IEnumerable<ICommandProvider>? _builtInCommands;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadAsync()
|
||||
{
|
||||
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
|
||||
|
||||
// Load Built In Commands First
|
||||
foreach (var provider in _builtInCommands)
|
||||
{
|
||||
CommandProviderWrapper wrapper = new(provider);
|
||||
ActionsProvider.Add(wrapper);
|
||||
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
|
||||
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
|
||||
await tlcManager!.LoadBuiltinsAsync();
|
||||
IsLoaded = true;
|
||||
|
||||
// TODO: would want to hydrate this from our services provider in the View layer, need to think about construction here...
|
||||
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(new MainListPage(this))));
|
||||
// Built-ins have loaded. We can display our page at this point.
|
||||
var page = new MainListPage(_serviceProvider);
|
||||
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(page!)));
|
||||
|
||||
// After loading built-ins, and starting navigation, kick off a thread to load extensions.
|
||||
tlcManager.LoadExtensionsCommand.Execute(null);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await tlcManager.LoadExtensionsCommand.ExecutionTask!;
|
||||
if (tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
await commandProvider.LoadTopLevelCommands();
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new ListItem(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class TopLevelCommandManager(IServiceProvider _serviceProvider)
|
||||
{
|
||||
private IEnumerable<ICommandProvider>? _builtInCommands;
|
||||
|
||||
public ObservableCollection<TopLevelCommandWrapper> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public async Task<bool> LoadBuiltinsAsync()
|
||||
{
|
||||
// Load built-In commands first. These are all in-proc, and
|
||||
// owned by our ServiceProvider.
|
||||
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
|
||||
foreach (var provider in _builtInCommands)
|
||||
{
|
||||
CommandProviderWrapper wrapper = new(provider);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
await commandProvider.LoadTopLevelCommands();
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new(i)));
|
||||
}
|
||||
}
|
||||
|
||||
// Load commands from our extensions.
|
||||
// Currently, this
|
||||
// * queries the package catalog,
|
||||
// * starts all the extensions,
|
||||
// * then fetches the top-level commands from them.
|
||||
// TODO In the future, we'll probably abstract some of this away, to have
|
||||
// separate extension tracking vs stub loading.
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadExtensionsAsync()
|
||||
{
|
||||
var extensionService = _serviceProvider.GetService<IExtensionService>()!;
|
||||
var extensions = await extensionService.GetInstalledExtensionsAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
try
|
||||
{
|
||||
await extension.StartExtensionAsync();
|
||||
CommandProviderWrapper wrapper = new(extension);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction of a top-level command. Currently owns just a live ICommandItem
|
||||
/// from an extension (or in-proc command provider), but in the future will
|
||||
/// also support stub top-level items.
|
||||
/// </summary>
|
||||
public partial class TopLevelCommandWrapper : ListItem
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model { get; }
|
||||
|
||||
public TopLevelCommandWrapper(ExtensionObject<ICommandItem> commandItem)
|
||||
: base(commandItem.Unsafe?.Command ?? new NoOpCommand())
|
||||
{
|
||||
// TODO: In reality, we should do an async fetch when we're created
|
||||
// from an extension object. Probably have an
|
||||
// `static async Task<TopLevelCommandWrapper> FromExtension(ExtensionObject<ICommandItem>)`
|
||||
// or a
|
||||
// `async Task PromoteStub(ExtensionObject<ICommandItem>)`
|
||||
Model = commandItem;
|
||||
try
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Title = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
Icon = new(model.Icon.Icon);
|
||||
MoreCommands = model.MoreCommands;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
@@ -12,6 +13,7 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
@@ -29,9 +31,7 @@ public partial class App : Application
|
||||
/// </summary>
|
||||
public static new App Current => (App)Application.Current;
|
||||
|
||||
private Window? _window;
|
||||
|
||||
public Window? AppWindow => _window;
|
||||
public Window? AppWindow { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
|
||||
@@ -56,8 +56,8 @@ public partial class App : Application
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
AppWindow = new MainWindow();
|
||||
AppWindow.Activate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,6 +68,9 @@ public partial class App : Application
|
||||
// TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463
|
||||
ServiceCollection services = new();
|
||||
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
// Built-in Commands
|
||||
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
|
||||
@@ -79,6 +82,10 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, RegistryCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, WindowsSettingsCommandsProvider>();
|
||||
|
||||
// Models
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
@@ -28,6 +26,8 @@ public sealed partial class LoadingPage : Page
|
||||
if (e.Parameter is ShellViewModel shellVM
|
||||
&& shellVM.LoadCommand != null)
|
||||
{
|
||||
// This will load the built-in commands, then navigate to the main page.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
shellVM.LoadCommand.Execute(null);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
|
||||
@@ -53,7 +53,17 @@ public sealed partial class ShellPage :
|
||||
|
||||
public void Receive(NavigateToListMessage message)
|
||||
{
|
||||
// The first time we navigate to a list (from loading -> main list),
|
||||
// clear out the back stack so that we can't go back again.
|
||||
var fromLoading = RootFrame.CanGoBack;
|
||||
|
||||
RootFrame.Navigate(typeof(ListPage), message.ViewModel, _slideRightTransition);
|
||||
|
||||
if (!fromLoading)
|
||||
{
|
||||
RootFrame.BackStack.Clear();
|
||||
}
|
||||
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user