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

@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -48,11 +49,6 @@ public partial class CommandBarViewModel : ObservableObject,
[ObservableProperty]
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
@@ -101,18 +97,9 @@ public partial class CommandBarViewModel : ObservableObject,
SecondaryCommand = SelectedItem.SecondaryCommand;
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
ContextMenuStack.Clear();
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
OnPropertyChanged(nameof(ContextMenu));
}
else
{
ShouldShowContextMenu = false;
}
ShouldShowContextMenu = SelectedItem.MoreCommands
.OfType<CommandContextItemViewModel>()
.Count() > 1;
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));
@@ -139,8 +126,18 @@ public partial class CommandBarViewModel : ObservableObject,
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
var keybindings = SelectedItem?.Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem))
{
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
}
return ContextKeybindingResult.Unhandled;
}
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
@@ -152,10 +149,6 @@ public partial class CommandBarViewModel : ObservableObject,
if (command.HasMoreCommands)
{
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
return ContextKeybindingResult.KeepOpen;
}
else
@@ -164,33 +157,6 @@ public partial class CommandBarViewModel : ObservableObject,
return ContextKeybindingResult.Hide;
}
}
public bool CanPopContextStack()
{
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
public void ClearContextStack()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
}
public enum ContextKeybindingResult

View File

@@ -2,12 +2,14 @@
// 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.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context)
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);

View File

@@ -46,25 +46,27 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandViewModel Command { get; private set; }
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
public List<IContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
IEnumerable<IContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
private List<CommandContextItemViewModel> ActualCommands => MoreCommands.OfType<CommandContextItemViewModel>().ToList();
public bool HasMoreCommands => ActualCommands.Count > 0;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? MoreCommands[0] : null;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public List<CommandContextItemViewModel> AllCommands
public List<IContextItemViewModel> AllCommands
{
get
{
List<CommandContextItemViewModel> l = _defaultCommandContextItem == null ?
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
new() :
[_defaultCommandContextItem];
@@ -177,18 +179,29 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
if (more != null)
{
MoreCommands = more
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.Select(item =>
{
if (item is ICommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
}
// Here, we're already theoretically in the async context, so we can
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
MoreCommands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
if (!string.IsNullOrEmpty(model.Command?.Name))
{
@@ -323,19 +336,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
if (more != null)
{
var newContextMenu = more
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
lock (MoreCommands)
{
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
}
newContextMenu.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
newContextMenu
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
}
else
{
@@ -376,7 +400,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
lock (MoreCommands)
{
MoreCommands.ForEach(c => c.SafeCleanup());
MoreCommands.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
MoreCommands.Clear();
}

View File

@@ -21,9 +21,9 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
[ObservableProperty]
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
public List<CommandContextItemViewModel> Commands { get; private set; } = [];
public List<IContextItemViewModel> Commands { get; private set; } = [];
public bool HasCommands => Commands.Count > 0;
public bool HasCommands => ActualCommands.Count > 0;
public DetailsViewModel? Details { get; private set; }
@@ -31,18 +31,19 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
public bool HasDetails => Details != null;
/////// ICommandBarContext ///////
public IEnumerable<CommandContextItemViewModel> MoreCommands => Commands.Skip(1);
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
public bool HasMoreCommands => Commands.Count > 1;
private List<CommandContextItemViewModel> ActualCommands => Commands.OfType<CommandContextItemViewModel>().ToList();
public bool HasMoreCommands => ActualCommands.Count > 1;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => HasCommands ? Commands[0] : null;
public CommandItemViewModel? PrimaryCommand => HasCommands ? ActualCommands[0] : null;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? Commands[1] : null;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[1] : null;
public List<CommandContextItemViewModel> AllCommands => Commands;
/////// /ICommandBarContext ///////
public List<IContextItemViewModel> AllCommands => Commands;
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
@@ -113,14 +114,27 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
}
Commands = model.Commands
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.ToList();
Commands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
.ToList()
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
var extensionDetails = model.Details;
if (extensionDetails != null)
@@ -159,19 +173,32 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
if (more != null)
{
var newContextMenu = more
.Where(contextItem => contextItem is ICommandContextItem)
.Select(contextItem => (contextItem as ICommandContextItem)!)
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
.ToList();
.ToList()
.Select(item =>
{
if (item is CommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorContextItemViewModel() as IContextItemViewModel;
}
})
.ToList();
lock (Commands)
{
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
}
Commands.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
}
else
{
@@ -246,10 +273,11 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
base.UnsafeCleanup();
Details?.SafeCleanup();
foreach (var item in Commands)
{
item.SafeCleanup();
}
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(item => item.SafeCleanup());
Commands.Clear();

View File

@@ -1,82 +0,0 @@
// 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 System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuStackViewModel : ObservableObject
{
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
private readonly IContextMenuContext _context;
private string _lastSearchText = string.Empty;
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel(IContextMenuContext context)
{
_context = context;
FilteredItems = [.. context.AllCommands];
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
_lastSearchText = searchText;
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
return;
}
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = _context.Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return item;
}
}
return null;
}
}

View File

@@ -0,0 +1,267 @@
// 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 System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Diagnostics.Utilities;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>,
IRecipient<OpenContextMenuMessage>
{
public ICommandBarContext? SelectedItem
{
get => field;
set
{
if (field != null)
{
field.PropertyChanged -= SelectedItemPropertyChanged;
}
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
}
}
[ObservableProperty]
private partial ObservableCollection<List<IContextItemViewModel>> ContextMenuStack { get; set; } = [];
private List<IContextItemViewModel>? CurrentContextMenu => ContextMenuStack.LastOrDefault();
[ObservableProperty]
public partial ObservableCollection<IContextItemViewModel> FilteredItems { get; set; } = [];
[ObservableProperty]
public partial bool FilterOnTop { get; set; } = false;
private string _lastSearchText = string.Empty;
public ContextMenuViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
}
public void Receive(UpdateCommandBarMessage message)
{
SelectedItem = message.ViewModel;
}
public void Receive(OpenContextMenuMessage message)
{
FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top;
ResetContextMenu();
OnPropertyChanging(nameof(FilterOnTop));
OnPropertyChanged(nameof(FilterOnTop));
}
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
{
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
if (SelectedItem != null)
{
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
}
UpdateContextItems();
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SelectedItem.HasMoreCommands):
UpdateContextItems();
break;
}
}
public void UpdateContextItems()
{
if (SelectedItem != null)
{
if (SelectedItem.MoreCommands.Count() > 1)
{
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
}
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
if (SelectedItem == null)
{
return;
}
_lastSearchText = searchText;
if (CurrentContextMenu == null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, []);
return;
}
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu]);
return;
}
var commands = CurrentContextMenu
.OfType<CommandContextItemViewModel>()
.Where(c => c.ShouldBeVisible);
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
if (CurrentContextMenu == null)
{
return [];
}
return CurrentContextMenu
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return InvokeCommand(item);
}
}
return null;
}
public bool CanPopContextStack()
{
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
private void PushContextStack(IEnumerable<IContextItemViewModel> commands)
{
ContextMenuStack.Add(commands.ToList());
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
private void ResetContextMenu()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(CurrentContextMenu));
OnPropertyChanged(nameof(CurrentContextMenu));
if (CurrentContextMenu != null)
{
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
}
}
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
{
if (command == null)
{
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
// Display the commands child commands
PushContextStack(command.AllCommands);
OnPropertyChanging(nameof(FilteredItems));
OnPropertyChanged(nameof(FilteredItems));
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
UpdateContextItems();
return ContextKeybindingResult.Hide;
}
}
}

