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
This commit is contained in:
Michael Jolley
2025-07-09 14:53:47 -05:00
committed by GitHub
parent 18b61ce9b7
commit 100fca4468
23 changed files with 984 additions and 477 deletions

View File

@@ -18,6 +18,7 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<CloseContextMenuMessage>,
IRecipient<TryCommandKeybindingMessage>,
ICurrentPageAware
{
@@ -39,9 +40,8 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -51,12 +51,43 @@ public sealed partial class CommandBar : UserControl,
return;
}
var options = new FlyoutShowOptions
if (message.Element == null)
{
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
UpdateUiForStackChange();
_ = 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)
@@ -74,17 +105,7 @@ public sealed partial class CommandBar : UserControl,
}
else if (result == ContextKeybindingResult.KeepOpen)
{
if (!MoreCommandsButton.Flyout.IsOpen)
{
var options = new FlyoutShowOptions
{
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
}
UpdateUiForStackChange();
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
msg.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
@@ -121,164 +142,15 @@ public sealed partial class CommandBar : UserControl,
e.Handled = true;
}
private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e)
private void MoreCommandsButton_Tapped(object sender, TappedRoutedEventArgs e)
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
}
else
{
UpdateUiForStackChange();
}
}
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
}
private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
private void ContextMenuFlyout_Opened(object sender, object e)
{
if (e.Handled)
{
return;
}
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
if (result == ContextKeybindingResult.Hide)
{
e.Handled = true;
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else if (result == ContextKeybindingResult.KeepOpen)
{
e.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
e.Handled = false;
}
}
private void Flyout_Opened(object sender, object e)
{
UpdateUiForStackChange();
}
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
{
ViewModel?.ClearContextStack();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var prop = e.PropertyName;
if (prop == nameof(ViewModel.ContextMenu))
{
UpdateUiForStackChange();
}
}
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
if (CommandsDropdown.SelectedIndex == -1)
{
CommandsDropdown.SelectedIndex = 0;
}
}
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (e.Key == VirtualKey.Enter)
{
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
{
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else
{
UpdateUiForStackChange();
}
e.Handled = true;
}
}
else if (e.Key == VirtualKey.Escape ||
(e.Key == VirtualKey.Left && altPressed))
{
if (ViewModel.CanPopContextStack())
{
ViewModel.PopContextStack();
UpdateUiForStackChange();
}
else
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
e.Handled = true;
}
CommandsDropdown_KeyDown(sender, e);
}
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Up)
{
// navigate previous
if (CommandsDropdown.SelectedIndex > 0)
{
CommandsDropdown.SelectedIndex--;
}
else
{
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
}
e.Handled = true;
}
else if (e.Key == VirtualKey.Down)
{
// navigate next
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
{
CommandsDropdown.SelectedIndex++;
}
else
{
CommandsDropdown.SelectedIndex = 0;
}
e.Handled = true;
}
}
private void UpdateUiForStackChange()
{
ContextFilterBox.Text = string.Empty;
ViewModel.ContextMenu?.SetSearchText(string.Empty);
CommandsDropdown.SelectedIndex = 0;
ContextFilterBox.Focus(FocusState.Programmatic);
// 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();
}
}