CmdPal: Use a factory for building the context menu VMs (#45572)

_targets #45566_

doesn't actually do anything, just moves around the instantiation of
command context item VMs.

This will let use add pin/unpin commands later

related to https://github.com/microsoft/PowerToys/issues/45191
related to https://github.com/microsoft/PowerToys/issues/45201
This commit is contained in:
Mike Griese
2026-02-24 06:26:33 -06:00
committed by GitHub
parent e8ccb7099e
commit 4f5837d4e9
9 changed files with 144 additions and 59 deletions

View File

@@ -9,11 +9,11 @@ using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels; namespace Microsoft.CmdPal.UI.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel public partial class CommandContextItemViewModel : CommandItemViewModel, IContextItemViewModel
{ {
private readonly KeyChord nullKeyChord = new(0, 0, 0); private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem); public new ExtensionObject<ICommandContextItem> Model { get; }
public bool IsCritical { get; private set; } public bool IsCritical { get; private set; }
@@ -21,6 +21,13 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord); public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
public CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context)
: base(new(contextItem), context)
{
Model = new(contextItem);
IsContextMenuItem = true;
}
public override void InitializeProperties() public override void InitializeProperties()
{ {
if (IsInitialized) if (IsInitialized)

View File

@@ -19,6 +19,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{ {
public ExtensionObject<ICommandItem> Model => _commandItemModel; public ExtensionObject<ICommandItem> Model => _commandItemModel;
private readonly IContextMenuFactory? _contextMenuFactory;
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; } private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null); private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
@@ -35,6 +37,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized); protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized);
public bool IsContextMenuItem { get; protected init; }
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error); public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
// These are properties that are "observable" from the extension object // These are properties that are "observable" from the extension object
@@ -96,10 +100,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_errorIcon.InitializeProperties(); _errorIcon.InitializeProperties();
} }
public CommandItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext) public CommandItemViewModel(
ExtensionObject<ICommandItem> item,
WeakReference<IPageContext> errorContext,
IContextMenuFactory? contextMenuFactory = null)
: base(errorContext) : base(errorContext)
{ {
_commandItemModel = item; _commandItemModel = item;
_contextMenuFactory = contextMenuFactory;
Command = new(null, errorContext); Command = new(null, errorContext);
} }
@@ -197,26 +205,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
return; return;
} }
var more = model.MoreCommands; BuildAndInitMoreCommands();
if (more is not null)
{
MoreCommands = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
}
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up
MoreCommands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
if (!string.IsNullOrEmpty(model.Command?.Name)) if (!string.IsNullOrEmpty(model.Command?.Name))
{ {
@@ -370,36 +359,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
break; break;
case nameof(model.MoreCommands): case nameof(model.MoreCommands):
var more = model.MoreCommands; BuildAndInitMoreCommands();
if (more is not null)
{
var newContextMenu = more
.Select<IContextItem, IContextItemViewModel>(item =>
{
return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel();
})
.ToList();
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
}
newContextMenu
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
lock (MoreCommands)
{
MoreCommands.Clear();
}
}
UpdateProperty(nameof(SecondaryCommand)); UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands)); UpdateProperty(nameof(HasMoreCommands));
@@ -477,6 +437,33 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher) public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _subtitleCache.GetOrUpdate(matcher, Subtitle); => _subtitleCache.GetOrUpdate(matcher, Subtitle);
/// <remarks>
/// * Does call SlowInitializeProperties on the created items.
/// * does NOT call UpdateProperty ; caller must do that.
/// </remarks>
private void BuildAndInitMoreCommands()
{
var model = _commandItemModel.Unsafe;
if (model is null)
{
return;
}
var more = model.MoreCommands;
var factory = _contextMenuFactory ?? DefaultContextMenuFactory.Instance;
var results = factory.UnsafeBuildAndInitMoreCommands(more, this);
List<IContextItemViewModel>? freedItems;
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
}
freedItems.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
}
protected override void UnsafeCleanup() protected override void UnsafeCleanup()
{ {
base.UnsafeCleanup(); base.UnsafeCleanup();

View File

@@ -10,17 +10,19 @@ public class CommandPalettePageViewModelFactory
: IPageViewModelFactoryService : IPageViewModelFactoryService
{ {
private readonly TaskScheduler _scheduler; private readonly TaskScheduler _scheduler;
private readonly IContextMenuFactory? _contextMenuFactory;
public CommandPalettePageViewModelFactory(TaskScheduler scheduler) public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory? contextMenuFactory)
{ {
_scheduler = scheduler; _scheduler = scheduler;
_contextMenuFactory = contextMenuFactory;
} }
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext) public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext)
{ {
return page switch return page switch
{ {
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext) { IsNested = nested }, IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsNested = nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext), IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
_ => null, _ => null,
}; };

View File

@@ -0,0 +1,43 @@
// 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.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class DefaultContextMenuFactory : IContextMenuFactory
{
public static readonly DefaultContextMenuFactory Instance = new();
private DefaultContextMenuFactory()
{
}
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
IContextItem[] items,
CommandItemViewModel commandItem)
{
List<IContextItemViewModel> results = [];
if (items is null)
{
return results;
}
foreach (var item in items)
{
if (item is ICommandContextItem contextItem)
{
var contextItemViewModel = new CommandContextItemViewModel(contextItem, commandItem.PageContext);
contextItemViewModel.SlowInitializeProperties();
results.Add(contextItemViewModel);
}
else
{
results.Add(new SeparatorViewModel());
}
}
return results;
}
}

