mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 18:57:19 +02:00
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>
This commit is contained in:
@@ -108,7 +108,7 @@
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for context item separators -->
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorContextItemViewModel">
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
|
||||
@@ -270,7 +270,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
private bool IsSeparator(object item)
|
||||
{
|
||||
return item is SeparatorContextItemViewModel;
|
||||
return item is SeparatorViewModel;
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.FiltersDropDown"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
<cmdpalUI:FilterTemplateSelector
|
||||
x:Key="FilterTemplateSelector"
|
||||
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="coreViewModels:FilterItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="16"
|
||||
Margin="4,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for separators -->
|
||||
<DataTemplate x:Key="SeparatorViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ComboBox
|
||||
Name="FiltersComboBox"
|
||||
x:Uid="FiltersComboBox"
|
||||
VerticalAlignment="Center"
|
||||
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.Filters, Mode=OneWay}"
|
||||
PlaceholderText="Filters"
|
||||
PreviewKeyDown="FiltersComboBox_PreviewKeyDown"
|
||||
SelectionChanged="FiltersComboBox_SelectionChanged"
|
||||
Style="{StaticResource ComboBoxStyle}"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ComboBox.ItemContainerTransitions>
|
||||
</ComboBox>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class FiltersDropDown : UserControl,
|
||||
ICurrentPageAware
|
||||
{
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
|
||||
set => SetValue(CurrentPageViewModelProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CurrentPageViewModelProperty =
|
||||
DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(FiltersDropDown), new PropertyMetadata(null, OnCurrentPageViewModelChanged));
|
||||
|
||||
private static void OnCurrentPageViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var @this = (FiltersDropDown)d;
|
||||
|
||||
if (@this != null
|
||||
&& e.OldValue is PageViewModel old)
|
||||
{
|
||||
old.PropertyChanged -= @this.Page_PropertyChanged;
|
||||
}
|
||||
|
||||
// If this new page does not implement ListViewModel or if
|
||||
// it doesn't contain Filters, we need to clear any filters
|
||||
// that may have been set.
|
||||
if (@this != null)
|
||||
{
|
||||
if (e.NewValue is ListViewModel listViewModel)
|
||||
{
|
||||
@this.ViewModel = listViewModel.Filters;
|
||||
}
|
||||
else
|
||||
{
|
||||
@this.ViewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (@this != null
|
||||
&& e.NewValue is PageViewModel page)
|
||||
{
|
||||
page.PropertyChanged += @this.Page_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public FiltersViewModel? ViewModel
|
||||
{
|
||||
get => (FiltersViewModel?)GetValue(ViewModelProperty);
|
||||
set => SetValue(ViewModelProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(FiltersViewModel), typeof(FiltersDropDown), new PropertyMetadata(null));
|
||||
|
||||
public FiltersDropDown()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
// Used to handle the case when a ListPage's `Filters` may have changed
|
||||
private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var property = e.PropertyName;
|
||||
|
||||
if (CurrentPageViewModel is ListViewModel list)
|
||||
{
|
||||
if (property == nameof(ListViewModel.Filters))
|
||||
{
|
||||
ViewModel = list.Filters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FiltersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (CurrentPageViewModel is ListViewModel listViewModel &&
|
||||
FiltersComboBox.SelectedItem is FilterItemViewModel filterItem)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateUp()
|
||||
{
|
||||
var newIndex = FiltersComboBox.SelectedIndex;
|
||||
|
||||
if (FiltersComboBox.SelectedIndex > 0)
|
||||
{
|
||||
newIndex--;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
if (newIndex < 0)
|
||||
{
|
||||
newIndex = FiltersComboBox.Items.Count - 1;
|
||||
|
||||
while (
|
||||
newIndex >= 0 &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
{
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = FiltersComboBox.Items.Count - 1;
|
||||
}
|
||||
|
||||
FiltersComboBox.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
private void NavigateDown()
|
||||
{
|
||||
var newIndex = FiltersComboBox.SelectedIndex;
|
||||
|
||||
if (FiltersComboBox.SelectedIndex == FiltersComboBox.Items.Count - 1)
|
||||
{
|
||||
newIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex++;
|
||||
|
||||
while (
|
||||
newIndex < FiltersComboBox.Items.Count &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
if (newIndex >= FiltersComboBox.Items.Count)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
while (
|
||||
newIndex < FiltersComboBox.Items.Count &&
|
||||
IsSeparator(FiltersComboBox.Items[newIndex]) &&
|
||||
newIndex != FiltersComboBox.SelectedIndex)
|
||||
{
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FiltersComboBox.SelectedIndex = newIndex;
|
||||
}
|
||||
|
||||
private bool IsSeparator(object item)
|
||||
{
|
||||
return item is SeparatorViewModel;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
{
|
||||
// TODO: In some cases we probably want commands to clear a filter
|
||||
// somewhere in the process, so we need to figure out when that is.
|
||||
@this.FilterBox.Text = page.Filter;
|
||||
@this.FilterBox.Text = page.SearchTextBox;
|
||||
@this.FilterBox.Select(@this.FilterBox.Text.Length, 0);
|
||||
|
||||
page.PropertyChanged += @this.Page_PropertyChanged;
|
||||
@@ -87,7 +87,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = string.Empty;
|
||||
CurrentPageViewModel.SearchTextBox = string.Empty;
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -145,7 +145,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
// Actually plumb Filtering to the view model
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user