Improve module enable/disable IPC and sorting reliability (#44734)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## 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.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44697
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Shawn Yuan
2026-01-15 15:29:46 +08:00
committed by GitHub
parent f48c4a9a6f
commit e314485e85
10 changed files with 297 additions and 105 deletions

View File

@@ -193,6 +193,102 @@ GeneralSettings get_general_settings()
return 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) void apply_general_settings(const json::JsonObject& general_configs, bool save)
{ {
std::wstring old_settings_json_string; std::wstring old_settings_json_string;

View File

@@ -38,4 +38,5 @@ struct GeneralSettings
json::JsonObject load_general_settings(); json::JsonObject load_general_settings();
GeneralSettings get_general_settings(); GeneralSettings get_general_settings();
void apply_general_settings(const json::JsonObject& general_configs, bool save = true); 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(); void start_enabled_powertoys();

View File

@@ -215,6 +215,12 @@ void dispatch_received_json(const std::wstring& json_to_parse)
// current_settings_ipc->send(settings_string); // 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") else if (name == L"powertoys")
{ {
dispatch_json_config_to_modules(value.GetObjectW()); dispatch_json_config_to_modules(value.GetObjectW());

View File

@@ -51,6 +51,7 @@ public sealed partial class AppsListPage : Page
if (ViewModel != null) if (ViewModel != null)
{ {
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
((ToggleMenuFlyoutItem)sender).IsChecked = true;
} }
} }
@@ -59,6 +60,7 @@ public sealed partial class AppsListPage : Page
if (ViewModel != null) if (ViewModel != null)
{ {
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
((ToggleMenuFlyoutItem)sender).IsChecked = true;
} }
} }
} }

View File

@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Threading.Tasks; using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.QuickAccess.Services; namespace Microsoft.PowerToys.QuickAccess.Services;
@@ -19,10 +21,10 @@ public interface IQuickAccessCoordinator
Task<bool> ShowDocumentationAsync(); Task<bool> ShowDocumentationAsync();
void NotifyUserSettingsInteraction();
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled); bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
void SendSortOrderUpdate(GeneralSettings generalSettings);
void ReportBug(); void ReportBug();
void OnModuleLaunched(ModuleType moduleType); void OnModuleLaunched(ModuleType moduleType);

View File

@@ -55,37 +55,8 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
return Task.FromResult(false); return Task.FromResult(false);
} }
public void NotifyUserSettingsInteraction()
{
Logger.LogDebug("QuickAccessCoordinator.NotifyUserSettingsInteraction invoked.");
}
public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled) public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled)
{ => TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update");
GeneralSettings? updatedSettings = null;
lock (_generalSettingsLock)
{
var repository = SettingsRepository<GeneralSettings>.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;
}
public void ReportBug() public void ReportBug()
{ {
@@ -131,20 +102,10 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}"); Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}");
} }
private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings) public void SendSortOrderUpdate(GeneralSettings generalSettings)
{ {
string payload; var outgoing = new OutGoingGeneralSettings(generalSettings);
try TrySendIpcMessage(outgoing.ToString(), "sort order update");
{
payload = new OutGoingGeneralSettings(updatedSettings).ToString();
}
catch (Exception ex)
{
Logger.LogError("QuickAccessCoordinator: failed to serialize general settings payload.", ex);
return;
}
TrySendIpcMessage(payload, "general settings update");
} }
private bool TrySendIpcMessage(string payload, string operationDescription) private bool TrySendIpcMessage(string payload, string operationDescription)

View File

@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.QuickAccess.ViewModels;
public sealed class AllAppsViewModel : Observable public sealed class AllAppsViewModel : Observable
{ {
private readonly object _sortLock = new object();
private readonly IQuickAccessCoordinator _coordinator; private readonly IQuickAccessCoordinator _coordinator;
private readonly ISettingsRepository<GeneralSettings> _settingsRepository; private readonly ISettingsRepository<GeneralSettings> _settingsRepository;
private readonly SettingsUtils _settingsUtils; private readonly SettingsUtils _settingsUtils;
@@ -30,6 +31,9 @@ public sealed class AllAppsViewModel : Observable
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new(); private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
private GeneralSettings _generalSettings; private GeneralSettings _generalSettings;
// Flag to prevent toggle operations during sorting to avoid race conditions.
private bool _isSorting;
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; } public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
public DashboardSortOrder DashboardSortOrder public DashboardSortOrder DashboardSortOrder
@@ -40,9 +44,9 @@ public sealed class AllAppsViewModel : Observable
if (_generalSettings.DashboardSortOrder != value) if (_generalSettings.DashboardSortOrder != value)
{ {
_generalSettings.DashboardSortOrder = value; _generalSettings.DashboardSortOrder = value;
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName()); _coordinator.SendSortOrderUpdate(_generalSettings);
OnPropertyChanged(); OnPropertyChanged();
RefreshFlyoutMenuItems(); SortFlyoutMenuItems();
} }
} }
} }
@@ -54,7 +58,6 @@ public sealed class AllAppsViewModel : Observable
_settingsUtils = SettingsUtils.Default; _settingsUtils = SettingsUtils.Default;
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils); _settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
_generalSettings = _settingsRepository.SettingsConfig; _generalSettings = _settingsRepository.SettingsConfig;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
_settingsRepository.SettingsChanged += OnSettingsChanged; _settingsRepository.SettingsChanged += OnSettingsChanged;
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; _resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
@@ -87,7 +90,6 @@ public sealed class AllAppsViewModel : Observable
_dispatcherQueue.TryEnqueue(() => _dispatcherQueue.TryEnqueue(() =>
{ {
_generalSettings = newSettings; _generalSettings = newSettings;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
OnPropertyChanged(nameof(DashboardSortOrder)); OnPropertyChanged(nameof(DashboardSortOrder));
RefreshFlyoutMenuItems(); RefreshFlyoutMenuItems();
}); });
@@ -120,30 +122,55 @@ public sealed class AllAppsViewModel : Observable
} }
} }
var sortedItems = DashboardSortOrder switch SortFlyoutMenuItems();
{ }
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
};
if (FlyoutMenuItems.Count == 0) private void SortFlyoutMenuItems()
{
if (_isSorting)
{ {
foreach (var item in sortedItems)
{
FlyoutMenuItems.Add(item);
}
return; return;
} }
for (int i = 0; i < sortedItems.Count; i++) lock (_sortLock)
{ {
var item = sortedItems[i]; _isSorting = true;
var oldIndex = FlyoutMenuItems.IndexOf(item); try
if (oldIndex != -1 && oldIndex != i)
{ {
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) private void EnabledChangedOnUI(ModuleListItem item)
{ {
var flyoutItem = (FlyoutMenuItem)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(); flyoutItem.UpdateStatus(!isEnabled);
return;
// Trigger re-sort immediately when status changes on UI
RefreshFlyoutMenuItems();
} }
}
private void ModuleEnabledChangedOnSettingsPage() _coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled);
{ SortFlyoutMenuItems();
RefreshFlyoutMenuItems();
} }
} }