View File

@@ -0,0 +1,12 @@
// 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.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public interface IContextMenuFactory
{
List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem);
}

View File

@@ -63,8 +63,8 @@ public partial class ListItemViewModel : CommandItemViewModel
} }
} }
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context) public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory? contextMenuFactory = null)
: base(new(model), context) : base(new(model), context, contextMenuFactory)
{ {
Model = new ExtensionObject<IListItem>(model); Model = new ExtensionObject<IListItem>(model);
} }

View File

@@ -32,6 +32,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private readonly ExtensionObject<IListPage> _model; private readonly ExtensionObject<IListPage> _model;
private readonly Lock _listLock = new(); private readonly Lock _listLock = new();
private readonly IContextMenuFactory? _contextMenuFactory;
private InterlockedBoolean _isLoading; private InterlockedBoolean _isLoading;
private bool _isFetching; private bool _isFetching;
@@ -88,11 +89,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
} }
} }
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext) public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext, IContextMenuFactory? contextMenuFactory)
: base(model, scheduler, host, providerContext) : base(model, scheduler, host, providerContext)
{ {
_model = new(model); _model = new(model);
EmptyContent = new(new(null), PageContext); _contextMenuFactory = contextMenuFactory;
EmptyContent = new(new(null), PageContext, _contextMenuFactory);
} }
private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)

View File

@@ -220,6 +220,7 @@ public partial class App : Application, IDisposable
// ViewModels // ViewModels
services.AddSingleton<ShellViewModel>(); services.AddSingleton<ShellViewModel>();
services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>(); services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
} }

View File

@@ -0,0 +1,31 @@
// 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.UI.ViewModels;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI;
internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFactory
{
private readonly SettingsModel _settingsModel;
private readonly TopLevelCommandManager _topLevelCommandManager;
public CommandPaletteContextMenuFactory(SettingsModel settingsModel, TopLevelCommandManager topLevelCommandManager)
{
_settingsModel = settingsModel;
_topLevelCommandManager = topLevelCommandManager;
}
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
IContextItem[] items,
CommandItemViewModel commandItem)
{
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
// TODO: #45201 Here, we'll want to add pin/unpin commands for pinning
// items to the top-level or to the dock.
return results;
}
}