[Shortcut Guide] Add page-local search (#49639)

## Summary of the Pull Request

Adds an accessible search box to the Shortcut Guide title bar that
filters shortcuts on the currently selected application page.

The query matches shortcut names, descriptions, modifier names, and
displayed key labels while preserving the existing pinned, recommended,
category, and taskbar grouping.

## PR Checklist

- [x] Closes: #48791
- [x] **Communication:** The UX and behavior were discussed before
implementation
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Not applicable
- [ ] **Documentation updated:** Not applicable

## Detailed Description of the Pull Request / Additional comments

- Adds a localized title-bar `AutoSuggestBox` with a find icon and UI
Automation identity.
- Filters only the selected app page using case-insensitive matching
across names, descriptions, modifiers, virtual-key display names, and
rendered special-key aliases.
- Keeps only sections containing matches and shows a polite live-region
no-results state with correct pane spacing.
- Preserves the query when switching app pages, but clears it when
Shortcut Guide closes.
- Adds `Ctrl+F` to focus search; the first `Escape` clears a query and
the next closes the overlay.
- Keeps query text local to the UI with no logging or telemetry.

Related issues: #48860 requests several broader navigation/readability
changes; #49459 requests direct physical-key interception rather than
text search.

## Screenshots

### Filter Windows shortcuts by displayed key label

<img
src="https://raw.githubusercontent.com/niels9001/PowerToys/pr-assets-shortcut-guide-search/.github/pr-assets/shortcut-guide-search/windows-alt-filter.png"
width="667" alt="Shortcut Guide Windows page filtered by Alt" />

### Keep the query while switching to the PowerToys page

<img
src="https://raw.githubusercontent.com/niels9001/PowerToys/pr-assets-shortcut-guide-search/.github/pr-assets/shortcut-guide-search/powertoys-opa-filter.png"
width="660" alt="Shortcut Guide PowerToys page filtered by opa" />

## Validation Steps Performed

- Built `ShortcutGuide.Ui` for ARM64 Debug with the repository build
scripts.
- Built `ShortcutGuide.UnitTests` for ARM64 Debug and passed all 23
tests (16 search cases plus 7 existing tests) with `vstest.console.exe`.
- Verified via UIA and guarded keyboard input that name/key-label
filtering updates immediately, empty sections disappear, and no matches
show the localized live-region state.
- Verified the query persists when switching Windows to PowerToys,
`Ctrl+F` focuses search, first `Escape` clears, second `Escape` closes,
and reopening starts with an empty query.
- Rebuilt after the final no-results accessibility and 16px top-margin
adjustment.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b8ffa76b-3cf0-4a67-9adb-a13c5dd9f125
Copilot-Session: 4a96c2c2-6954-4784-8257-e0de0fac15a7
Copilot-Session: 1f00def4-e790-4071-96c6-a81c9c2adba5
Copilot-Session: 76e284a6-9a03-4105-bae6-4ed7fc92042d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b3fb20d-6e9d-4fef-a5cd-f8921d28c220
This commit is contained in:
Niels Laute
2026-08-14 09:32:02 +02:00
committed by Boliang Zhang (from Dev Box)
parent 633011c58f
commit 34ffe7b35c
11 changed files with 488 additions and 44 deletions

View File

@@ -25,6 +25,9 @@ The **Hold Windows key** setting is independent of the activation shortcut:
- **Show taskbar indicators** is the default and always hides the indicators when the Windows key is released.
- **Open Shortcut Guide** can close on Windows-key release or remain open.
- Use the title-bar search box to filter shortcuts on the selected application page
- Press Ctrl+F to focus search. Escape clears an active search before dismissing the overlay
-
The hold duration accepts values from 100 through 5,000 milliseconds and defaults to 900 milliseconds.
## Build and Debug Instructions

View File

@@ -0,0 +1,168 @@
// 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 Common.Search.FuzzSearch;
using ShortcutGuide.Models;
namespace ShortcutGuide.Helpers
{
public static class ShortcutSearchMatcher
{
public static bool Matches(ShortcutEntry shortcut, string? query)
{
string searchText = query?.Trim() ?? string.Empty;
if (searchText.Length == 0)
{
return true;
}
if (MatchesText(shortcut.Name, searchText) || MatchesText(shortcut.Description, searchText))
{
return true;
}
foreach (var description in shortcut.Shortcut ?? [])
{
foreach (string chordLabel in GetChordSearchLabels(description))
{
if (MatchesText(chordLabel, searchText))
{
return true;
}
}
foreach (string label in GetSearchLabels(description))
{
if (MatchesText(label, searchText))
{
return true;
}
}
}
return false;
}
private static IEnumerable<string> GetChordSearchLabels(ShortcutDescription description)
{
var displayedLabels = new List<string>();
var semanticLabels = new List<string>();
if (description.Win)
{
displayedLabels.Add("Win");
semanticLabels.Add("Windows");
}
if (description.Ctrl)
{
displayedLabels.Add("Ctrl");
semanticLabels.Add("Control");
}
if (description.Alt)
{
displayedLabels.Add("Alt");
semanticLabels.Add("Alt");
}
if (description.Shift)
{
displayedLabels.Add("Shift");
semanticLabels.Add("Shift");
}
var keyLabels = (description.Keys ?? []).Select(GetKeySearchLabel);
displayedLabels.AddRange(keyLabels);
semanticLabels.AddRange(keyLabels);
yield return string.Join(' ', displayedLabels);
if (!displayedLabels.SequenceEqual(semanticLabels, StringComparer.Ordinal))
{
yield return string.Join(' ', semanticLabels);
}
}
private static IEnumerable<string> GetSearchLabels(ShortcutDescription description)
{
if (description.Win)
{
yield return "Win Windows";
}
if (description.Ctrl)
{
yield return "Ctrl Control";
}
if (description.Alt)
{
yield return "Alt";
}
if (description.Shift)
{
yield return "Shift";
}
foreach (string key in description.Keys ?? [])
{
yield return GetKeySearchLabel(key);
}
}
private static string GetKeySearchLabel(string key)
{
if (int.TryParse(key, out int keyCode))
{
return keyCode switch
{
37 => "Left Left Arrow",
38 => "Up Up Arrow",
39 => "Right Right Arrow",
40 => "Down Down Arrow",
_ => Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetKeyName((uint)keyCode),
};
}
return key switch
{
"Up" or "<Up>" => "Up Up Arrow",
"Down" or "<Down>" => "Down Down Arrow",
"Left" or "<Left>" => "Left Left Arrow",
"Right" or "<Right>" => "Right Right Arrow",
"Back" or "<Backspace>" => "Back Backspace",
"<TASKBAR1-9>" => "Num",
"<ArrowUD>" => "Up Down Arrow",
"<ArrowLR>" => "Left Right Arrow",
"<Arrow>" => "Left Right Up Down Arrow",
"<Enter>" => "Enter",
"<LessThan>" => "<",
"<GreaterThan>" => ">",
"<Escape>" => "Esc Escape",
string value when value.StartsWith('<') && value.EndsWith('>') => value.Trim('<', '>'),
_ => key,
};
}
private static bool MatchesText(string? value, string searchText)
{
if (value is null)
{
return false;
}
if (value.Contains(searchText, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return StringMatcher.FuzzyMatch(searchText, value).IsSearchPrecisionScoreMet();
}
}
}

View File

@@ -100,6 +100,7 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.Search\Common.Search.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />

View File

@@ -238,6 +238,7 @@ namespace ShortcutGuide
await OverlayWindow.MainPaneControl.Open();
OverlayWindow.UpdateTaskbarPaneLayout();
OverlayWindow.MainPaneControl.Visibility = Visibility.Visible;
OverlayWindow.MainPaneControl.FocusSearch();
}
});
}

