2025-07-09 14:53:47 -05:00
|
|
|
// 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 CommunityToolkit.Mvvm.Messaging;
|
2025-09-02 17:53:53 -07:00
|
|
|
using CommunityToolkit.WinUI;
|
2026-02-23 04:05:09 -08:00
|
|
|
using Microsoft.CmdPal.Common.Text;
|
2025-07-28 18:46:16 -05:00
|
|
|
using Microsoft.CmdPal.UI.Messages;
|
2026-02-23 04:05:09 -08:00
|
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
|
|
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
CmdPal: Add precomputed fuzzy string matching to Command Palette (#44090)
## Summary of the Pull Request
This PR improves fuzzy matching in Command Palette by:
- Precomputing normalized strings to enable faster comparisons
- Reducing memory allocations during matching, effectively down to zero
It also introduces several behavioral improvements:
- Strips diacritics from the normalized search string to improve
matching across languages
- Suppresses the same-case bonus when the query consists entirely of
lowercase characters -- reflecting typical user input patterns
- Allows skipping word separators -- enabling queries like Power Point
to match PowerPoint
This implementation is currently kept internal and is used only on the
home page. For other scenarios, the `FuzzyStringMatcher` from
`Microsoft.CommandPalette.Extensions.Toolkit` is being improved instead.
`PrecomputedFuzzyMatcher` offers up to a 100× performance improvement
over the current `FuzzyStringMatcher`, and approximately 2–5× better
performance compared to the improved version.
The improvement might seem small, but it adds up and becomes quite
noticeable when filtering the entire home page—whether the user starts a
new search or changes the query non-incrementally (e.g., using
backspace).
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #45226
- [x] Closes: #44066
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **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)
- [ ] **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
2026-02-09 20:37:59 +01:00
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2025-07-09 14:53:47 -05:00
|
|
|
using Microsoft.UI.Input;
|
|
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
|
|
|
|
using Microsoft.UI.Xaml.Input;
|
|
|
|
|
using Windows.System;
|
|
|
|
|
using Windows.UI.Core;
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.CmdPal.UI.Controls;
|
|
|
|
|
|
|
|
|
|
public sealed partial class ContextMenu : UserControl,
|
|
|
|
|
IRecipient<OpenContextMenuMessage>,
|
|
|
|
|
IRecipient<UpdateCommandBarMessage>,
|
|
|
|
|
IRecipient<TryCommandKeybindingMessage>
|
|
|
|
|
{
|
CmdPal: Add precomputed fuzzy string matching to Command Palette (#44090)
## Summary of the Pull Request
This PR improves fuzzy matching in Command Palette by:
- Precomputing normalized strings to enable faster comparisons
- Reducing memory allocations during matching, effectively down to zero
It also introduces several behavioral improvements:
- Strips diacritics from the normalized search string to improve
matching across languages
- Suppresses the same-case bonus when the query consists entirely of
lowercase characters -- reflecting typical user input patterns
- Allows skipping word separators -- enabling queries like Power Point
to match PowerPoint
This implementation is currently kept internal and is used only on the
home page. For other scenarios, the `FuzzyStringMatcher` from
`Microsoft.CommandPalette.Extensions.Toolkit` is being improved instead.
`PrecomputedFuzzyMatcher` offers up to a 100× performance improvement
over the current `FuzzyStringMatcher`, and approximately 2–5× better
performance compared to the improved version.
The improvement might seem small, but it adds up and becomes quite
noticeable when filtering the entire home page—whether the user starts a
new search or changes the query non-incrementally (e.g., using
backspace).
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #45226
- [x] Closes: #44066
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **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)
- [ ] **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
2026-02-09 20:37:59 +01:00
|
|
|
public ContextMenuViewModel ViewModel { get; }
|
2025-07-09 14:53:47 -05:00
|
|
|
|
|
|
|
|
public ContextMenu()
|
|
|
|
|
{
|
|
|
|
|
this.InitializeComponent();
|
|
|
|
|
|
CmdPal: Add precomputed fuzzy string matching to Command Palette (#44090)
## Summary of the Pull Request
This PR improves fuzzy matching in Command Palette by:
- Precomputing normalized strings to enable faster comparisons
- Reducing memory allocations during matching, effectively down to zero
It also introduces several behavioral improvements:
- Strips diacritics from the normalized search string to improve
matching across languages
- Suppresses the same-case bonus when the query consists entirely of
lowercase characters -- reflecting typical user input patterns
- Allows skipping word separators -- enabling queries like Power Point
to match PowerPoint
This implementation is currently kept internal and is used only on the
home page. For other scenarios, the `FuzzyStringMatcher` from
`Microsoft.CommandPalette.Extensions.Toolkit` is being improved instead.
`PrecomputedFuzzyMatcher` offers up to a 100× performance improvement
over the current `FuzzyStringMatcher`, and approximately 2–5× better
performance compared to the improved version.
The improvement might seem small, but it adds up and becomes quite
noticeable when filtering the entire home page—whether the user starts a
new search or changes the query non-incrementally (e.g., using
backspace).
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #45226
- [x] Closes: #44066
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **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)
- [ ] **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
2026-02-09 20:37:59 +01:00
|
|
|
ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>());
|
|
|
|
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
|
|
|
|
|
2025-07-09 14:53:47 -05:00
|
|
|
// RegisterAll isn't AOT compatible
|
|
|
|
|
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
|
|
|
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
|
|
|
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Receive(OpenContextMenuMessage message)
|
|
|
|
|
{
|
2025-07-28 18:46:16 -05:00
|
|
|
ViewModel.FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top;
|
|
|
|
|
ViewModel.ResetContextMenu();
|
|
|
|
|
|
2025-07-09 14:53:47 -05:00
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Receive(UpdateCommandBarMessage message)
|
|
|
|
|
{
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Receive(TryCommandKeybindingMessage msg)
|
|
|
|
|
{
|
|
|
|
|
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
|
|
|
|
|
|
|
|
|
|
if (result == ContextKeybindingResult.Hide)
|
|
|
|
|
{
|
|
|
|
|
msg.Handled = true;
|
|
|
|
|
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
else if (result == ContextKeybindingResult.KeepOpen)
|
|
|
|
|
{
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
msg.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
else if (result == ContextKeybindingResult.Unhandled)
|
|
|
|
|
{
|
|
|
|
|
msg.Handled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CommandsDropdown_ItemClick(object sender, ItemClickEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.ClickedItem is CommandContextItemViewModel item)
|
|
|
|
|
{
|
|
|
|
|
if (InvokeCommand(item) == ContextKeybindingResult.Hide)
|
|
|
|
|
{
|
|
|
|
|
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 20:17:27 -05:00
|
|
|
private void CommandsDropdown_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
|
|
|
|
|
|
|
|
|
if (result == ContextKeybindingResult.Hide)
|
|
|
|
|
{
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
else if (result == ContextKeybindingResult.KeepOpen)
|
|
|
|
|
{
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
else if (result == ContextKeybindingResult.Unhandled)
|
|
|
|
|
{
|
|
|
|
|
e.Handled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 17:53:53 -07:00
|
|
|
/// <summary>
|
|
|
|
|
/// Handles Escape to close the context menu and return focus to the "More" button.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Key == VirtualKey.Escape)
|
|
|
|
|
{
|
|
|
|
|
// Close the context menu (if not already handled)
|
|
|
|
|
WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage());
|
|
|
|
|
|
|
|
|
|
// Find the parent CommandBar and set focus to MoreCommandsButton
|
|
|
|
|
var parent = this.FindParent<CommandBar>();
|
|
|
|
|
parent?.FocusMoreCommandsButton();
|
|
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-09 14:53:47 -05:00
|
|
|
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
var prop = e.PropertyName;
|
|
|
|
|
|
|
|
|
|
if (prop == nameof(ContextMenuViewModel.FilteredItems))
|
|
|
|
|
{
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
ViewModel?.SetSearchText(ContextFilterBox.Text);
|
|
|
|
|
|
|
|
|
|
if (CommandsDropdown.SelectedIndex == -1)
|
|
|
|
|
{
|
|
|
|
|
CommandsDropdown.SelectedIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
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 (e.Key == VirtualKey.Enter)
|
|
|
|
|
{
|
|
|
|
|
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
|
|
|
|
|
{
|
|
|
|
|
if (InvokeCommand(item) == ContextKeybindingResult.Hide)
|
|
|
|
|
{
|
|
|
|
|
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (e.Key == VirtualKey.Escape ||
|
|
|
|
|
(e.Key == VirtualKey.Left && altPressed))
|
|
|
|
|
{
|
|
|
|
|
if (ViewModel.CanPopContextStack())
|
|
|
|
|
{
|
|
|
|
|
ViewModel.PopContextStack();
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>();
|
|
|
|
|
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
|
|
|
UpdateUiForStackChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Key == VirtualKey.Up)
|
|
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
NavigateUp();
|
|
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
else if (e.Key == VirtualKey.Down)
|
|
|
|
|
{
|
|
|
|
|
NavigateDown();
|
|
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
2025-07-28 20:17:27 -05:00
|
|
|
|
|
|
|
|
CommandsDropdown_PreviewKeyDown(sender, e);
|
2025-07-16 06:25:24 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void NavigateUp()
|
|
|
|
|
{
|
|
|
|
|
var newIndex = CommandsDropdown.SelectedIndex;
|
|
|
|
|
|
|
|
|
|
if (CommandsDropdown.SelectedIndex > 0)
|
|
|
|
|
{
|
|
|
|
|
newIndex--;
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
newIndex >= 0 &&
|
|
|
|
|
IsSeparator(CommandsDropdown.Items[newIndex]) &&
|
|
|
|
|
newIndex != CommandsDropdown.SelectedIndex)
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
newIndex--;
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
2025-07-16 06:25:24 -05:00
|
|
|
|
|
|
|
|
if (newIndex < 0)
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
newIndex = CommandsDropdown.Items.Count - 1;
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
newIndex >= 0 &&
|
|
|
|
|
IsSeparator(CommandsDropdown.Items[newIndex]) &&
|
|
|
|
|
newIndex != CommandsDropdown.SelectedIndex)
|
|
|
|
|
{
|
|
|
|
|
newIndex--;
|
|
|
|
|
}
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
2025-07-16 06:25:24 -05:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
newIndex = CommandsDropdown.Items.Count - 1;
|
|
|
|
|
}
|
2025-07-09 14:53:47 -05:00
|
|
|
|
2025-07-16 06:25:24 -05:00
|
|
|
CommandsDropdown.SelectedIndex = newIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void NavigateDown()
|
|
|
|
|
{
|
|
|
|
|
var newIndex = CommandsDropdown.SelectedIndex;
|
|
|
|
|
|
|
|
|
|
if (CommandsDropdown.SelectedIndex == CommandsDropdown.Items.Count - 1)
|
|
|
|
|
{
|
|
|
|
|
newIndex = 0;
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
2025-07-16 06:25:24 -05:00
|
|
|
else
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
newIndex++;
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
newIndex < CommandsDropdown.Items.Count &&
|
|
|
|
|
IsSeparator(CommandsDropdown.Items[newIndex]) &&
|
|
|
|
|
newIndex != CommandsDropdown.SelectedIndex)
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
newIndex++;
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
2025-07-16 06:25:24 -05:00
|
|
|
|
|
|
|
|
if (newIndex >= CommandsDropdown.Items.Count)
|
2025-07-09 14:53:47 -05:00
|
|
|
{
|
2025-07-16 06:25:24 -05:00
|
|
|
newIndex = 0;
|
2025-07-09 14:53:47 -05:00
|
|
|
|
2025-07-16 06:25:24 -05:00
|
|
|
while (
|
|
|
|
|
newIndex < CommandsDropdown.Items.Count &&
|
|
|
|
|
IsSeparator(CommandsDropdown.Items[newIndex]) &&
|
|
|
|
|
newIndex != CommandsDropdown.SelectedIndex)
|
|
|
|
|
{
|
|
|
|
|
newIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
2025-07-16 06:25:24 -05:00
|
|
|
|
|
|
|
|
CommandsDropdown.SelectedIndex = newIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsSeparator(object item)
|
|
|
|
|
{
|
CmdPal: Filters for DynamicListPage? Yes, please. (#40783)
Closes: #40382
## To-do list
- [x] Add support for "single-select" filters to DynamicListPage
- [x] Filters can contain icons
- [x] Filter list can contain separators
- [x] Update Windows Services built-in extension to support filtering by
all, started, stopped, and pending services
- [x] Update SampleExtension dynamic list sample to filter.
## Example of filters in use
```C#
internal sealed partial class ServicesListPage : DynamicListPage
{
public ServicesListPage()
{
Icon = Icons.ServicesIcon;
Name = "Windows Services";
var filters = new ServiceFilters();
filters.PropChanged += Filters_PropChanged;
Filters = filters;
}
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged();
public override IListItem[] GetItems()
{
// ServiceHelper.Search knows how to filter based on the CurrentFilterIds provided
var items = ServiceHelper.Search(SearchText, Filters.CurrentFilterIds).ToArray();
return items;
}
}
public partial class ServiceFilters : Filters
{
public ServiceFilters()
{
// This would be a default selection. Not providing this will cause the filter
// control to display the "Filter" placeholder text.
CurrentFilterIds = ["all"];
}
public override IFilterItem[] GetFilters()
{
return [
new Filter() { Id = "all", Name = "All Services" },
new Separator(),
new Filter() { Id = "running", Name = "Running", Icon = Icons.GreenCircleIcon },
new Filter() { Id = "stopped", Name = "Stopped", Icon = Icons.RedCircleIcon },
new Filter() { Id = "paused", Name = "Paused", Icon = Icons.PauseIcon },
];
}
}
```
## Current example of behavior
https://github.com/user-attachments/assets/2e325763-ad3a-4445-bbe2-a840df08d0b3
---------
Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-08-21 05:40:09 -05:00
|
|
|
return item is SeparatorViewModel;
|
2025-07-09 14:53:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateUiForStackChange()
|
|
|
|
|
{
|
|
|
|
|
ContextFilterBox.Text = string.Empty;
|
|
|
|
|
ViewModel?.SetSearchText(string.Empty);
|
|
|
|
|
CommandsDropdown.SelectedIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Manually focuses our search box. This needs to be called after we're actually
|
|
|
|
|
/// In the UI tree - if we're in a Flyout, that's not until Opened()
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal void FocusSearchBox()
|
|
|
|
|
{
|
|
|
|
|
ContextFilterBox.Focus(FocusState.Programmatic);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ContextKeybindingResult InvokeCommand(CommandItemViewModel command) => ViewModel.InvokeCommand(command);
|
|
|
|
|
}
|