add sort feature

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
This commit is contained in:
Shawn Yuan (from Dev Box)
2025-11-24 13:12:37 +08:00
parent 16733718c3
commit a67326d86d
4 changed files with 211 additions and 47 deletions

View File

@@ -5,7 +5,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:Microsoft.PowerToys.QuickAccess.ViewModels"
xmlns:local="using:Microsoft.PowerToys.QuickAccess.Flyout"
mc:Ignorable="d">
<Page.Resources>
<local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
</Page.Resources>
<Grid Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -16,25 +20,49 @@
x:Uid="AllAppsTxt"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<Button
x:Uid="BackBtn"
Padding="8,4,8,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="BackButton_Click">
<Button.Content>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="12">
<FontIcon
Margin="0,2,0,0"
FontSize="12"
Glyph="&#xe76b;" />
<TextBlock x:Uid="BackLabel" Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
</Button.Content>
</Button>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button
x:Uid="Dashboard_SortBy"
VerticalAlignment="Center"
Style="{StaticResource SubtleButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="Dashboard_SortBy_ToolTip" />
</ToolTipService.ToolTip>
<Button.Content>
<FontIcon FontSize="16" Glyph="&#xE8CB;" />
</Button.Content>
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
x:Uid="Dashboard_SortAlphabetical"
Click="SortAlphabetical_Click"
IsChecked="{x:Bind ViewModel.DashboardSortOrder, Mode=OneWay, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Alphabetical}" />
<ToggleMenuFlyoutItem
x:Uid="Dashboard_SortByStatus"
Click="SortByStatus_Click"
IsChecked="{x:Bind ViewModel.DashboardSortOrder, Mode=OneWay, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=ByStatus}" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button
x:Uid="BackBtn"
Padding="8,4,8,4"
VerticalAlignment="Center"
Click="BackButton_Click">
<Button.Content>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="12">
<FontIcon
Margin="0,2,0,0"
FontSize="12"
Glyph="&#xe76b;" />
<TextBlock x:Uid="BackLabel" Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</Grid>
<ListView
Grid.Row="1"

View File

@@ -2,7 +2,9 @@
// 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 Microsoft.PowerToys.QuickAccess.ViewModels;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
@@ -42,4 +44,20 @@ public sealed partial class AppsListPage : Page
Frame.Navigate(typeof(LaunchPage), _context, new SlideNavigationTransitionInfo { Effect = SlideNavigationTransitionEffect.FromLeft });
}
private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
{
if (ViewModel != null)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
}
}
private void SortByStatus_Click(object sender, RoutedEventArgs e)
{
if (ViewModel != null)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
}
}
}

View File

