diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs index a5d34eea63..10ff3ce431 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs @@ -2,7 +2,6 @@ // 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.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI; @@ -106,7 +105,8 @@ public sealed partial class CommandBarViewModel : ObservableObject, { switch (e.PropertyName) { - case nameof(SelectedItem.HasMoreCommands): + case nameof(SelectedItem.CanOpenContextMenu): + case nameof(SelectedItem.SecondaryCommand): UpdateContextItems(); break; } @@ -122,9 +122,7 @@ public sealed partial class CommandBarViewModel : ObservableObject, } SecondaryCommand = SelectedItem.SecondaryCommand; - var moreCommands = SelectedItem.MoreCommands; - - ShouldShowContextMenu = moreCommands.Count > 1 && SelectedItem.HasMoreCommands; + ShouldShowContextMenu = SelectedItem.CanOpenContextMenu; OnPropertyChanged(nameof(HasSecondaryCommand)); OnPropertyChanged(nameof(SecondaryCommand)); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index e85d3b1fb1..9038d9ef63 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -86,6 +86,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand; + public bool CanOpenContextMenu => AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible); + public bool ShouldBeVisible => !string.IsNullOrEmpty(Name); public bool HasTitle => !string.IsNullOrEmpty(Title); @@ -224,6 +226,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa UpdateProperty(nameof(MoreCommands)); UpdateProperty(nameof(AllCommands)); UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands)); + UpdateProperty(nameof(CanOpenContextMenu)); UpdateProperty(nameof(IsSelectedInitialized)); } @@ -336,6 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa UpdateProperty(nameof(Title)); UpdateProperty(nameof(Icon)); UpdateProperty(nameof(HasText)); + UpdateProperty(nameof(CanOpenContextMenu)); break; case nameof(Title): @@ -367,7 +371,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa case nameof(model.MoreCommands): BuildAndInitMoreCommands(); - UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands)); + UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands), nameof(CanOpenContextMenu)); break; case nameof(DataPackage): @@ -395,6 +399,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa _itemTitle = model.Title; _titleCache.Invalidate(); UpdateProperty(nameof(Title), nameof(Name)); + UpdateProperty(nameof(CanOpenContextMenu)); if (_defaultCommandContextItemViewModel is not null) { @@ -532,6 +537,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa UpdateProperty(nameof(SecondaryCommand)); UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(HasMoreCommands)); + UpdateProperty(nameof(CanOpenContextMenu)); } catch (Exception ex) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs index 41c9735808..a370738040 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs @@ -37,6 +37,8 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext 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; @@ -184,6 +186,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(HasCommands)); UpdateProperty(nameof(HasMoreCommands)); + UpdateProperty(nameof(CanOpenContextMenu)); UpdateProperty(nameof(MoreCommands)); UpdateProperty(nameof(AllCommands)); DoOnUiThread( diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuViewModel.cs index 3ddf3e94a3..1ed74000a3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuViewModel.cs @@ -47,7 +47,20 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider) { _fuzzyMatcherProvider = fuzzyMatcherProvider; - WeakReferenceMessenger.Default.Register(this); + } + + public void HookCommandBar() + { + var messenger = WeakReferenceMessenger.Default; + if (!messenger.IsRegistered(this)) + { + messenger.Register(this); + } + } + + public void UnhookCommandBar() + { + WeakReferenceMessenger.Default.Unregister(this); } public void Receive(UpdateCommandBarMessage message) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs index cb3f2e68f7..ca2c90dca5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs @@ -22,6 +22,8 @@ public interface IContextMenuContext : INotifyPropertyChanged public bool HasMoreCommands { get; } + public bool CanOpenContextMenu { get; } + public IReadOnlyList AllCommands { get; } /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs index 904ed06c62..f5b6d97457 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs @@ -52,6 +52,8 @@ public sealed partial class CommandBar : UserControl, return; } + ContextControl.PrepareForOpen(message.ContextMenuFilterLocation); + _ = DispatcherQueue.TryEnqueue( () => { @@ -67,6 +69,13 @@ public sealed partial class CommandBar : UserControl, else { // This is invoked from a specific element + if (!(ContextControl.ViewModel.SelectedItem?.CanOpenContextMenu ?? false)) + { + return; + } + + ContextControl.PrepareForOpen(message.ContextMenuFilterLocation); + _ = DispatcherQueue.TryEnqueue( () => { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml.cs index 817734f134..c0db98b54e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml.cs @@ -18,19 +18,32 @@ using Windows.System; namespace Microsoft.CmdPal.UI.Controls; public sealed partial class ContextMenu : UserControl, - IRecipient, IRecipient, IRecipient { public static readonly DependencyProperty ShowFilterBoxProperty = DependencyProperty.Register(nameof(ShowFilterBox), typeof(bool), typeof(ContextMenu), new PropertyMetadata(true)); + public static readonly DependencyProperty SubscribeToCommandBarProperty = + DependencyProperty.Register(nameof(SubscribeToCommandBar), typeof(bool), typeof(ContextMenu), new PropertyMetadata(true, OnSubscribeToCommandBarChanged)); + public bool ShowFilterBox { get => (bool)GetValue(ShowFilterBoxProperty); set => SetValue(ShowFilterBoxProperty, value); } + /// + /// Gets or sets a value indicating whether this control listens to the command bar's + /// selection and keybinding messages. Set to false for standalone usage (e.g. dock) + /// where the caller manages selection and opening directly. + /// + public bool SubscribeToCommandBar + { + get => (bool)GetValue(SubscribeToCommandBarProperty); + set => SetValue(SubscribeToCommandBarProperty, value); + } + public ContextMenuViewModel ViewModel { get; } public ContextMenu() @@ -40,15 +53,57 @@ public sealed partial class ContextMenu : UserControl, ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService()); ViewModel.PropertyChanged += ViewModel_PropertyChanged; - // RegisterAll isn't AOT compatible - WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); + if (SubscribeToCommandBar) + { + HookCommandBar(); + } } - public void Receive(OpenContextMenuMessage message) + private static void OnSubscribeToCommandBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - ViewModel.FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top; + if (d is ContextMenu control) + { + if (e.NewValue is true) + { + control.HookCommandBar(); + } + else + { + control.UnhookCommandBar(); + } + } + } + + private void HookCommandBar() + { + var messenger = WeakReferenceMessenger.Default; + + if (!messenger.IsRegistered(this)) + { + messenger.Register(this); + } + + if (!messenger.IsRegistered(this)) + { + messenger.Register(this); + } + + ViewModel.HookCommandBar(); + } + + private void UnhookCommandBar() + { + var messenger = WeakReferenceMessenger.Default; + + messenger.Unregister(this); + messenger.Unregister(this); + + ViewModel.UnhookCommandBar(); + } + + internal void PrepareForOpen(ContextMenuFilterLocation filterLocation) + { + ViewModel.FilterOnTop = filterLocation == ContextMenuFilterLocation.Top; ViewModel.ResetContextMenu(); UpdateUiForStackChange(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml index efa13917b0..fd7a05d0bf 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml @@ -103,7 +103,7 @@ Opened="ContextMenuFlyout_Opened" ShouldConstrainToRootBounds="False" SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}"> - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs index dd0f6c75b5..cda48125ca 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Runtime.InteropServices; using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; +using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Dock; using Microsoft.CmdPal.UI.ViewModels.Messages; @@ -264,6 +265,13 @@ public sealed partial class DockControl : UserControl, IRecipient