From 446bb9f2418446b1b7157c7500f2f93b0000a00a Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 14 Aug 2026 09:32:02 +0200 Subject: [PATCH] [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 Shortcut Guide Windows page filtered by Alt ### Keep the query while switching to the PowerToys page 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 --- doc/devdocs/modules/shortcut_guide.md | 3 + .../Helpers/ShortcutSearchMatcher.cs | 168 ++++++++++++++++++ .../ShortcutGuide.Ui/ShortcutGuide.Ui.csproj | 1 + .../ShortcutGuideXAML/App.xaml.cs | 1 + .../Controls/MainPaneControl.xaml | 23 ++- .../Controls/MainPaneControl.xaml.cs | 47 ++++- .../ShortcutGuideXAML/OverlayWindow.xaml.cs | 22 ++- .../Pages/ShortcutsPage.xaml | 30 +++- .../Pages/ShortcutsPage.xaml.cs | 122 ++++++++++--- .../Strings/en-us/Resources.resw | 3 + .../SearchTests/ShortcutSearchMatcherTests.cs | 112 ++++++++++++ 11 files changed, 488 insertions(+), 44 deletions(-) create mode 100644 src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/ShortcutSearchMatcher.cs create mode 100644 src/modules/ShortcutGuide/ShortcutGuide.UnitTests/SearchTests/ShortcutSearchMatcherTests.cs diff --git a/doc/devdocs/modules/shortcut_guide.md b/doc/devdocs/modules/shortcut_guide.md index a22af52336..8cf9726cfc 100644 --- a/doc/devdocs/modules/shortcut_guide.md +++ b/doc/devdocs/modules/shortcut_guide.md @@ -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 diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/ShortcutSearchMatcher.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/ShortcutSearchMatcher.cs new file mode 100644 index 0000000000..2b39166d82 --- /dev/null +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/ShortcutSearchMatcher.cs @@ -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 GetChordSearchLabels(ShortcutDescription description) + { + var displayedLabels = new List(); + var semanticLabels = new List(); + + 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 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 Arrow", + "Down" or "" => "Down Down Arrow", + "Left" or "" => "Left Left Arrow", + "Right" or "" => "Right Right Arrow", + "Back" or "" => "Back Backspace", + "" => "Num", + "" => "Up Down Arrow", + "" => "Left Right Arrow", + "" => "Left Right Up Down Arrow", + "" => "Enter", + "" => "<", + "" => ">", + "" => "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(); + } + } +} diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj index 79709c7346..13d599a25f 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj @@ -100,6 +100,7 @@ + diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs index c2c0795e7a..5ff74521a7 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs @@ -238,6 +238,7 @@ namespace ShortcutGuide await OverlayWindow.MainPaneControl.Open(); OverlayWindow.UpdateTaskbarPaneLayout(); OverlayWindow.MainPaneControl.Visibility = Visibility.Visible; + OverlayWindow.MainPaneControl.FocusSearch(); } }); } diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/Controls/MainPaneControl.xaml b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/Controls/MainPaneControl.xaml index 76e2fb49e0..3f2287d9e0 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/Controls/MainPaneControl.xaml +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/Controls/MainPaneControl.xaml @@ -26,6 +26,7 @@ + @@ -40,10 +41,30 @@ Grid.Column="1" VerticalAlignment="Center" Style="{ThemeResource CaptionTextBlockStyle}" /> + + + + + + + +