mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
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.   This PR covers: - closes #38308 - closes #39211 - closes #38307 - closes #38261
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user