ContextMenu.xaml.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using CommunityToolkit.Mvvm.Messaging; 6 using CommunityToolkit.WinUI; 7 using Microsoft.CmdPal.Core.ViewModels; 8 using Microsoft.CmdPal.Core.ViewModels.Messages; 9 using Microsoft.CmdPal.UI.Messages; 10 using Microsoft.UI.Input; 11 using Microsoft.UI.Xaml; 12 using Microsoft.UI.Xaml.Controls; 13 using Microsoft.UI.Xaml.Input; 14 using Windows.System; 15 using Windows.UI.Core; 16 17 namespace Microsoft.CmdPal.UI.Controls; 18 19 public sealed partial class ContextMenu : UserControl, 20 IRecipient<OpenContextMenuMessage>, 21 IRecipient<UpdateCommandBarMessage>, 22 IRecipient<TryCommandKeybindingMessage> 23 { 24 public ContextMenuViewModel ViewModel { get; } = new(); 25 26 public ContextMenu() 27 { 28 this.InitializeComponent(); 29 30 // RegisterAll isn't AOT compatible 31 WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this); 32 WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this); 33 WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this); 34 35 if (ViewModel is not null) 36 { 37 ViewModel.PropertyChanged += ViewModel_PropertyChanged; 38 } 39 } 40 41 public void Receive(OpenContextMenuMessage message) 42 { 43 ViewModel.FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top; 44 ViewModel.ResetContextMenu(); 45 46 UpdateUiForStackChange(); 47 } 48 49 public void Receive(UpdateCommandBarMessage message) 50 { 51 UpdateUiForStackChange(); 52 } 53 54 public void Receive(TryCommandKeybindingMessage msg) 55 { 56 var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key); 57 58 if (result == ContextKeybindingResult.Hide) 59 { 60 msg.Handled = true; 61 WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>(); 62 UpdateUiForStackChange(); 63 } 64 else if (result == ContextKeybindingResult.KeepOpen) 65 { 66 UpdateUiForStackChange(); 67 msg.Handled = true; 68 } 69 else if (result == ContextKeybindingResult.Unhandled) 70 { 71 msg.Handled = false; 72 } 73 } 74 75 private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e) 76 { 77 if (e.ClickedItem is CommandContextItemViewModel item) 78 { 79 if (InvokeCommand(item) == ContextKeybindingResult.Hide) 80 { 81 WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>(); 82 } 83 84 UpdateUiForStackChange(); 85 } 86 } 87 88 private void CommandsDropdown_PreviewKeyDown(object sender, KeyRoutedEventArgs e) 89 { 90 if (e.Handled) 91 { 92 return; 93 } 94 95 var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); 96 var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); 97 var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); 98 var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) || 99 InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); 100 101 var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key); 102 103 if (result == ContextKeybindingResult.Hide) 104 { 105 e.Handled = true; 106 WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>(); 107 UpdateUiForStackChange(); 108 } 109 else if (result == ContextKeybindingResult.KeepOpen) 110 { 111 e.Handled = true; 112 } 113 else if (result == ContextKeybindingResult.Unhandled) 114 { 115 e.Handled = false; 116 } 117 } 118 119 /// <summary> 120 /// Handles Escape to close the context menu and return focus to the "More" button. 121 /// </summary> 122 private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e) 123 { 124 if (e.Key == VirtualKey.Escape) 125 { 126 // Close the context menu (if not already handled) 127 WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage()); 128 129 // Find the parent CommandBar and set focus to MoreCommandsButton 130 var parent = this.FindParent<CommandBar>(); 131 parent?.FocusMoreCommandsButton(); 132 133 e.Handled = true; 134 } 135 } 136 137 private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) 138 { 139 var prop = e.PropertyName; 140 141 if (prop == nameof(ContextMenuViewModel.FilteredItems)) 142 { 143 UpdateUiForStackChange(); 144 } 145 } 146 147 private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e) 148 { 149 ViewModel?.SetSearchText(ContextFilterBox.Text); 150 151 if (CommandsDropdown.SelectedIndex == -1) 152 { 153 CommandsDropdown.SelectedIndex = 0; 154 } 155 } 156 157 private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e) 158 { 159 var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); 160 var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); 161 var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); 162 var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) || 163 InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); 164 165 if (e.Key == VirtualKey.Enter) 166 { 167 if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item) 168 { 169 if (InvokeCommand(item) == ContextKeybindingResult.Hide) 170 { 171 WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>(); 172 } 173 174 UpdateUiForStackChange(); 175 176 e.Handled = true; 177 } 178 } 179 else if (e.Key == VirtualKey.Escape || 180 (e.Key == VirtualKey.Left && altPressed)) 181 { 182 if (ViewModel.CanPopContextStack()) 183 { 184 ViewModel.PopContextStack(); 185 UpdateUiForStackChange(); 186 } 187 else 188 { 189 WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>(); 190 WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>(); 191 UpdateUiForStackChange(); 192 } 193 194 e.Handled = true; 195 } 196 } 197 198 private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e) 199 { 200 if (e.Key == VirtualKey.Up) 201 { 202 NavigateUp(); 203 204 e.Handled = true; 205 } 206 else if (e.Key == VirtualKey.Down) 207 { 208 NavigateDown(); 209 210 e.Handled = true; 211 } 212 213 CommandsDropdown_PreviewKeyDown(sender, e); 214 } 215 216 private void NavigateUp() 217 { 218 var newIndex = CommandsDropdown.SelectedIndex; 219 220 if (CommandsDropdown.SelectedIndex > 0) 221 { 222 newIndex--; 223 224 while ( 225 newIndex >= 0 && 226 IsSeparator(CommandsDropdown.Items[newIndex]) && 227 newIndex != CommandsDropdown.SelectedIndex) 228 { 229 newIndex--; 230 } 231 232 if (newIndex < 0) 233 { 234 newIndex = CommandsDropdown.Items.Count - 1; 235 236 while ( 237 newIndex >= 0 && 238 IsSeparator(CommandsDropdown.Items[newIndex]) && 239 newIndex != CommandsDropdown.SelectedIndex) 240 { 241 newIndex--; 242 } 243 } 244 } 245 else 246 { 247 newIndex = CommandsDropdown.Items.Count - 1; 248 } 249 250 CommandsDropdown.SelectedIndex = newIndex; 251 } 252 253 private void NavigateDown() 254 { 255 var newIndex = CommandsDropdown.SelectedIndex; 256 257 if (CommandsDropdown.SelectedIndex == CommandsDropdown.Items.Count - 1) 258 { 259 newIndex = 0; 260 } 261 else 262 { 263 newIndex++; 264 265 while ( 266 newIndex < CommandsDropdown.Items.Count && 267 IsSeparator(CommandsDropdown.Items[newIndex]) && 268 newIndex != CommandsDropdown.SelectedIndex) 269 { 270 newIndex++; 271 } 272 273 if (newIndex >= CommandsDropdown.Items.Count) 274 { 275 newIndex = 0; 276 277 while ( 278 newIndex < CommandsDropdown.Items.Count && 279 IsSeparator(CommandsDropdown.Items[newIndex]) && 280 newIndex != CommandsDropdown.SelectedIndex) 281 { 282 newIndex++; 283 } 284 } 285 } 286 287 CommandsDropdown.SelectedIndex = newIndex; 288 } 289 290 private bool IsSeparator(object item) 291 { 292 return item is SeparatorViewModel; 293 } 294 295 private void UpdateUiForStackChange() 296 { 297 ContextFilterBox.Text = string.Empty; 298 ViewModel?.SetSearchText(string.Empty); 299 CommandsDropdown.SelectedIndex = 0; 300 } 301 302 /// <summary> 303 /// Manually focuses our search box. This needs to be called after we're actually 304 /// In the UI tree - if we're in a Flyout, that's not until Opened() 305 /// </summary> 306 internal void FocusSearchBox() 307 { 308 ContextFilterBox.Focus(FocusState.Programmatic); 309 } 310 311 private ContextKeybindingResult InvokeCommand(CommandItemViewModel command) => ViewModel.InvokeCommand(command); 312 }