AllAppsViewModel.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Collections.ObjectModel; 8 using System.Linq; 9 using global::PowerToys.GPOWrapper; 10 using ManagedCommon; 11 using Microsoft.PowerToys.QuickAccess.Helpers; 12 using Microsoft.PowerToys.QuickAccess.Services; 13 using Microsoft.PowerToys.Settings.UI.Controls; 14 using Microsoft.PowerToys.Settings.UI.Library; 15 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 16 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 17 using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; 18 using Microsoft.UI.Dispatching; 19 using Microsoft.Windows.ApplicationModel.Resources; 20 21 namespace Microsoft.PowerToys.QuickAccess.ViewModels; 22 23 public sealed class AllAppsViewModel : Observable 24 { 25 private readonly object _sortLock = new object(); 26 private readonly IQuickAccessCoordinator _coordinator; 27 private readonly ISettingsRepository<GeneralSettings> _settingsRepository; 28 private readonly SettingsUtils _settingsUtils; 29 private readonly ResourceLoader _resourceLoader; 30 private readonly DispatcherQueue _dispatcherQueue; 31 private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new(); 32 private GeneralSettings _generalSettings; 33 34 // Flag to prevent toggle operations during sorting to avoid race conditions. 35 private bool _isSorting; 36 37 public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; } 38 39 public DashboardSortOrder DashboardSortOrder 40 { 41 get => _generalSettings.DashboardSortOrder; 42 set 43 { 44 if (_generalSettings.DashboardSortOrder != value) 45 { 46 _generalSettings.DashboardSortOrder = value; 47 _coordinator.SendSortOrderUpdate(_generalSettings); 48 OnPropertyChanged(); 49 SortFlyoutMenuItems(); 50 } 51 } 52 } 53 54 public AllAppsViewModel(IQuickAccessCoordinator coordinator) 55 { 56 _coordinator = coordinator; 57 _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); 58 _settingsUtils = SettingsUtils.Default; 59 _settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils); 60 _generalSettings = _settingsRepository.SettingsConfig; 61 _settingsRepository.SettingsChanged += OnSettingsChanged; 62 63 _resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; 64 FlyoutMenuItems = new ObservableCollection<FlyoutMenuItem>(); 65 66 BuildFlyoutMenuItems(); 67 RefreshFlyoutMenuItems(); 68 } 69 70 private void BuildFlyoutMenuItems() 71 { 72 _allFlyoutMenuItems.Clear(); 73 foreach (ModuleType moduleType in Enum.GetValues<ModuleType>()) 74 { 75 if (moduleType == ModuleType.GeneralSettings) 76 { 77 continue; 78 } 79 80 _allFlyoutMenuItems.Add(new FlyoutMenuItem 81 { 82 Tag = moduleType, 83 EnabledChangedCallback = EnabledChangedOnUI, 84 }); 85 } 86 } 87 88 private void OnSettingsChanged(GeneralSettings newSettings) 89 { 90 _dispatcherQueue.TryEnqueue(() => 91 { 92 _generalSettings = newSettings; 93 OnPropertyChanged(nameof(DashboardSortOrder)); 94 RefreshFlyoutMenuItems(); 95 }); 96 } 97 98 public void RefreshSettings() 99 { 100 if (_settingsRepository.ReloadSettings()) 101 { 102 OnSettingsChanged(_settingsRepository.SettingsConfig); 103 } 104 } 105 106 private void RefreshFlyoutMenuItems() 107 { 108 foreach (var item in _allFlyoutMenuItems) 109 { 110 var moduleType = item.Tag; 111 var gpo = Helpers.ModuleGpoHelper.GetModuleGpoConfiguration(moduleType); 112 var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled; 113 var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType)); 114 115 item.Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)); 116 item.IsLocked = isLocked; 117 item.Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType); 118 119 if (item.IsEnabled != isEnabled) 120 { 121 item.UpdateStatus(isEnabled); 122 } 123 } 124 125 SortFlyoutMenuItems(); 126 } 127 128 private void SortFlyoutMenuItems() 129 { 130 if (_isSorting) 131 { 132 return; 133 } 134 135 lock (_sortLock) 136 { 137 _isSorting = true; 138 try 139 { 140 var sortedItems = DashboardSortOrder switch 141 { 142 DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(), 143 _ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(), 144 }; 145 146 if (FlyoutMenuItems.Count == 0) 147 { 148 foreach (var item in sortedItems) 149 { 150 FlyoutMenuItems.Add(item); 151 } 152 153 return; 154 } 155 156 for (int i = 0; i < sortedItems.Count; i++) 157 { 158 var item = sortedItems[i]; 159 var oldIndex = FlyoutMenuItems.IndexOf(item); 160 161 if (oldIndex != -1 && oldIndex != i) 162 { 163 FlyoutMenuItems.Move(oldIndex, i); 164 } 165 } 166 } 167 finally 168 { 169 // Use dispatcher to reset flag after UI updates complete 170 _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => 171 { 172 _isSorting = false; 173 }); 174 } 175 } 176 } 177 178 private void EnabledChangedOnUI(ModuleListItem item) 179 { 180 var flyoutItem = (FlyoutMenuItem)item; 181 var isEnabled = flyoutItem.IsEnabled; 182 183 // Ignore toggle operations during sorting to prevent race conditions. 184 // Revert the toggle state since UI already changed due to TwoWay binding. 185 if (_isSorting) 186 { 187 flyoutItem.UpdateStatus(!isEnabled); 188 return; 189 } 190 191 _coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled); 192 SortFlyoutMenuItems(); 193 } 194 }