Add the list item requested shortcuts back (#38573)
Some checks are pending
Spell checking / Check Spelling (push) Waiting to run
Spell checking / Report (Push) (push) Blocked by required conditions
Spell checking / Report (PR) (push) Blocked by required conditions
Spell checking / Update PR (push) Waiting to run

* [x] Re-adds the context menu shortcut text
* [x] Hooks up the keybindings to the search box so that you can just press the keys while you have an item selected, and do a context command
* [x] Hook these keybindings up to the context flyout itself
* [x] Adds a sample for testing

Solves #38271
This commit is contained in:
Mike Griese
2025-04-17 06:13:11 -05:00
committed by GitHub
parent 6cf73ce839
commit 05218e8af6
14 changed files with 243 additions and 9 deletions

View File

@@ -7,11 +7,15 @@ using CommunityToolkit.Mvvm.ComponentModel;
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<UpdateCommandBarMessage>
IRecipient<UpdateCommandBarMessage>,
IRecipient<UpdateItemKeybindingsMessage>
{
public ICommandBarContext? SelectedItem
{
@@ -49,13 +53,18 @@ public partial class CommandBarViewModel : ObservableObject,
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(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 partial class CommandBarViewModel : ObservableObject,
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(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<PerformCommandMessage>(new(item));
return true;
}
}
return false;
}
}

View File

@@ -9,12 +9,16 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context)
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> 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 partial class CommandContextItemViewModel(ICommandContextItem contextItem
}
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(

View File

@@ -398,6 +398,23 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
/// <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>
internal Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
[Flags]

View File

@@ -344,6 +344,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));

View File

@@ -51,6 +51,12 @@ public record PerformCommandMessage
Context = context.Unsafe;
}
public PerformCommandMessage(CommandContextItemViewModel contextCommand)
{
Command = contextCommand.Command.Model;
Context = contextCommand.Model.Unsafe;
}
public PerformCommandMessage(ConfirmResultViewModel vm)
{
Command = vm.PrimaryCommand.Model;

View File

@@ -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<KeyChord, CommandContextItemViewModel>? Keys);

View File

@@ -53,14 +53,14 @@
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}" />
<!--<TextBlock
<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}}" />-->
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
</Grid>
</DataTemplate>
@@ -263,6 +263,7 @@
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">

View File

@@ -6,10 +6,13 @@ using CommunityToolkit.Mvvm.Messaging;
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;
@@ -89,4 +92,23 @@ public sealed partial class CommandBar : UserControl,
MoreCommandsButton.Flyout.Hide();
}
}
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);
if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
{
e.Handled = true;
}
}
}

View File

@@ -8,6 +8,8 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -21,6 +23,7 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient<GoHomeMessage>,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateItemKeybindingsMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -31,6 +34,8 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -69,6 +74,7 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void ClearSearch()
@@ -105,7 +111,9 @@ public sealed partial class SearchBar : UserControl,
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 (ctrlPressed && e.Key == VirtualKey.Enter)
{
// ctrl+enter
@@ -164,6 +172,19 @@ public sealed partial class SearchBar : UserControl,
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
}
if (_keyBindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
if (_keyBindings.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<PerformCommandMessage>(new(item));
e.Handled = true;
}
}
}
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
@@ -282,4 +303,9 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(UpdateItemKeybindingsMessage message)
{
_keyBindings = message.Keys;
}
}

View File

@@ -187,6 +187,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
@@ -427,8 +429,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
_settingsWindow.Activate();
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
});
}

View File

@@ -0,0 +1,3 @@
GetForegroundWindow
GetWindowTextLength
GetWindowText

View File

@@ -4,6 +4,8 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
using Windows.Win32;
namespace SamplePagesExtension;
@@ -65,6 +67,68 @@ internal sealed partial class SampleListPage : ListPage
Subtitle = "and I'll take you to a page with markdown content",
Tags = [new Tag("Sample Tag")],
},
new ListItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Primary command invoked",
State = MessageState.Info,
});
t.Show();
})
{
Result = CommandResult.KeepOpen(),
Icon = new IconInfo("\uE712"),
})
{
Title = "You can add context menu items too. Press Ctrl+k",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"),
MoreCommands = [
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Secondary command invoked",
State = MessageState.Warning,
});
t.Show();
})
{
Name = "Secondary command",
Icon = new IconInfo("\uF147"), // Dial 2
Result = CommandResult.KeepOpen(),
})
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Third command invoked",
State = MessageState.Error,
});
t.Show();
})
{
Name = "Do it",
Icon = new IconInfo("\uF148"), // dial 3
Result = CommandResult.KeepOpen(),
})
{
Title = "A third command too",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
}
],
},
new ListItem(new SendMessageCommand())
{
Title = "I send lots of messages",
@@ -91,7 +155,35 @@ internal sealed partial class SampleListPage : ListPage
})
{
Title = "Confirm twice before doing something",
},
new ListItem(
new AnonymousCommand(() =>
{
var fg = PInvoke.GetForegroundWindow();
var bufferSize = PInvoke.GetWindowTextLength(fg) + 1;
unsafe
{
fixed (char* windowNameChars = new char[bufferSize])
{
if (PInvoke.GetWindowText(fg, windowNameChars, bufferSize) == 0)
{
var emptyToast = new ToastStatusMessage(new StatusMessage() { Message = "FG Window didn't have a title", State = MessageState.Warning });
emptyToast.Show();
}
var windowName = new string(windowNameChars);
var nameToast = new ToastStatusMessage(new StatusMessage() { Message = $"FG Window is {windowName}", State = MessageState.Success });
nameToast.Show();
}
}
})
{
Result = CommandResult.KeepOpen(),
})
{
Title = "Get the name of the Foreground window",
},
];
}
}

View File

@@ -33,6 +33,12 @@
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget

View File

@@ -2,14 +2,19 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
using Windows.System;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class KeyChordHelpers
{
public static KeyChord FromModifiers(bool ctrl, bool alt, bool shift, bool win, int vkey, int scanCode)
public static KeyChord FromModifiers(
bool ctrl = false,
bool alt = false,
bool shift = false,
bool win = false,
int vkey = 0,
int scanCode = 0)
{
var modifiers = (ctrl ? VirtualKeyModifiers.Control : VirtualKeyModifiers.None)
| (alt ? VirtualKeyModifiers.Menu : VirtualKeyModifiers.None)
@@ -18,4 +23,15 @@ public partial class KeyChordHelpers
;
return new(modifiers, vkey, scanCode);
}
public static KeyChord FromModifiers(
bool ctrl = false,
bool alt = false,
bool shift = false,
bool win = false,
VirtualKey vkey = VirtualKey.None,
int scanCode = 0)
{
return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode);
}
}