From 8ac747c832c6b8acb6a6988eb139744af468cddf Mon Sep 17 00:00:00 2001 From: "Muyuan Li (from Dev Box)" Date: Tue, 21 Jul 2026 16:42:38 +0800 Subject: [PATCH] FancyZones: apply edited custom layout spacing/sensitivity/zoneCount to active work areas immediately When a custom layout is edited in the FancyZones editor, active work areas already using that layout only refreshed the grid shape/edges. Scalar properties (spacing, sensitivity radius, and zone count) were read from a stale applied-layouts snapshot, so edits to them did not take effect on existing windows until PowerToys/FancyZones was restarted. WorkArea::CalculateZoneSet now re-derives the scalar properties from the live CustomLayouts store (CustomLayouts::instance().GetLayout(uuid)) for Custom-type layouts, matching the values used on the apply path. Scoped to Custom layouts only; at apply-time the values equal the snapshot, so only genuine edits change behavior. Adds a regression test (EditedCustomLayoutSpacingRefreshesExistingWorkArea) that creates a work area, edits the custom layout spacing, re-inits the layout, and asserts the new spacing gap is applied to existing zones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19f7171d-0ca4-48dc-95ef-e50f505a9097 --- .../fancyzones/FancyZonesLib/FancyZones.cpp | 1 + .../fancyzones/FancyZonesLib/WorkArea.cpp | 18 +++- .../UnitTests/WorkArea.Spec.cpp | 98 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index 77a2ca6a7ae9..a3e5c8484243 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -739,6 +739,7 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa else if (message == WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE) { CustomLayouts::instance().LoadData(); + RefreshLayouts(); } else if (message == WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE) { diff --git a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp index 717a13492b21..ef112c5b4ff3 100644 --- a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp @@ -5,6 +5,7 @@ #include "FancyZonesData/AppliedLayouts.h" #include "FancyZonesData/AppZoneHistory.h" +#include "FancyZonesData/CustomLayouts.h" #include "ZonesOverlay.h" #include "Settings.h" #include @@ -303,13 +304,28 @@ void WorkArea::InitSnappedWindows() void WorkArea::CalculateZoneSet() { - const auto appliedLayout = AppliedLayouts::instance().GetDeviceLayout(m_uniqueId); + auto appliedLayout = AppliedLayouts::instance().GetDeviceLayout(m_uniqueId); if (!appliedLayout.has_value()) { Logger::error(L"Layout wasn't applied. Can't init layout on work area {}x{}", m_workAreaRect.width(), m_workAreaRect.height()); return; } + // For custom layouts the spacing, sensitivity radius and zone count live in the custom + // layout definition (custom-layouts.json), while the applied-layouts.json snapshot keeps a + // copy taken at apply time. Editing those properties only rewrites custom-layouts.json, so + // without this sync the snapshot stays stale and the edits don't take effect until the layout + // is re-applied (see GH #44058). Re-derive the scalar properties from the current custom + // layout using the same logic the apply path uses (CustomLayouts::GetLayout also derives the + // canvas zone count from the zone list, keeping it consistent with Layout::Init validation). + if (appliedLayout->type == FancyZonesDataTypes::ZoneSetLayoutType::Custom) + { + if (const auto refreshed = CustomLayouts::instance().GetLayout(appliedLayout->uuid)) + { + appliedLayout = refreshed; + } + } + m_layout = std::make_unique(appliedLayout.value()); m_layout->Init(m_workAreaRect, m_uniqueId.monitorId.monitor); } diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkArea.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkArea.Spec.cpp index ce3963a3eb4d..150f76ad846d 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkArea.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/WorkArea.Spec.cpp @@ -1,10 +1,13 @@ #include "pch.h" +#include #include +#include #include #include #include +#include #include #include #include @@ -37,6 +40,7 @@ namespace FancyZonesUnitTests AppZoneHistory::instance().LoadData(); AppliedLayouts::instance().LoadData(); + CustomLayouts::instance().LoadData(); DefaultLayouts::instance().LoadData(); } @@ -44,6 +48,7 @@ namespace FancyZonesUnitTests { std::filesystem::remove(AppliedLayouts::AppliedLayoutsFileName()); std::filesystem::remove(AppZoneHistory::AppZoneHistoryFileName()); + std::filesystem::remove(CustomLayouts::CustomLayoutsFileName()); std::filesystem::remove(DefaultLayouts::DefaultLayoutsFileName()); } @@ -173,6 +178,99 @@ namespace FancyZonesUnitTests Assert::AreEqual(static_cast(4), actualLayout->Zones().size()); Assert::IsTrue(GUID_NULL == actualLayout->Id()); } + + // Saves a 1x2 custom grid layout to custom-layouts.json so it can be resolved by uuid. + void SaveCustomGridLayout(const GUID& uuid, bool showSpacing, int spacing, int sensitivityRadius) + { + json::JsonObject root{}; + json::JsonArray layoutsArray{}; + + json::JsonObject gridLayoutJson{}; + gridLayoutJson.SetNamedValue(NonLocalizable::CustomLayoutsIds::UuidID, json::value(FancyZonesUtils::GuidToString(uuid).value())); + gridLayoutJson.SetNamedValue(NonLocalizable::CustomLayoutsIds::NameID, json::value(L"Custom grid layout")); + gridLayoutJson.SetNamedValue(NonLocalizable::CustomLayoutsIds::TypeID, json::value(NonLocalizable::CustomLayoutsIds::GridID)); + + json::JsonArray rowsPercentage{}; + rowsPercentage.Append(json::value(10000)); + + json::JsonArray columnsPercentage{}; + columnsPercentage.Append(json::value(5000)); + columnsPercentage.Append(json::value(5000)); + + json::JsonArray cells{}; + { + json::JsonArray cellsRow{}; + cellsRow.Append(json::value(0)); + cellsRow.Append(json::value(1)); + cells.Append(cellsRow); + } + + json::JsonObject info{}; + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::RowsID, json::value(1)); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::ColumnsID, json::value(2)); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::RowsPercentageID, rowsPercentage); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::ColumnsPercentageID, columnsPercentage); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::CellChildMapID, cells); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::ShowSpacingID, json::value(showSpacing)); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::SpacingID, json::value(spacing)); + info.SetNamedValue(NonLocalizable::CustomLayoutsIds::SensitivityRadiusID, json::value(sensitivityRadius)); + + gridLayoutJson.SetNamedValue(NonLocalizable::CustomLayoutsIds::InfoID, info); + layoutsArray.Append(gridLayoutJson); + root.SetNamedValue(NonLocalizable::CustomLayoutsIds::CustomLayoutsArrayID, layoutsArray); + + json::to_file(CustomLayouts::CustomLayoutsFileName(), root); + CustomLayouts::instance().LoadData(); + } + + // Regression test for GH #44058: editing a custom layout's spacing only rewrites + // custom-layouts.json, while applied-layouts.json keeps a snapshot taken at apply time. + // An already-created WorkArea must pick up the edited spacing when it is refreshed + // (WorkArea::InitLayout, which is what the custom-layouts file-update handler calls via + // RefreshLayouts). Without re-deriving the scalar properties from the current custom + // layout the stale snapshot is used and the edit has no effect until re-apply. + TEST_METHOD (EditedCustomLayoutSpacingRefreshesExistingWorkArea) + { + const auto uuid = FancyZonesUtils::GuidFromString(L"{5A9D6A0F-4C6E-4C0C-8B1B-9F3E7C2D1A11}").value(); + + auto zoneGap = [](const auto& zones) -> LONG { + std::vector rects; + for (const auto& [id, zone] : zones) + { + rects.push_back(zone.GetZoneRect()); + } + std::sort(rects.begin(), rects.end(), [](const RECT& a, const RECT& b) { return a.left < b.left; }); + return rects[1].left - rects[0].right; + }; + + // The layout and its applied snapshot initially have no spacing. + SaveCustomGridLayout(uuid, /*showSpacing*/ false, /*spacing*/ 0, /*sensitivityRadius*/ 5); + LayoutData snapshot{ + .uuid = uuid, + .type = FancyZonesDataTypes::ZoneSetLayoutType::Custom, + .showSpacing = false, + .spacing = 0, + .zoneCount = 2, + .sensitivityRadius = 5, + }; + AppliedLayouts::instance().ApplyLayout(m_workAreaId, snapshot); + + auto workArea = WorkArea::Create({}, m_workAreaId, m_emptyUniqueId, m_workAreaRect); + Assert::IsFalse(workArea == nullptr); + Assert::IsNotNull(workArea->GetLayout().get()); + Assert::AreEqual(static_cast(2), workArea->GetLayout()->Zones().size()); + Assert::AreEqual(0L, zoneGap(workArea->GetLayout()->Zones()), L"Zones should be adjacent before the edit"); + + // User edits the layout to add spacing; only custom-layouts.json is rewritten, the + // applied-layouts snapshot stays stale. + SaveCustomGridLayout(uuid, /*showSpacing*/ true, /*spacing*/ 100, /*sensitivityRadius*/ 30); + + // The custom-layouts file-update handler refreshes active work areas via InitLayout. + workArea->InitLayout(); + + Assert::AreEqual(static_cast(2), workArea->GetLayout()->Zones().size()); + Assert::IsTrue(zoneGap(workArea->GetLayout()->Zones()) > 20, L"Edited spacing was not applied to the existing work area"); + } }; TEST_CLASS (WorkAreaSnapUnitTests)