mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 12:18:50 +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.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
@@ -48,11 +49,6 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial PageViewModel? CurrentPage { get; set; }
|
public partial PageViewModel? CurrentPage { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
|
|
||||||
|
|
||||||
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
|
|
||||||
|
|
||||||
public CommandBarViewModel()
|
public CommandBarViewModel()
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
@@ -101,18 +97,9 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||||
|
|
||||||
if (SelectedItem.MoreCommands.Count() > 1)
|
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||||
{
|
.OfType<CommandContextItemViewModel>()
|
||||||
ShouldShowContextMenu = true;
|
.Count() > 1;
|
||||||
|
|
||||||
ContextMenuStack.Clear();
|
|
||||||
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
|
|
||||||
OnPropertyChanged(nameof(ContextMenu));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ShouldShowContextMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||||
OnPropertyChanged(nameof(SecondaryCommand));
|
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)
|
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||||
{
|
{
|
||||||
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
|
var keybindings = SelectedItem?.Keybindings();
|
||||||
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
|
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)
|
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
|
||||||
@@ -152,10 +149,6 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
if (command.HasMoreCommands)
|
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;
|
return ContextKeybindingResult.KeepOpen;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -164,33 +157,6 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
return ContextKeybindingResult.Hide;
|
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
|
public enum ContextKeybindingResult
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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.CmdPal.UI.ViewModels.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
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);
|
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 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 string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
|
||||||
|
|
||||||
public CommandItemViewModel? PrimaryCommand => this;
|
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 bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||||
|
|
||||||
public List<CommandContextItemViewModel> AllCommands
|
public List<IContextItemViewModel> AllCommands
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
List<CommandContextItemViewModel> l = _defaultCommandContextItem == null ?
|
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
|
||||||
new() :
|
new() :
|
||||||
[_defaultCommandContextItem];
|
[_defaultCommandContextItem];
|
||||||
|
|
||||||
@@ -177,18 +179,29 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
if (more != null)
|
if (more != null)
|
||||||
{
|
{
|
||||||
MoreCommands = more
|
MoreCommands = more
|
||||||
.Where(contextItem => contextItem is ICommandContextItem)
|
.Select(item =>
|
||||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
{
|
||||||
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
|
if (item is ICommandContextItem contextItem)
|
||||||
|
{
|
||||||
|
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here, we're already theoretically in the async context, so we can
|
// Here, we're already theoretically in the async context, so we can
|
||||||
// use Initialize straight up
|
// use Initialize straight up
|
||||||
MoreCommands.ForEach(contextItem =>
|
MoreCommands
|
||||||
{
|
.OfType<CommandContextItemViewModel>()
|
||||||
contextItem.SlowInitializeProperties();
|
.ToList()
|
||||||
});
|
.ForEach(contextItem =>
|
||||||
|
{
|
||||||
|
contextItem.SlowInitializeProperties();
|
||||||
|
});
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.Command?.Name))
|
if (!string.IsNullOrEmpty(model.Command?.Name))
|
||||||
{
|
{
|
||||||
@@ -323,19 +336,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
if (more != null)
|
if (more != null)
|
||||||
{
|
{
|
||||||
var newContextMenu = more
|
var newContextMenu = more
|
||||||
.Where(contextItem => contextItem is ICommandContextItem)
|
.Select(item =>
|
||||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
{
|
||||||
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
|
if (item is CommandContextItem contextItem)
|
||||||
|
{
|
||||||
|
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
lock (MoreCommands)
|
lock (MoreCommands)
|
||||||
{
|
{
|
||||||
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
|
ListHelpers.InPlaceUpdateList(MoreCommands, newContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
newContextMenu.ForEach(contextItem =>
|
newContextMenu
|
||||||
{
|
.OfType<CommandContextItemViewModel>()
|
||||||
contextItem.InitializeProperties();
|
.ToList()
|
||||||
});
|
.ForEach(contextItem =>
|
||||||
|
{
|
||||||
|
contextItem.InitializeProperties();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -376,7 +400,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
|
|
||||||
lock (MoreCommands)
|
lock (MoreCommands)
|
||||||
{
|
{
|
||||||
MoreCommands.ForEach(c => c.SafeCleanup());
|
MoreCommands.OfType<CommandContextItemViewModel>()
|
||||||
|
.ToList()
|
||||||
|
.ForEach(c => c.SafeCleanup());
|
||||||
MoreCommands.Clear();
|
MoreCommands.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
|
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; }
|
public DetailsViewModel? Details { get; private set; }
|
||||||
|
|
||||||
@@ -31,18 +31,19 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
|||||||
public bool HasDetails => Details != null;
|
public bool HasDetails => Details != null;
|
||||||
|
|
||||||
/////// ICommandBarContext ///////
|
/////// 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 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;
|
public List<IContextItemViewModel> AllCommands => Commands;
|
||||||
/////// /ICommandBarContext ///////
|
|
||||||
|
|
||||||
// Remember - "observable" properties from the model (via PropChanged)
|
// Remember - "observable" properties from the model (via PropChanged)
|
||||||
// cannot be marked [ObservableProperty]
|
// cannot be marked [ObservableProperty]
|
||||||
@@ -113,14 +114,27 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
Commands = model.Commands
|
Commands = model.Commands
|
||||||
.Where(contextItem => contextItem is ICommandContextItem)
|
.ToList()
|
||||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
.Select(item =>
|
||||||
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
|
{
|
||||||
.ToList();
|
if (item is CommandContextItem contextItem)
|
||||||
Commands.ForEach(contextItem =>
|
{
|
||||||
{
|
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||||
contextItem.InitializeProperties();
|
}
|
||||||
});
|
else
|
||||||
|
{
|
||||||
|
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Commands
|
||||||
|
.OfType<CommandContextItemViewModel>()
|
||||||
|
.ToList()
|
||||||
|
.ForEach(contextItem =>
|
||||||
|
{
|
||||||
|
contextItem.InitializeProperties();
|
||||||
|
});
|
||||||
|
|
||||||
var extensionDetails = model.Details;
|
var extensionDetails = model.Details;
|
||||||
if (extensionDetails != null)
|
if (extensionDetails != null)
|
||||||
@@ -159,19 +173,32 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
|||||||
if (more != null)
|
if (more != null)
|
||||||
{
|
{
|
||||||
var newContextMenu = more
|
var newContextMenu = more
|
||||||
.Where(contextItem => contextItem is ICommandContextItem)
|
.ToList()
|
||||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
.Select(item =>
|
||||||
.Select(contextItem => new CommandContextItemViewModel(contextItem, PageContext))
|
{
|
||||||
.ToList();
|
if (item is CommandContextItem contextItem)
|
||||||
|
{
|
||||||
|
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
lock (Commands)
|
lock (Commands)
|
||||||
{
|
{
|
||||||
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
|
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands.ForEach(contextItem =>
|
Commands
|
||||||
{
|
.OfType<CommandContextItemViewModel>()
|
||||||
contextItem.SlowInitializeProperties();
|
.ToList()
|
||||||
});
|
.ForEach(contextItem =>
|
||||||
|
{
|
||||||
|
contextItem.SlowInitializeProperties();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -246,10 +273,11 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
|||||||
base.UnsafeCleanup();
|
base.UnsafeCleanup();
|
||||||
|
|
||||||
Details?.SafeCleanup();
|
Details?.SafeCleanup();
|
||||||
foreach (var item in Commands)
|
|
||||||
{
|
Commands
|
||||||
item.SafeCleanup();
|
.OfType<CommandContextItemViewModel>()
|
||||||
}
|
.ToList()
|
||||||
|
.ForEach(item => item.SafeCleanup());
|
||||||
|
|
||||||
Commands.Clear();
|
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.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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;
|
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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 interface IContextMenuContext : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
|
public IEnumerable<IContextItemViewModel> MoreCommands { get; }
|
||||||
|
|
||||||
public bool HasMoreCommands { get; }
|
public bool HasMoreCommands { get; }
|
||||||
|
|
||||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
public List<IContextItemViewModel> AllCommands { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a mapping of key -> command item for this particular item's
|
/// 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()
|
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||||
{
|
{
|
||||||
return MoreCommands
|
return MoreCommands
|
||||||
|
.OfType<CommandContextItemViewModel>()
|
||||||
.Where(c => c.HasRequestedShortcut)
|
.Where(c => c.HasRequestedShortcut)
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
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;
|
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
|
////// IListItem
|
||||||
ITag[] IListItem.Tags => Tags.ToArray();
|
ITag[] IListItem.Tags => Tags.ToArray();
|
||||||
|
|||||||
@@ -27,78 +27,28 @@
|
|||||||
TrueValue="Collapsed" />
|
TrueValue="Collapsed" />
|
||||||
|
|
||||||
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
||||||
<cmdpalUI:KeyChordToStringConverter x:Key="KeyChordToStringConverter" />
|
|
||||||
|
|
||||||
<StackLayout
|
<StackLayout
|
||||||
x:Name="VerticalStackLayout"
|
x:Name="VerticalStackLayout"
|
||||||
Orientation="Vertical"
|
Orientation="Vertical"
|
||||||
Spacing="4" />
|
Spacing="4" />
|
||||||
|
|
||||||
<cmdpalUI:ContextItemTemplateSelector
|
<Style
|
||||||
x:Key="ContextItemTemplateSelector"
|
x:Name="ContextMenuFlyoutStyle"
|
||||||
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
|
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||||
Default="{StaticResource DefaultContextMenuViewModelTemplate}" />
|
TargetType="FlyoutPresenter">
|
||||||
|
<Style.Setters>
|
||||||
|
<Setter Property="Margin" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
</Style.Setters>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Template for context items in the context item menu -->
|
<Flyout
|
||||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="viewModels:CommandContextItemViewModel">
|
x:Name="ContextMenuFlyout"
|
||||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||||
<Grid.ColumnDefinitions>
|
Opened="ContextMenuFlyout_Opened">
|
||||||
<ColumnDefinition Width="32" />
|
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||||
<ColumnDefinition Width="*" />
|
</Flyout>
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<cpcontrols:IconBox
|
|
||||||
Width="16"
|
|
||||||
Height="16"
|
|
||||||
Margin="4,0,0,0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="{x:Bind Title, Mode=OneWay}" />
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="2"
|
|
||||||
Margin="16,0,0,0"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
|
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
|
||||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<!-- Template for context items flagged as critical -->
|
|
||||||
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="viewModels:CommandContextItemViewModel">
|
|
||||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="32" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<cpcontrols:IconBox
|
|
||||||
Width="16"
|
|
||||||
Height="16"
|
|
||||||
Margin="4,0,0,0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Style="{StaticResource ContextItemTitleTextBlockCriticalStyle}"
|
|
||||||
Text="{x:Bind Title, Mode=OneWay}" />
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="2"
|
|
||||||
Margin="16,0,0,0"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Style="{StaticResource ContextItemCaptionTextBlockCriticalStyle}"
|
|
||||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<Style x:Key="HotkeyStyle" TargetType="Border">
|
<Style x:Key="HotkeyStyle" TargetType="Border">
|
||||||
<Style.Setters>
|
<Style.Setters>
|
||||||
@@ -252,45 +202,9 @@
|
|||||||
Content="{ui:FontIcon Glyph=,
|
Content="{ui:FontIcon Glyph=,
|
||||||
FontSize=16}"
|
FontSize=16}"
|
||||||
Style="{StaticResource SubtleButtonStyle}"
|
Style="{StaticResource SubtleButtonStyle}"
|
||||||
|
Tapped="MoreCommandsButton_Tapped"
|
||||||
ToolTipService.ToolTip="Ctrl+K"
|
ToolTipService.ToolTip="Ctrl+K"
|
||||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}" />
|
||||||
<Button.Flyout>
|
|
||||||
<Flyout
|
|
||||||
Closing="Flyout_Closing"
|
|
||||||
Opened="Flyout_Opened"
|
|
||||||
Placement="TopEdgeAlignedRight">
|
|
||||||
<StackPanel>
|
|
||||||
<ListView
|
|
||||||
x:Name="CommandsDropdown"
|
|
||||||
MinWidth="248"
|
|
||||||
Margin="-16,-12,-16,-12"
|
|
||||||
IsItemClickEnabled="True"
|
|
||||||
ItemClick="CommandsDropdown_ItemClick"
|
|
||||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
|
||||||
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
|
|
||||||
KeyDown="CommandsDropdown_KeyDown"
|
|
||||||
SelectionMode="Single">
|
|
||||||
<ListView.ItemContainerStyle>
|
|
||||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
|
||||||
<Setter Property="MinHeight" Value="0" />
|
|
||||||
<Setter Property="Padding" Value="12,7,12,7" />
|
|
||||||
</Style>
|
|
||||||
</ListView.ItemContainerStyle>
|
|
||||||
<ListView.ItemContainerTransitions>
|
|
||||||
<TransitionCollection />
|
|
||||||
</ListView.ItemContainerTransitions>
|
|
||||||
</ListView>
|
|
||||||
<TextBox
|
|
||||||
x:Name="ContextFilterBox"
|
|
||||||
x:Uid="ContextFilterBox"
|
|
||||||
Margin="-12,12,-12,-12"
|
|
||||||
KeyDown="ContextFilterBox_KeyDown"
|
|
||||||
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
|
||||||
TextChanged="ContextFilterBox_TextChanged" />
|
|
||||||
</StackPanel>
|
|
||||||
</Flyout>
|
|
||||||
</Button.Flyout>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace Microsoft.CmdPal.UI.Controls;
|
|||||||
|
|
||||||
public sealed partial class CommandBar : UserControl,
|
public sealed partial class CommandBar : UserControl,
|
||||||
IRecipient<OpenContextMenuMessage>,
|
IRecipient<OpenContextMenuMessage>,
|
||||||
|
IRecipient<CloseContextMenuMessage>,
|
||||||
IRecipient<TryCommandKeybindingMessage>,
|
IRecipient<TryCommandKeybindingMessage>,
|
||||||
ICurrentPageAware
|
ICurrentPageAware
|
||||||
{
|
{
|
||||||
@@ -39,9 +40,8 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
|
|
||||||
// RegisterAll isn't AOT compatible
|
// RegisterAll isn't AOT compatible
|
||||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||||
|
|
||||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(OpenContextMenuMessage message)
|
public void Receive(OpenContextMenuMessage message)
|
||||||
@@ -51,12 +51,43 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = new FlyoutShowOptions
|
if (message.Element == null)
|
||||||
{
|
{
|
||||||
ShowMode = FlyoutShowMode.Standard,
|
_ = DispatcherQueue.TryEnqueue(
|
||||||
};
|
() =>
|
||||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
{
|
||||||
UpdateUiForStackChange();
|
ContextMenuFlyout.ShowAt(
|
||||||
|
MoreCommandsButton,
|
||||||
|
new FlyoutShowOptions()
|
||||||
|
{
|
||||||
|
ShowMode = FlyoutShowMode.Standard,
|
||||||
|
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = DispatcherQueue.TryEnqueue(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
ContextMenuFlyout.ShowAt(
|
||||||
|
message.Element!,
|
||||||
|
new FlyoutShowOptions()
|
||||||
|
{
|
||||||
|
ShowMode = FlyoutShowMode.Standard,
|
||||||
|
Placement = (FlyoutPlacementMode)message.FlyoutPlacementMode!,
|
||||||
|
Position = message.Point,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(CloseContextMenuMessage message)
|
||||||
|
{
|
||||||
|
if (ContextMenuFlyout.IsOpen)
|
||||||
|
{
|
||||||
|
ContextMenuFlyout.Hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(TryCommandKeybindingMessage msg)
|
public void Receive(TryCommandKeybindingMessage msg)
|
||||||
@@ -74,17 +105,7 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
}
|
}
|
||||||
else if (result == ContextKeybindingResult.KeepOpen)
|
else if (result == ContextKeybindingResult.KeepOpen)
|
||||||
{
|
{
|
||||||
if (!MoreCommandsButton.Flyout.IsOpen)
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||||
{
|
|
||||||
var options = new FlyoutShowOptions
|
|
||||||
{
|
|
||||||
ShowMode = FlyoutShowMode.Standard,
|
|
||||||
};
|
|
||||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
|
|
||||||
msg.Handled = true;
|
msg.Handled = true;
|
||||||
}
|
}
|
||||||
else if (result == ContextKeybindingResult.Unhandled)
|
else if (result == ContextKeybindingResult.Unhandled)
|
||||||
@@ -121,164 +142,15 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e)
|
private void MoreCommandsButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.ClickedItem is CommandContextItemViewModel item)
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||||
{
|
|
||||||
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
|
||||||
{
|
|
||||||
MoreCommandsButton.Flyout.Hide();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
|
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||||
{
|
{
|
||||||
if (e.Handled)
|
// We need to wait until our flyout is opened to try and toss focus
|
||||||
{
|
// at its search box. The control isn't in the UI tree before that
|
||||||
return;
|
ContextControl.FocusSearchBox();
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
MoreCommandsButton.Flyout.Hide();
|
|
||||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
||||||
}
|
|
||||||
else if (result == ContextKeybindingResult.KeepOpen)
|
|
||||||
{
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
else if (result == ContextKeybindingResult.Unhandled)
|
|
||||||
{
|
|
||||||
e.Handled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Flyout_Opened(object sender, object e)
|
|
||||||
{
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
|
|
||||||
{
|
|
||||||
ViewModel?.ClearContextStack();
|
|
||||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
var prop = e.PropertyName;
|
|
||||||
if (prop == nameof(ViewModel.ContextMenu))
|
|
||||||
{
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
||||||
{
|
|
||||||
ViewModel.ContextMenu?.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 (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
|
||||||
{
|
|
||||||
MoreCommandsButton.Flyout.Hide();
|
|
||||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.Key == VirtualKey.Escape ||
|
|
||||||
(e.Key == VirtualKey.Left && altPressed))
|
|
||||||
{
|
|
||||||
if (ViewModel.CanPopContextStack())
|
|
||||||
{
|
|
||||||
ViewModel.PopContextStack();
|
|
||||||
UpdateUiForStackChange();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MoreCommandsButton.Flyout.Hide();
|
|
||||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandsDropdown_KeyDown(sender, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key == VirtualKey.Up)
|
|
||||||
{
|
|
||||||
// navigate previous
|
|
||||||
if (CommandsDropdown.SelectedIndex > 0)
|
|
||||||
{
|
|
||||||
CommandsDropdown.SelectedIndex--;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
else if (e.Key == VirtualKey.Down)
|
|
||||||
{
|
|
||||||
// navigate next
|
|
||||||
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
|
|
||||||
{
|
|
||||||
CommandsDropdown.SelectedIndex++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CommandsDropdown.SelectedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateUiForStackChange()
|
|
||||||
{
|
|
||||||
ContextFilterBox.Text = string.Empty;
|
|
||||||
ViewModel.ContextMenu?.SetSearchText(string.Empty);
|
|
||||||
CommandsDropdown.SelectedIndex = 0;
|
|
||||||
ContextFilterBox.Focus(FocusState.Programmatic);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
Normal file
159
src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<UserControl
|
||||||
|
x:Class="Microsoft.CmdPal.UI.Controls.ContextMenu"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||||
|
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||||
|
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||||
|
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||||
|
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||||
|
Background="Transparent"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<cmdpalUI:KeyChordToStringConverter x:Key="KeyChordToStringConverter" />
|
||||||
|
|
||||||
|
<cmdpalUI:ContextItemTemplateSelector
|
||||||
|
x:Key="ContextItemTemplateSelector"
|
||||||
|
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
|
||||||
|
Default="{StaticResource DefaultContextMenuViewModelTemplate}"
|
||||||
|
Separator="{StaticResource SeparatorContextMenuViewModelTemplate}" />
|
||||||
|
|
||||||
|
<!-- Template for context items in the context item menu -->
|
||||||
|
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||||
|
<Grid AutomationProperties.Name="{x:Bind Title}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="32" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<cpcontrols:IconBox
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
SourceKey="{x:Bind Icon}"
|
||||||
|
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Bind Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="16,0,0,0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Template for context items flagged as critical -->
|
||||||
|
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||||
|
<Grid AutomationProperties.Name="{x:Bind Title}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="32" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<cpcontrols:IconBox
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||||
|
SourceKey="{x:Bind Icon}"
|
||||||
|
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ContextItemTitleTextBlockCriticalStyle}"
|
||||||
|
Text="{x:Bind Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="16,0,0,0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ContextItemCaptionTextBlockCriticalStyle}"
|
||||||
|
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Template for context item separators -->
|
||||||
|
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="viewmodels:SeparatorContextItemViewModel">
|
||||||
|
<Rectangle
|
||||||
|
Height="1"
|
||||||
|
Margin="-16,-12,-12,-12"
|
||||||
|
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid x:Name="ContextMenuGrid">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel x:Name="CommandsPanel">
|
||||||
|
<ListView
|
||||||
|
x:Name="CommandsDropdown"
|
||||||
|
MinWidth="248"
|
||||||
|
IsItemClickEnabled="True"
|
||||||
|
ItemClick="CommandsDropdown_ItemClick"
|
||||||
|
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||||
|
KeyDown="CommandsDropdown_KeyDown"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ListView.ItemContainerStyle>
|
||||||
|
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="12,8" />
|
||||||
|
</Style>
|
||||||
|
</ListView.ItemContainerStyle>
|
||||||
|
<ListView.ItemContainerTransitions>
|
||||||
|
<TransitionCollection />
|
||||||
|
</ListView.ItemContainerTransitions>
|
||||||
|
</ListView>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox
|
||||||
|
x:Name="ContextFilterBox"
|
||||||
|
x:Uid="ContextFilterBox"
|
||||||
|
Margin="4"
|
||||||
|
KeyDown="ContextFilterBox_KeyDown"
|
||||||
|
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
||||||
|
TextChanged="ContextFilterBox_TextChanged" />
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="ContextMenuOrder">
|
||||||
|
<VisualState x:Name="FilterOnTop">
|
||||||
|
<VisualState.StateTriggers>
|
||||||
|
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="True" />
|
||||||
|
</VisualState.StateTriggers>
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="CommandsPanel.(Grid.Row)" Value="1" />
|
||||||
|
<Setter Target="ContextFilterBox.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="CommandsDropdown.Margin" Value="0, 0, 0, 4" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="FilterOnBottom">
|
||||||
|
<VisualState.StateTriggers>
|
||||||
|
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="False" />
|
||||||
|
</VisualState.StateTriggers>
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="CommandsPanel.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="ContextFilterBox.(Grid.Row)" Value="1" />
|
||||||
|
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 0" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// 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.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
|
using Microsoft.CmdPal.Ext.System;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
using Microsoft.CmdPal.UI.Views;
|
||||||
|
using Microsoft.UI.Input;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Windows.System;
|
||||||
|
using Windows.UI.Core;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.Controls;
|
||||||
|
|
||||||
|
public sealed partial class ContextMenu : UserControl,
|
||||||
|
IRecipient<OpenContextMenuMessage>,
|
||||||
|
IRecipient<UpdateCommandBarMessage>,
|
||||||
|
IRecipient<TryCommandKeybindingMessage>
|
||||||
|
{
|
||||||
|
public ContextMenuViewModel ViewModel { get; } = new();
|
||||||
|
|
||||||
|
public ContextMenu()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// RegisterAll isn't AOT compatible
|
||||||
|
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||||
|
|
||||||
|
if (ViewModel != null)
|
||||||
|
{
|
||||||
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(OpenContextMenuMessage message)
|
||||||
|
{
|
||||||
|
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<CloseContextMenuMessage>();
|
||||||
|
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<CloseContextMenuMessage>();
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUiForStackChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommandsDropdown_KeyDown(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<CloseContextMenuMessage>();
|
||||||
|
UpdateUiForStackChange();
|
||||||
|
}
|
||||||
|
else if (result == ContextKeybindingResult.KeepOpen)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (result == ContextKeybindingResult.Unhandled)
|
||||||
|
{
|
||||||
|
e.Handled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CloseContextMenuMessage>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CloseContextMenuMessage>();
|
||||||
|
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||||
|
UpdateUiForStackChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandsDropdown_KeyDown(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == VirtualKey.Up)
|
||||||
|
{
|
||||||
|
// navigate previous
|
||||||
|
if (CommandsDropdown.SelectedIndex > 0)
|
||||||
|
{
|
||||||
|
CommandsDropdown.SelectedIndex--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (e.Key == VirtualKey.Down)
|
||||||
|
{
|
||||||
|
// navigate next
|
||||||
|
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
|
||||||
|
{
|
||||||
|
CommandsDropdown.SelectedIndex++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CommandsDropdown.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUiForStackChange()
|
||||||
|
{
|
||||||
|
ContextFilterBox.Text = string.Empty;
|
||||||
|
ViewModel?.SetSearchText(string.Empty);
|
||||||
|
CommandsDropdown.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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()
|
||||||
|
/// </summary>
|
||||||
|
internal void FocusSearchBox()
|
||||||
|
{
|
||||||
|
ContextFilterBox.Focus(FocusState.Programmatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContextKeybindingResult InvokeCommand(CommandItemViewModel command) => ViewModel.InvokeCommand(command);
|
||||||
|
}
|
||||||
@@ -122,7 +122,7 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
else if (ctrlPressed && e.Key == VirtualKey.K)
|
else if (ctrlPressed && e.Key == VirtualKey.K)
|
||||||
{
|
{
|
||||||
// ctrl+k
|
// ctrl+k
|
||||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>();
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
else if (e.Key == VirtualKey.Right)
|
else if (e.Key == VirtualKey.Right)
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.Bot.AdaptiveExpressions.Core;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI;
|
namespace Microsoft.CmdPal.UI;
|
||||||
|
|
||||||
@@ -14,8 +17,29 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
|||||||
|
|
||||||
public DataTemplate? Critical { get; set; }
|
public DataTemplate? Critical { get; set; }
|
||||||
|
|
||||||
protected override DataTemplate? SelectTemplateCore(object item)
|
public DataTemplate? Separator { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||||
{
|
{
|
||||||
return ((CommandContextItemViewModel)item).IsCritical ? Critical : Default;
|
DataTemplate? dataTemplate = Default;
|
||||||
|
|
||||||
|
if (dependencyObject is ListViewItem li)
|
||||||
|
{
|
||||||
|
li.IsEnabled = true;
|
||||||
|
|
||||||
|
if (item is SeparatorContextItemViewModel)
|
||||||
|
{
|
||||||
|
li.IsEnabled = false;
|
||||||
|
li.AllowFocusWhenDisabled = false;
|
||||||
|
li.AllowFocusOnInteraction = false;
|
||||||
|
dataTemplate = Separator;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dataTemplate = ((CommandContextItemViewModel)item).IsCritical ? Critical : Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTemplate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,22 +118,23 @@
|
|||||||
ItemClick="ItemsList_ItemClick"
|
ItemClick="ItemsList_ItemClick"
|
||||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||||
|
RightTapped="ItemsList_RightTapped"
|
||||||
SelectionChanged="ItemsList_SelectionChanged">
|
SelectionChanged="ItemsList_SelectionChanged">
|
||||||
<ListView.ItemContainerTransitions>
|
<ListView.ItemContainerTransitions>
|
||||||
<TransitionCollection />
|
<TransitionCollection />
|
||||||
</ListView.ItemContainerTransitions>
|
</ListView.ItemContainerTransitions>
|
||||||
<!--<ListView.GroupStyle>
|
<!--<ListView.GroupStyle>
|
||||||
<GroupStyle HidesIfEmpty="True">
|
<GroupStyle HidesIfEmpty="True">
|
||||||
<GroupStyle.HeaderTemplate>
|
<GroupStyle.HeaderTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,16,0,0"
|
Margin="0,16,0,0"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{Binding Key, Mode=OneWay}" />
|
Text="{Binding Key, Mode=OneWay}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</GroupStyle.HeaderTemplate>
|
</GroupStyle.HeaderTemplate>
|
||||||
</GroupStyle>
|
</GroupStyle>
|
||||||
</ListView.GroupStyle>-->
|
</ListView.GroupStyle>-->
|
||||||
</ListView>
|
</ListView>
|
||||||
</controls:Case>
|
</controls:Case>
|
||||||
<controls:Case Value="True">
|
<controls:Case Value="True">
|
||||||
|
|||||||
@@ -294,4 +294,31 @@ public sealed partial class ListPage : Page,
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ItemsList_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OriginalSource is FrameworkElement element &&
|
||||||
|
element.DataContext is ListItemViewModel item)
|
||||||
|
{
|
||||||
|
if (ItemsList.SelectedItem != item)
|
||||||
|
{
|
||||||
|
ItemsList.SelectedItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||||
|
|
||||||
|
var pos = e.GetPosition(element);
|
||||||
|
|
||||||
|
_ = DispatcherQueue.TryEnqueue(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||||
|
new OpenContextMenuMessage(
|
||||||
|
element,
|
||||||
|
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||||
|
pos,
|
||||||
|
ContextMenuFilterLocation.Top));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
using Microsoft.UI.Xaml.Media.Animation;
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ internal sealed partial class SampleListPage : ListPage
|
|||||||
Title = "I'm a second command",
|
Title = "I'm a second command",
|
||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||||
},
|
},
|
||||||
|
new SeparatorContextItem(),
|
||||||
new CommandContextItem(
|
new CommandContextItem(
|
||||||
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
|
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 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.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
public partial class SeparatorContextItem : ISeparatorContextItem
|
||||||
|
{
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user