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. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
@@ -106,7 +105,8 @@ public sealed partial class CommandBarViewModel : ObservableObject,
{ {
switch (e.PropertyName) switch (e.PropertyName)
{ {
case nameof(SelectedItem.HasMoreCommands): case nameof(SelectedItem.CanOpenContextMenu):
case nameof(SelectedItem.SecondaryCommand):
UpdateContextItems(); UpdateContextItems();
break; break;
} }
@@ -122,9 +122,7 @@ public sealed partial class CommandBarViewModel : ObservableObject,
} }
SecondaryCommand = SelectedItem.SecondaryCommand; SecondaryCommand = SelectedItem.SecondaryCommand;
var moreCommands = SelectedItem.MoreCommands; ShouldShowContextMenu = SelectedItem.CanOpenContextMenu;
ShouldShowContextMenu = moreCommands.Count > 1 && SelectedItem.HasMoreCommands;
OnPropertyChanged(nameof(HasSecondaryCommand)); OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand)); OnPropertyChanged(nameof(SecondaryCommand));

View File

@@ -86,6 +86,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand; public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
public bool CanOpenContextMenu => AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name); public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public bool HasTitle => !string.IsNullOrEmpty(Title); public bool HasTitle => !string.IsNullOrEmpty(Title);
@@ -224,6 +226,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(MoreCommands)); UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands)); UpdateProperty(nameof(AllCommands));
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands)); UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
UpdateProperty(nameof(CanOpenContextMenu));
UpdateProperty(nameof(IsSelectedInitialized)); UpdateProperty(nameof(IsSelectedInitialized));
} }
@@ -336,6 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(Title)); UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon)); UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasText)); UpdateProperty(nameof(HasText));
UpdateProperty(nameof(CanOpenContextMenu));
break; break;
case nameof(Title): case nameof(Title):
@@ -367,7 +371,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
case nameof(model.MoreCommands): case nameof(model.MoreCommands):
BuildAndInitMoreCommands(); BuildAndInitMoreCommands();
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands)); UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands), nameof(CanOpenContextMenu));
break; break;
case nameof(DataPackage): case nameof(DataPackage):
@@ -395,6 +399,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_itemTitle = model.Title; _itemTitle = model.Title;
_titleCache.Invalidate(); _titleCache.Invalidate();
UpdateProperty(nameof(Title), nameof(Name)); UpdateProperty(nameof(Title), nameof(Name));
UpdateProperty(nameof(CanOpenContextMenu));
if (_defaultCommandContextItemViewModel is not null) if (_defaultCommandContextItemViewModel is not null)
{ {
@@ -532,6 +537,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(SecondaryCommand)); UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands)); UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(CanOpenContextMenu));
} }
catch (Exception ex) 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 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 string SecondaryCommandName => _snapshot.SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand; public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand;
@@ -184,6 +186,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasCommands)); UpdateProperty(nameof(HasCommands));
UpdateProperty(nameof(HasMoreCommands)); UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(CanOpenContextMenu));
UpdateProperty(nameof(MoreCommands)); UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands)); UpdateProperty(nameof(AllCommands));
DoOnUiThread( DoOnUiThread(

View File

@@ -47,7 +47,20 @@ public partial class ContextMenuViewModel : ObservableObject,
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider) public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
{ {
_fuzzyMatcherProvider = 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) public void Receive(UpdateCommandBarMessage message)

View File

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

View File

@@ -52,6 +52,8 @@ public sealed partial class CommandBar : UserControl,
return; return;
} }
ContextControl.PrepareForOpen(message.ContextMenuFilterLocation);
_ = DispatcherQueue.TryEnqueue( _ = DispatcherQueue.TryEnqueue(
() => () =>
{ {
@@ -67,6 +69,13 @@ public sealed partial class CommandBar : UserControl,
else else
{ {
// This is invoked from a specific element // This is invoked from a specific element
if (!(ContextControl.ViewModel.SelectedItem?.CanOpenContextMenu ?? false))
{
return;
}
ContextControl.PrepareForOpen(message.ContextMenuFilterLocation);
_ = DispatcherQueue.TryEnqueue( _ = DispatcherQueue.TryEnqueue(
() => () =>
{ {

View File

@@ -18,19 +18,32 @@ using Windows.System;
namespace Microsoft.CmdPal.UI.Controls; namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ContextMenu : UserControl, public sealed partial class ContextMenu : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<UpdateCommandBarMessage>, IRecipient<UpdateCommandBarMessage>,
IRecipient<TryCommandKeybindingMessage> IRecipient<TryCommandKeybindingMessage>
{ {
public static readonly DependencyProperty ShowFilterBoxProperty = public static readonly DependencyProperty ShowFilterBoxProperty =
DependencyProperty.Register(nameof(ShowFilterBox), typeof(bool), typeof(ContextMenu), new PropertyMetadata(true)); 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 public bool ShowFilterBox
{ {
get => (bool)GetValue(ShowFilterBoxProperty); get => (bool)GetValue(ShowFilterBoxProperty);
set => SetValue(ShowFilterBoxProperty, value); 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 ContextMenuViewModel ViewModel { get; }
public ContextMenu() public ContextMenu()
@@ -40,15 +53,57 @@ public sealed partial class ContextMenu : UserControl,
ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>()); ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>());
ViewModel.PropertyChanged += ViewModel_PropertyChanged; ViewModel.PropertyChanged += ViewModel_PropertyChanged;
// RegisterAll isn't AOT compatible if (SubscribeToCommandBar)
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this); {
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this); HookCommandBar();
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this); }
} }
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(); ViewModel.ResetContextMenu();
UpdateUiForStackChange(); UpdateUiForStackChange();

View File

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

View File

@@ -7,6 +7,7 @@ using System.Collections.Specialized;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock; using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages; 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 // Stores the band that was right-clicked for edit mode context menu
private DockBandViewModel? _editModeContextBand; private DockBandViewModel? _editModeContextBand;
@@ -297,10 +305,11 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
} }
// Normal mode - show the command context menu // Normal mode - show the command context menu
if (item.HasMoreCommands) if (item.CanOpenContextMenu)
{ {
ContextControl.ViewModel.SelectedItem = item; ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = true; ContextControl.ShowFilterBox = true;
ContextControl.PrepareForOpen(GetDockContextMenuFilterLocation());
PreparePopupForShow(ContextMenuFlyout, dockItem); PreparePopupForShow(ContextMenuFlyout, dockItem);
ContextMenuFlyout.ShowAt( ContextMenuFlyout.ShowAt(
dockItem, dockItem,
@@ -390,6 +399,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{ {
ContextControl.ViewModel.SelectedItem = item; ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = false; ContextControl.ShowFilterBox = false;
ContextControl.PrepareForOpen(GetDockContextMenuFilterLocation());
PreparePopupForShow(ContextMenuFlyout, RootGrid); PreparePopupForShow(ContextMenuFlyout, RootGrid);
ContextMenuFlyout.ShowAt( ContextMenuFlyout.ShowAt(
this.RootGrid, this.RootGrid,