mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
CmdPal: Replace FiltersDropDown ComboBox with searchable dropdown (#45747)
## Summary of the Pull Request Replaces the ComboBox-based filter control with a DropDownButton and Flyout containing a searchable TextBox and ListView. - Add type-to-search: typing while button is focused opens the flyout and filters items by name - Designed to match appearance of the context menu - Add keyboard navigation: `Up`/`Down `moves selection from search box, `Enter` confirms, `Escape` clears search text (or closes if empty), `F4` opens the dropdown - Add `Alt+F` shortcut on ShellPage to toggle filter focus - Style flyout to match ContextMenu (item padding, separators, search box appearance) - Show "No results" empty state when search matches nothing - After confirming selection, return focus to the main search box - Add accessibility - Update `FilterTemplateSelector` to support both ComboBoxItem and ListViewItem containers - Guard against infinite loop in navigation when only separators exist ## Pictures? Moving! https://github.com/user-attachments/assets/60e232ae-8cee-4759-a9a7-d7edbf78719e <img width="315" height="212" alt="image" src="https://github.com/user-attachments/assets/b6e1a895-064c-47e1-9184-26dbb46fdf05" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #41648 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **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
This commit is contained in:
@@ -5,16 +5,15 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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;
|
||||
|
||||
@@ -101,13 +100,9 @@ public sealed partial class ContextMenu : UserControl,
|
||||
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 mods = KeyModifiers.GetCurrent();
|
||||
|
||||
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
var result = ViewModel?.CheckKeybinding(mods.Ctrl, mods.Alt, mods.Shift, mods.Win, e.Key);
|
||||
|
||||
if (result == ContextKeybindingResult.Hide)
|
||||
{
|
||||
@@ -165,11 +160,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
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);
|
||||
var modifiers = KeyModifiers.GetCurrent();
|
||||
|
||||
if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
@@ -186,7 +177,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape ||
|
||||
(e.Key == VirtualKey.Left && altPressed))
|
||||
(e.Key == VirtualKey.Left && modifiers.Alt))
|
||||
{
|
||||
if (ViewModel.CanPopContextStack())
|
||||
{
|
||||
|
||||
@@ -22,20 +22,9 @@
|
||||
Default="{StaticResource FilterItemViewModelTemplate}"
|
||||
Separator="{StaticResource SeparatorViewModelTemplate}" />
|
||||
|
||||
<Style
|
||||
x:Name="ComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Style.Setters>
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Setter Property="Margin" Value="0,0,12,0" />
|
||||
<Setter Property="Padding" Value="16,4" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Template for the filter items -->
|
||||
<DataTemplate x:Key="FilterItemViewModelTemplate" x:DataType="viewModels:FilterItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
|
||||
<Grid Padding="12,8,12,8" AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -58,34 +47,100 @@
|
||||
<DataTemplate x:Key="SeparatorViewModelTemplate" x:DataType="viewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ComboBox
|
||||
Name="FiltersComboBox"
|
||||
x:Uid="FiltersComboBox"
|
||||
<DropDownButton
|
||||
x:Name="FilterDropDownButton"
|
||||
x:Uid="FiltersDropDown"
|
||||
MinWidth="200"
|
||||
Margin="0,0,12,0"
|
||||
Padding="16,5,8,5"
|
||||
VerticalAlignment="Center"
|
||||
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.Filters, Mode=OneWay}"
|
||||
PlaceholderText="Filters"
|
||||
PreviewKeyDown="FiltersComboBox_PreviewKeyDown"
|
||||
SelectedValue="{x:Bind ViewModel.CurrentFilter, Mode=OneWay}"
|
||||
SelectionChanged="FiltersComboBox_SelectionChanged"
|
||||
Style="{StaticResource ComboBoxStyle}"
|
||||
HorizontalContentAlignment="Left"
|
||||
PreviewKeyDown="FilterDropDownButton_PreviewKeyDown"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Collapsed}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
<DropDownButton.Content>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cpcontrols:IconBox
|
||||
x:Name="SelectedFilterIcon"
|
||||
Width="16"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock x:Name="SelectedFilterText" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DropDownButton.Content>
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout
|
||||
x:Name="FilterFlyout"
|
||||
Closed="FilterFlyout_Closed"
|
||||
Opened="FilterFlyout_Opened"
|
||||
Placement="BottomEdgeAlignedRight">
|
||||
<Flyout.FlyoutPresenterStyle>
|
||||
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.ItemContainerTransitions>
|
||||
</Flyout.FlyoutPresenterStyle>
|
||||
<Grid MinWidth="200">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox
|
||||
x:Name="FilterSearchBox"
|
||||
x:Uid="FilterSearchBox"
|
||||
Margin="0"
|
||||
Padding="10,7,6,8"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
|
||||
BorderThickness="0,0,0,2"
|
||||
CornerRadius="8,8,0,0"
|
||||
PreviewKeyDown="FilterSearchBox_PreviewKeyDown"
|
||||
Style="{StaticResource SearchTextBoxStyle}"
|
||||
TextChanged="FilterSearchBox_TextChanged" />
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
BorderBrush="{ThemeResource MenuFlyoutSeparatorBackground}"
|
||||
BorderThickness="0,0,0,1" />
|
||||
<ListView
|
||||
x:Name="FilterListView"
|
||||
x:Uid="FilterListView"
|
||||
Grid.Row="2"
|
||||
MaxHeight="300"
|
||||
Margin="0,4,0,4"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="FilterListView_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ComboBox.ItemContainerTransitions>
|
||||
</ComboBox>
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
<TextBlock
|
||||
x:Name="NoResultsText"
|
||||
x:Uid="FiltersDropDown_NoResults"
|
||||
Grid.Row="2"
|
||||
Margin="0,16"
|
||||
HorizontalAlignment="Center"
|
||||
AutomationProperties.LiveSetting="Polite"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</UserControl>
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
@@ -14,6 +20,11 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
public sealed partial class FiltersDropDown : UserControl,
|
||||
ICurrentPageAware
|
||||
{
|
||||
private bool _isDropDownOpen;
|
||||
private string? _pendingSearchText;
|
||||
private IFilterItemViewModel[] _allItems = [];
|
||||
private FilterItemViewModel? _lastSelectedFilter;
|
||||
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
||||
@@ -62,11 +73,146 @@ public sealed partial class FiltersDropDown : UserControl,
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(FiltersViewModel), typeof(FiltersDropDown), new PropertyMetadata(null));
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(FiltersViewModel), typeof(FiltersDropDown), new PropertyMetadata(null, OnViewModelChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dropdown is currently open or the button has keyboard focus.
|
||||
/// </summary>
|
||||
public bool IsActive => _isDropDownOpen ||
|
||||
FilterDropDownButton.FocusState != FocusState.Unfocused;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the filter control is visible (has filters to show).
|
||||
/// </summary>
|
||||
public bool IsFilterVisible => ViewModel?.ShouldShowFilters ?? false;
|
||||
|
||||
private static readonly string _defaultFilterText = ResourceLoaderInstance.GetString("FiltersDropDown_DefaultText");
|
||||
|
||||
public FiltersDropDown()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
SelectedFilterText.Text = _defaultFilterText;
|
||||
FilterDropDownButton.AddHandler(
|
||||
CharacterReceivedEvent,
|
||||
new TypedEventHandler<UIElement, CharacterReceivedRoutedEventArgs>(FilterDropDownButton_CharacterReceived),
|
||||
true);
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not FiltersDropDown @this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.OldValue is FiltersViewModel oldVm)
|
||||
{
|
||||
oldVm.PropertyChanged -= @this.ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
if (e.NewValue is FiltersViewModel newVm)
|
||||
{
|
||||
newVm.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
@this.OnFiltersChanged();
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(FiltersViewModel.Filters)
|
||||
or nameof(FiltersViewModel.CurrentFilter)
|
||||
or nameof(FiltersViewModel.ShouldShowFilters))
|
||||
{
|
||||
OnFiltersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFiltersChanged()
|
||||
{
|
||||
_allItems = ViewModel?.Filters ?? [];
|
||||
UpdateFilteredList();
|
||||
UpdateSelectedFilterDisplay();
|
||||
}
|
||||
|
||||
private void UpdateSelectedFilterDisplay()
|
||||
{
|
||||
if (ViewModel?.CurrentFilter is FilterItemViewModel filter)
|
||||
{
|
||||
SelectedFilterText.Text = filter.Name;
|
||||
SelectedFilterIcon.SourceKey = filter.Icon;
|
||||
SelectedFilterIcon.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedFilterText.Text = _defaultFilterText;
|
||||
SelectedFilterIcon.SourceKey = null;
|
||||
SelectedFilterIcon.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFilteredList()
|
||||
{
|
||||
if (FilterListView == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var searchText = FilterSearchBox?.Text?.Trim() ?? string.Empty;
|
||||
|
||||
IFilterItemViewModel[] filtered;
|
||||
if (string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
filtered = _allItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
var list = new List<IFilterItemViewModel>();
|
||||
foreach (var item in _allItems)
|
||||
{
|
||||
if (item is FilterItemViewModel filterItem &&
|
||||
filterItem.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) > -1)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
filtered = list.ToArray();
|
||||
}
|
||||
|
||||
FilterListView.ItemsSource = filtered;
|
||||
|
||||
var hasResults = filtered.Length > 0;
|
||||
FilterListView.Visibility = hasResults ? Visibility.Visible : Visibility.Collapsed;
|
||||
NoResultsText.Visibility = hasResults ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
// Restore selection to current filter if present
|
||||
if (_lastSelectedFilter != null && Array.IndexOf(filtered, _lastSelectedFilter) >= 0)
|
||||
{
|
||||
FilterListView.SelectedItem = _lastSelectedFilter;
|
||||
}
|
||||
else if (ViewModel?.CurrentFilter != null && Array.IndexOf(filtered, ViewModel.CurrentFilter) >= 0)
|
||||
{
|
||||
FilterListView.SelectedItem = ViewModel.CurrentFilter;
|
||||
}
|
||||
else if (hasResults)
|
||||
{
|
||||
// Select the first non-separator item
|
||||
IFilterItemViewModel? first = null;
|
||||
foreach (var item in filtered)
|
||||
{
|
||||
if (item is not SeparatorViewModel)
|
||||
{
|
||||
first = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first != null)
|
||||
{
|
||||
FilterListView.SelectedItem = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used to handle the case when a ListPage's `Filters` may have changed
|
||||
@@ -83,55 +229,239 @@ public sealed partial class FiltersDropDown : UserControl,
|
||||
}
|
||||
}
|
||||
|
||||
private void FiltersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
private void FilterDropDownButton_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
|
||||
{
|
||||
if (CurrentPageViewModel is ListViewModel listViewModel &&
|
||||
FiltersComboBox.SelectedItem is FilterItemViewModel filterItem)
|
||||
// Redirect printable (non-space) characters to open flyout and type into search
|
||||
if (!char.IsControl(args.Character) && args.Character != ' ')
|
||||
{
|
||||
OpenFlyoutAndType(args.Character.ToString());
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterDropDownButton_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var modifiers = KeyModifiers.GetCurrent();
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Down when modifiers.OnlyAlt:
|
||||
goto case VirtualKey.F4;
|
||||
|
||||
case VirtualKey.Down or VirtualKey.Up:
|
||||
{
|
||||
if (!_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.ShowAt(FilterDropDownButton);
|
||||
}
|
||||
|
||||
if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
NavigateDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigateUp();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case VirtualKey.F4:
|
||||
{
|
||||
if (!_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.ShowAt(FilterDropDownButton);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterSearchBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var modifiers = KeyModifiers.GetCurrent();
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Down:
|
||||
NavigateDown();
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Up:
|
||||
NavigateUp();
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Enter:
|
||||
SelectCurrentAndClose();
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Escape:
|
||||
if (!string.IsNullOrEmpty(FilterSearchBox.Text))
|
||||
{
|
||||
FilterSearchBox.Text = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseDropDownAndFocusSearch();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.F when modifiers.Alt:
|
||||
CloseDropDownAndFocusSearch();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterSearchBox_TextChanged(object sender, TextChangedEventArgs e) =>
|
||||
UpdateFilteredList();
|
||||
|
||||
private void FilterListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is FilterItemViewModel filterItem)
|
||||
{
|
||||
SelectFilter(filterItem);
|
||||
CloseDropDownAndFocusSearch();
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterFlyout_Opened(object sender, object e)
|
||||
{
|
||||
_isDropDownOpen = true;
|
||||
|
||||
FilterSearchBox.Text = _pendingSearchText ?? string.Empty;
|
||||
FilterSearchBox.SelectionStart = FilterSearchBox.Text.Length;
|
||||
_pendingSearchText = null;
|
||||
|
||||
UpdateFilteredList();
|
||||
FilterSearchBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void FilterFlyout_Closed(object sender, object e)
|
||||
{
|
||||
_isDropDownOpen = false;
|
||||
_pendingSearchText = null;
|
||||
FilterSearchBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void OpenFlyoutAndType(string text)
|
||||
{
|
||||
_pendingSearchText = (_pendingSearchText ?? string.Empty) + text;
|
||||
if (!_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.ShowAt(FilterDropDownButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
FilterSearchBox.Text = _pendingSearchText;
|
||||
FilterSearchBox.SelectionStart = FilterSearchBox.Text.Length;
|
||||
FilterSearchBox.Focus(FocusState.Programmatic);
|
||||
_pendingSearchText = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the filter dropdown flyout.
|
||||
/// </summary>
|
||||
public void OpenDropDown()
|
||||
{
|
||||
if (!_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.ShowAt(FilterDropDownButton);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the filter dropdown flyout and returns focus to the main search box.
|
||||
/// </summary>
|
||||
public void CloseDropDownAndFocusSearch()
|
||||
{
|
||||
if (_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.Hide();
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the filter dropdown flyout.
|
||||
/// </summary>
|
||||
public void CloseDropDown()
|
||||
{
|
||||
if (_isDropDownOpen)
|
||||
{
|
||||
FilterFlyout.Hide();
|
||||
}
|
||||
|
||||
FilterDropDownButton.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves focus to this control (the dropdown button).
|
||||
/// </summary>
|
||||
public void FocusControl()
|
||||
{
|
||||
FilterDropDownButton.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void SelectCurrentAndClose()
|
||||
{
|
||||
if (FilterListView.SelectedItem is FilterItemViewModel filterItem)
|
||||
{
|
||||
SelectFilter(filterItem);
|
||||
}
|
||||
|
||||
CloseDropDownAndFocusSearch();
|
||||
}
|
||||
|
||||
private void SelectFilter(FilterItemViewModel filterItem)
|
||||
{
|
||||
_lastSelectedFilter = filterItem;
|
||||
|
||||
if (CurrentPageViewModel is ListViewModel listViewModel)
|
||||
{
|
||||
listViewModel.UpdateCurrentFilter(filterItem.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void FiltersComboBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Up)
|
||||
{
|
||||
NavigateUp();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
NavigateDown();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
// Update display immediately (UpdateCurrentFilter is async)
|
||||
SelectedFilterText.Text = filterItem.Name;
|
||||
SelectedFilterIcon.SourceKey = filterItem.Icon;
|
||||
SelectedFilterIcon.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void NavigateUp()
|
||||
{
|
||||
var newIndex = FiltersComboBox.SelectedIndex;
|
||||
if (FilterListView.ItemsSource is not IFilterItemViewModel[] items || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FiltersComboBox.SelectedIndex > 0)
|
||||
if (!HasSelectableItem(items))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newIndex = FilterListView.SelectedIndex;
|
||||
|
||||
if (newIndex > 0)
|
||||
{
|
||||
newIndex--;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
while (newIndex >= 0 && IsSeparator(items[newIndex]))
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
if (newIndex < 0)
|
||||
{
|
||||
newIndex = FiltersComboBox.Items.Count - 1;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
newIndex = items.Length - 1;
|
||||
while (newIndex >= 0 && IsSeparator(items[newIndex]))
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
@@ -139,17 +469,35 @@ public sealed partial class FiltersDropDown : UserControl,
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = FiltersComboBox.Items.Count - 1;
|
||||
newIndex = items.Length - 1;
|
||||
while (newIndex >= 0 && IsSeparator(items[newIndex]))
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
FiltersComboBox.SelectedIndex = newIndex;
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
FilterListView.SelectedIndex = newIndex;
|
||||
FilterListView.ScrollIntoView(FilterListView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateDown()
|
||||
{
|
||||
var newIndex = FiltersComboBox.SelectedIndex;
|
||||
if (FilterListView.ItemsSource is not IFilterItemViewModel[] items || items.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FiltersComboBox.SelectedIndex == FiltersComboBox.Items.Count - 1)
|
||||
if (!HasSelectableItem(items))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newIndex = FilterListView.SelectedIndex;
|
||||
|
||||
if (newIndex >= items.Length - 1)
|
||||
{
|
||||
newIndex = 0;
|
||||
}
|
||||
@@ -157,33 +505,40 @@ public sealed partial class FiltersDropDown : UserControl,
|
||||
{
|
||||
newIndex++;
|
||||
|
||||
while (
|
||||
newIndex < FiltersComboBox.Items.Count &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
while (newIndex < items.Length && IsSeparator(items[newIndex]))
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= FiltersComboBox.Items.Count)
|
||||
if (newIndex >= items.Length)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
while (
|
||||
newIndex < FiltersComboBox.Items.Count &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
while (newIndex < items.Length && IsSeparator(items[newIndex]))
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FiltersComboBox.SelectedIndex = newIndex;
|
||||
if (newIndex < items.Length)
|
||||
{
|
||||
FilterListView.SelectedIndex = newIndex;
|
||||
FilterListView.ScrollIntoView(FilterListView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSeparator(object item)
|
||||
private static bool IsSeparator(object item) => item is SeparatorViewModel;
|
||||
|
||||
private static bool HasSelectableItem(IFilterItemViewModel[] items)
|
||||
{
|
||||
return item is SeparatorViewModel;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!IsSeparator(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using CoreVirtualKeyStates = Windows.UI.Core.CoreVirtualKeyStates;
|
||||
using VirtualKey = Windows.System.VirtualKey;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
@@ -125,8 +124,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
return;
|
||||
}
|
||||
|
||||
var ctrlPressed = (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control) & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down;
|
||||
if (ctrlPressed && e.Key == VirtualKey.I)
|
||||
if (KeyModifiers.GetCurrent().Ctrl && e.Key == VirtualKey.I)
|
||||
{
|
||||
// Today you learned that Ctrl+I in a TextBox will insert a tab
|
||||
// We don't want that, so we'll suppress it, this way it can be used for other purposes
|
||||
|
||||
@@ -16,21 +16,38 @@ internal sealed partial class FilterTemplateSelector : DataTemplateSelector
|
||||
public DataTemplate? Separator { get; set; }
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "Microsoft.UI.Xaml.Controls.ComboBoxItem", "Microsoft.WinUI")]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "Microsoft.UI.Xaml.Controls.ListViewItem", "Microsoft.WinUI")]
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
DataTemplate? dataTemplate = Default;
|
||||
|
||||
if (dependencyObject is ComboBoxItem comboBoxItem)
|
||||
{
|
||||
comboBoxItem.IsEnabled = true;
|
||||
var isSeparator = item is SeparatorViewModel;
|
||||
|
||||
if (item is SeparatorViewModel)
|
||||
switch (dependencyObject)
|
||||
{
|
||||
case ComboBoxItem comboBoxItem:
|
||||
comboBoxItem.IsEnabled = !isSeparator;
|
||||
if (isSeparator)
|
||||
{
|
||||
comboBoxItem.IsEnabled = false;
|
||||
comboBoxItem.AllowFocusWhenDisabled = false;
|
||||
comboBoxItem.AllowFocusOnInteraction = false;
|
||||
dataTemplate = Separator;
|
||||
}
|
||||
|
||||
break;
|
||||
case ListViewItem listViewItem:
|
||||
listViewItem.IsEnabled = !isSeparator;
|
||||
if (isSeparator)
|
||||
{
|
||||
listViewItem.MinHeight = 0;
|
||||
listViewItem.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (isSeparator)
|
||||
{
|
||||
dataTemplate = Separator;
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.UI.Input;
|
||||
using Windows.System;
|
||||
using Windows.UI.Core;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the current keyboard modifier state (Ctrl, Alt, Shift, Win).
|
||||
/// </summary>
|
||||
internal readonly struct KeyModifiers
|
||||
{
|
||||
public bool Ctrl { get; }
|
||||
|
||||
public bool Alt { get; }
|
||||
|
||||
public bool Shift { get; }
|
||||
|
||||
public bool Win { get; }
|
||||
|
||||
private KeyModifiers(bool ctrl, bool alt, bool shift, bool win)
|
||||
{
|
||||
Ctrl = ctrl;
|
||||
Alt = alt;
|
||||
Shift = shift;
|
||||
Win = win;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a snapshot of the modifier keys currently held down.
|
||||
/// </summary>
|
||||
public static KeyModifiers GetCurrent()
|
||||
{
|
||||
var ctrl = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var alt = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var shift = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var win = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
return new KeyModifiers(ctrl, alt, shift, win);
|
||||
}
|
||||
|
||||
public bool OnlyAlt => Alt && !Ctrl && !Shift && !Win;
|
||||
|
||||
public bool OnlyCtrl => Ctrl && !Alt && !Shift && !Win;
|
||||
|
||||
public bool None => !Ctrl && !Alt && !Shift && !Win;
|
||||
}
|
||||
@@ -504,6 +504,23 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleFilterFocus()
|
||||
{
|
||||
if (!FiltersDropDown.IsFilterVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FiltersDropDown.IsActive)
|
||||
{
|
||||
FiltersDropDown.CloseDropDownAndFocusSearch();
|
||||
}
|
||||
else
|
||||
{
|
||||
FiltersDropDown.OpenDropDown();
|
||||
}
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
@@ -688,34 +705,32 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private static void ShellPage_OnPreviewKeyDown(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);
|
||||
var modifiers = KeyModifiers.GetCurrent();
|
||||
|
||||
var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
|
||||
var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed;
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Left when onlyAlt: // Alt+Left arrow
|
||||
case VirtualKey.Left when modifiers.OnlyAlt: // Alt+Left arrow
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Home when onlyAlt: // Alt+Home
|
||||
case VirtualKey.Home when modifiers.OnlyAlt: // Alt+Home
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
|
||||
e.Handled = true;
|
||||
break;
|
||||
case (VirtualKey)188 when onlyCtrl: // Ctrl+,
|
||||
case (VirtualKey)188 when modifiers.OnlyCtrl: // Ctrl+,
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.F when modifiers.OnlyAlt: // Alt+F: toggle filter focus
|
||||
((ShellPage)sender).ToggleFilterFocus();
|
||||
e.Handled = true;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
TryCommandKeybindingMessage msg = new(modifiers.Ctrl, modifiers.Alt, modifiers.Shift, modifiers.Win, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
@@ -725,8 +740,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private static void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
if (ctrlPressed && e.Key == VirtualKey.Enter)
|
||||
var mods = KeyModifiers.GetCurrent();
|
||||
if (mods.Ctrl && e.Key == VirtualKey.Enter)
|
||||
{
|
||||
// ctrl+enter
|
||||
WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
|
||||
@@ -737,7 +752,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (ctrlPressed && e.Key == VirtualKey.K)
|
||||
else if (mods.Ctrl && e.Key == VirtualKey.K)
|
||||
{
|
||||
// ctrl+k
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
||||
|
||||
@@ -16,7 +16,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
using Windows.UI.Core;
|
||||
using WinUIEx;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar;
|
||||
@@ -251,8 +250,7 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
break;
|
||||
|
||||
case VirtualKey.Left:
|
||||
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
if (altPressed)
|
||||
if (KeyModifiers.GetCurrent().Alt)
|
||||
{
|
||||
TryGoBack();
|
||||
}
|
||||
|
||||
@@ -928,4 +928,25 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
<data name="FiltersDropDown.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Filters</value>
|
||||
</data>
|
||||
<data name="FiltersDropDown.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Filters (Alt+F)</value>
|
||||
</data>
|
||||
<data name="FiltersDropDown_DefaultText" xml:space="preserve">
|
||||
<value>Filters</value>
|
||||
</data>
|
||||
<data name="FilterSearchBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Search filters</value>
|
||||
</data>
|
||||
<data name="FilterSearchBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search filters...</value>
|
||||
</data>
|
||||
<data name="FilterListView.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Filter options</value>
|
||||
</data>
|
||||
<data name="FiltersDropDown_NoResults.Text" xml:space="preserve">
|
||||
<value>No results</value>
|
||||
</data>
|
||||
</root>
|
||||
Reference in New Issue
Block a user