From e148a8928869f38c335bd67aed6893eaf64c2548 Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:40:32 +0800 Subject: [PATCH] Powertoys Extension: Trigger latest layout/monitor refresh for fancyzone in cmdpal extension (#44756) ## Summary of the Pull Request Best effort trigger refresh for the monitor/layout refresh for fancyzones, user may experience error for first time run but will get correct result in next visit. This bring better experience than the always-stale-state, while still remain performant command. ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **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 Validated locally that second time visit will bring correct monitor when plug/unplug monitor --- .../Helpers/FancyZonesDataService.cs | 34 +++++++++++++------ .../Helpers/FancyZonesNotifier.cs | 12 +++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs index 639128d7f7..d81a6f9e11 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs @@ -41,16 +41,13 @@ internal static class FancyZonesDataService try { - if (!File.Exists(FZPaths.EditorParameters)) - { - error = Resources.FancyZones_MonitorDataNotFound; - Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}"); - return false; - } - - Logger.LogInfo($"TryGetMonitors: File exists, reading..."); - var editorParams = FancyZonesDataIO.ReadEditorParameters(); - Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}"); + // Request FancyZones to save current monitor configuration. + // The editor-parameters.json file is only written when: + // 1. Opening the FancyZones Editor + // 2. Receiving the WM_PRIV_SAVE_EDITOR_PARAMETERS message + // Without this, monitor changes (plug/unplug) won't be reflected in the file. + var editorParams = ReadEditorParametersWithRefresh(); + Logger.LogInfo($"TryGetMonitors: ReadEditorParametersWithRefreshWithRefresh returned. Monitors={editorParams.Monitors?.Count ?? -1}"); var editorMonitors = editorParams.Monitors; if (editorMonitors is null || editorMonitors.Count == 0) @@ -74,6 +71,23 @@ internal static class FancyZonesDataService } } + /// + /// Requests FancyZones to save the current monitor configuration and reads the file. + /// This is a best-effort approach for performance: we send the save request and immediately + /// read the file without waiting. If the file hasn't been updated yet, the next call will + /// see the updated data since FancyZones processes the message asynchronously. + /// + private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh() + { + // Request FancyZones to save the current monitor configuration. + // This is fire-and-forget for performance - we don't wait for the save to complete. + // If this is the first call after a monitor change, we may read stale data, but the + // next call will see the updated file since FancyZones will have processed the message. + FancyZonesNotifier.NotifySaveEditorParameters(); + + return FancyZonesDataIO.ReadEditorParameters(); + } + public static IReadOnlyList GetLayouts() { Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs index ada8ee7b17..256331a951 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs @@ -10,13 +10,25 @@ namespace PowerToysExtension.Helpers; internal static class FancyZonesNotifier { private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}"; + private const string SaveEditorParametersMessage = "{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}"; + private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage); + private static readonly uint WmPrivSaveEditorParameters = RegisterWindowMessageW(SaveEditorParametersMessage); public static void NotifyAppliedLayoutsChanged() { _ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero); } + /// + /// Notifies FancyZones to save the current monitor configuration to editor-parameters.json. + /// This is needed because FancyZones only writes this file when opening the editor or when explicitly requested. + /// + public static void NotifySaveEditorParameters() + { + _ = PostMessageW(new IntPtr(0xFFFF), WmPrivSaveEditorParameters, UIntPtr.Zero, IntPtr.Zero); + } + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern uint RegisterWindowMessageW(string lpString);