View File

@@ -26,6 +26,7 @@
<Grid Padding="16,0" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -40,10 +41,30 @@
Grid.Column="1"
VerticalAlignment="Center"
Style="{ThemeResource CaptionTextBlockStyle}" />
<AutoSuggestBox
x:Name="SearchBox"
x:Uid="SearchBox"
Grid.Column="2"
MinWidth="120"
MaxWidth="220"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
AutomationProperties.AutomationId="ShortcutGuide_SearchBox"
TextChanged="SearchBox_TextChanged">
<AutoSuggestBox.QueryIcon>
<SymbolIcon Symbol="Find" />
</AutoSuggestBox.QueryIcon>
<AutoSuggestBox.KeyboardAccelerators>
<KeyboardAccelerator
Key="F"
Invoked="OnFindInvoked"
Modifiers="Control" />
</AutoSuggestBox.KeyboardAccelerators>
</AutoSuggestBox>
<Button
x:Name="CloseButton"
x:Uid="CloseButton"
Grid.Column="2"
Grid.Column="3"
Width="32"
Height="32"
Padding="0"

View File

@@ -34,6 +34,7 @@ namespace ShortcutGuide.Controls
private List<string> _lastNavItemIds = [];
private ShortcutFile? _shortcutFile;
private string _selectedAppName = string.Empty;
private string _searchQuery = string.Empty;
/// <summary>
/// Raised whenever the user selects a different app in the nav list.
@@ -60,6 +61,8 @@ namespace ShortcutGuide.Controls
public async Task Open()
{
this.ResetSearch();
// Same background work the original MainWindow ran in its
// constructor: wait for the index-generation thread to finish
// and then enumerate the apps to populate the nav list.
@@ -94,11 +97,29 @@ namespace ShortcutGuide.Controls
_shortcutFile = null;
_currentApplicationIds.Clear();
this.ResetSearch();
_getAppIdsTask?.Dispose();
_getAppIdsTask = null;
}
internal bool TryClearSearch()
{
if (string.IsNullOrWhiteSpace(_searchQuery))
{
return false;
}
this.SearchBox.Text = string.Empty;
this.SearchBox.Focus(FocusState.Programmatic);
return true;
}
internal void FocusSearch()
{
this.SearchBox.Focus(FocusState.Programmatic);
}
internal string SelectedAppName => _selectedAppName;
private void OnUnloaded(object sender, RoutedEventArgs e)
@@ -247,7 +268,7 @@ namespace ShortcutGuide.Controls
// alive by live ComWrappers CCWs), leaking ~one page per open.
ShortcutsPage page = this.ContentFrame.Content as ShortcutsPage
?? this.NavigateToShortcutsPage();
page.SetShortcuts(file, this._selectedAppName);
page.SetShortcuts(file, this._selectedAppName, _searchQuery);
}
SelectedAppTaskbarVisibilityChanged?.Invoke(this, exposesTaskbarSection);
@@ -268,5 +289,29 @@ namespace ShortcutGuide.Controls
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
_searchQuery = sender.Text;
if (this.ContentFrame.Content is ShortcutsPage currentPage)
{
currentPage.SetSearchQuery(_searchQuery);
}
}
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
this.SearchBox.Focus(FocusState.Programmatic);
args.Handled = true;
}
private void ResetSearch()
{
_searchQuery = string.Empty;
if (!string.IsNullOrEmpty(this.SearchBox.Text))
{
this.SearchBox.Text = string.Empty;
}
}
}
}

