Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
Michael Jolley 100fca4468 CmdPal: Refactoring ContextMenu adding separators, IsCritical styling, and right-click context menus for list items (#40189)
Refactored ContextMenu into it's own control to allow displaying in
CommandBar and in response to right click on list items.

- Adds "critical" styling to context menu items flagged as `IsCritical`.
This will use the theme to style with correct color.
- Added `SeparatorContextItem` and modified `MoreCommands` to allow for
both `CommandContextItem`s and `SeparatorContextItem`s.
- Right clicking a list item with a context menu will open the context
menu at the position of the click and position the filter box at the top
of the context menu.


![image](https://github.com/user-attachments/assets/3bef6b04-28bb-4a17-b731-d9ed20c0566f)


![image](https://github.com/user-attachments/assets/37ed497c-6d98-4f04-8114-d9952127ca2e)


This PR covers:

- closes #38308
- closes #39211
- closes #38307
- closes #38261
2025-07-09 14:53:47 -05:00

157 lines
5.4 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 CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Windows.UI.Core;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<CloseContextMenuMessage>,
IRecipient<TryCommandKeybindingMessage>,
ICurrentPageAware
{
public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
set => SetValue(CurrentPageViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for CurrentPage. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentPageViewModelProperty =
DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(CommandBar), new PropertyMetadata(null));
public CommandBar()
{
this.InitializeComponent();
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
}
public void Receive(OpenContextMenuMessage message)
{
if (!ViewModel.ShouldShowContextMenu)
{
return;
}
if (message.Element == null)
{
_ = DispatcherQueue.TryEnqueue(
() =>
{
ContextMenuFlyout.ShowAt(
MoreCommandsButton,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
});
}
else
{
_ = DispatcherQueue.TryEnqueue(
() =>
{
ContextMenuFlyout.ShowAt(
message.Element!,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = (FlyoutPlacementMode)message.FlyoutPlacementMode!,
Position = message.Point,
});
});
}
}
public void Receive(CloseContextMenuMessage message)
{
if (ContextMenuFlyout.IsOpen)
{
ContextMenuFlyout.Hide();
}
}
public void Receive(TryCommandKeybindingMessage msg)
{
if (!ViewModel.ShouldShowContextMenu)
{
return;
}
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
if (result == ContextKeybindingResult.Hide)
{
msg.Handled = true;
}
else if (result == ContextKeybindingResult.KeepOpen)
{
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
msg.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
msg.Handled = false;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ViewModel.InvokePrimaryCommand();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ViewModel.InvokeSecondaryCommand();
}
private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e)
{
if (CurrentPageViewModel?.StatusMessages.Count > 0)
{
StatusMessagesFlyout.ShowAt(
placementTarget: IconRoot,
showOptions: new FlyoutShowOptions() { ShowMode = FlyoutShowMode.Standard });
}
}
private void SettingsIcon_Tapped(object sender, TappedRoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
e.Handled = true;
}
private void MoreCommandsButton_Tapped(object sender, TappedRoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
}
private void ContextMenuFlyout_Opened(object sender, object e)
{
// We need to wait until our flyout is opened to try and toss focus
// at its search box. The control isn't in the UI tree before that
ContextControl.FocusSearchBox();
}
}