From a67326d86dc9faa607d765ae43eacc6c5060cbbb Mon Sep 17 00:00:00 2001 From: "Shawn Yuan (from Dev Box)" Date: Mon, 24 Nov 2025 13:12:37 +0800 Subject: [PATCH] add sort feature Signed-off-by: Shawn Yuan (from Dev Box) --- .../QuickAccessXAML/Flyout/AppsListPage.xaml | 66 +++++--- .../Flyout/AppsListPage.xaml.cs | 18 +++ .../Flyout/EnumToBooleanConverter.cs | 29 ++++ .../ViewModels/AllAppsViewModel.cs | 145 ++++++++++++++---- 4 files changed, 211 insertions(+), 47 deletions(-) create mode 100644 src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/EnumToBooleanConverter.cs diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml index f2a0ece8d5..b04a25f0fe 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml @@ -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"> + + + @@ -16,25 +20,49 @@ x:Uid="AllAppsTxt" VerticalAlignment="Center" Style="{StaticResource BodyStrongTextBlockStyle}" /> - + + + + _settingsRepository; + private readonly ISettingsUtils _settingsUtils; private readonly ResourceLoader _resourceLoader; private readonly DispatcherQueue _dispatcherQueue; private GeneralSettings _generalSettings; public ObservableCollection 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.GetInstance(settingsUtils); + _settingsUtils = new SettingsUtils(); + _settingsRepository = SettingsRepository.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(); - foreach (ModuleType moduleType in Enum.GetValues()) - { - 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(); - FlyoutMenuItems.Add(new FlyoutMenuItem + foreach (ModuleType moduleType in Enum.GetValues()) { - 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(); } }