// 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 CommunityToolkit.WinUI; using Microsoft.CmdPal.Common.Text; using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Windows.System; using Windows.UI.Core; namespace Microsoft.CmdPal.UI.Controls; public sealed partial class ContextMenu : UserControl, IRecipient, IRecipient, IRecipient { public ContextMenuViewModel ViewModel { get; } public ContextMenu() { this.InitializeComponent(); 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); } public void Receive(OpenContextMenuMessage message) { ViewModel.FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top; ViewModel.ResetContextMenu(); UpdateUiForStackChange(); } public void Receive(UpdateCommandBarMessage message) { UpdateUiForStackChange(); } public void Receive(TryCommandKeybindingMessage msg) { var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key); if (result == ContextKeybindingResult.Hide) { msg.Handled = true; WeakReferenceMessenger.Default.Send(); UpdateUiForStackChange(); } else if (result == ContextKeybindingResult.KeepOpen) { UpdateUiForStackChange(); msg.Handled = true; } else if (result == ContextKeybindingResult.Unhandled) { msg.Handled = false; } } private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e) { if (e.ClickedItem is CommandContextItemViewModel item) { if (InvokeCommand(item) == ContextKeybindingResult.Hide) { WeakReferenceMessenger.Default.Send(); } UpdateUiForStackChange(); } } private void CommandsDropdown_PreviewKeyDown(object sender, KeyRoutedEventArgs 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; WeakReferenceMessenger.Default.Send(); UpdateUiForStackChange(); } else if (result == ContextKeybindingResult.KeepOpen) { e.Handled = true; } else if (result == ContextKeybindingResult.Unhandled) { e.Handled = false; } } /// /// Handles Escape to close the context menu and return focus to the "More" button. /// private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e) { if (e.Key == VirtualKey.Escape) { // Close the context menu (if not already handled) WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage()); // Find the parent CommandBar and set focus to MoreCommandsButton var parent = this.FindParent(); parent?.FocusMoreCommandsButton(); e.Handled = true; } } private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { var prop = e.PropertyName; if (prop == nameof(ContextMenuViewModel.FilteredItems)) { UpdateUiForStackChange(); } } private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e) { ViewModel?.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 (InvokeCommand(item) == ContextKeybindingResult.Hide) { WeakReferenceMessenger.Default.Send(); } UpdateUiForStackChange(); e.Handled = true; } } else if (e.Key == VirtualKey.Escape || (e.Key == VirtualKey.Left && altPressed)) { if (ViewModel.CanPopContextStack()) { ViewModel.PopContextStack(); UpdateUiForStackChange(); } else { WeakReferenceMessenger.Default.Send(); WeakReferenceMessenger.Default.Send(); UpdateUiForStackChange(); } e.Handled = true; } } private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e) { if (e.Key == VirtualKey.Up) { NavigateUp(); e.Handled = true; } else if (e.Key == VirtualKey.Down) { NavigateDown(); e.Handled = true; } CommandsDropdown_PreviewKeyDown(sender, e); } private void NavigateUp() { var newIndex = CommandsDropdown.SelectedIndex; if (CommandsDropdown.SelectedIndex > 0) { newIndex--; while ( newIndex >= 0 && IsSeparator(CommandsDropdown.Items[newIndex]) && newIndex != CommandsDropdown.SelectedIndex) { newIndex--; } if (newIndex < 0) { newIndex = CommandsDropdown.Items.Count - 1; while ( newIndex >= 0 && IsSeparator(CommandsDropdown.Items[newIndex]) && newIndex != CommandsDropdown.SelectedIndex) { newIndex--; } } } else { newIndex = CommandsDropdown.Items.Count - 1; } CommandsDropdown.SelectedIndex = newIndex; } private void NavigateDown() { var newIndex = CommandsDropdown.SelectedIndex; if (CommandsDropdown.SelectedIndex == CommandsDropdown.Items.Count - 1) { newIndex = 0; } else { newIndex++; while ( newIndex < CommandsDropdown.Items.Count && IsSeparator(CommandsDropdown.Items[newIndex]) && newIndex != CommandsDropdown.SelectedIndex) { newIndex++; } if (newIndex >= CommandsDropdown.Items.Count) { newIndex = 0; while ( newIndex < CommandsDropdown.Items.Count && IsSeparator(CommandsDropdown.Items[newIndex]) && newIndex != CommandsDropdown.SelectedIndex) { newIndex++; } } } CommandsDropdown.SelectedIndex = newIndex; } private bool IsSeparator(object item) { return item is SeparatorViewModel; } private void UpdateUiForStackChange() { ContextFilterBox.Text = string.Empty; ViewModel?.SetSearchText(string.Empty); CommandsDropdown.SelectedIndex = 0; } /// /// Manually focuses our search box. This needs to be called after we're actually /// In the UI tree - if we're in a Flyout, that's not until Opened() /// internal void FocusSearchBox() { ContextFilterBox.Focus(FocusState.Programmatic); } private ContextKeybindingResult InvokeCommand(CommandItemViewModel command) => ViewModel.InvokeCommand(command); }