@@ -0,0 +1,29 @@
// 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 Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.QuickAccess.Flyout;
public partial class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null || parameter == null)
{
return false;
}
var enumString = value.ToString();
var parameterString = parameter.ToString();
return string.Equals(enumString, parameterString, StringComparison.OrdinalIgnoreCase);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.QuickAccess.Helpers;
@@ -20,18 +22,34 @@ public sealed class AllAppsViewModel : Observable
{
private readonly IQuickAccessCoordinator _coordinator;
private readonly ISettingsRepository<GeneralSettings> _settingsRepository;
private readonly ISettingsUtils _settingsUtils;
private readonly ResourceLoader _resourceLoader;
private readonly DispatcherQueue _dispatcherQueue;
private GeneralSettings _generalSettings;
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
public DashboardSortOrder DashboardSortOrder
{
get => _generalSettings.DashboardSortOrder;
set
{
if (_generalSettings.DashboardSortOrder != value)
{
_generalSettings.DashboardSortOrder = value;
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName());
OnPropertyChanged();
RefreshFlyoutMenuItems();
}
}
}
public AllAppsViewModel(IQuickAccessCoordinator coordinator)
{
_coordinator = coordinator;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
var settingsUtils = new SettingsUtils();
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils);
_settingsUtils = new SettingsUtils();
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
_generalSettings = _settingsRepository.SettingsConfig;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
_settingsRepository.SettingsChanged += OnSettingsChanged;
@@ -39,35 +57,90 @@ public sealed class AllAppsViewModel : Observable
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
FlyoutMenuItems = new ObservableCollection<FlyoutMenuItem>();
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
{
AddFlyoutMenuItem(moduleType);
}
RefreshFlyoutMenuItems();
}
private void OnSettingsChanged(GeneralSettings newSettings)
{
_dispatcherQueue.TryEnqueue(() =>
{
ModuleEnabledChangedOnSettingsPage();
_generalSettings = newSettings;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
OnPropertyChanged(nameof(DashboardSortOrder));
RefreshFlyoutMenuItems();
});
}
private void AddFlyoutMenuItem(ModuleType moduleType)
private void RefreshFlyoutMenuItems()
{
var gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType);
var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled;
var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType));
var desiredItems = new List<FlyoutMenuItem>();
FlyoutMenuItems.Add(new FlyoutMenuItem
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
{
Label = _resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType)),
IsEnabled = isEnabled,
IsLocked = isLocked,
Tag = moduleType,
Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType),
EnabledChangedCallback = EnabledChangedOnUI,
});
var gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType);
var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled;
var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType));
var existingItem = FlyoutMenuItems.FirstOrDefault(x => x.Tag == moduleType);
if (existingItem != null)
{
existingItem.Label = _resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType));
existingItem.IsLocked = isLocked;
existingItem.Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType);
if (existingItem.IsEnabled != isEnabled)
{
var callback = existingItem.EnabledChangedCallback;
existingItem.EnabledChangedCallback = null;
existingItem.IsEnabled = isEnabled;
existingItem.EnabledChangedCallback = callback;
}
desiredItems.Add(existingItem);
}
else
{
desiredItems.Add(new FlyoutMenuItem
{
Label = _resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType)),
IsEnabled = isEnabled,
IsLocked = isLocked,
Tag = moduleType,
Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType),
EnabledChangedCallback = EnabledChangedOnUI,
});
}
}
var sortedItems = DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => desiredItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => desiredItems.OrderBy(x => x.Label).ToList(),
};
for (int i = FlyoutMenuItems.Count - 1; i >= 0; i--)
{
if (!sortedItems.Contains(FlyoutMenuItems[i]))
{
FlyoutMenuItems.RemoveAt(i);
}
}
for (int i = 0; i < sortedItems.Count; i++)
{
var item = sortedItems[i];
var oldIndex = FlyoutMenuItems.IndexOf(item);
if (oldIndex < 0)
{
FlyoutMenuItems.Insert(i, item);
}
else if (oldIndex != i)
{
FlyoutMenuItems.Move(oldIndex, i);
}
}
}
private void EnabledChangedOnUI(FlyoutMenuItem item)
@@ -75,20 +148,36 @@ public sealed class AllAppsViewModel : Observable
if (_coordinator.UpdateModuleEnabled(item.Tag, item.IsEnabled))
{
_coordinator.NotifyUserSettingsInteraction();
// If sorting by status, we might want to re-sort, but that could be jarring.
// DashboardViewModel calls RequestConflictData but doesn't seem to re-sort immediately on toggle?
// Actually DashboardViewModel calls RefreshModuleList() in ModuleEnabledChangedOnSettingsPage, but that's from settings change.
// EnabledChangedOnUI in DashboardViewModel calls UpdateGeneralSettingsCallback.
// If we want to re-sort on toggle, we should call RefreshFlyoutMenuItems().
// But usually users don't like items jumping around when they toggle them.
// So let's leave it for now.
}
}
private void ModuleEnabledChangedOnSettingsPage()
{
_generalSettings = _settingsRepository.SettingsConfig;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
// This is called when settings change (via OnSettingsChanged -> this).
// But OnSettingsChanged already calls RefreshFlyoutMenuItems.
// However, ModuleEnabledChangedOnSettingsPage is also passed as a callback to GeneralSettings.
// Wait, GeneralSettings.AddEnabledModuleChangeNotification adds it to a list.
// But GeneralSettings is just a data object. Who calls the notification?
// It seems GeneralSettings doesn't have logic to call it itself unless something calls it.
// In DashboardViewModel, it's called in OnSettingsChanged.
foreach (var item in FlyoutMenuItems)
{
if (!item.IsLocked)
{
item.IsEnabled = ModuleHelper.GetIsModuleEnabled(_generalSettings, item.Tag);
}
}
// In my implementation of OnSettingsChanged, I call RefreshFlyoutMenuItems directly.
// So I might not need ModuleEnabledChangedOnSettingsPage to do much, or I can remove it if I don't use the callback mechanism inside GeneralSettings (which seems to be for internal notification within the object, but GeneralSettings is just a POCO-like object with some logic).
// Actually, let's look at DashboardViewModel again.
// It adds ModuleEnabledChangedOnSettingsPage to generalSettingsConfig.AddEnabledModuleChangeNotification.
// And OnSettingsChanged calls ModuleEnabledChangedOnSettingsPage.
// I'll stick to calling RefreshFlyoutMenuItems in OnSettingsChanged.
// And I'll keep ModuleEnabledChangedOnSettingsPage for compatibility if needed, but maybe just redirect it.
RefreshFlyoutMenuItems();
}
}