Skip to content

Commit 86ff5c0

Browse files
committed
Fixed nested menus and added separators
1 parent cfeb5d4 commit 86ff5c0

File tree

8 files changed

+191
-47
lines changed

8 files changed

+191
-47
lines changed

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The Microsoft Corporation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using CommunityToolkit.Mvvm.Messaging;
6+
using Microsoft.CmdPal.UI.ViewModels.Messages;
57
using Microsoft.CmdPal.UI.ViewModels.Models;
68
using Microsoft.CommandPalette.Extensions;
79

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ public void SlowInitializeProperties()
181181
MoreCommands = more
182182
.Select(item =>
183183
{
184-
if (item is CommandContextItem contextItem)
184+
if (item is ICommandContextItem contextItem)
185185
{
186186
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
187187
}

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuViewModel.cs

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using CommunityToolkit.Mvvm.ComponentModel;
77
using CommunityToolkit.Mvvm.Messaging;
88
using Microsoft.CmdPal.UI.ViewModels.Messages;
9+
using Microsoft.CommandPalette.Extensions;
910
using Microsoft.CommandPalette.Extensions.Toolkit;
11+
using Microsoft.Diagnostics.Utilities;
1012
using Windows.System;
1113

1214
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -31,6 +33,11 @@ public ICommandBarContext? SelectedItem
3133
}
3234
}
3335

36+
[ObservableProperty]
37+
private partial ObservableCollection<List<IContextItemViewModel>> ContextMenuStack { get; set; } = [];
38+
39+
private List<IContextItemViewModel>? CurrentContextMenu => ContextMenuStack.LastOrDefault();
40+
3441
[ObservableProperty]
3542
public partial ObservableCollection<IContextItemViewModel> FilteredItems { get; set; } = [];
3643

@@ -44,7 +51,6 @@ public ContextMenuViewModel()
4451
public void Receive(UpdateCommandBarMessage message)
4552
{
4653
SelectedItem = message.ViewModel;
47-
OnPropertyChanged(nameof(FilteredItems));
4854
}
4955

5056
private void SetSelectedItem(ICommandBarContext? value)
@@ -76,10 +82,13 @@ private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.P
7682

7783
public void UpdateContextItems()
7884
{
79-
FilteredItems.Clear();
8085
if (SelectedItem != null)
8186
{
82-
FilteredItems = [.. SelectedItem.AllCommands];
87+
if (SelectedItem.MoreCommands.Count() > 1)
88+
{
89+
ContextMenuStack.Clear();
90+
PushContextStack(SelectedItem.AllCommands);
91+
}
8392
}
8493
}
8594

@@ -97,15 +106,22 @@ public void SetSearchText(string searchText)
97106

98107
_lastSearchText = searchText;
99108

100-
var commands = SelectedItem.AllCommands
101-
.OfType<CommandContextItemViewModel>()
102-
.Where(c => c.ShouldBeVisible);
109+
if (CurrentContextMenu == null)
110+
{
111+
ListHelpers.InPlaceUpdateList(FilteredItems, []);
112+
return;
113+
}
114+
103115
if (string.IsNullOrEmpty(searchText))
104116
{
105-
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
117+
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu]);
106118
return;
107119
}
108120

121+
var commands = CurrentContextMenu
122+
.OfType<CommandContextItemViewModel>()
123+
.Where(c => c.ShouldBeVisible);
124+
109125
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
110126
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
111127
}
@@ -129,19 +145,95 @@ private static int ScoreContextCommand(string query, CommandContextItemViewModel
129145
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
130146
}
131147