View File

@@ -146,12 +146,11 @@ namespace ShortcutGuide
this.Activated += OnActivated;
// Esc closes the overlay regardless of which pseudo-window has
// keyboard focus (handled at the Window.Content root because the
// event bubbles up from whichever inner element has focus).
// Handle Esc before focused controls such as AutoSuggestBox can
// consume it, so search is cleared before the overlay closes.
if (this.Content is UIElement contentRoot)
{
contentRoot.KeyUp += OnContentKeyUp;
contentRoot.PreviewKeyDown += OnContentPreviewKeyDown;
}
ApplyThemeFromSettings();
@@ -223,12 +222,25 @@ namespace ShortcutGuide
}
}
private void OnContentKeyUp(object sender, KeyRoutedEventArgs e)
private void OnContentPreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Escape)
{
if (e.KeyStatus.WasKeyDown)
{
e.Handled = true;
return;
}
if (this.MainPane.TryClearSearch())
{
e.Handled = true;
return;
}
_closeType = "Escape";
CloseAnimated();
e.Handled = true;
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="ShortcutGuide.Pages.ShortcutsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@@ -42,12 +42,24 @@
ShortcutTemplate="{StaticResource ShortcutTemplate}"
SubtitleTemplate="{StaticResource SubtitleTemplate}" />
</Page.Resources>
<ScrollViewer>
<ItemsRepeater
x:Name="MainItemsRepeater"
Margin="0,0,0,24"
ElementClearing="MainItemsRepeater_ElementClearing"
ItemTemplate="{StaticResource RowTemplateSelector}"
ItemsSource="{x:Bind Rows, Mode=OneWay}" />
</ScrollViewer>
<Grid>
<ScrollViewer>
<ItemsRepeater
x:Name="MainItemsRepeater"
Margin="0,0,0,24"
ElementClearing="MainItemsRepeater_ElementClearing"
ItemTemplate="{StaticResource RowTemplateSelector}"
ItemsSource="{x:Bind Rows, Mode=OneWay}" />
</ScrollViewer>
<TextBlock
x:Name="NoResultsTextBlock"
Margin="16,16,16,8"
VerticalAlignment="Top"
AutomationProperties.AutomationId="ShortcutGuide_NoSearchResults"
AutomationProperties.LiveSetting="Polite"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="Collapsed" />
</Grid>
</Page>

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using ShortcutGuide.Controls;
@@ -22,6 +23,7 @@ namespace ShortcutGuide.Pages
private ShortcutFile? _shortcutFile;
private string _appName = string.Empty;
private string _searchQuery = string.Empty;
private bool _isEventSubscribed;
public ObservableCollection<ShortcutListItem> Rows { get; } = new();
@@ -63,10 +65,22 @@ namespace ShortcutGuide.Pages
/// (and its <c>ItemsRepeater</c>) is reused across opens instead of
/// being recreated, so only the <see cref="Rows"/> collection changes.
/// </summary>
public void SetShortcuts(ShortcutFile file, string appName)
public void SetShortcuts(ShortcutFile file, string appName, string searchQuery)
{
this._appName = appName;
this._shortcutFile = file;
this._searchQuery = searchQuery;
this.RebuildRows();
}
public void SetSearchQuery(string searchQuery)
{
if (string.Equals(this._searchQuery, searchQuery, StringComparison.Ordinal))
{
return;
}
this._searchQuery = searchQuery;
this.RebuildRows();
}
@@ -80,8 +94,10 @@ namespace ShortcutGuide.Pages
{
// Clear the collection to trigger ElementClearing for all items
this.Rows.Clear();
this.UpdateNoResultsState(false);
_shortcutFile = null;
_appName = string.Empty;
_searchQuery = string.Empty;
}
private void UnsubscribeFromEvents()
@@ -99,40 +115,53 @@ namespace ShortcutGuide.Pages
if (this._shortcutFile is not { } file)
{
this.UpdateNoResultsState(false);
return;
}
// 1. Pinned (always shown, with empty-state placeholder).
this.Rows.Add(ShortcutListItem.Header(
ResourceLoaderInstance.ResourceLoader.GetString("PinnedHeaderTxt/Text")));
string normalizedQuery = this._searchQuery.Trim();
bool isSearchActive = normalizedQuery.Length > 0;
bool hasMatches = false;
// 1. Pinned (always shown with an empty-state placeholder when not searching).
var pinned = App.PinnedShortcuts.TryGetValue(this._appName, out var pinnedItems)
? (IReadOnlyList<ShortcutEntry>)pinnedItems
: Array.Empty<ShortcutEntry>();
if (pinned.Count == 0)
var filteredPinned = FilterShortcuts(pinned, normalizedQuery);
if (filteredPinned.Count > 0 || !isSearchActive)
{
this.Rows.Add(ShortcutListItem.Empty(
ResourceLoaderInstance.ResourceLoader.GetString("PinnedEmptyText/Text")));
}
else
{
foreach (var s in pinned)
this.Rows.Add(ShortcutListItem.Header(
ResourceLoaderInstance.ResourceLoader.GetString("PinnedHeaderTxt/Text")));
if (filteredPinned.Count == 0)
{
this.Rows.Add(ShortcutListItem.ForShortcut(s));
this.Rows.Add(ShortcutListItem.Empty(
ResourceLoaderInstance.ResourceLoader.GetString("PinnedEmptyText/Text")));
}
else
{
AddShortcuts(filteredPinned);
}
}
// 2. Recommended (only if non-empty).
// 2. Recommended (only if matching shortcuts are present).
var recommended = file.Shortcuts?
.SelectMany(c => c.Properties ?? Array.Empty<ShortcutEntry>())
.Where(s => s.Recommended)
.ToList() ?? new List<ShortcutEntry>();
if (recommended.Count > 0)
var filteredRecommended = FilterShortcuts(recommended, normalizedQuery);
if (filteredRecommended.Count > 0)
{
this.Rows.Add(ShortcutListItem.Header(
ResourceLoaderInstance.ResourceLoader.GetString("RecommendedHeaderText/Text")));
foreach (var s in recommended)
AddShortcuts(filteredRecommended);
}
void AddShortcuts(IReadOnlyList<ShortcutEntry> shortcuts)
{
foreach (var shortcut in shortcuts)
{
this.Rows.Add(ShortcutListItem.ForShortcut(s));
this.Rows.Add(ShortcutListItem.ForShortcut(shortcut));
hasMatches = true;
}
}
@@ -155,32 +184,69 @@ namespace ShortcutGuide.Pages
continue;
}
var items = category.Properties ?? Array.Empty<ShortcutEntry>();
if (items.Length == 0)
var items = FilterShortcuts(category.Properties ?? Array.Empty<ShortcutEntry>(), normalizedQuery);
if (items.Count == 0)
{
continue;
}
this.Rows.Add(ShortcutListItem.Header(name));
foreach (var s in items)
{
this.Rows.Add(ShortcutListItem.ForShortcut(s));
}
AddShortcuts(items);
}
}
// 4. Taskbar (Windows only).
if (taskbarCategory is { } tb && tb.Properties is { Length: > 0 } taskbarItems)
{
this.Rows.Add(ShortcutListItem.Header(
ResourceLoaderInstance.ResourceLoader.GetString("TaskbarHeaderTxt/Text")));
this.Rows.Add(ShortcutListItem.Subtitle(
ResourceLoaderInstance.ResourceLoader.GetString("TaskbarDescriptionTxt/Text")));
foreach (var s in taskbarItems)
var filteredTaskbarItems = FilterShortcuts(taskbarItems, normalizedQuery);
if (filteredTaskbarItems.Count > 0)
{
this.Rows.Add(ShortcutListItem.ForShortcut(s));
this.Rows.Add(ShortcutListItem.Header(
ResourceLoaderInstance.ResourceLoader.GetString("TaskbarHeaderTxt/Text")));
this.Rows.Add(ShortcutListItem.Subtitle(
ResourceLoaderInstance.ResourceLoader.GetString("TaskbarDescriptionTxt/Text")));
AddShortcuts(filteredTaskbarItems);
}
}
this.UpdateNoResultsState(isSearchActive && !hasMatches);
}
private void UpdateNoResultsState(bool isVisible)
{
if (isVisible)
{
if (this.NoResultsTextBlock.Visibility == Visibility.Visible)
{
return;
}
this.NoResultsTextBlock.Visibility = Visibility.Visible;
this.NoResultsTextBlock.Text = ResourceLoaderInstance.ResourceLoader.GetString("SearchBlank");
var peer = FrameworkElementAutomationPeer.FromElement(this.NoResultsTextBlock)
?? FrameworkElementAutomationPeer.CreatePeerForElement(this.NoResultsTextBlock);
if (peer is not null && AutomationPeer.ListenerExists(AutomationEvents.LiveRegionChanged))
{
peer.RaiseAutomationEvent(AutomationEvents.LiveRegionChanged);
}
}
else
{
this.NoResultsTextBlock.Visibility = Visibility.Collapsed;
this.NoResultsTextBlock.Text = string.Empty;
}
}
private static IReadOnlyList<ShortcutEntry> FilterShortcuts(IReadOnlyList<ShortcutEntry> shortcuts, string query)
{
if (query.Length == 0)
{
return shortcuts;
}
return shortcuts
.Where(shortcut => ShortcutSearchMatcher.Matches(shortcut, query))
.ToArray();
}
private void OnPinnedShortcutsChanged(object? sender, string appName)

