diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs index 4f589a4e2f..22dad16504 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs @@ -189,7 +189,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } else { - return new SeparatorContextItemViewModel() as IContextItemViewModel; + return new SeparatorViewModel() as IContextItemViewModel; } }) .ToList(); @@ -350,7 +350,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } else { - return new SeparatorContextItemViewModel() as IContextItemViewModel; + return new SeparatorViewModel() as IContextItemViewModel; } }) .ToList(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs index 0c0f7c7c12..9665908474 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs @@ -119,7 +119,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC } else { - return new SeparatorContextItemViewModel(); + return new SeparatorViewModel(); } }) .ToList(); @@ -178,7 +178,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC } else { - return new SeparatorContextItemViewModel(); + return new SeparatorViewModel(); } }) .ToList(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs new file mode 100644 index 0000000000..78fdb26286 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs @@ -0,0 +1,57 @@ +// 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.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.Core.ViewModels; + +public partial class FilterItemViewModel : ExtensionObjectViewModel, IFilterItemViewModel +{ + private ExtensionObject _model; + + public string Id { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public IconInfoViewModel Icon { get; set; } = new(null); + + internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized; + + protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized); + + public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error); + + public FilterItemViewModel(IFilter filter, WeakReference context) + : base(context) + { + _model = new(filter); + } + + public override void InitializeProperties() + { + if (IsInitialized) + { + return; + } + + var filter = _model.Unsafe; + if (filter == null) + { + return; // throw? + } + + Id = filter.Id; + Name = filter.Name; + Icon = new(filter.Icon); + if (Icon is not null) + { + Icon.InitializeProperties(); + } + + UpdateProperty(nameof(Id)); + UpdateProperty(nameof(Name)); + UpdateProperty(nameof(Icon)); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs new file mode 100644 index 0000000000..511581558c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs @@ -0,0 +1,81 @@ +// 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.ComponentModel; +using Microsoft.CmdPal.Core.ViewModels.Models; +using Microsoft.CommandPalette.Extensions; + +namespace Microsoft.CmdPal.Core.ViewModels; + +public partial class FiltersViewModel : ExtensionObjectViewModel +{ + private readonly ExtensionObject _filtersModel = new(null); + + [ObservableProperty] + public partial string CurrentFilterId { get; set; } = string.Empty; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShouldShowFilters))] + public partial IFilterItemViewModel[] Filters { get; set; } = []; + + public bool ShouldShowFilters => Filters.Length > 0; + + public FiltersViewModel(ExtensionObject filters, WeakReference context) + : base(context) + { + _filtersModel = filters; + } + + public override void InitializeProperties() + { + try + { + if (_filtersModel.Unsafe is not null) + { + var filters = _filtersModel.Unsafe.GetFilters(); + Filters = filters.Select(filter => + { + var filterItem = filter as IFilter; + if (filterItem != null) + { + var filterVM = new FilterItemViewModel(filterItem!, PageContext); + filterVM.InitializeProperties(); + + return filterVM; + } + else + { + return new SeparatorViewModel(); + } + }).ToArray(); + + CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId; + + return; + } + } + catch (Exception ex) + { + ShowException(ex, _filtersModel.Unsafe?.GetType().Name); + } + + Filters = []; + CurrentFilterId = string.Empty; + } + + public override void SafeCleanup() + { + base.SafeCleanup(); + + foreach (var filter in Filters) + { + if (filter is FilterItemViewModel filterVM) + { + filterVM.SafeCleanup(); + } + } + + Filters = []; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs index cad1af9d4d..a8f65b2634 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs @@ -2,12 +2,7 @@ // 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.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.CmdPal.Core.ViewModels; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs new file mode 100644 index 0000000000..fb324bb42f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CmdPal.Core.ViewModels; + +public interface IFilterItemViewModel +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs index 10fc9445ad..f89b2a5906 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs @@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable [ObservableProperty] public partial ObservableCollection FilteredItems { get; set; } = []; + public FiltersViewModel? Filters { get; set; } + private ObservableCollection Items { get; set; } = []; private readonly ExtensionObject _model; @@ -86,7 +88,7 @@ public partial class ListViewModel : PageViewModel, IDisposable // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems(); - protected override void OnFilterUpdated(string filter) + protected override void OnSearchTextBoxUpdated(string searchTextBox) { //// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view //// and manage filtering below, but we should be smarter about this and understand caching and other requirements... @@ -104,7 +106,7 @@ public partial class ListViewModel : PageViewModel, IDisposable { if (_model.Unsafe is IDynamicListPage dynamic) { - dynamic.SearchText = filter; + dynamic.SearchText = searchTextBox; } } catch (Exception ex) @@ -127,6 +129,26 @@ public partial class ListViewModel : PageViewModel, IDisposable } } + public void UpdateCurrentFilter(string currentFilterId) + { + // We're getting called on the UI thread. + // Hop off to a BG thread to update the extension. + _ = Task.Run(() => + { + try + { + if (_model.Unsafe is IListPage listPage) + { + listPage.Filters?.CurrentFilterId = currentFilterId; + } + } + catch (Exception ex) + { + ShowException(ex, _model?.Unsafe?.Name); + } + }); + } + //// Run on background thread, from InitializeAsync or Model_ItemsChanged private void FetchItems() { @@ -305,7 +327,7 @@ public partial class ListViewModel : PageViewModel, IDisposable /// Apply our current filter text to the list of items, and update /// FilteredItems to match the results. /// - private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter)); + private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox)); /// /// Helper to generate a weighting for a given list item, based on title, @@ -507,6 +529,10 @@ public partial class ListViewModel : PageViewModel, IDisposable EmptyContent = new(new(model.EmptyContent), PageContext); EmptyContent.SlowInitializeProperties(); + Filters = new(new(model.Filters), PageContext); + Filters.InitializeProperties(); + UpdateProperty(nameof(Filters)); + FetchItems(); model.ItemsChanged += Model_ItemsChanged; } @@ -578,6 +604,10 @@ public partial class ListViewModel : PageViewModel, IDisposable EmptyContent = new(new(model.EmptyContent), PageContext); EmptyContent.SlowInitializeProperties(); break; + case nameof(Filters): + Filters = new(new(model.Filters), PageContext); + Filters.InitializeProperties(); + break; case nameof(IsLoading): UpdateEmptyContent(); break; @@ -641,6 +671,8 @@ public partial class ListViewModel : PageViewModel, IDisposable FilteredItems.Clear(); } + Filters?.SafeCleanup(); + var model = _model.Unsafe; if (model is not null) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 046c9fae93..5c445615be 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -32,7 +32,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext // This is set from the SearchBar [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowSuggestion))] - public partial string Filter { get; set; } = string.Empty; + public partial string SearchTextBox { get; set; } = string.Empty; [ObservableProperty] public virtual partial string PlaceholderText { get; private set; } = "Type here to search..."; @@ -41,7 +41,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext [NotifyPropertyChangedFor(nameof(ShowSuggestion))] public virtual partial string TextToSuggest { get; protected set; } = string.Empty; - public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != Filter; + public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != SearchTextBox; [ObservableProperty] public partial AppExtensionHost ExtensionHost { get; private set; } @@ -167,9 +167,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext } } - partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue); + partial void OnSearchTextBoxChanged(string oldValue, string newValue) => OnSearchTextBoxUpdated(newValue); - protected virtual void OnFilterUpdated(string filter) + protected virtual void OnSearchTextBoxUpdated(string searchTextBox) { // The base page has no notion of data, so we do nothing here... // subclasses should override. diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs similarity index 73% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs rename to src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs index 8d896bd341..a1c4696b35 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs @@ -9,6 +9,10 @@ using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.Core.ViewModels; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem +public partial class SeparatorViewModel() : + IContextItemViewModel, + IFilterItemViewModel, + ISeparatorContextItem, + ISeparatorFilterItem { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml index f3c4e5413e..e23fb815b0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml @@ -108,7 +108,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs new file mode 100644 index 0000000000..b51376dfa3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs @@ -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; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs index 7b34594b46..a5f02d76cb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -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; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs index 3ab4dd1c0b..09fa902a4b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs @@ -28,7 +28,7 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector { li.IsEnabled = true; - if (item is SeparatorContextItemViewModel) + if (item is SeparatorViewModel) { li.IsEnabled = false; li.AllowFocusWhenDisabled = false; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs new file mode 100644 index 0000000000..2d12c82b28 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs @@ -0,0 +1,36 @@ +// 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.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.CmdPal.UI; + +internal sealed partial class FilterTemplateSelector : DataTemplateSelector +{ + public DataTemplate? Default { get; set; } + + public DataTemplate? Separator { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject) + { + DataTemplate? dataTemplate = Default; + + if (dependencyObject is ComboBoxItem comboBoxItem) + { + comboBoxItem.IsEnabled = true; + + if (item is SeparatorViewModel) + { + comboBoxItem.IsEnabled = false; + comboBoxItem.AllowFocusWhenDisabled = false; + comboBoxItem.AllowFocusOnInteraction = false; + dataTemplate = Separator; + } + } + + return dataTemplate; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index 8f7c6ac7bd..dbb3818518 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -176,6 +176,7 @@ + @@ -320,6 +321,18 @@ + + + + + + + + + (); newCommands.AddRange(commands); - newCommands.Add(new SeparatorContextItem()); + newCommands.Add(new Separator()); // 0x50 = P // Full key chord would be Ctrl+P diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs index 6e874b1581..feb7aac9c0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs @@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WindowsServices.Helpers; public static class ServiceHelper { - public static IEnumerable Search(string search) + public static IEnumerable Search(string search, string filterId) { var services = ServiceController.GetServices().OrderBy(s => s.DisplayName); IEnumerable serviceList = []; @@ -44,6 +44,21 @@ public static class ServiceHelper serviceList = servicesStartsWith.Concat(servicesContains); } + switch (filterId) + { + case "running": + serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Running); + break; + case "stopped": + serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Stopped); + break; + case "paused": + serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Paused); + break; + case "all": + break; + } + var result = serviceList.Select(s => { var serviceResult = ServiceResult.CreateServiceController(s); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs new file mode 100644 index 0000000000..179315b0c3 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs @@ -0,0 +1,26 @@ +// 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.Ext.WindowsServices; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ServiceFilters : Filters +{ + public ServiceFilters() + { + CurrentFilterId = "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 }, + ]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs index 1f361b6b10..4892a1594b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs @@ -16,13 +16,19 @@ internal sealed partial class ServicesListPage : DynamicListPage { 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(0); public override IListItem[] GetItems() { - var items = ServiceHelper.Search(SearchText).ToArray(); + var items = ServiceHelper.Search(SearchText, Filters.CurrentFilterId).ToArray(); return items; } diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleDynamicListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleDynamicListPage.cs index c284c7d784..b0044379be 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleDynamicListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleDynamicListPage.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Runtime.InteropServices.WindowsRuntime; using Microsoft.CommandPalette.Extensions; @@ -16,9 +17,14 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage Icon = new IconInfo(string.Empty); Name = "Dynamic List"; IsLoading = true; + var filters = new SampleFilters(); + filters.PropChanged += Filters_PropChanged; + Filters = filters; } - public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(newSearch.Length); + private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged(); + + public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(); public override IListItem[] GetItems() { @@ -28,6 +34,23 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }]; } + if (!string.IsNullOrEmpty(Filters.CurrentFilterId)) + { + switch (Filters.CurrentFilterId) + { + case "mod2": + items = items.Where((item, index) => (index + 1) % 2 == 0).ToArray(); + break; + case "mod3": + items = items.Where((item, index) => (index + 1) % 3 == 0).ToArray(); + break; + case "all": + default: + // No filtering + break; + } + } + if (items.Length > 0) { items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box"; @@ -36,3 +59,18 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage return items; } } + +#pragma warning disable SA1402 // File may only contain a single type +public partial class SampleFilters : Filters +#pragma warning restore SA1402 // File may only contain a single type +{ + public override IFilterItem[] GetFilters() + { + return + [ + new Filter() { Id = "all", Name = "All" }, + new Filter() { Id = "mod2", Name = "Every 2nd", Icon = new IconInfo("2") }, + new Filter() { Id = "mod3", Name = "Every 3rd", Icon = new IconInfo("3") }, + ]; + } +} diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs index 2c3fabf5c6..4d0cd8b6f4 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs @@ -81,7 +81,7 @@ internal sealed partial class SampleListPage : ListPage Title = "I'm a second command", RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1), }, - new SeparatorContextItem(), + new Separator(), new CommandContextItem( new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3 { diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs new file mode 100644 index 0000000000..2379382d1e --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Filters.cs @@ -0,0 +1,23 @@ +// 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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public abstract partial class Filters : BaseObservable, IFilters +{ + public string CurrentFilterId + { + get => field; + set + { + field = value; + OnPropertyChanged(nameof(CurrentFilterId)); + } + } + + = string.Empty; + + // This method should be overridden in derived classes to provide the actual filters. + public abstract IFilterItem[] GetFilters(); +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SeparatorContextItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs similarity index 76% rename from src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SeparatorContextItem.cs rename to src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs index c851634f59..d47eff6b22 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/SeparatorContextItem.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Separator.cs @@ -4,6 +4,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit; -public partial class SeparatorContextItem : ISeparatorContextItem +public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem { } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl index 3105b0647d..51ddbdc572 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.idl @@ -122,7 +122,7 @@ namespace Microsoft.CommandPalette.Extensions interface ISeparatorFilterItem requires IFilterItem {} [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] - interface IFilter requires IFilterItem { + interface IFilter requires INotifyPropChanged, IFilterItem { String Id { get; }; String Name { get; }; IIconInfo Icon { get; }; @@ -131,7 +131,7 @@ namespace Microsoft.CommandPalette.Extensions [contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)] interface IFilters { String CurrentFilterId { get; set; }; - IFilterItem[] Filters(); + IFilterItem[] GetFilters(); } struct Color