132-
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
148+
/// <summary>
149+
/// Generates a mapping of key -> command item for this particular item's
150+
/// MoreCommands. (This won't include the primary Command, but it will
151+
/// include the secondary one). This map can be used to quickly check if a
152+
/// shortcut key was pressed
153+
/// </summary>
154+
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
155+
/// that have a shortcut key set.</returns>
156+
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
133157
{
134-
var keybindings = SelectedItem?.Keybindings();
158+
if (CurrentContextMenu == null)
159+
{
160+
return [];
161+
}
162+
163+
return CurrentContextMenu
164+
.OfType<CommandContextItemViewModel>()
165+
.Where(c => c.HasRequestedShortcut)
166+
.ToDictionary(
167+
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
168+
c => c);
169+
}
170+
171+
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
172+
{
173+
var keybindings = Keybindings();
135174
if (keybindings != null)
136175
{
137176
// Does the pressed key match any of the keybindings?
138177
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
139178
if (keybindings.TryGetValue(pressedKeyChord, out var item))
140179
{
141-
return item;
180+
return InvokeCommand(item);
142181
}
143182
}
144183

145184
return null;
146185
}
186+
187+
public bool CanPopContextStack()
188+
{
189+
return ContextMenuStack.Count > 1;
190+
}
191+
192+
public void PopContextStack()
193+
{
194+
if (ContextMenuStack.Count > 1)
195+
{
196+
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
197+
}
198+
199+
OnPropertyChanging(nameof(CurrentContextMenu));
200+
OnPropertyChanged(nameof(CurrentContextMenu));
201+
202+
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
203+
}
204+
205+
private void PushContextStack(IEnumerable<IContextItemViewModel> commands)
206+
{
207+
ContextMenuStack.Add(commands.ToList());
208+
OnPropertyChanging(nameof(CurrentContextMenu));
209+
OnPropertyChanged(nameof(CurrentContextMenu));
210+
211+
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
212+
}
213+
214+
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
215+
{
216+
if (command == null)
217+
{
218+
return ContextKeybindingResult.Unhandled;
219+
}
220+
221+
if (command.HasMoreCommands)
222+
{
223+
// Execute the command
224+
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
225+
226+
// Display the commands child commands
227+
PushContextStack(command.MoreCommands);
228+
OnPropertyChanging(nameof(FilteredItems));
229+
OnPropertyChanged(nameof(FilteredItems));
230+
return ContextKeybindingResult.KeepOpen;
231+
}
232+
else
233+
{
234+
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
235+
UpdateContextItems();
236+
return ContextKeybindingResult.Hide;
237+
}
238+
}
147239
}

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,17 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
5353
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
5454

5555
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
56-
.OfType<CommandContextItemViewModel>()
57-
.Select(i => i.Model.Unsafe).ToArray();
58-
59-
// Yo. You gotta throw that unsafe interface ExtensionObject jam on the SeparatorItemViewModel.
60-
// Good luck champ!
61-
56+
.Select(item =>
57+
{
58+
if (item is SeparatorContextItemViewModel)
59+
{
60+
return item as IContextItem;
61+
}
62+
else
63+
{
64+
return ((CommandContextItemViewModel)item).Model.Unsafe as IContextItem;
65+
}
66+
}).ToArray();
6267

6368
////// IListItem
6469
ITag[] IListItem.Tags => Tags.ToArray();

src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,11 @@
9393

9494
<!-- Template for context item separators -->
9595
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="viewmodels:SeparatorContextItemViewModel">
96-
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
96+
<StackPanel Margin="0,0,0,0" Orientation="Vertical">
9797
<Border
98-
Margin="8,0,0,0"
99-
BorderBrush="{ThemeResource TextFillColorSecondaryBrush}"
100-
BorderThickness="0,0,0,2">
101-
<TextBlock
102-
Margin="-8,0,0,8"
103-
FontWeight="SemiBold"
104-
IsTextSelectionEnabled="True"
105-
Text=" "
106-
TextWrapping="WrapWholeWords" />
98+
Margin="0,0,0,0"
99+
BorderBrush="{ThemeResource MenuFlyoutSeparatorThemeBrush}"
100+
BorderThickness="0,0,0,1">
107101
</Border>
108102
</StackPanel>
109103
</DataTemplate>

