mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-10 05:06:36 +02:00
CmdPal: Prevent crash on duplicate keybindings; simplify matching (#41714)
## Summary of the Pull Request Handles duplicate keybindings by using the first occurrence and ignoring the rest (in `ContextMenuViewModel.Keybindings` and `IContextMenuContext.Keybindings`). Replaces LINQ with direct iteration for clarity. Simplifies `CheckKeybinding` by removing redundant null checks and clarifying the key-to-binding matching logic, improving both readability and efficiency. Add a new method to `KeyChordHelpers.FormatForDebug` that formats KeyChord as string to help debugging. Makes `KeyChordHelpers` class a static class. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #41712 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [x] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Validated using a custom extension that has a duplicate item in the context menu.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -117,36 +118,46 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
/// Generates a mapping of key -> command item for this particular item's
|
/// Generates a mapping of key -> command item for this particular item's
|
||||||
/// MoreCommands. (This won't include the primary Command, but it will
|
/// 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
|
/// include the secondary one). This map can be used to quickly check if a
|
||||||
/// shortcut key was pressed
|
/// shortcut key was pressed. In case there are duplicate keybindings, the first
|
||||||
|
/// one is used and the rest are ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
|
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
|
||||||
/// that have a shortcut key set.</returns>
|
/// that have a shortcut key set.</returns>
|
||||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
private Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||||
{
|
{
|
||||||
if (CurrentContextMenu is null)
|
var result = new Dictionary<KeyChord, CommandContextItemViewModel>();
|
||||||
|
|
||||||
|
var menu = CurrentContextMenu;
|
||||||
|
if (menu is null)
|
||||||
{
|
{
|
||||||
return [];
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CurrentContextMenu
|
foreach (var item in menu)
|
||||||
.OfType<CommandContextItemViewModel>()
|
{
|
||||||
.Where(c => c.HasRequestedShortcut)
|
if (item is CommandContextItemViewModel cmd && cmd.HasRequestedShortcut)
|
||||||
.ToDictionary(
|
{
|
||||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
var key = cmd.RequestedShortcut ?? new KeyChord(0, 0, 0);
|
||||||
c => c);
|
var added = result.TryAdd(key, cmd);
|
||||||
|
if (!added)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Ignoring duplicate keyboard shortcut {KeyChordHelpers.FormatForDebug(key)} on command '{cmd.Title ?? cmd.Name ?? "(unknown)"}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 keybindings = Keybindings();
|
var keybindings = Keybindings();
|
||||||
if (keybindings is not 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))
|
||||||
{
|
{
|
||||||
// Does the pressed key match any of the keybindings?
|
return InvokeCommand(item);
|
||||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
|
||||||
if (keybindings.TryGetValue(pressedKeyChord, out var item))
|
|
||||||
{
|
|
||||||
return InvokeCommand(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
|
|
||||||
@@ -32,12 +34,28 @@ public interface IContextMenuContext : INotifyPropertyChanged
|
|||||||
/// that have a shortcut key set.</returns>
|
/// that have a shortcut key set.</returns>
|
||||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||||
{
|
{
|
||||||
return MoreCommands
|
var result = new Dictionary<KeyChord, CommandContextItemViewModel>();
|
||||||
.OfType<CommandContextItemViewModel>()
|
|
||||||
.Where(c => c.HasRequestedShortcut)
|
var menu = MoreCommands;
|
||||||
.ToDictionary(
|
if (menu is null)
|
||||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
{
|
||||||
c => c);
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in menu)
|
||||||
|
{
|
||||||
|
if (item is CommandContextItemViewModel cmd && cmd.HasRequestedShortcut)
|
||||||
|
{
|
||||||
|
var key = cmd.RequestedShortcut ?? new KeyChord(0, 0, 0);
|
||||||
|
var added = result.TryAdd(key, cmd);
|
||||||
|
if (!added)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Ignoring duplicate keyboard shortcut {KeyChordHelpers.FormatForDebug(key)} on command '{cmd.Title ?? cmd.Name ?? "(unknown)"}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,12 @@ public partial class EvilSamplesPage : ListPage
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
new ListItem(new EvilDuplicateRequestedShortcut())
|
||||||
|
{
|
||||||
|
Title = "Evil keyboard shortcuts",
|
||||||
|
Subtitle = "Two commands with the same shortcut and more...",
|
||||||
|
Icon = new IconInfo("\uE765"),
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
public EvilSamplesPage()
|
public EvilSamplesPage()
|
||||||
@@ -414,3 +419,42 @@ internal sealed partial class EvilFastUpdatesPage : DynamicListPage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||||
|
internal sealed partial class EvilDuplicateRequestedShortcut : ListPage
|
||||||
|
{
|
||||||
|
private readonly IListItem[] _items =
|
||||||
|
[
|
||||||
|
new ListItem(new NoOpCommand())
|
||||||
|
{
|
||||||
|
Title = "I'm evil!",
|
||||||
|
Subtitle = "I have multiple commands sharing the same keyboard shortcut",
|
||||||
|
MoreCommands = [
|
||||||
|
new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me too executed").Show())
|
||||||
|
{
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Title = "Me too",
|
||||||
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||||
|
},
|
||||||
|
new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me three executed").Show())
|
||||||
|
{
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Title = "Me three",
|
||||||
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public override IListItem[] GetItems() => _items;
|
||||||
|
|
||||||
|
public EvilDuplicateRequestedShortcut()
|
||||||
|
{
|
||||||
|
Icon = new IconInfo(string.Empty);
|
||||||
|
Name = "Open";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Windows.System;
|
|||||||
|
|
||||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
public partial class KeyChordHelpers
|
public static partial class KeyChordHelpers
|
||||||
{
|
{
|
||||||
public static KeyChord FromModifiers(
|
public static KeyChord FromModifiers(
|
||||||
bool ctrl = false,
|
bool ctrl = false,
|
||||||
@@ -34,4 +34,28 @@ public partial class KeyChordHelpers
|
|||||||
{
|
{
|
||||||
return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode);
|
return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string FormatForDebug(KeyChord value)
|
||||||
|
{
|
||||||
|
var result = string.Empty;
|
||||||
|
|
||||||
|
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Control))
|
||||||
|
{
|
||||||
|
result += "Ctrl+";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Shift))
|
||||||
|
{
|
||||||
|
result += "Shift+";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Modifiers.HasFlag(VirtualKeyModifiers.Menu))
|
||||||
|
{
|
||||||
|
result += "Alt+";
|
||||||
|
}
|
||||||
|
|
||||||
|
result += (VirtualKey)value.Vkey;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user