View File

@@ -117,5 +117,47 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break; case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break;
} }
} }
/// <summary>
/// Gets the module key name used in IPC messages and settings JSON.
/// These names match the JsonPropertyName attributes in EnabledModules class.
/// </summary>
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(),
};
}
} }
} }

View File

@@ -65,11 +65,19 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void SortAlphabetical_Click(object sender, RoutedEventArgs e) private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
{ {
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
} }
private void SortByStatus_Click(object sender, RoutedEventArgs e) private void SortByStatus_Click(object sender, RoutedEventArgs e)
{ {
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
} }
} }
} }

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class DashboardViewModel : PageViewModelBase public partial class DashboardViewModel : PageViewModelBase
{ {
private readonly object _sortLock = new object();
protected override string ModuleName => "Dashboard"; protected override string ModuleName => "Dashboard";
private Dispatcher dispatcher; 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. // Flag to prevent circular updates when a UI toggle triggers settings changes.
private bool _isUpdatingFromUI; private bool _isUpdatingFromUI;
// Flag to prevent toggle operations during sorting to avoid race conditions.
private bool _isSorting;
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public AllHotkeyConflictsData AllHotkeyConflictsData public AllHotkeyConflictsData AllHotkeyConflictsData
@@ -80,15 +85,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => generalSettingsConfig.DashboardSortOrder; get => generalSettingsConfig.DashboardSortOrder;
set set
{ {
if (Set(ref _dashboardSortOrder, value)) if (_dashboardSortOrder != value)
{ {
_dashboardSortOrder = value;
generalSettingsConfig.DashboardSortOrder = value; generalSettingsConfig.DashboardSortOrder = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig); OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
// Save settings to file
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
SendConfigMSG(outgoing.ToString()); SendConfigMSG(outgoing.ToString());
// Notify UI before sorting so menu updates its checked state
OnPropertyChanged(nameof(DashboardSortOrder));
SortModuleList(); SortModuleList();
} }
} }
@@ -103,7 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
dispatcher = Dispatcher.CurrentDispatcher; dispatcher = Dispatcher.CurrentDispatcher;
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
generalSettingsConfig = settingsRepository.SettingsConfig; generalSettingsConfig = settingsRepository.SettingsConfig;
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
_settingsRepository.SettingsChanged += OnSettingsChanged; _settingsRepository.SettingsChanged += OnSettingsChanged;
// Initialize dashboard sort order from settings // Initialize dashboard sort order from settings
@@ -128,7 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
dispatcher.BeginInvoke(() => dispatcher.BeginInvoke(() =>
{ {
generalSettingsConfig = newSettings; 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(); 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. /// 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 /// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
/// to avoid destroying and recreating UI elements. /// to avoid destroying and recreating UI elements.
/// Temporarily disables interaction on all items during sorting to prevent race conditions.
/// </summary> /// </summary>
private void SortModuleList() 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; return;
} }
// Otherwise, update the collection in place using Move to avoid UI glitches. lock (_sortLock)
for (int i = 0; i < sortedItems.Count; i++)
{ {
var currentItem = sortedItems[i]; _isSorting = true;
var currentIndex = AllModules.IndexOf(currentItem); try
if (currentIndex != -1 && currentIndex != i)
{ {
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));
} }
/// <summary> /// <summary>
@@ -279,10 +311,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var dashboardListItem = (DashboardListItem)item; var dashboardListItem = (DashboardListItem)item;
var isEnabled = dashboardListItem.IsEnabled; 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; _isUpdatingFromUI = true;
try 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) if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
{ {