mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
Initial prototype for extension list search
Adds a rough implementation that appears to work, but it hasn’t been tested. Don’t rely on it yet—I’m too tired to test properly or write a polished commit message.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
// 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.UI.ViewModels.Messages;
|
||||
|
||||
public record ReloadFinishedMessage();
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Provides filtering over the list of provider settings view models.
|
||||
/// Intended to be used by the UI to bind a TextBox (SearchText) and an ItemsRepeater (FilteredProviders).
|
||||
/// </summary>
|
||||
public partial class SettingsExtensionsViewModel : ObservableObject
|
||||
{
|
||||
private readonly ObservableCollection<ProviderSettingsViewModel> _source;
|
||||
private readonly TaskScheduler _uiScheduler;
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> FilteredProviders { get; } = [];
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
if (_searchText != value)
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged();
|
||||
ApplyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ItemCounterText
|
||||
{
|
||||
get
|
||||
{
|
||||
var hasQuery = !string.IsNullOrWhiteSpace(_searchText);
|
||||
var count = hasQuery ? FilteredProviders.Count : _source.Count;
|
||||
var suffix = hasQuery ? "extensions found" : "extensions installed";
|
||||
return $"{count} {suffix}";
|
||||
}
|
||||
}
|
||||
|
||||
private bool _showManualReloadOverlay;
|
||||
|
||||
public bool ShowManualReloadOverlay
|
||||
{
|
||||
get => _showManualReloadOverlay;
|
||||
private set
|
||||
{
|
||||
if (_showManualReloadOverlay != value)
|
||||
{
|
||||
_showManualReloadOverlay = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowNoResultsPanel => !string.IsNullOrWhiteSpace(_searchText) && FilteredProviders.Count == 0;
|
||||
|
||||
public bool HasResults => !ShowNoResultsPanel;
|
||||
|
||||
public IRelayCommand<string?> OpenStoreWithExtensionCommand { get; }
|
||||
|
||||
public IRelayCommand ReloadExtensionsCommand { get; }
|
||||
|
||||
public SettingsExtensionsViewModel(ObservableCollection<ProviderSettingsViewModel> source, TaskScheduler uiScheduler)
|
||||
{
|
||||
_source = source;
|
||||
_uiScheduler = uiScheduler;
|
||||
_source.CollectionChanged += Source_CollectionChanged;
|
||||
ApplyFilter();
|
||||
|
||||
OpenStoreWithExtensionCommand = new RelayCommand<string?>(OpenStoreWithExtension);
|
||||
ReloadExtensionsCommand = new RelayCommand(ReloadExtensions);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ReloadFinishedMessage>(this, (_, _) =>
|
||||
{
|
||||
Task.Factory.StartNew(() => ShowManualReloadOverlay = false, CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
|
||||
});
|
||||
}
|
||||
|
||||
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
var query = _searchText;
|
||||
var filtered = ListHelpers.FilterList(_source, query, Matches);
|
||||
ListHelpers.InPlaceUpdateList(FilteredProviders, filtered);
|
||||
OnPropertyChanged(nameof(ItemCounterText));
|
||||
OnPropertyChanged(nameof(HasResults));
|
||||
OnPropertyChanged(nameof(ShowNoResultsPanel));
|
||||
}
|
||||
|
||||
private static int Matches(string query, ProviderSettingsViewModel item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
return Contains(item.DisplayName, query)
|
||||
|| Contains(item.ExtensionName, query)
|
||||
|| Contains(item.ExtensionSubtext, query)
|
||||
? 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static bool Contains(string? haystack, string needle)
|
||||
{
|
||||
return !string.IsNullOrEmpty(haystack) && haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void OpenStoreWithExtension(string? query)
|
||||
{
|
||||
const string ExtensionsAssocUri = "ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette";
|
||||
ShellHelpers.OpenInShell(ExtensionsAssocUri);
|
||||
}
|
||||
|
||||
private void ReloadExtensions()
|
||||
{
|
||||
ShowManualReloadOverlay = true;
|
||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
|
||||
{
|
||||
_settings = settings;
|
||||
@@ -135,6 +137,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider);
|
||||
CommandProviders.Add(settingsModel);
|
||||
}
|
||||
|
||||
Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
|
||||
}
|
||||
|
||||
private IEnumerable<CommandProviderWrapper> GetCommandProviders()
|
||||
|
||||
@@ -259,6 +259,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
IsLoading = false;
|
||||
|
||||
// Send on the current thread; receivers should marshal to UI if needed
|
||||
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,107 @@
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<converters:BoolNegationConverter x:Key="InvertedBoolConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Page.KeyboardAccelerators>
|
||||
<KeyboardAccelerator Modifiers="Control" Key="F" Invoked="OnFindInvoked"/>
|
||||
</Page.KeyboardAccelerators>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
|
||||
<ScrollViewer Grid.Row="1" x:Name="RootScrollViewer">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind viewModel.CommandProviders, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
|
||||
<!-- toolbar 1 -->
|
||||
<Grid Padding="0,0,0,8">
|
||||
<AutoSuggestBox x:Uid="Settings_ExtensionsPage_SearchBox"
|
||||
x:Name="SearchBox"
|
||||
Width="320"
|
||||
MaxWidth="320"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{x:Bind viewModel.Extensions.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<AutoSuggestBox.QueryIcon>
|
||||
<SymbolIcon Symbol="Find" />
|
||||
</AutoSuggestBox.QueryIcon>
|
||||
</AutoSuggestBox>
|
||||
</Grid>
|
||||
|
||||
<!-- toolbar 2 -->
|
||||
<Grid Padding="0,8,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="ItemCounterTextBlock"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind viewModel.Extensions.ItemCounterText, Mode=OneWay}" />
|
||||
|
||||
<!-- Right actions: spinner + label (when reloading) then button -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center"
|
||||
Visibility="{x:Bind viewModel.Extensions.ShowManualReloadOverlay, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ProgressRing Width="16" Height="16" IsActive="True" />
|
||||
<TextBlock x:Uid="Settings_ExtensionsPage_Reloading_Text"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Uid="Settings_ExtensionsPage_More_Button"
|
||||
x:Name="MoreButton"
|
||||
VerticalAlignment="Center">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
||||
<MenuFlyoutItem x:Uid="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem"
|
||||
Command="{x:Bind viewModel.Extensions.ReloadExtensionsCommand}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
<FontIcon Glyph="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Empty state when no results match the current search -->
|
||||
<Grid x:Name="NoResultsPanel"
|
||||
x:Load="{x:Bind viewModel.Extensions.ShowNoResultsPanel, Mode=OneWay}"
|
||||
Padding="48"
|
||||
CornerRadius="4">
|
||||
<StackPanel Spacing="8"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Uid="Settings_ExtensionsPage_NoResults_Primary"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock x:Uid="Settings_ExtensionsPage_NoResults_Secondary"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
TextAlignment="Center" />
|
||||
<HyperlinkButton x:Uid="Settings_ExtensionsPage_NoResults_Hyperlink"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}"
|
||||
Margin="0,16,0,0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ItemsRepeater x:Name="ProvidersRepeater"
|
||||
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
|
||||
<controls:SettingsCard
|
||||
@@ -54,6 +142,16 @@
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
<TextBlock x:Uid="Settings_ExtensionsPage_RelatedItemsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_ExtensionsPage_ManageExtensions_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}" />
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
@@ -35,4 +36,10 @@ public sealed partial class ExtensionsPage : Page
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||
{
|
||||
SearchBox?.Focus(FocusState.Keyboard);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,4 +441,49 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="NavigationPaneOpened" xml:space="preserve">
|
||||
<value>Navigation page opened</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Get more extension in the Microsoft Store</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Opens the Microsoft Store to show available third-party extensions</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_RelatedItemsHeader.Text" xml:space="preserve">
|
||||
<value>Related links</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_SearchBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search extensions</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_NoResults_Primary.Text" xml:space="preserve">
|
||||
<value>No extensions found</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_NoResults_Secondary.Text" xml:space="preserve">
|
||||
<value>Try a different search term</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_NoResults_Hyperlink.Content" xml:space="preserve">
|
||||
<value>Browse Microsoft Store for Command Palette extensions</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_SearchBox.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Press Ctrl+F to focus the search box</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_SearchBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.AcceleratorKey" xml:space="preserve">
|
||||
<value>Ctrl+F</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_SearchBox.AccessKey" xml:space="preserve">
|
||||
<value>S</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_NoResults_Hyperlink.AccessKey" xml:space="preserve">
|
||||
<value>B</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_ManageExtensions_SettingsCard.AccessKey" xml:space="preserve">
|
||||
<value>M</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_More_Button.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>See more</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem.Text" xml:space="preserve">
|
||||
<value>Reload extensions</value>
|
||||
</data>
|
||||
<data name="Settings_ExtensionsPage_Reloading_Text.Text" xml:space="preserve">
|
||||
<value>Reloading extensions…</value>
|
||||
</data>
|
||||
</root>
|
||||
Reference in New Issue
Block a user