View File

@@ -0,0 +1,15 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.UI.ViewModels;
public interface IContextItemViewModel
{
}

View File

@@ -0,0 +1,12 @@
// 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.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to announce that a context menu should close
/// </summary>
public record CloseContextMenuMessage
{
}

View File

@@ -2,11 +2,21 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
/// <summary>
/// Used to perform a list item's secondary command when the user presses ctrl+enter in the search box
/// Used to announce the context menu should open
/// </summary>
public record OpenContextMenuMessage
public record OpenContextMenuMessage(FrameworkElement? Element, FlyoutPlacementMode? FlyoutPlacementMode, Point? Point, ContextMenuFilterLocation ContextMenuFilterLocation)
{
}
public enum ContextMenuFilterLocation
{
Top,
Bottom,
}

View File

@@ -16,11 +16,11 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
public IEnumerable<IContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
public List<IContextItemViewModel> AllCommands { get; }
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
@@ -33,6 +33,7 @@ public interface IContextMenuContext : INotifyPropertyChanged
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.OfType<CommandContextItemViewModel>()
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),

View File

@@ -0,0 +1,12 @@
// 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 Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
{
}

View File

@@ -52,7 +52,18 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands.Select(i => i.Model.Unsafe).ToArray();
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
.Select(item =>
{
if (item is ISeparatorContextItem)
{
return item as IContextItem;
}
else
{
return ((CommandContextItemViewModel)item).Model.Unsafe;
}
}).ToArray();
////// IListItem
ITag[] IListItem.Tags => Tags.ToArray();