diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs index 2f507495c4..c919e3131f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs @@ -9,11 +9,11 @@ using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.UI.ViewModels; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel +public partial class CommandContextItemViewModel : CommandItemViewModel, IContextItemViewModel { private readonly KeyChord nullKeyChord = new(0, 0, 0); - public new ExtensionObject Model { get; } = new(contextItem); + public new ExtensionObject Model { get; } 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 CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) + : base(new(contextItem), context) + { + Model = new(contextItem); + IsContextMenuItem = true; + } + public override void InitializeProperties() { if (IsInitialized) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index 25078ace70..42d8272349 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -19,6 +19,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa { public ExtensionObject Model => _commandItemModel; + private readonly IContextMenuFactory? _contextMenuFactory; + private ExtensionObject? ExtendedAttributesProvider { get; set; } private readonly ExtensionObject _commandItemModel = new(null); @@ -35,6 +37,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa protected bool IsSelectedInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.SelectionInitialized); + public bool IsContextMenuItem { get; protected init; } + public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error); // These are properties that are "observable" from the extension object @@ -96,10 +100,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa _errorIcon.InitializeProperties(); } - public CommandItemViewModel(ExtensionObject item, WeakReference errorContext) + public CommandItemViewModel( + ExtensionObject item, + WeakReference errorContext, + IContextMenuFactory? contextMenuFactory = null) : base(errorContext) { _commandItemModel = item; + _contextMenuFactory = contextMenuFactory; Command = new(null, errorContext); } @@ -197,26 +205,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa return; } - var more = model.MoreCommands; - if (more is not null) - { - MoreCommands = more - .Select(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() - .ToList() - .ForEach(contextItem => - { - contextItem.SlowInitializeProperties(); - }); + BuildAndInitMoreCommands(); if (!string.IsNullOrEmpty(model.Command?.Name)) { @@ -370,36 +359,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa break; case nameof(model.MoreCommands): - var more = model.MoreCommands; - if (more is not null) - { - var newContextMenu = more - .Select(item => - { - return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel(); - }) - .ToList(); - lock (MoreCommands) - { - ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu); - } - - newContextMenu - .OfType() - .ToList() - .ForEach(contextItem => - { - contextItem.InitializeProperties(); - }); - } - else - { - lock (MoreCommands) - { - MoreCommands.Clear(); - } - } - + BuildAndInitMoreCommands(); UpdateProperty(nameof(SecondaryCommand)); UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(HasMoreCommands)); @@ -477,6 +437,33 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher) => _subtitleCache.GetOrUpdate(matcher, Subtitle); + /// + /// * Does call SlowInitializeProperties on the created items. + /// * does NOT call UpdateProperty ; caller must do that. + /// + 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? freedItems; + lock (MoreCommands) + { + ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems); + } + + freedItems.OfType() + .ToList() + .ForEach(c => c.SafeCleanup()); + } + protected override void UnsafeCleanup() { base.UnsafeCleanup(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs index 155f755c66..a8bd6899e2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPalettePageViewModelFactory.cs @@ -10,17 +10,19 @@ public class CommandPalettePageViewModelFactory : IPageViewModelFactoryService { private readonly TaskScheduler _scheduler; + private readonly IContextMenuFactory? _contextMenuFactory; - public CommandPalettePageViewModelFactory(TaskScheduler scheduler) + public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory? contextMenuFactory) { _scheduler = scheduler; + _contextMenuFactory = contextMenuFactory; } public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext) { 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), _ => null, }; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DefaultContextMenuFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DefaultContextMenuFactory.cs new file mode 100644 index 0000000000..db49ac85f2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DefaultContextMenuFactory.cs @@ -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 UnsafeBuildAndInitMoreCommands( + IContextItem[] items, + CommandItemViewModel commandItem) + { + List 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; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IContextMenuFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IContextMenuFactory.cs new file mode 100644 index 0000000000..715269380a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/IContextMenuFactory.cs @@ -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 UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs index 7b8f9c35a6..dd7e4a4096 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListItemViewModel.cs @@ -63,8 +63,8 @@ public partial class ListItemViewModel : CommandItemViewModel } } - public ListItemViewModel(IListItem model, WeakReference context) - : base(new(model), context) + public ListItemViewModel(IListItem model, WeakReference context, IContextMenuFactory? contextMenuFactory = null) + : base(new(model), context, contextMenuFactory) { Model = new ExtensionObject(model); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs index 6454ccdae8..6f5d133e7a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -32,6 +32,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private readonly ExtensionObject _model; private readonly Lock _listLock = new(); + private readonly IContextMenuFactory? _contextMenuFactory; private InterlockedBoolean _isLoading; 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) { _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) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index ead2d4f209..3e4e4adb65 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -220,6 +220,7 @@ public partial class App : Application, IDisposable // ViewModels services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs new file mode 100644 index 0000000000..684d39299a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CommandPaletteContextMenuFactory.cs @@ -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 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; + } +}