/
githubmirror
/
PowerToys
Обзор
Документация
Войти
/
githubmirror
/
PowerToys
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
365 строк
12 KB
Jiří Polášek
CmdPal: Fix Dock context menu following active item in Command Bar (#46420)
28 мар 2026, 02:38
Не верифицирован
28 мар 2026, 02:38
f686155
Код
Авторство
О чём код?
// 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.CodeAnalysis; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.UI.ViewModels; public partial class ContentPageViewModel : PageViewModel, ICommandBarContext { private readonly ExtensionObject<IContentPage> _model; private readonly Lock _commandsLock = new(); private volatile CommandSnapshot _snapshot = CommandSnapshot.Empty; [ObservableProperty] public partial ObservableCollection<ContentViewModel> Content { get; set; } = []; private List<IContextItemViewModel> Commands { get; } = []; public bool HasCommands => _snapshot.PrimaryCommand is not null; public DetailsViewModel? Details { get; private set; } [MemberNotNullWhen(true, nameof(Details))] public bool HasDetails => Details is not null; /////// ICommandBarContext /////// public IReadOnlyList<IContextItemViewModel> MoreCommands => _snapshot.MoreCommands; public bool HasMoreCommands => _snapshot.SecondaryCommand is not null; public bool CanOpenContextMenu => _snapshot.AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible); public string SecondaryCommandName => _snapshot.SecondaryCommand?.Name ?? string.Empty; public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand; public CommandItemViewModel? SecondaryCommand => _snapshot.SecondaryCommand; public IReadOnlyList<IContextItemViewModel> AllCommands => _snapshot.AllCommands; // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext) : base(model, scheduler, host, providerContext) { _model = new(model); } // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent(); //// Run on background thread, from InitializeAsync or Model_ItemsChanged private void FetchContent() { List<ContentViewModel> newContent = []; try { var newItems = _model.Unsafe!.GetContent(); foreach (var item in newItems) { var viewModel = ViewModelFromContent(item, PageContext); if (viewModel is not null) { viewModel.InitializeProperties(); newContent.Add(viewModel); } } } catch (Exception ex) { ShowException(ex, _model?.Unsafe?.Name); throw; } var oneContent = newContent.Count == 1; newContent.ForEach(c => c.OnlyControlOnPage = oneContent); // Now, back to a UI thread to update the observable collection DoOnUiThread( () => { ListHelpers.InPlaceUpdateList(Content, newContent); }); } public virtual ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context) { // The core ContentPageViewModel doesn't actually handle any content, // so we just return null here. // The real content is handled by the derived class CommandPaletteContentPageViewModel return null; } public override void InitializeProperties() { base.InitializeProperties(); var model = _model.Unsafe; if (model is null) { return; // throw? } var commands = BuildCommandViewModels(model.Commands); InitializeCommandViewModels(commands, static contextItem => contextItem.InitializeProperties()); lock (_commandsLock) { ListHelpers.InPlaceUpdateList(Commands, commands); RefreshCommandSnapshotsUnsafe(); } var extensionDetails = model.Details; if (extensionDetails is not null) { Details = new(extensionDetails, PageContext); Details.InitializeProperties(); } UpdateDetails(); FetchContent(); model.ItemsChanged += Model_ItemsChanged; DoOnUiThread( () => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this)); }); } protected override void FetchProperty(string propertyName) { base.FetchProperty(propertyName); var model = this._model.Unsafe; if (model is null) { return; // throw? } switch (propertyName) { case nameof(Commands): var more = model.Commands; if (more is not null) { var newContextMenu = BuildCommandViewModels(more); InitializeCommandViewModels(newContextMenu, static contextItem => contextItem.SlowInitializeProperties()); List<IContextItemViewModel> removedItems; lock (_commandsLock) { ListHelpers.InPlaceUpdateList(Commands, newContextMenu, out removedItems); RefreshCommandSnapshotsUnsafe(); } CleanupCommandViewModels(removedItems); } else { List<IContextItemViewModel> removedItems; lock (_commandsLock) { removedItems = [.. Commands]; Commands.Clear(); RefreshCommandSnapshotsUnsafe(); } CleanupCommandViewModels(removedItems); } UpdateProperty(nameof(PrimaryCommand)); UpdateProperty(nameof(SecondaryCommand)); UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(HasCommands)); UpdateProperty(nameof(HasMoreCommands)); UpdateProperty(nameof(CanOpenContextMenu)); UpdateProperty(nameof(MoreCommands)); UpdateProperty(nameof(AllCommands)); DoOnUiThread( () => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this)); }); break; case nameof(Details): var extensionDetails = model.Details; Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; UpdateDetails(); break; } UpdateProperty(propertyName); } private void UpdateDetails() { UpdateProperty(nameof(Details)); UpdateProperty(nameof(HasDetails)); DoOnUiThread( () => { if (HasDetails) { WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details)); } else { WeakReferenceMessenger.Default.Send<HideDetailsMessage>(); } }); } private List<IContextItemViewModel> BuildCommandViewModels(IContextItem[]? items) { if (items is null) { return []; } return items .Select<IContextItem, IContextItemViewModel>(item => { if (item is ICommandContextItem contextItem) { return new CommandContextItemViewModel(contextItem, PageContext); } return new SeparatorViewModel(); }) .ToList(); } private static void InitializeCommandViewModels(IEnumerable<IContextItemViewModel> commands, Action<CommandContextItemViewModel> initialize) { foreach (var contextItem in commands.OfType<CommandContextItemViewModel>()) { initialize(contextItem); } } private static void CleanupCommandViewModels(IEnumerable<IContextItemViewModel> commands) { foreach (var contextItem in commands.OfType<CommandContextItemViewModel>()) { contextItem.SafeCleanup(); } } private void RefreshCommandSnapshotsUnsafe() { var allCommands = (IContextItemViewModel[])[.. Commands]; var moreCommands = allCommands.Length > 1 ? allCommands[1..] : []; CommandContextItemViewModel? primary = null; CommandContextItemViewModel? secondary = null; foreach (var item in allCommands) { if (item is not CommandContextItemViewModel command) { continue; } if (primary is null) { primary = command; } else if (secondary is null) { secondary = command; break; } } _snapshot = new(allCommands, moreCommands, primary, secondary); } // InvokeItemCommand is what this will be in Xaml due to source generator // this comes in on Enter keypresses in the SearchBox [RelayCommand] private void InvokePrimaryCommand(ContentPageViewModel page) { if (PrimaryCommand is not null) { WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); } } // this comes in on Ctrl+Enter keypresses in the SearchBox [RelayCommand] private void InvokeSecondaryCommand(ContentPageViewModel page) { if (SecondaryCommand is not null) { WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); } } protected override void UnsafeCleanup() { base.UnsafeCleanup(); Details?.SafeCleanup(); List<IContextItemViewModel> removedItems; lock (_commandsLock) { removedItems = [.. Commands]; Commands.Clear(); RefreshCommandSnapshotsUnsafe(); } CleanupCommandViewModels(removedItems); foreach (var item in Content) { item.SafeCleanup(); } Content.Clear(); var model = _model.Unsafe; if (model is not null) { model.ItemsChanged -= Model_ItemsChanged; } } /// <summary> /// Immutable bundle of derived command state, published atomically via a /// single volatile write so readers never see a torn snapshot. /// </summary> private sealed class CommandSnapshot( IContextItemViewModel[] allCommands, IContextItemViewModel[] moreCommands, CommandContextItemViewModel? primaryCommand, CommandContextItemViewModel? secondaryCommand) { public static CommandSnapshot Empty { get; } = new([], [], null, null); public IContextItemViewModel[] AllCommands { get; } = allCommands; public IContextItemViewModel[] MoreCommands { get; } = moreCommands; public CommandContextItemViewModel? PrimaryCommand { get; } = primaryCommand; public CommandContextItemViewModel? SecondaryCommand { get; } = secondaryCommand; } }