src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml.cs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,31 @@ public void Receive(TryCommandKeybindingMessage msg)
5353
{
5454
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
5555

56-
if (result != null)
56+
if (result == ContextKeybindingResult.Hide)
5757
{
58-
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(result.Command.Model, result.Model));
58+
msg.Handled = true;
5959
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
60+
}
61+
else if (result == ContextKeybindingResult.KeepOpen)
62+
{
63+
UpdateUiForStackChange();
6064
msg.Handled = true;
6165
}
66+
else if (result == ContextKeybindingResult.Unhandled)
67+
{
68+
msg.Handled = false;
69+
}
6270
}
6371

6472
private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e)
6573
{
6674
if (e.ClickedItem is CommandContextItemViewModel item)
6775
{
68-
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
69-
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
76+
if (InvokeCommand(item) == ContextKeybindingResult.Hide)
77+
{
78+
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
79+
}
80+
7081
UpdateUiForStackChange();
7182
}
7283
}
@@ -86,13 +97,20 @@ private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
8697

8798
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
8899

89-
if (result != null)
100+
if (result == ContextKeybindingResult.Hide)
90101
{
91-
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(result.Command.Model, result.Model));
102+
e.Handled = true;
92103
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
93104
UpdateUiForStackChange();
105+
}
106+
else if (result == ContextKeybindingResult.KeepOpen)
107+
{
94108
e.Handled = true;
95109
}
110+
else if (result == ContextKeybindingResult.Unhandled)
111+
{
112+
e.Handled = false;
113+
}
96114
}
97115

98116
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -126,16 +144,32 @@ private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
126144
{
127145
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
128146
{
129-
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
130-
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
131-
UpdateUiForStackChange();
147+
if (InvokeCommand(item) == ContextKeybindingResult.Hide)
148+
{
149+
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
150+
}
151+
else
152+
{
153+
UpdateUiForStackChange();
154+
}
155+
132156
e.Handled = true;
133157
}
134158
}
135159
else if (e.Key == VirtualKey.Escape ||
136160
(e.Key == VirtualKey.Left && altPressed))
137161
{
138-
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
162+
if (ViewModel.CanPopContextStack())
163+
{
164+
ViewModel.PopContextStack();
165+
UpdateUiForStackChange();
166+
}
167+
else
168+
{
169+
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
170+
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
171+
}
172+
139173
e.Handled = true;
140174
}
141175

@@ -181,4 +215,6 @@ private void UpdateUiForStackChange()
181215
CommandsDropdown.SelectedIndex = 0;
182216
ContextFilterBox.Focus(FocusState.Programmatic);
183217
}
218+
219+
private ContextKeybindingResult InvokeCommand(CommandItemViewModel command) => ViewModel.InvokeCommand(command);
184220
}

src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
// The Microsoft Corporation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using Microsoft.Bot.AdaptiveExpressions.Core;
56
using Microsoft.CmdPal.UI.ViewModels;
67
using Microsoft.UI.Xaml;
78
using Microsoft.UI.Xaml.Controls;
9+
using Microsoft.UI.Xaml.Controls.Primitives;
10+
using Microsoft.UI.Xaml.Data;
811

912
namespace Microsoft.CmdPal.UI;
1013

@@ -16,13 +19,25 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
1619

1720
public DataTemplate? Separator { get; set; }
1821

19-
protected override DataTemplate? SelectTemplateCore(object item)
22+
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
2023
{
21-
if (item is SeparatorContextItemViewModel)
24+
DataTemplate? dataTemplate = Default;
25+
26+
if (dependencyObject is ListViewItem li)
2227
{
23-
return Separator;
28+
li.IsEnabled = true;
29+
30+
if (item is SeparatorContextItemViewModel)
31+
{
32+
li.IsEnabled = false;
33+
dataTemplate = Separator;
34+
}
35+
else
36+
{
37+
dataTemplate = ((CommandContextItemViewModel)item).IsCritical ? Critical : Default;
38+
}
2439
}
2540

26-
return ((CommandContextItemViewModel)item).IsCritical ? Critical : Default;
41+
return dataTemplate;
2742
}
2843
}

0 commit comments

Comments
 (0)