CmdPal: Fix Dock context menu following active item in Command Bar (#46420)

## Summary of the Pull Request

This PR decouples the Dock control context menu from the item selected
in the Shell Page list / Command Bar.

- Adds a new property to the context menu to control whether it should
react to messages like `UpdateCommandBarMessage`
- The `DockControl` context menu no longer follows those messages

Additional changes:

- Ensures the context menu for Dock-selected search box position
reflects the Dock position (when the Dock is at the bottom, the search
box is also at the bottom)
- Consistently displays the dock item context menu even for items with a
single context menu action (instead of showing the Dock menu, which was
confusing)

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46404
- [x] Closes: #45892
<!-- - [ ] 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
This commit is contained in:
Jiří Polášek
2026-03-28 00:38:09 +01:00
committed by GitHub
parent 9afa1ec71d
commit f686155d9b
9 changed files with 112 additions and 16 deletions

View File

@@ -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));

View File

@@ -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)
{

View File

@@ -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(

View File

@@ -47,7 +47,20 @@ public partial class ContextMenuViewModel : ObservableObject,
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
{
_fuzzyMatcherProvider = fuzzyMatcherProvider;
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
}
public void HookCommandBar()
{
var messenger = WeakReferenceMessenger.Default;
if (!messenger.IsRegistered<UpdateCommandBarMessage>(this))
{
messenger.Register<UpdateCommandBarMessage>(this);
}
}
public void UnhookCommandBar()
{
WeakReferenceMessenger.Default.Unregister<UpdateCommandBarMessage>(this);
}
public void Receive(UpdateCommandBarMessage message)

View File

@@ -22,6 +22,8 @@ public interface IContextMenuContext : INotifyPropertyChanged
public bool HasMoreCommands { get; }
public bool CanOpenContextMenu { get; }
public IReadOnlyList<IContextItemViewModel> AllCommands { get; }
/// <summary>

View File

@@ -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(
() =>
{

View File

@@ -18,19 +18,32 @@ using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ContextMenu : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<UpdateCommandBarMessage>,
IRecipient<TryCommandKeybindingMessage>
{
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);
}
/// <summary>
/// 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.
/// </summary>
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<IFuzzyMatcherProvider>());
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(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<UpdateCommandBarMessage>(this))
{
messenger.Register<UpdateCommandBarMessage>(this);
}
if (!messenger.IsRegistered<TryCommandKeybindingMessage>(this))
{
messenger.Register<TryCommandKeybindingMessage>(this);
}
ViewModel.HookCommandBar();
}
private void UnhookCommandBar()
{
var messenger = WeakReferenceMessenger.Default;
messenger.Unregister<UpdateCommandBarMessage>(this);
messenger.Unregister<TryCommandKeybindingMessage>(this);
ViewModel.UnhookCommandBar();
}
internal void PrepareForOpen(ContextMenuFilterLocation filterLocation)
{
ViewModel.FilterOnTop = filterLocation == ContextMenuFilterLocation.Top;
ViewModel.ResetContextMenu();
UpdateUiForStackChange();

View File

@@ -103,7 +103,7 @@
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<cpcontrols:ContextMenu x:Name="ContextControl" />
<cpcontrols:ContextMenu x:Name="ContextControl" SubscribeToCommandBar="False" />
</Flyout>
<!-- Edit mode context menu for dock bands -->

View File

@@ -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<CloseContextMe
}
}
private ContextMenuFilterLocation GetDockContextMenuFilterLocation()
{
return DockSide == DockSide.Bottom
? ContextMenuFilterLocation.Bottom
: ContextMenuFilterLocation.Top;
}
// Stores the band that was right-clicked for edit mode context menu
private DockBandViewModel? _editModeContextBand;
@@ -297,10 +305,11 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
}
// Normal mode - show the command context menu
if (item.HasMoreCommands)
if (item.CanOpenContextMenu)
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = true;
ContextControl.PrepareForOpen(GetDockContextMenuFilterLocation());
PreparePopupForShow(ContextMenuFlyout, dockItem);
ContextMenuFlyout.ShowAt(
dockItem,
@@ -390,6 +399,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = false;
ContextControl.PrepareForOpen(GetDockContextMenuFilterLocation());
PreparePopupForShow(ContextMenuFlyout, RootGrid);
ContextMenuFlyout.ShowAt(
this.RootGrid,