Just like I drew it up

This commit is contained in:
Mike Griese
2026-02-12 10:03:48 -06:00
parent 50e702ad9b
commit 64872a5467
8 changed files with 98 additions and 10 deletions

View File

@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
@@ -153,6 +154,9 @@ public sealed class CommandProviderWrapper
// On a BG thread here
var fallbacks = model.FallbackCommands();
// Load pinned commands from saved settings
var pinnedCommands = LoadPinnedCommands(model, providerSettings);
if (model is ICommandProvider2 two)
{
UnsafePreCacheApiAdditions(two);
@@ -175,7 +179,7 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
@@ -206,7 +210,7 @@ public sealed class CommandProviderWrapper
}
}
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
@@ -220,13 +224,20 @@ public sealed class CommandProviderWrapper
return topLevelViewModel;
};
var topLevelList = new List<TopLevelViewModel>();
if (commands is not null)
{
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
.ToArray();
topLevelList.AddRange(commands.Select(c => makeAndAdd(c, false)));
}
if (pinnedCommands is not null)
{
topLevelList.AddRange(pinnedCommands.Select(c => makeAndAdd(c, false)));
}
TopLevelItems = topLevelList.ToArray();
if (fallbacks is not null)
{
FallbackItems = fallbacks
@@ -235,6 +246,32 @@ public sealed class CommandProviderWrapper
}
}
private ICommandItem[] LoadPinnedCommands(ICommandProvider model, ProviderSettings providerSettings)
{
var pinnedItems = new List<ICommandItem>();
if (model is ICommandProvider4 provider4)
{
foreach (var pinnedId in providerSettings.PinnedCommandIds)
{
try
{
var commandItem = provider4.GetCommandItem(pinnedId);
if (commandItem is not null)
{
pinnedItems.Add(commandItem);
}
}
catch (Exception e)
{
Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}");
}
}
}
return pinnedItems.ToArray();
}
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
{
var apiExtensions = provider.GetApiExtensionStubs();
@@ -248,6 +285,21 @@ public sealed class CommandProviderWrapper
}
}
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
if (!providerSettings.PinnedCommandIds.Contains(commandId))
{
providerSettings.PinnedCommandIds.Add(commandId);
SettingsModel.SaveSettings(settings);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
}
}
public CommandProviderContext GetProviderContext()
{
return new() { ProviderId = ProviderId };

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record PinCommandItemMessage(string ProviderId, string CommandId)
{
}

View File

@@ -18,6 +18,8 @@ public class ProviderSettings
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
public List<string> PinnedCommandIds { get; set; } = [];
[JsonIgnore]
public string ProviderDisplayName { get; set; } = string.Empty;

View File

@@ -22,6 +22,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>,
IRecipient<PinCommandItemMessage>,
IPageContext,
IDisposable
{
@@ -42,6 +43,7 @@ public partial class TopLevelCommandManager : ObservableObject,
_commandProviderCache = commandProviderCache;
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
}
@@ -414,6 +416,21 @@ public partial class TopLevelCommandManager : ObservableObject,
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
public void Receive(PinCommandItemMessage message)
{
var wrapper = LookupProvider(message.ProviderId);
wrapper?.PinCommand(message.CommandId, _serviceProvider);
}
private CommandProviderWrapper? LookupProvider(string providerId)
{
lock (_commandProvidersLock)
{
return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId)
?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId);
}
}
void IPageContext.ShowException(Exception ex, string? extensionHint)
{
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");

View File

@@ -27,7 +27,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
private readonly CommandProviderContext _commandProviderContext;
public CommandProviderContext ProviderContext { get; private set; }
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
@@ -57,7 +57,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => _commandProviderContext.ProviderId;
public string CommandProviderId => ProviderContext.ProviderId;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
@@ -199,7 +199,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
_serviceProvider = serviceProvider;
_settings = settings;
_providerSettings = providerSettings;
_commandProviderContext = commandProviderContext;
ProviderContext = commandProviderContext;
_commandItemViewModel = item;
IsFallback = isFallback;

View File

@@ -32,7 +32,7 @@ internal sealed class PowerToysAppHostService : IAppHostService
CommandProviderContext? topLevelId = null;
if (command is TopLevelViewModel topLevelViewModel)
{
topLevelId = topLevelViewModel.CommandProviderContext;
topLevelId = topLevelViewModel.ProviderContext;
}
return topLevelId ?? currentContext ?? throw new InvalidOperationException("No command provider context could be found for the given command, and no current context was provided.");

View File

@@ -6,7 +6,7 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2, ICommandProvider4
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -22,6 +22,8 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
public virtual ICommand? GetCommand(string id) => null;
public virtual ICommandItem? GetCommandItem(string id) => null;
public virtual ICommandSettings? Settings { get; protected set; }
public virtual bool Frozen { get; protected set; } = true;

View File

@@ -405,6 +405,12 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider4 requires ICommandProvider
{
ICommandItem GetCommandItem(String id);
};
}