mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-10 04:20:23 +02:00
## Summary of the Pull Request This PR fixes opening of the list item context menu through right-click, which affected mainly 3rd party out-of-process extensions. - Keeps the first context-menu request alive when a top-level item is selected but its out-of-proc MoreCommands are still hydrating. - Short-circuits CanOpenContextMenu when a valid synthetic primary command is already available, so items with a usable primary action can open immediately without waiting for late menu hydration. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46625 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
652 lines
21 KiB
C#
652 lines
21 KiB
C#
// 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.Diagnostics.CodeAnalysis;
|
|
using Microsoft.CmdPal.Common;
|
|
using Microsoft.CmdPal.Common.Helpers;
|
|
using Microsoft.CmdPal.Common.Text;
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
|
using Microsoft.CmdPal.UI.ViewModels.Models;
|
|
using Microsoft.CommandPalette.Extensions;
|
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
using Windows.ApplicationModel.DataTransfer;
|
|
|
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
|
|
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
|
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext, IPrecomputedListItem
|
|
{
|
|
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
|
|
|
private readonly IContextMenuFactory? _contextMenuFactory;
|
|
|
|
private readonly Lock _moreCommandsLock = new();
|
|
private readonly List<IContextItemViewModel> _moreCommands = [];
|
|
private volatile CommandContextItemViewModel? _secondaryMoreCommand;
|
|
private volatile IContextItemViewModel[] _moreCommandsSnapshot = [];
|
|
private volatile IContextItemViewModel[] _allCommandsSnapshot = [];
|
|
|
|
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
|
|
|
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
|
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
|
|
|
private FuzzyTargetCache _titleCache;
|
|
private FuzzyTargetCache _subtitleCache;
|
|
|
|
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
|
|
|
|
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
|
|
|
|
protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized);
|
|
|
|
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
|
|
// itself, in the sense that they get raised by PropChanged events from the
|
|
// extension. However, we don't want to actually make them
|
|
// [ObservableProperty]s, because PropChanged comes in off the UI thread,
|
|
// and ObservableProperty is not smart enough to raise the PropertyChanged
|
|
// on the UI thread.
|
|
public string Name => Command.Name;
|
|
|
|
private string _itemTitle = string.Empty;
|
|
|
|
protected string ItemTitle => _itemTitle;
|
|
|
|
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
|
|
|
public virtual string Subtitle { get; private set; } = string.Empty;
|
|
|
|
private IconInfoViewModel _icon = new(null);
|
|
|
|
public IconInfoViewModel Icon => _icon.IsSet ? _icon : Command.Icon;
|
|
|
|
public CommandViewModel Command { get; private set; }
|
|
|
|
// Reuse a cached read-only snapshot so repeated reads don't allocate.
|
|
public IReadOnlyList<IContextItemViewModel> MoreCommands => _moreCommandsSnapshot;
|
|
|
|
IReadOnlyList<IContextItemViewModel> IContextMenuContext.MoreCommands => _moreCommandsSnapshot;
|
|
|
|
protected Lock MoreCommandsLock => _moreCommandsLock;
|
|
|
|
protected List<IContextItemViewModel> UnsafeMoreCommands => _moreCommands;
|
|
|
|
public bool HasMoreCommands => _secondaryMoreCommand is not null;
|
|
|
|
public string SecondaryCommandName => _secondaryMoreCommand?.Name ?? string.Empty;
|
|
|
|
public CommandItemViewModel? PrimaryCommand => this;
|
|
|
|
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
|
|
|
|
public bool CanOpenContextMenu =>
|
|
|
|
// BEAR LOADING: A visible synthetic primary command makes the item
|
|
// context-openable immediately, even if out-of-proc MoreCommands are still
|
|
// hydrating. Without this fast path, the first open request can race slow
|
|
// menu initialization and get dropped.
|
|
_defaultCommandContextItemViewModel?.ShouldBeVisible == true ||
|
|
_moreCommandsSnapshot.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
|
|
|
|
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
|
|
|
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
|
|
|
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
|
|
|
public virtual bool HasText => HasTitle || HasSubtitle;
|
|
|
|
public DataPackageView? DataPackage { get; private set; }
|
|
|
|
public IReadOnlyList<IContextItemViewModel> AllCommands => _allCommandsSnapshot;
|
|
|
|
private static readonly IconInfoViewModel _errorIcon;
|
|
|
|
static CommandItemViewModel()
|
|
{
|
|
_errorIcon = new(new IconInfo("\uEA39")); // ErrorBadge
|
|
_errorIcon.InitializeProperties();
|
|
}
|
|
|
|
public CommandItemViewModel(
|
|
ExtensionObject<ICommandItem> item,
|
|
WeakReference<IPageContext> errorContext,
|
|
IContextMenuFactory? contextMenuFactory)
|
|
: base(errorContext)
|
|
{
|
|
_commandItemModel = item;
|
|
_contextMenuFactory = contextMenuFactory;
|
|
Command = new(null, errorContext);
|
|
}
|
|
|
|
public void FastInitializeProperties()
|
|
{
|
|
if (IsFastInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var model = _commandItemModel.Unsafe;
|
|
if (model is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var command = model.Command;
|
|
Command = new(command, PageContext);
|
|
Command.FastInitializeProperties();
|
|
|
|
_itemTitle = model.Title;
|
|
Subtitle = model.Subtitle;
|
|
_titleCache.Invalidate();
|
|
_subtitleCache.Invalidate();
|
|
TryCreateDefaultCommandContextItem(command);
|
|
|
|
Initialized |= InitializedState.FastInitialized;
|
|
}
|
|
|
|
//// Called from ListViewModel on background thread started in ListPage.xaml.cs
|
|
public override void InitializeProperties()
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!IsFastInitialized)
|
|
{
|
|
FastInitializeProperties();
|
|
}
|
|
|
|
var model = _commandItemModel.Unsafe;
|
|
if (model is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Command.InitializeProperties();
|
|
|
|
var icon = model.Icon;
|
|
if (icon is not null)
|
|
{
|
|
_icon = new(icon);
|
|
_icon.InitializeProperties();
|
|
}
|
|
|
|
// TODO: Do these need to go into FastInit?
|
|
model.PropChanged += Model_PropChanged;
|
|
Command.PropertyChanged += Command_PropertyChanged;
|
|
|
|
UpdateProperty(nameof(Name));
|
|
UpdateProperty(nameof(Title));
|
|
UpdateProperty(nameof(Subtitle));
|
|
UpdateProperty(nameof(Icon));
|
|
|
|
// Load-bearing: if you don't raise a IsInitialized here, then
|
|
// TopLevelViewModel will never know what the command's ID is, so it
|
|
// will never be able to load Hotkeys & aliases
|
|
UpdateProperty(nameof(IsInitialized));
|
|
|
|
if (model is IExtendedAttributesProvider extendedAttributesProvider)
|
|
{
|
|
ExtendedAttributesProvider = new ExtensionObject<IExtendedAttributesProvider>(extendedAttributesProvider);
|
|
var properties = extendedAttributesProvider.GetProperties();
|
|
UpdateDataPackage(properties);
|
|
}
|
|
|
|
Initialized |= InitializedState.Initialized;
|
|
}
|
|
|
|
public virtual void SlowInitializeProperties()
|
|
{
|
|
if (IsSelectedInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!IsInitialized)
|
|
{
|
|
InitializeProperties();
|
|
}
|
|
|
|
var model = _commandItemModel.Unsafe;
|
|
if (model is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BuildAndInitMoreCommands();
|
|
|
|
TryCreateDefaultCommandContextItem(model.Command);
|
|
|
|
lock (_moreCommandsLock)
|
|
{
|
|
RefreshMoreCommandStateUnsafe();
|
|
}
|
|
|
|
Initialized |= InitializedState.SelectionInitialized;
|
|
UpdateProperty(nameof(MoreCommands));
|
|
UpdateProperty(nameof(AllCommands));
|
|
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
|
|
UpdateProperty(nameof(CanOpenContextMenu));
|
|
UpdateProperty(nameof(IsSelectedInitialized));
|
|
}
|
|
|
|
public bool SafeFastInit()
|
|
{
|
|
try
|
|
{
|
|
FastInitializeProperties();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CoreLogger.LogError("error fast initializing CommandItemViewModel", ex);
|
|
Command = new(null, PageContext);
|
|
_itemTitle = "Error";
|
|
Subtitle = "Item failed to load";
|
|
ClearMoreCommands();
|
|
_icon = _errorIcon;
|
|
_titleCache.Invalidate();
|
|
_subtitleCache.Invalidate();
|
|
Initialized |= InitializedState.Error;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool SafeSlowInit()
|
|
{
|
|
try
|
|
{
|
|
SlowInitializeProperties();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Initialized |= InitializedState.Error;
|
|
CoreLogger.LogError("error slow initializing CommandItemViewModel", ex);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool SafeInitializeProperties()
|
|
{
|
|
try
|
|
{
|
|
InitializeProperties();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CoreLogger.LogError("error initializing CommandItemViewModel", ex);
|
|
Command = new(null, PageContext);
|
|
_itemTitle = "Error";
|
|
Subtitle = "Item failed to load";
|
|
ClearMoreCommands();
|
|
_icon = _errorIcon;
|
|
_titleCache.Invalidate();
|
|
_subtitleCache.Invalidate();
|
|
Initialized |= InitializedState.Error;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
|
{
|
|
try
|
|
{
|
|
FetchProperty(args.PropertyName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ShowException(ex, _commandItemModel?.Unsafe?.Title);
|
|
}
|
|
}
|
|
|
|
protected virtual void FetchProperty(string propertyName)
|
|
{
|
|
var model = this._commandItemModel.Unsafe;
|
|
if (model is null)
|
|
{
|
|
return; // throw?
|
|
}
|
|
|
|
switch (propertyName)
|
|
{
|
|
case nameof(Command):
|
|
Command.PropertyChanged -= Command_PropertyChanged;
|
|
var command = model.Command;
|
|
Command = new(command, PageContext);
|
|
Command.InitializeProperties();
|
|
Command.PropertyChanged += Command_PropertyChanged;
|
|
|
|
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
|
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
|
_itemTitle = model.Title;
|
|
|
|
if (_defaultCommandContextItemViewModel is not null)
|
|
{
|
|
_defaultCommandContextItemViewModel.Command = Command;
|
|
_defaultCommandContextItemViewModel.UpdateTitle(_itemTitle);
|
|
UpdateDefaultContextItemIcon();
|
|
}
|
|
else
|
|
{
|
|
TryCreateDefaultCommandContextItem(command);
|
|
}
|
|
|
|
UpdateProperty(nameof(Name));
|
|
UpdateProperty(nameof(Title));
|
|
UpdateProperty(nameof(Icon));
|
|
UpdateProperty(nameof(HasText));
|
|
UpdateProperty(nameof(CanOpenContextMenu));
|
|
break;
|
|
|
|
case nameof(Title):
|
|
_itemTitle = model.Title;
|
|
_titleCache.Invalidate();
|
|
UpdateProperty(nameof(HasText));
|
|
break;
|
|
|
|
case nameof(Subtitle):
|
|
var modelSubtitle = model.Subtitle;
|
|
this.Subtitle = modelSubtitle;
|
|
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
|
_subtitleCache.Invalidate();
|
|
UpdateProperty(nameof(HasText));
|
|
break;
|
|
|
|
case nameof(Icon):
|
|
var oldIcon = _icon;
|
|
_icon = new(model.Icon);
|
|
_icon.InitializeProperties();
|
|
if (oldIcon.IsSet || _icon.IsSet)
|
|
{
|
|
UpdateProperty(nameof(Icon));
|
|
}
|
|
|
|
UpdateDefaultContextItemIcon();
|
|
|
|
break;
|
|
|
|
case nameof(model.MoreCommands):
|
|
BuildAndInitMoreCommands();
|
|
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands), nameof(CanOpenContextMenu));
|
|
|
|
break;
|
|
case nameof(DataPackage):
|
|
UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties());
|
|
break;
|
|
}
|
|
|
|
UpdateProperty(propertyName);
|
|
}
|
|
|
|
private void Command_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
var propertyName = e.PropertyName;
|
|
var model = _commandItemModel.Unsafe;
|
|
if (model is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (propertyName)
|
|
{
|
|
case nameof(Command.Name):
|
|
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
|
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
|
_itemTitle = model.Title;
|
|
_titleCache.Invalidate();
|
|
UpdateProperty(nameof(Title), nameof(Name));
|
|
UpdateProperty(nameof(CanOpenContextMenu));
|
|
|
|
if (_defaultCommandContextItemViewModel is not null)
|
|
{
|
|
_defaultCommandContextItemViewModel.UpdateTitle(model.Command.Name);
|
|
}
|
|
else
|
|
{
|
|
TryCreateDefaultCommandContextItem(model.Command);
|
|
}
|
|
|
|
break;
|
|
|
|
case nameof(Command.Icon):
|
|
UpdateDefaultContextItemIcon();
|
|
UpdateProperty(nameof(Icon));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates <see cref="_defaultCommandContextItemViewModel"/> when it does not exist
|
|
/// yet and the current command has a non-empty name. This covers the case
|
|
/// where an extension initially exposes a <c>NoOpCommand</c> (empty name)
|
|
/// and later switches to a concrete command after <see cref="SlowInitializeProperties"/> has already run.
|
|
/// When a new instance is created, the snapshot is refreshed and
|
|
/// <see cref="AllCommands"/> is notified.
|
|
/// </summary>
|
|
private void TryCreateDefaultCommandContextItem(ICommand? commandModel)
|
|
{
|
|
if (_defaultCommandContextItemViewModel is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We only synthesize the primary entry when the command is already
|
|
// usable; a null/empty primary must still fall back to late
|
|
// MoreCommands-based opening.
|
|
if (string.IsNullOrEmpty(Command.Name) || commandModel is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(commandModel), PageContext)
|
|
{
|
|
_itemTitle = Name,
|
|
Subtitle = Subtitle,
|
|
Command = Command,
|
|
|
|
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
|
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
|
|
};
|
|
|
|
UpdateDefaultContextItemIcon();
|
|
|
|
lock (_moreCommandsLock)
|
|
{
|
|
RefreshMoreCommandStateUnsafe();
|
|
}
|
|
|
|
UpdateProperty(nameof(AllCommands));
|
|
}
|
|
|
|
private void UpdateDefaultContextItemIcon() =>
|
|
|
|
// Command icon takes precedence over our icon on the primary command
|
|
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
|
|
|
private void UpdateTitle(string? title)
|
|
{
|
|
_itemTitle = title ?? string.Empty;
|
|
_titleCache.Invalidate();
|
|
UpdateProperty(nameof(Title));
|
|
}
|
|
|
|
private void UpdateIcon(IIconInfo? iconInfo)
|
|
{
|
|
_icon = new(iconInfo);
|
|
_icon.InitializeProperties();
|
|
UpdateProperty(nameof(Icon));
|
|
}
|
|
|
|
private void UpdateDataPackage(IDictionary<string, object?>? properties)
|
|
{
|
|
DataPackage =
|
|
properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true &&
|
|
dataPackageView is DataPackageView view
|
|
? view
|
|
: null;
|
|
UpdateProperty(nameof(DataPackage));
|
|
}
|
|
|
|
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
|
|
=> _titleCache.GetOrUpdate(matcher, Title);
|
|
|
|
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
|
|
=> _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 (_moreCommandsLock)
|
|
{
|
|
ListHelpers.InPlaceUpdateList(_moreCommands, results, out freedItems);
|
|
RefreshMoreCommandStateUnsafe();
|
|
}
|
|
|
|
freedItems.OfType<CommandContextItemViewModel>()
|
|
.ToList()
|
|
.ForEach(c => c.SafeCleanup());
|
|
}
|
|
|
|
public void RefreshMoreCommands()
|
|
{
|
|
Task.Run(RefreshMoreCommandsSynchronous);
|
|
}
|
|
|
|
private void RefreshMoreCommandsSynchronous()
|
|
{
|
|
try
|
|
{
|
|
BuildAndInitMoreCommands();
|
|
UpdateProperty(nameof(MoreCommands));
|
|
UpdateProperty(nameof(AllCommands));
|
|
UpdateProperty(nameof(SecondaryCommand));
|
|
UpdateProperty(nameof(SecondaryCommandName));
|
|
UpdateProperty(nameof(HasMoreCommands));
|
|
UpdateProperty(nameof(CanOpenContextMenu));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Handle any exceptions that might occur during the refresh process
|
|
CoreLogger.LogError("Error refreshing MoreCommands in CommandItemViewModel", ex);
|
|
ShowException(ex, _commandItemModel?.Unsafe?.Title);
|
|
}
|
|
}
|
|
|
|
protected override void UnsafeCleanup()
|
|
{
|
|
base.UnsafeCleanup();
|
|
|
|
List<IContextItemViewModel> freedItems;
|
|
CommandContextItemViewModel? freedDefault;
|
|
lock (_moreCommandsLock)
|
|
{
|
|
freedItems = [.. _moreCommands];
|
|
_moreCommands.Clear();
|
|
|
|
// Null out here so the single RefreshMoreCommandStateUnsafe call
|
|
// produces an _allCommandsSnapshot that excludes the default command.
|
|
freedDefault = _defaultCommandContextItemViewModel;
|
|
_defaultCommandContextItemViewModel = null;
|
|
|
|
RefreshMoreCommandStateUnsafe();
|
|
}
|
|
|
|
// Cleanup outside lock to avoid holding it during RPC calls
|
|
freedItems.OfType<CommandContextItemViewModel>()
|
|
.ToList()
|
|
.ForEach(c => c.SafeCleanup());
|
|
freedDefault?.SafeCleanup();
|
|
|
|
// _listItemIcon.SafeCleanup();
|
|
_icon = new(null); // necessary?
|
|
|
|
Command.PropertyChanged -= Command_PropertyChanged;
|
|
Command.SafeCleanup();
|
|
|
|
var model = _commandItemModel.Unsafe;
|
|
if (model is not null)
|
|
{
|
|
model.PropChanged -= Model_PropChanged;
|
|
}
|
|
}
|
|
|
|
public override void SafeCleanup()
|
|
{
|
|
base.SafeCleanup();
|
|
Initialized |= InitializedState.CleanedUp;
|
|
}
|
|
|
|
protected void RefreshMoreCommandStateUnsafe()
|
|
{
|
|
_moreCommandsSnapshot = [.. _moreCommands];
|
|
|
|
_secondaryMoreCommand = null;
|
|
foreach (var item in _moreCommands)
|
|
{
|
|
if (item is CommandContextItemViewModel command)
|
|
{
|
|
_secondaryMoreCommand = command;
|
|
break;
|
|
}
|
|
}
|
|
|
|
_allCommandsSnapshot = _defaultCommandContextItemViewModel is null ?
|
|
_moreCommandsSnapshot :
|
|
[_defaultCommandContextItemViewModel, .. _moreCommandsSnapshot];
|
|
}
|
|
|
|
private void ClearMoreCommands()
|
|
{
|
|
List<IContextItemViewModel> freedItems;
|
|
lock (_moreCommandsLock)
|
|
{
|
|
freedItems = [.. _moreCommands];
|
|
_moreCommands.Clear();
|
|
RefreshMoreCommandStateUnsafe();
|
|
}
|
|
|
|
freedItems.OfType<CommandContextItemViewModel>()
|
|
.ToList()
|
|
.ForEach(c => c.SafeCleanup());
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
internal enum InitializedState
|
|
{
|
|
Uninitialized = 0,
|
|
FastInitialized = 1,
|
|
Initialized = 2,
|
|
SelectionInitialized = 4,
|
|
Error = 8,
|
|
CleanedUp = 16,
|
|
}
|