From c4fc884d7f4932556438ec38fafeafae147657c9 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 6 Feb 2026 05:51:43 -0600 Subject: [PATCH] Plumb through to CommandItemVM info about the extension... so we can determine if we want to pin things or not. We need to know if the extension supports the version of the API with that method on it. It feels super weird to just have CommandItemViewModel do this though - I think the more correct solution would be to have a ContextMenuFactory that generates the context menu for a command item, and then have the UI layer pass that in. But also Jolley's refactor will probably deal away with all this so --- .../AppExtensionHost.cs | 4 +- .../CommandItemViewModel.cs | 159 ++++++++++++------ .../GlobalLogPageContext.cs | 4 +- .../PageViewModel.cs | 4 + .../CommandProviderWrapper.cs | 12 +- .../Dock/DockViewModel.cs | 2 + .../TopLevelCommandManager.cs | 2 + .../TopLevelViewModel.cs | 8 +- .../AllAppsCommandProvider.cs | 6 +- 9 files changed, 146 insertions(+), 55 deletions(-) diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs index 125d8d78f4..e5fddf7ad2 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -25,6 +25,8 @@ public abstract partial class AppExtensionHost : IExtensionHost public ObservableCollection StatusMessages { get; } = []; + public virtual bool SupportsDockBands { get; set; } + public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd; public void DebugLog(string message) diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs index 04046eea0c..e01d4f3868 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs @@ -212,27 +212,28 @@ 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(); - } + BuildAndInitMoreCommands(); - // Here, we're already theoretically in the async context, so we can - // use Initialize straight up - MoreCommands - .OfType() - .ToList() - .ForEach(contextItem => - { - contextItem.SlowInitializeProperties(); - }); + // 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(); + // }); if (!string.IsNullOrEmpty(model.Command?.Name)) { _defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext) @@ -382,36 +383,36 @@ 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(); - } - } + // 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)); @@ -513,6 +514,68 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa base.SafeCleanup(); Initialized |= InitializedState.CleanedUp; } + + /// + /// * 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; + List results = []; + if (more is not null) + { + foreach (var item in more) + { + if (item is ICommandContextItem contextItem) + { + var contextItemViewModel = new CommandContextItemViewModel(contextItem, PageContext); + contextItemViewModel.SlowInitializeProperties(); + results.Add(contextItemViewModel); + } + else + { + results.Add(new SeparatorViewModel()); + } + } + + // MoreCommands = more + // .Select(item => + // { + // return item is ICommandContextItem contextItem ? new CommandContextItemViewModel(contextItem, PageContext) : new SeparatorViewModel(); + // }) + // .ToList(); + } + + if (PageContext.TryGetTarget(out var pageContext)) + { + if (pageContext.ExtensionSupportsPinning) + { + // test: just add a bunch of separators + results.Add(new SeparatorViewModel()); + results.Add(new SeparatorViewModel()); + results.Add(new SeparatorViewModel()); + } + } + + // var oldMoreCommands = MoreCommands; + // MoreCommands = results; + List? freedItems; + lock (MoreCommands) + { + ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems); + } + + freedItems.OfType() + .ToList() + .ForEach(c => c.SafeCleanup()); + } } [Flags] diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/GlobalLogPageContext.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/GlobalLogPageContext.cs index 228ccc5f4b..8bf504272f 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/GlobalLogPageContext.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/GlobalLogPageContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -8,6 +8,8 @@ public class GlobalLogPageContext : IPageContext { public TaskScheduler Scheduler { get; private init; } + bool IPageContext.ExtensionSupportsPinning => false; + public void ShowException(Exception ex, string? extensionHint) { /*do nothing*/ } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 61e0b1e7f3..6d3cad8af7 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -79,6 +79,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public IconInfoViewModel Icon { get; protected set; } + bool IPageContext.ExtensionSupportsPinning => ExtensionHost.SupportsDockBands; + public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost) : base((IPageContext?)null) { @@ -267,6 +269,8 @@ public interface IPageContext void ShowException(Exception ex, string? extensionHint = null); TaskScheduler Scheduler { get; } + + bool ExtensionSupportsPinning { get; } } public interface IPageViewModelFactoryService diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index f4efbb4e46..a7d47c75eb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -165,6 +165,7 @@ public sealed class CommandProviderWrapper if (model is ICommandProvider3 supportsDockBands) { + ExtensionHost.SupportsDockBands = true; var bands = supportsDockBands.GetDockBands(); if (bands is not null) { @@ -213,7 +214,16 @@ public sealed class CommandProviderWrapper Func make = (ICommandItem? i, TopLevelType t) => { CommandItemViewModel commandItemViewModel = new(new(i), pageContext); - TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i); + TopLevelViewModel topLevelViewModel = new( + item: commandItemViewModel, + topLevelType: t, + extensionHost: ExtensionHost, + commandProviderId: ProviderId, + settings: settings, + providerSettings: providerSettings, + serviceProvider: serviceProvider, + commandItem: i/*, + providerSupportsPinning: SupportsDockBands*/); topLevelViewModel.InitializeProperties(); return topLevelViewModel; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs index e1643fc59e..ae42562fa5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs @@ -33,6 +33,8 @@ public sealed partial class DockViewModel : IDisposable, public ObservableCollection AllItems => _topLevelCommandManager.DockBands; + bool IPageContext.ExtensionSupportsPinning => false; + public DockViewModel( TopLevelCommandManager tlcManager, SettingsModel settings, diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index c3bfdf7eca..a0ee21860b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -60,6 +60,8 @@ public partial class TopLevelCommandManager : ObservableObject, } } + bool IPageContext.ExtensionSupportsPinning => false; + public async Task LoadBuiltinsAsync() { var s = new Stopwatch(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index c6496780f4..6d995174aa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -28,6 +28,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx private readonly string _commandProviderId; + // private readonly bool _providerSupportsPinning; private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id; private string _fallbackId = string.Empty; @@ -58,6 +59,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon; + // public bool ProviderSupportsPinning => _providerSupportsPinning; + ////// ICommandItem public string Title => _commandItemViewModel.Title; @@ -204,12 +207,15 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx SettingsModel settings, ProviderSettings providerSettings, IServiceProvider serviceProvider, - ICommandItem? commandItem) + ICommandItem? commandItem)/*, + bool providerSupportsPinning*/ { _serviceProvider = serviceProvider; _settings = settings; _providerSettings = providerSettings; _commandProviderId = commandProviderId; + + // _providerSupportsPinning = providerSupportsPinning; _commandItemViewModel = item; IsFallback = topLevelType == TopLevelType.Fallback; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index 27922f9b7b..b88d34e5cd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -167,7 +167,7 @@ public partial class AllAppsCommandProvider : CommandProvider } // ... Now, combine those two - List both = bestAppMatch is null ? nameMatches : [.. nameMatches, bestAppMatch]; + var both = bestAppMatch is null ? nameMatches : [.. nameMatches, bestAppMatch]; if (both.Count == 1) { @@ -191,7 +191,7 @@ public partial class AllAppsCommandProvider : CommandProvider public override ICommandItem? GetCommandItemById(string id) { - if (id == _listItem.Command.Id) + if (id == _listItem.Command?.Id) { return _listItem; }