mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-30 22:15:11 +00:00
Add the list item requested shortcuts back (#38573)
* [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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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]
|
||||
|
@@ -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));
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
@@ -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">
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
GetForegroundWindow
|
||||
GetWindowTextLength
|
||||
GetWindowText
|
@@ -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",
|
||||
},
|
||||
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user