From e314485e8503b3cc4b5d43120e0c9e778c87411a Mon Sep 17 00:00:00 2001 From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:29:46 +0800 Subject: [PATCH] Improve module enable/disable IPC and sorting reliability (#44734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request - Refactored the runner logic for handling individual module enable/disable updates. Instead of receiving the entire settings.json via IPC, it now processes only single-module state updates, which avoids race conditions and fixes a bug where modules could end up being skipped. - Fixed an issue where the sort order option could be deselected — it is now enforced as a mutually exclusive choice. - Fixed a potential race condition when updating the AppList control’s sorting. ## PR Checklist - [x] Closes: #44697 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/runner/general_settings.cpp | 96 ++++++++++++++++ src/runner/general_settings.h | 1 + src/runner/settings_window.cpp | 6 + .../Flyout/AppsListPage.xaml.cs | 2 + .../Services/IQuickAccessCoordinator.cs | 6 +- .../Services/QuickAccessCoordinator.cs | 47 +------- .../ViewModels/AllAppsViewModel.cs | 87 +++++++++----- .../Helpers/ModuleHelper.cs | 42 +++++++ .../SettingsXAML/Views/DashboardPage.xaml.cs | 8 ++ .../ViewModels/DashboardViewModel.cs | 107 +++++++++++++----- 10 files changed, 297 insertions(+), 105 deletions(-) diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index 4deb36ec17..6225ed8f2a 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -193,6 +193,102 @@ GeneralSettings get_general_settings() return settings; } +void apply_module_status_update(const json::JsonObject& module_config, bool save) +{ + Logger::info(L"apply_module_status_update: {}", std::wstring{ module_config.ToString() }); + + // Expected format: {"ModuleName": true/false} - only one module per update + auto iter = module_config.First(); + if (!iter.HasCurrent()) + { + Logger::warn(L"apply_module_status_update: Empty module config"); + return; + } + + const auto& element = iter.Current(); + const auto value = element.Value(); + if (value.ValueType() != json::JsonValueType::Boolean) + { + Logger::warn(L"apply_module_status_update: Invalid value type for module status"); + return; + } + + const std::wstring name{ element.Key().c_str() }; + if (modules().find(name) == modules().end()) + { + Logger::warn(L"apply_module_status_update: Module {} not found", name); + return; + } + + PowertoyModule& powertoy = modules().at(name); + const bool module_inst_enabled = powertoy->is_enabled(); + bool target_enabled = value.GetBoolean(); + + auto gpo_rule = powertoy->gpo_policy_enabled_configuration(); + if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled) + { + // Apply the GPO Rule. + target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled; + } + + if (module_inst_enabled == target_enabled) + { + Logger::info(L"apply_module_status_update: Module {} already in target state {}", name, target_enabled); + return; + } + + if (target_enabled) + { + Logger::info(L"apply_module_status_update: Enabling powertoy {}", name); + powertoy->enable(); + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + hkmng.EnableHotkeyByModule(name); + + // Trigger AI capability detection when ImageResizer is enabled + if (name == L"Image Resizer") + { + Logger::info(L"ImageResizer enabled, triggering AI capability detection"); + DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled + } + } + else + { + Logger::info(L"apply_module_status_update: Disabling powertoy {}", name); + powertoy->disable(); + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + hkmng.DisableHotkeyByModule(name); + } + // Sync the hotkey state with the module state, so it can be removed for disabled modules. + powertoy.UpdateHotkeyEx(); + + if (save) + { + // Load existing settings and only update the specific module's enabled state + json::JsonObject current_settings = PTSettingsHelper::load_general_settings(); + + json::JsonObject enabled; + if (current_settings.HasKey(L"enabled")) + { + enabled = current_settings.GetNamedObject(L"enabled"); + } + + // Check if the saved state is different from the requested state + bool current_saved = enabled.HasKey(name) ? enabled.GetNamedBoolean(name, true) : true; + + if (current_saved != target_enabled) + { + // Update only this module's enabled state + enabled.SetNamedValue(name, json::value(target_enabled)); + current_settings.SetNamedValue(L"enabled", enabled); + + PTSettingsHelper::save_general_settings(current_settings); + + GeneralSettings settings_for_trace = get_general_settings(); + Trace::SettingsChanged(settings_for_trace); + } + } +} + void apply_general_settings(const json::JsonObject& general_configs, bool save) { std::wstring old_settings_json_string; diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index ac93a1fdfd..487e3216da 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -38,4 +38,5 @@ struct GeneralSettings json::JsonObject load_general_settings(); GeneralSettings get_general_settings(); void apply_general_settings(const json::JsonObject& general_configs, bool save = true); +void apply_module_status_update(const json::JsonObject& module_config, bool save = true); void start_enabled_powertoys(); \ No newline at end of file diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index a1ec3f81b9..e46572f579 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -215,6 +215,12 @@ void dispatch_received_json(const std::wstring& json_to_parse) // current_settings_ipc->send(settings_string); // } } + else if (name == L"module_status") + { + // Handle single module enable/disable update + // Expected format: {"module_status": {"ModuleName": true/false}} + apply_module_status_update(value.GetObjectW()); + } else if (name == L"powertoys") { dispatch_json_config_to_modules(value.GetObjectW()); diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs index 1212855fe4..9c1422d726 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs @@ -51,6 +51,7 @@ public sealed partial class AppsListPage : Page if (ViewModel != null) { ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; + ((ToggleMenuFlyoutItem)sender).IsChecked = true; } } @@ -59,6 +60,7 @@ public sealed partial class AppsListPage : Page if (ViewModel != null) { ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; + ((ToggleMenuFlyoutItem)sender).IsChecked = true; } } } diff --git a/src/settings-ui/QuickAccess.UI/Services/IQuickAccessCoordinator.cs b/src/settings-ui/QuickAccess.UI/Services/IQuickAccessCoordinator.cs index 33de96b8af..c9a4c5d77f 100644 --- a/src/settings-ui/QuickAccess.UI/Services/IQuickAccessCoordinator.cs +++ b/src/settings-ui/QuickAccess.UI/Services/IQuickAccessCoordinator.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; + using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; namespace Microsoft.PowerToys.QuickAccess.Services; @@ -19,10 +21,10 @@ public interface IQuickAccessCoordinator Task ShowDocumentationAsync(); - void NotifyUserSettingsInteraction(); - bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled); + void SendSortOrderUpdate(GeneralSettings generalSettings); + void ReportBug(); void OnModuleLaunched(ModuleType moduleType); diff --git a/src/settings-ui/QuickAccess.UI/Services/QuickAccessCoordinator.cs b/src/settings-ui/QuickAccess.UI/Services/QuickAccessCoordinator.cs index dee883f61c..0e25630d54 100644 --- a/src/settings-ui/QuickAccess.UI/Services/QuickAccessCoordinator.cs +++ b/src/settings-ui/QuickAccess.UI/Services/QuickAccessCoordinator.cs @@ -55,37 +55,8 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa return Task.FromResult(false); } - public void NotifyUserSettingsInteraction() - { - Logger.LogDebug("QuickAccessCoordinator.NotifyUserSettingsInteraction invoked."); - } - public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled) - { - GeneralSettings? updatedSettings = null; - lock (_generalSettingsLock) - { - var repository = SettingsRepository.GetInstance(_settingsUtils); - var generalSettings = repository.SettingsConfig; - var current = ModuleHelper.GetIsModuleEnabled(generalSettings, moduleType); - if (current == isEnabled) - { - return false; - } - - ModuleHelper.SetIsModuleEnabled(generalSettings, moduleType, isEnabled); - _settingsUtils.SaveSettings(generalSettings.ToJsonString()); - Logger.LogInfo($"QuickAccess updated module '{moduleType}' enabled state to {isEnabled}."); - updatedSettings = generalSettings; - } - - if (updatedSettings != null) - { - SendGeneralSettingsUpdate(updatedSettings); - } - - return true; - } + => TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update"); public void ReportBug() { @@ -131,20 +102,10 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}"); } - private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings) + public void SendSortOrderUpdate(GeneralSettings generalSettings) { - string payload; - try - { - payload = new OutGoingGeneralSettings(updatedSettings).ToString(); - } - catch (Exception ex) - { - Logger.LogError("QuickAccessCoordinator: failed to serialize general settings payload.", ex); - return; - } - - TrySendIpcMessage(payload, "general settings update"); + var outgoing = new OutGoingGeneralSettings(generalSettings); + TrySendIpcMessage(outgoing.ToString(), "sort order update"); } private bool TrySendIpcMessage(string payload, string operationDescription) diff --git a/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs b/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs index 7daaf4b3fd..eb00296263 100644 --- a/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs +++ b/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs @@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.QuickAccess.ViewModels; public sealed class AllAppsViewModel : Observable { + private readonly object _sortLock = new object(); private readonly IQuickAccessCoordinator _coordinator; private readonly ISettingsRepository _settingsRepository; private readonly SettingsUtils _settingsUtils; @@ -30,6 +31,9 @@ public sealed class AllAppsViewModel : Observable private readonly List _allFlyoutMenuItems = new(); private GeneralSettings _generalSettings; + // Flag to prevent toggle operations during sorting to avoid race conditions. + private bool _isSorting; + public ObservableCollection FlyoutMenuItems { get; } public DashboardSortOrder DashboardSortOrder @@ -40,9 +44,9 @@ public sealed class AllAppsViewModel : Observable if (_generalSettings.DashboardSortOrder != value) { _generalSettings.DashboardSortOrder = value; - _settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName()); + _coordinator.SendSortOrderUpdate(_generalSettings); OnPropertyChanged(); - RefreshFlyoutMenuItems(); + SortFlyoutMenuItems(); } } } @@ -54,7 +58,6 @@ public sealed class AllAppsViewModel : Observable _settingsUtils = SettingsUtils.Default; _settingsRepository = SettingsRepository.GetInstance(_settingsUtils); _generalSettings = _settingsRepository.SettingsConfig; - _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage); _settingsRepository.SettingsChanged += OnSettingsChanged; _resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; @@ -87,7 +90,6 @@ public sealed class AllAppsViewModel : Observable _dispatcherQueue.TryEnqueue(() => { _generalSettings = newSettings; - _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage); OnPropertyChanged(nameof(DashboardSortOrder)); RefreshFlyoutMenuItems(); }); @@ -120,30 +122,55 @@ public sealed class AllAppsViewModel : Observable } } - var sortedItems = DashboardSortOrder switch - { - DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(), - _ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(), - }; + SortFlyoutMenuItems(); + } - if (FlyoutMenuItems.Count == 0) + private void SortFlyoutMenuItems() + { + if (_isSorting) { - foreach (var item in sortedItems) - { - FlyoutMenuItems.Add(item); - } - return; } - for (int i = 0; i < sortedItems.Count; i++) + lock (_sortLock) { - var item = sortedItems[i]; - var oldIndex = FlyoutMenuItems.IndexOf(item); - - if (oldIndex != -1 && oldIndex != i) + _isSorting = true; + try { - FlyoutMenuItems.Move(oldIndex, i); + var sortedItems = DashboardSortOrder switch + { + DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(), + _ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(), + }; + + if (FlyoutMenuItems.Count == 0) + { + foreach (var item in sortedItems) + { + FlyoutMenuItems.Add(item); + } + + return; + } + + for (int i = 0; i < sortedItems.Count; i++) + { + var item = sortedItems[i]; + var oldIndex = FlyoutMenuItems.IndexOf(item); + + if (oldIndex != -1 && oldIndex != i) + { + FlyoutMenuItems.Move(oldIndex, i); + } + } + } + finally + { + // Use dispatcher to reset flag after UI updates complete + _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => + { + _isSorting = false; + }); } } } @@ -151,17 +178,17 @@ public sealed class AllAppsViewModel : Observable private void EnabledChangedOnUI(ModuleListItem item) { var flyoutItem = (FlyoutMenuItem)item; - if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled)) + var isEnabled = flyoutItem.IsEnabled; + + // Ignore toggle operations during sorting to prevent race conditions. + // Revert the toggle state since UI already changed due to TwoWay binding. + if (_isSorting) { - _coordinator.NotifyUserSettingsInteraction(); - - // Trigger re-sort immediately when status changes on UI - RefreshFlyoutMenuItems(); + flyoutItem.UpdateStatus(!isEnabled); + return; } - } - private void ModuleEnabledChangedOnSettingsPage() - { - RefreshFlyoutMenuItems(); + _coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled); + SortFlyoutMenuItems(); } } diff --git a/src/settings-ui/Settings.UI.Library/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI.Library/Helpers/ModuleHelper.cs index ded7c4e15e..f96eac8ce8 100644 --- a/src/settings-ui/Settings.UI.Library/Helpers/ModuleHelper.cs +++ b/src/settings-ui/Settings.UI.Library/Helpers/ModuleHelper.cs @@ -117,5 +117,47 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break; } } + + /// + /// Gets the module key name used in IPC messages and settings JSON. + /// These names match the JsonPropertyName attributes in EnabledModules class. + /// + public static string GetModuleKey(ModuleType moduleType) + { + return moduleType switch + { + ModuleType.AdvancedPaste => AdvancedPasteSettings.ModuleName, + ModuleType.AlwaysOnTop => AlwaysOnTopSettings.ModuleName, + ModuleType.Awake => AwakeSettings.ModuleName, + ModuleType.CmdPal => "CmdPal", // No dedicated settings class + ModuleType.ColorPicker => ColorPickerSettings.ModuleName, + ModuleType.CropAndLock => CropAndLockSettings.ModuleName, + ModuleType.CursorWrap => CursorWrapSettings.ModuleName, + ModuleType.EnvironmentVariables => EnvironmentVariablesSettings.ModuleName, + ModuleType.FancyZones => FancyZonesSettings.ModuleName, + ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName, + ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName, + ModuleType.Hosts => HostsSettings.ModuleName, + ModuleType.ImageResizer => ImageResizerSettings.ModuleName, + ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName, + ModuleType.LightSwitch => LightSwitchSettings.ModuleName, + ModuleType.MouseHighlighter => MouseHighlighterSettings.ModuleName, + ModuleType.MouseJump => MouseJumpSettings.ModuleName, + ModuleType.MousePointerCrosshairs => MousePointerCrosshairsSettings.ModuleName, + ModuleType.MouseWithoutBorders => MouseWithoutBordersSettings.ModuleName, + ModuleType.NewPlus => NewPlusSettings.ModuleName, + ModuleType.Peek => PeekSettings.ModuleName, + ModuleType.PowerRename => PowerRenameSettings.ModuleName, + ModuleType.PowerLauncher => PowerLauncherSettings.ModuleName, + ModuleType.PowerAccent => PowerAccentSettings.ModuleName, + ModuleType.RegistryPreview => RegistryPreviewSettings.ModuleName, + ModuleType.MeasureTool => MeasureToolSettings.ModuleName, + ModuleType.ShortcutGuide => ShortcutGuideSettings.ModuleName, + ModuleType.PowerOCR => PowerOcrSettings.ModuleName, + ModuleType.Workspaces => WorkspacesSettings.ModuleName, + ModuleType.ZoomIt => ZoomItSettings.ModuleName, + _ => moduleType.ToString(), + }; + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs index 639eb5f747..0fc9b7d153 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs @@ -65,11 +65,19 @@ namespace Microsoft.PowerToys.Settings.UI.Views private void SortAlphabetical_Click(object sender, RoutedEventArgs e) { ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; + if (sender is ToggleMenuFlyoutItem item) + { + item.IsChecked = true; + } } private void SortByStatus_Click(object sender, RoutedEventArgs e) { ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; + if (sender is ToggleMenuFlyoutItem item) + { + item.IsChecked = true; + } } } } diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs index ed0babe364..bfa418ba8b 100644 --- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs @@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { public partial class DashboardViewModel : PageViewModelBase { + private readonly object _sortLock = new object(); + protected override string ModuleName => "Dashboard"; private Dispatcher dispatcher; @@ -51,6 +53,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // Flag to prevent circular updates when a UI toggle triggers settings changes. private bool _isUpdatingFromUI; + // Flag to prevent toggle operations during sorting to avoid race conditions. + private bool _isSorting; + private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); public AllHotkeyConflictsData AllHotkeyConflictsData @@ -80,15 +85,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels get => generalSettingsConfig.DashboardSortOrder; set { - if (Set(ref _dashboardSortOrder, value)) + if (_dashboardSortOrder != value) { + _dashboardSortOrder = value; generalSettingsConfig.DashboardSortOrder = value; OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig); - // Save settings to file - SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString()); - SendConfigMSG(outgoing.ToString()); + + // Notify UI before sorting so menu updates its checked state + OnPropertyChanged(nameof(DashboardSortOrder)); + SortModuleList(); } } @@ -103,7 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels dispatcher = Dispatcher.CurrentDispatcher; _settingsRepository = settingsRepository; generalSettingsConfig = settingsRepository.SettingsConfig; - generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage); + _settingsRepository.SettingsChanged += OnSettingsChanged; // Initialize dashboard sort order from settings @@ -128,7 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels dispatcher.BeginInvoke(() => { generalSettingsConfig = newSettings; - generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage); + + // Update local field and notify UI if sort order changed + if (_dashboardSortOrder != generalSettingsConfig.DashboardSortOrder) + { + _dashboardSortOrder = generalSettingsConfig.DashboardSortOrder; + OnPropertyChanged(nameof(DashboardSortOrder)); + } + ModuleEnabledChangedOnSettingsPage(); }); } @@ -198,40 +212,58 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels /// Sorts the module list according to the current sort order and updates the AllModules collection. /// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place /// to avoid destroying and recreating UI elements. + /// Temporarily disables interaction on all items during sorting to prevent race conditions. /// private void SortModuleList() { - var sortedItems = (DashboardSortOrder switch + if (_isSorting) { - DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label), - _ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical - }).ToList(); - - // If AllModules is empty (first load), just populate it. - if (AllModules.Count == 0) - { - foreach (var item in sortedItems) - { - AllModules.Add(item); - } - return; } - // Otherwise, update the collection in place using Move to avoid UI glitches. - for (int i = 0; i < sortedItems.Count; i++) + lock (_sortLock) { - var currentItem = sortedItems[i]; - var currentIndex = AllModules.IndexOf(currentItem); - - if (currentIndex != -1 && currentIndex != i) + _isSorting = true; + try { - AllModules.Move(currentIndex, i); + var sortedItems = (DashboardSortOrder switch + { + DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label), + _ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical + }).ToList(); + + // If AllModules is empty (first load), just populate it. + if (AllModules.Count == 0) + { + foreach (var item in sortedItems) + { + AllModules.Add(item); + } + + return; + } + + // Otherwise, update the collection in place using Move to avoid UI glitches. + for (int i = 0; i < sortedItems.Count; i++) + { + var currentItem = sortedItems[i]; + var currentIndex = AllModules.IndexOf(currentItem); + + if (currentIndex != -1 && currentIndex != i) + { + AllModules.Move(currentIndex, i); + } + } + } + finally + { + // Use dispatcher to reset flag after UI updates complete + dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () => + { + _isSorting = false; + }); } } - - // Notify that DashboardSortOrder changed so the menu updates its checked state. - OnPropertyChanged(nameof(DashboardSortOrder)); } /// @@ -279,10 +311,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels var dashboardListItem = (DashboardListItem)item; var isEnabled = dashboardListItem.IsEnabled; + // Ignore toggle operations during sorting to prevent race conditions. + // Revert the toggle state since UI already changed due to TwoWay binding. + if (_isSorting) + { + dashboardListItem.UpdateStatus(!isEnabled); + return; + } + _isUpdatingFromUI = true; try { - Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled); + // Send optimized IPC message with only the module status update + // Format: {"module_status": {"ModuleName": true/false}} + string moduleKey = ModuleHelper.GetModuleKey(dashboardListItem.Tag); + string moduleStatusJson = $"{{\"module_status\": {{\"{moduleKey}\": {isEnabled.ToString().ToLowerInvariant()}}}}}"; + SendConfigMSG(moduleStatusJson); + + // Update local settings config to keep UI in sync + ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, dashboardListItem.Tag, isEnabled); if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true) {