diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs index 9b5be8a973e0..420f41f49f1e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs @@ -7,11 +7,15 @@ using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.System; namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandBarViewModel : ObservableObject, - IRecipient + IRecipient, + IRecipient { public ICommandBarContext? SelectedItem { @@ -49,13 +53,18 @@ public ICommandBarContext? SelectedItem [ObservableProperty] public partial ObservableCollection ContextCommands { get; set; } = []; + private Dictionary? _contextKeybindings; + public CommandBarViewModel() { WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel; + public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys; + private void SetSelectedItem(ICommandBarContext? value) { if (value != null) @@ -131,4 +140,22 @@ public void InvokeSecondaryCommand() WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); } } + + public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key) + { + if (_contextKeybindings != null) + { + // Does the pressed key match any of the keybindings? + var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0); + if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item)) + { + // TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message + // so that the correct item is activated. + WeakReferenceMessenger.Default.Send(new(item)); + return true; + } + } + + return false; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs index f3475fd96446..dfbb1a982a90 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandContextItemViewModel.cs @@ -9,12 +9,16 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) : CommandItemViewModel(new(contextItem), context) { + private readonly KeyChord nullKeyChord = new(0, 0, 0); + public new ExtensionObject Model { get; } = new(contextItem); public bool IsCritical { get; private set; } public KeyChord? RequestedShortcut { get; private set; } + public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord); + public override void InitializeProperties() { if (IsInitialized) @@ -31,6 +35,9 @@ public override void InitializeProperties() } IsCritical = contextItem.IsCritical; + + // I actually don't think this will ever actually be null, because + // KeyChord is a struct, which isn't nullable in WinRT if (contextItem.RequestedShortcut != null) { RequestedShortcut = new( diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index 37d223b00072..8634b63278f5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -398,6 +398,23 @@ public override void SafeCleanup() base.SafeCleanup(); Initialized |= InitializedState.CleanedUp; } + + /// + /// 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 + /// + /// a dictionary of KeyChord -> Context commands, for all commands + /// that have a shortcut key set. + internal Dictionary Keybindings() + { + return MoreCommands + .Where(c => c.HasRequestedShortcut) + .ToDictionary( + c => c.RequestedShortcut ?? new KeyChord(0, 0, 0), + c => c); + } } [Flags] diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs index f1c6aa469525..c728a434fb86 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -128,7 +128,7 @@ private void FetchItems() try { - IListItem[] newItems = _model.Unsafe!.GetItems(); + var newItems = _model.Unsafe!.GetItems(); // Collect all the items into new viewmodels Collection newViewModels = []; @@ -136,7 +136,7 @@ private void FetchItems() // TODO we can probably further optimize this by also keeping a // HashSet of every ExtensionObject we currently have, and only // building new viewmodels for the ones we haven't already built. - foreach (IListItem? item in newItems) + foreach (var item in newItems) { ListItemViewModel viewModel = new(item, new(this)); @@ -147,8 +147,8 @@ private void FetchItems() } } - IEnumerable firstTwenty = newViewModels.Take(20); - foreach (ListItemViewModel? item in firstTwenty) + var firstTwenty = newViewModels.Take(20); + foreach (var item in firstTwenty) { item?.SafeInitializeProperties(); } @@ -233,7 +233,7 @@ private void InitializeItemsTask(CancellationToken ct) iterable = Items.ToArray(); } - foreach (ListItemViewModel item in iterable) + foreach (var item in iterable) { ct.ThrowIfCancellationRequested(); @@ -266,8 +266,8 @@ private static int ScoreListItem(string query, CommandItemViewModel listItem) return 1; } - MatchResult nameMatch = StringMatcher.FuzzySearch(query, listItem.Title); - MatchResult descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle); + var nameMatch = StringMatcher.FuzzySearch(query, listItem.Title); + var descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle); return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max(); } @@ -280,7 +280,7 @@ private struct ScoredListItemViewModel // Similarly stolen from ListHelpers.FilterList public static IEnumerable FilterList(IEnumerable items, string query) { - IOrderedEnumerable scores = items + var scores = items .Where(i => !i.IsInErrorState) .Select(li => new ScoredListItemViewModel() { ViewModel = li, Score = ScoreListItem(query, li) }) .Where(score => score.Score > 0) @@ -342,6 +342,8 @@ private void UpdateSelectedItem(ListItemViewModel item) { WeakReferenceMessenger.Default.Send(new(item)); + WeakReferenceMessenger.Default.Send(new(item.Keybindings())); + if (ShowDetails && item.HasDetails) { WeakReferenceMessenger.Default.Send(new(item.Details)); @@ -359,7 +361,7 @@ public override void InitializeProperties() { base.InitializeProperties(); - IListPage? model = _model.Unsafe; + var model = _model.Unsafe; if (model == null) { return; // throw? @@ -385,7 +387,7 @@ public override void InitializeProperties() public void LoadMoreIfNeeded() { - IListPage? model = this._model.Unsafe; + var model = this._model.Unsafe; if (model == null) { return; @@ -412,7 +414,7 @@ protected override void FetchProperty(string propertyName) { base.FetchProperty(propertyName); - IListPage? model = this._model.Unsafe; + var model = this._model.Unsafe; if (model == null) { return; // throw? @@ -475,13 +477,13 @@ protected override void UnsafeCleanup() lock (_listLock) { - foreach (ListItemViewModel item in Items) + foreach (var item in Items) { item.SafeCleanup(); } Items.Clear(); - foreach (ListItemViewModel item in FilteredItems) + foreach (var item in FilteredItems) { item.SafeCleanup(); } @@ -489,7 +491,7 @@ protected override void UnsafeCleanup() FilteredItems.Clear(); } - IListPage? model = _model.Unsafe; + var model = _model.Unsafe; if (model != null) { model.ItemsChanged -= Model_ItemsChanged; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs index 9bc0c730e8df..b0e5e4829aaa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/PerformCommandMessage.cs @@ -51,6 +51,12 @@ public PerformCommandMessage(ExtensionObject command, ExtensionObject< Context = context.Unsafe; } + public PerformCommandMessage(CommandContextItemViewModel contextCommand) + { + Command = contextCommand.Command.Model; + Context = contextCommand.Model.Unsafe; + } + public PerformCommandMessage(ConfirmResultViewModel vm) { Command = vm.PrimaryCommand.Model; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs new file mode 100644 index 000000000000..2054d3d8fd5f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs @@ -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. + +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.UI.ViewModels.Messages; + +public record UpdateItemKeybindingsMessage(Dictionary? Keys); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml index 9f7b4d407106..4a692fcc20fe 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml @@ -53,14 +53,14 @@ Grid.Column="1" VerticalAlignment="Center" Text="{x:Bind Title, Mode=OneWay}" /> - + Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" /> @@ -263,6 +263,7 @@ ItemClick="CommandsDropdown_ItemClick" ItemTemplate="{StaticResource ContextMenuViewModelTemplate}" ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}" + KeyDown="CommandsDropdown_KeyDown" SelectionMode="None">