mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-10 21:41:51 +02:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user