View File

@@ -165,6 +165,9 @@
<data name="SearchBox.PlaceholderText" xml:space="preserve">
<value>Search shortcuts</value>
</data>
<data name="SearchBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Search shortcuts</value>
</data>
<data name="SettingsButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Open settings</value>
</data>

View File

@@ -0,0 +1,112 @@
// 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.
#nullable enable
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ShortcutGuide.Helpers;
using ShortcutGuide.Models;
namespace ShortcutGuide.UnitTests.SearchTests;
[TestClass]
public sealed class ShortcutSearchMatcherTests
{
[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void Matches_EmptyQuery_ReturnsTrue(string? query)
{
Assert.IsTrue(ShortcutSearchMatcher.Matches(CreateShortcut(), query));
}
[TestMethod]
[DataRow("TOGGLE")]
[DataRow("virtual desktops")]
public void Matches_NameOrDescription_IsCaseInsensitive(string query)
{
var shortcut = CreateShortcut(name: "Toggle desktop", description: "Manage Virtual Desktops");
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, query));
}
[TestMethod]
[DataRow("tgl dsk")]
[DataRow("mng vrtl")]
public void Matches_NonContiguousFuzzyQuery_ReturnsTrue(string query)
{
var shortcut = CreateShortcut(name: "Toggle desktop", description: "Manage Virtual Desktops");
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, query));
}
[TestMethod]
[DataRow("windows", true, false, false, false)]
[DataRow("control", false, true, false, false)]
[DataRow("alt", false, false, true, false)]
[DataRow("shift", false, false, false, true)]
public void Matches_ModifierSemanticName_ReturnsTrue(string query, bool win, bool ctrl, bool alt, bool shift)
{
var shortcut = CreateShortcut(
shortcutDescriptions: [new ShortcutDescription(ctrl, shift, alt, win, ["K"])]);
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, query));
}
[TestMethod]
public void Matches_DisplayedChord_ReturnsTrue()
{
var shortcut = CreateShortcut(
name: "Open Snipping Tool",
shortcutDescriptions: [new ShortcutDescription(false, true, false, true, ["S"])]);
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, "Win Shift S"));
}
[TestMethod]
[DataRow("Windows K", false, false, false, true)]
[DataRow("Control K", true, false, false, false)]
public void Matches_SemanticModifierInChord_ReturnsTrue(string query, bool ctrl, bool shift, bool alt, bool win)
{
var shortcut = CreateShortcut(
shortcutDescriptions: [new ShortcutDescription(ctrl, shift, alt, win, ["K"])]);
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, query));
}
[TestMethod]
[DataRow("K", "K")]
[DataRow("F1", "112")]
[DataRow("Esc", "<Escape>")]
[DataRow("Num", "<TASKBAR1-9>")]
[DataRow("<", "<LessThan>")]
[DataRow("Left Arrow", "<Left>")]
public void Matches_DisplayedKeyLabel_ReturnsTrue(string query, string key)
{
var shortcut = CreateShortcut(
shortcutDescriptions: [new ShortcutDescription(false, false, false, false, [key])]);
Assert.IsTrue(ShortcutSearchMatcher.Matches(shortcut, query));
}
[TestMethod]
public void Matches_UnrelatedQuery_ReturnsFalse()
{
var shortcut = CreateShortcut(
name: "Open settings",
description: "Configure the app",
shortcutDescriptions: [new ShortcutDescription(true, false, false, true, ["I"])]);
Assert.IsFalse(ShortcutSearchMatcher.Matches(shortcut, "screenshot"));
}
private static ShortcutEntry CreateShortcut(
string name = "Toggle desktop",
string? description = "Manage desktops",
ShortcutDescription[]? shortcutDescriptions = null)
{
return new ShortcutEntry(name, description, false, shortcutDescriptions ?? []);
}
}