From 53f830bb3893cedf06497c00c7f8f14b7d8e70c9 Mon Sep 17 00:00:00 2001 From: stefansjfw <57057282+stefansjfw@users.noreply.github.com> Date: Mon, 10 Feb 2020 14:59:51 +0100 Subject: [PATCH] Migrate FancyZones data persisting from Registry to JSON file (#1194) * Migrate FancyZones data persisting from Registry to JSON file * Address PR comment: Remove redundant check * Addres PR comment: Remove unused Dpi and add CmdArgs enum * Address PR comment: Make methods const and inline * Address PR comments: Expose GenerateUniqueId function and use const ref instead of passing wstring by value * Address PR comment: Use lamdba as callback * Address PR comment: Move GenerateUniqueId to ZoneWindowUtils namespace * Address PR comment: Use regular comparison instead of std::wstring::compare * Address PR comment: Use std::wstring_view for tmp file paths * Address PR comment: Use scoped lock when accessing member data * Address PR comment: Remove typedefs to increase code readability * Address PR comment: removed nullptr checks with corresponding tests * Address PR comment: Move ZoneSet object instead of copying * Address PR comment: Make FancyZonesData instance const where possible * Remove unnecessary gutter variable during calculating zone coordinates * Remove uneeded subclass * Avoid unnecessary copying and reserve space for vector if possible * Save FancyZones data after exiting editor * App zone history (#18) * added window and zone set ids to app zone history * Rename JSON file * Remove AppZoneHistory migration * Move parsing of ZoneWindow independent temp files outside of it * Unit tests update (#19) * check device existence in map * updated ZoneSet tests * updated JsonHelpers tests * Use single zone count information * Remove uneeded tests * Remove one more test * Remove uneeded line * Address PR comments - Missing whitespace * Update zoneset data for new virtual desktops (#21) * update active zone set with actual data * Introduce Blank zone set (used to indicate that no layout applied yet). Move parsing completely outside of ZoneWindow. * Fix unit tests to match modifications in implementation * Fix applying layouts on startup (second monitor) Co-authored-by: vldmr11080 <57061786+vldmr11080@users.noreply.github.com> Co-authored-by: Seraphima --- src/common/settings_helpers.h | 2 + .../fancyzones/dll/FancyZonesModule.vcxproj | 3 - .../dll/FancyZonesModule.vcxproj.filters | 1 - src/modules/fancyzones/dll/dllmain.cpp | 76 +- src/modules/fancyzones/dll/fancyzones.def | 4 - .../editor/FancyZonesEditor/App.xaml.cs | 36 +- .../FancyZonesEditor/EditorOverlay.xaml.cs | 19 +- .../FancyZonesEditor/FancyZonesEditor.csproj | 6 +- .../FancyZonesEditor/MainWindow.xaml.cs | 18 +- .../Models/CanvasLayoutModel.cs | 118 +- .../Models/GridLayoutModel.cs | 385 ++-- .../FancyZonesEditor/Models/LayoutModel.cs | 220 ++- .../FancyZonesEditor/Models/Settings.cs | 177 +- src/modules/fancyzones/lib/FancyZones.cpp | 266 ++- src/modules/fancyzones/lib/FancyZones.h | 14 +- .../fancyzones/lib/FancyZonesLib.vcxproj | 3 + .../lib/FancyZonesLib.vcxproj.filters | 10 +- src/modules/fancyzones/lib/JsonHelpers.cpp | 950 ++++++++++ src/modules/fancyzones/lib/JsonHelpers.h | 240 +++ src/modules/fancyzones/lib/RegistryHelpers.h | 126 -- src/modules/fancyzones/lib/Settings.cpp | 18 +- src/modules/fancyzones/lib/Settings.h | 2 +- src/modules/fancyzones/lib/Zone.cpp | 2 +- src/modules/fancyzones/lib/Zone.h | 2 +- src/modules/fancyzones/lib/ZoneSet.cpp | 530 +++++- src/modules/fancyzones/lib/ZoneSet.h | 33 +- src/modules/fancyzones/lib/ZoneWindow.cpp | 1531 +++++++-------- src/modules/fancyzones/lib/ZoneWindow.h | 11 +- src/modules/fancyzones/lib/util.cpp | 27 + src/modules/fancyzones/lib/util.h | 12 +- .../tests/UnitTests/FancyZones.Spec.cpp | 475 +++++ .../UnitTests/FancyZonesSettings.Spec.cpp | 714 +++++++ .../tests/UnitTests/JsonHelpers.Tests.cpp | 1659 +++++++++++++++++ .../tests/UnitTests/RegistryHelpers.Spec.cpp | 35 - .../tests/UnitTests/UnitTests.vcxproj | 5 +- .../tests/UnitTests/UnitTests.vcxproj.filters | 17 +- .../fancyzones/tests/UnitTests/Util.cpp | 149 ++ src/modules/fancyzones/tests/UnitTests/Util.h | 9 +- .../fancyzones/tests/UnitTests/Zone.Spec.cpp | 448 ++++- .../tests/UnitTests/ZoneSet.Spec.cpp | 1368 ++++++++++++-- .../tests/UnitTests/ZoneWindow.Spec.cpp | 680 ++++++- 41 files changed, 8496 insertions(+), 1905 deletions(-) delete mode 100644 src/modules/fancyzones/dll/fancyzones.def create mode 100644 src/modules/fancyzones/lib/JsonHelpers.cpp create mode 100644 src/modules/fancyzones/lib/JsonHelpers.h create mode 100644 src/modules/fancyzones/lib/util.cpp create mode 100644 src/modules/fancyzones/tests/UnitTests/FancyZones.Spec.cpp create mode 100644 src/modules/fancyzones/tests/UnitTests/FancyZonesSettings.Spec.cpp create mode 100644 src/modules/fancyzones/tests/UnitTests/JsonHelpers.Tests.cpp delete mode 100644 src/modules/fancyzones/tests/UnitTests/RegistryHelpers.Spec.cpp create mode 100644 src/modules/fancyzones/tests/UnitTests/Util.cpp diff --git a/src/common/settings_helpers.h b/src/common/settings_helpers.h index 6143bc2e5c..9dba9359ca 100644 --- a/src/common/settings_helpers.h +++ b/src/common/settings_helpers.h @@ -6,6 +6,8 @@ namespace PTSettingsHelper { + std::wstring get_module_save_folder_location(std::wstring_view powertoy_name); + void save_module_settings(std::wstring_view powertoy_name, json::JsonObject& settings); json::JsonObject load_module_settings(std::wstring_view powertoy_name); void save_general_settings(const json::JsonObject& settings); diff --git a/src/modules/fancyzones/dll/FancyZonesModule.vcxproj b/src/modules/fancyzones/dll/FancyZonesModule.vcxproj index ce37e749c2..ad6aefab2a 100644 --- a/src/modules/fancyzones/dll/FancyZonesModule.vcxproj +++ b/src/modules/fancyzones/dll/FancyZonesModule.vcxproj @@ -71,7 +71,6 @@ true $(OutDir)$(TargetName)$(TargetExt) gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies) - fancyzones.def @@ -96,7 +95,6 @@ true $(OutDir)$(TargetName)$(TargetExt) gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies) - fancyzones.def @@ -121,7 +119,6 @@ - diff --git a/src/modules/fancyzones/dll/FancyZonesModule.vcxproj.filters b/src/modules/fancyzones/dll/FancyZonesModule.vcxproj.filters index 33fe26da87..10d25e8762 100644 --- a/src/modules/fancyzones/dll/FancyZonesModule.vcxproj.filters +++ b/src/modules/fancyzones/dll/FancyZonesModule.vcxproj.filters @@ -26,7 +26,6 @@ - diff --git a/src/modules/fancyzones/dll/dllmain.cpp b/src/modules/fancyzones/dll/dllmain.cpp index e5056c13e1..1358c5ed1c 100644 --- a/src/modules/fancyzones/dll/dllmain.cpp +++ b/src/modules/fancyzones/dll/dllmain.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -33,76 +32,6 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReser return TRUE; } -// This function is exported and called from FancyZonesEditor.exe to save a layout from the editor. -STDAPI PersistZoneSet( - PCWSTR activeKey, // Registry key holding ActiveZoneSet - PCWSTR resolutionKey, // Registry key to persist ZoneSet to - HMONITOR monitor, - WORD layoutId, // LayoutModel Id - int zoneCount, // Number of zones in zones - int zones[]) // Array of zones serialized in left/top/right/bottom chunks -{ - // See if we have already persisted this layout we can update. - UUID id{GUID_NULL}; - if (wil::unique_hkey key{ RegistryHelpers::OpenKey(resolutionKey) }) - { - ZoneSetPersistedData data{}; - DWORD dataSize = sizeof(data); - wchar_t value[256]{}; - DWORD valueLength = ARRAYSIZE(value); - DWORD i = 0; - while (RegEnumValueW(key.get(), i++, value, &valueLength, nullptr, nullptr, reinterpret_cast(&data), &dataSize) == ERROR_SUCCESS) - { - if (data.LayoutId == layoutId) - { - if (data.ZoneCount == zoneCount) - { - CLSIDFromString(value, &id); - break; - } - } - valueLength = ARRAYSIZE(value); - dataSize = sizeof(data); - } - } - - if (id == GUID_NULL) - { - // No existing layout found so let's create a new one. - UuidCreate(&id); - } - - if (id != GUID_NULL) - { - winrt::com_ptr zoneSet = MakeZoneSet( - ZoneSetConfig( - id, - layoutId, - MonitorFromPoint({}, MONITOR_DEFAULTTOPRIMARY), - resolutionKey)); - - for (int i = 0; i < zoneCount; i++) - { - const int baseIndex = i * 4; - const int left = zones[baseIndex]; - const int top = zones[baseIndex+1]; - const int right = zones[baseIndex+2]; - const int bottom = zones[baseIndex+3]; - zoneSet->AddZone(MakeZone({ left, top, right, bottom })); - } - zoneSet->Save(); - - wil::unique_cotaskmem_string zoneSetId; - if (SUCCEEDED_LOG(StringFromCLSID(id, &zoneSetId))) - { - RegistryHelpers::SetString(activeKey, L"ActiveZoneSetId", zoneSetId.get()); - } - - return S_OK; - } - return E_FAIL; -} - class FancyZonesModule : public PowertoyModuleIface { public: @@ -147,7 +76,7 @@ public: if (!m_app) { Trace::FancyZones::EnableFancyZones(true); - m_app = MakeFancyZones(reinterpret_cast(&__ImageBase), m_settings.get()); + m_app = MakeFancyZones(reinterpret_cast(&__ImageBase), m_settings); if (m_app) { m_app->Run(); @@ -200,12 +129,15 @@ public: { app_name = GET_RESOURCE_STRING(IDS_FANCYZONES); m_settings = MakeFancyZonesSettings(reinterpret_cast(&__ImageBase), FancyZonesModule::get_name()); + JSONHelpers::FancyZonesDataInstance().LoadFancyZonesData(); } private: void Disable(bool const traceEvent) { if (m_app) { + const auto& fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + fancyZonesData.SaveFancyZonesData(); if (traceEvent) { Trace::FancyZones::EnableFancyZones(false); diff --git a/src/modules/fancyzones/dll/fancyzones.def b/src/modules/fancyzones/dll/fancyzones.def deleted file mode 100644 index 32441a425a..0000000000 --- a/src/modules/fancyzones/dll/fancyzones.def +++ /dev/null @@ -1,4 +0,0 @@ -LIBRARY fancyzones.dll - -EXPORTS - PersistZoneSet PRIVATE diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs index 72e529c85d..185dbc2d12 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs @@ -15,8 +15,6 @@ namespace FancyZonesEditor { public Settings ZoneSettings { get; } - private ushort _idInitial = 0; - public App() { ZoneSettings = new Settings(); @@ -24,37 +22,29 @@ namespace FancyZonesEditor private void OnStartup(object sender, StartupEventArgs e) { - if (e.Args.Length > 1) - { - ushort.TryParse(e.Args[1], out _idInitial); - } - LayoutModel foundModel = null; - if (_idInitial != 0) + foreach (LayoutModel model in ZoneSettings.DefaultModels) { - foreach (LayoutModel model in ZoneSettings.DefaultModels) + if (model.Type == Settings.ActiveZoneSetLayoutType) { - if (model.Id == _idInitial) + // found match + foundModel = model; + break; + } + } + + if (foundModel == null) + { + foreach (LayoutModel model in Settings.CustomModels) + { + if ("{" + model.Guid.ToString().ToUpper() + "}" == Settings.ActiveZoneSetUUid.ToUpper()) { // found match foundModel = model; break; } } - - if (foundModel == null) - { - foreach (LayoutModel model in ZoneSettings.CustomModels) - { - if (model.Id == _idInitial) - { - // found match - foundModel = model; - break; - } - } - } } if (foundModel == null) diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/EditorOverlay.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/EditorOverlay.xaml.cs index 4197c89a43..f51ae3ef50 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/EditorOverlay.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/EditorOverlay.xaml.cs @@ -18,8 +18,11 @@ namespace FancyZonesEditor private readonly Settings _settings = ((App)Application.Current).ZoneSettings; private LayoutPreview _layoutPreview; + private UserControl _editor; + private static MainWindow _mainWindow = new MainWindow(); + public Int32Rect[] GetZoneRects() { if (_editor != null) @@ -79,27 +82,23 @@ namespace FancyZonesEditor public void ShowLayoutPicker() { - DataContext = null; - _editor = null; _layoutPreview = new LayoutPreview { IsActualSize = true, Opacity = 0.5, }; + Content = _layoutPreview; - MainWindow window = new MainWindow - { - Owner = this, - ShowActivated = true, - Topmost = true, - }; - window.Show(); + _mainWindow.Owner = this; + _mainWindow.ShowActivated = true; + _mainWindow.Topmost = true; + _mainWindow.Show(); // window is set to topmost to make sure it shows on top of PowerToys settings page // we can reset topmost flag now - window.Topmost = false; + _mainWindow.Topmost = false; } // These event handlers are used to track the current state of the Shift and Ctrl keys on the keyboard diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj b/src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj index d110357db7..15dcade86e 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj +++ b/src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj @@ -47,6 +47,7 @@ + @@ -194,6 +195,9 @@ 2.0.0-alpha0455 + + 4.6.0 + 1.1.118 runtime; build; native; contentfiles; analyzers; buildtransitive @@ -208,4 +212,4 @@ - \ No newline at end of file + diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs index b646ab6b00..8481a5b113 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs @@ -21,7 +21,6 @@ namespace FancyZonesEditor public const int MaxZones = 40; private readonly Settings _settings = ((App)Application.Current).ZoneSettings; private static readonly string _defaultNamePrefix = "Custom Layout "; - private bool _editing = false; public int WrapPanelItemSize { get; set; } = 262; @@ -67,7 +66,7 @@ namespace FancyZonesEditor { WindowLayout window = new WindowLayout(); window.Show(); - Close(); + Hide(); } private void LayoutItem_Click(object sender, MouseButtonEventArgs e) @@ -95,12 +94,11 @@ namespace FancyZonesEditor } model.IsSelected = false; - _editing = true; - Close(); + Hide(); bool isPredefinedLayout = Settings.IsPredefinedLayout(model); - if (!_settings.CustomModels.Contains(model) || isPredefinedLayout) + if (!Settings.CustomModels.Contains(model) || isPredefinedLayout) { if (isPredefinedLayout) { @@ -110,7 +108,7 @@ namespace FancyZonesEditor } int maxCustomIndex = 0; - foreach (LayoutModel customModel in _settings.CustomModels) + foreach (LayoutModel customModel in Settings.CustomModels) { string name = customModel.Name; if (name.StartsWith(_defaultNamePrefix)) @@ -165,10 +163,8 @@ namespace FancyZonesEditor private void OnClosing(object sender, EventArgs e) { - if (!_editing) - { - EditorOverlay.Current.Close(); - } + LayoutModel.SerializeDeletedCustomZoneSets(); + EditorOverlay.Current.Close(); } private void OnInitialized(object sender, EventArgs e) @@ -178,7 +174,7 @@ namespace FancyZonesEditor private void SetSelectedItem() { - foreach (LayoutModel model in _settings.CustomModels) + foreach (LayoutModel model in Settings.CustomModels) { if (model.IsSelected) { diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/CanvasLayoutModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/CanvasLayoutModel.cs index 548463f222..094aa3a03a 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/CanvasLayoutModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/CanvasLayoutModel.cs @@ -2,7 +2,10 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; +using System.IO; +using System.Text.Json; using System.Windows; namespace FancyZonesEditor.Models @@ -11,40 +14,27 @@ namespace FancyZonesEditor.Models // Free form Layout Model, which specifies independent zone rects public class CanvasLayoutModel : LayoutModel { - private static readonly ushort _latestVersion = 0; - - public CanvasLayoutModel(ushort version, string name, ushort id, byte[] data) - : base(name, id) + public CanvasLayoutModel(string uuid, string name, LayoutType type, int referenceWidth, int referenceHeight, IList zones) + : base(uuid, name, type) { - if (version == _latestVersion) - { - Load(data); - } + _referenceWidth = referenceWidth; + _referenceHeight = referenceHeight; + Zones = zones; } - public CanvasLayoutModel(string name, ushort id, int referenceWidth, int referenceHeight) - : base(name, id) + public CanvasLayoutModel(string name, LayoutType type, int referenceWidth, int referenceHeight) + : base(name, type) { // Initialize Reference Size _referenceWidth = referenceWidth; _referenceHeight = referenceHeight; } - public CanvasLayoutModel(string name, ushort id) - : base(name, id) - { - } - public CanvasLayoutModel(string name) : base(name) { } - public CanvasLayoutModel() - : base() - { - } - // ReferenceWidth - the reference width for the layout rect that all Zones are relative to public int ReferenceWidth { @@ -104,26 +94,6 @@ namespace FancyZonesEditor.Models FirePropertyChanged("Zones"); } - private void Load(byte[] data) - { - // Initialize this CanvasLayoutModel based on the given persistence data - // Skip version (2 bytes), id (2 bytes), and type (1 bytes) - int i = 5; - _referenceWidth = (data[i++] * 256) + data[i++]; - _referenceHeight = (data[i++] * 256) + data[i++]; - - int count = data[i++]; - - while (count-- > 0) - { - Zones.Add(new Int32Rect( - (data[i++] * 256) + data[i++], - (data[i++] * 256) + data[i++], - (data[i++] * 256) + data[i++], - (data[i++] * 256) + data[i++])); - } - } - // Clone // Implements the LayoutModel.Clone abstract method // Clones the data from this CanvasLayoutModel to a new CanvasLayoutModel @@ -143,44 +113,50 @@ namespace FancyZonesEditor.Models return layout; } - // GetPersistData - // Implements the LayoutModel.GetPersistData abstract method - // Returns the state of this GridLayoutModel in persisted format - protected override byte[] GetPersistData() + // PersistData + // Implements the LayoutModel.PersistData abstract method + protected override void PersistData() { - byte[] data = new byte[10 + (Zones.Count * 8)]; - int i = 0; - - // Common persisted values between all layout types - data[i++] = (byte)(_latestVersion / 256); - data[i++] = (byte)(_latestVersion % 256); - data[i++] = 1; // LayoutModelType: 1 == CanvasLayoutModel - data[i++] = (byte)(Id / 256); - data[i++] = (byte)(Id % 256); - - // End common - data[i++] = (byte)(_referenceWidth / 256); - data[i++] = (byte)(_referenceWidth % 256); - data[i++] = (byte)(_referenceHeight / 256); - data[i++] = (byte)(_referenceHeight % 256); - data[i++] = (byte)Zones.Count; - - foreach (Int32Rect rect in Zones) + FileStream outputStream = File.Open(Settings.AppliedZoneSetTmpFile, FileMode.Create); + JsonWriterOptions writerOptions = new JsonWriterOptions { - data[i++] = (byte)(rect.X / 256); - data[i++] = (byte)(rect.X % 256); + SkipValidation = true, + }; + using (var writer = new Utf8JsonWriter(outputStream, writerOptions)) + { + writer.WriteStartObject(); + writer.WriteString("uuid", "{" + Guid.ToString().ToUpper() + "}"); + writer.WriteString("name", Name); - data[i++] = (byte)(rect.Y / 256); - data[i++] = (byte)(rect.Y % 256); + writer.WriteString("type", "canvas"); - data[i++] = (byte)(rect.Width / 256); - data[i++] = (byte)(rect.Width % 256); + writer.WriteStartObject("info"); - data[i++] = (byte)(rect.Height / 256); - data[i++] = (byte)(rect.Height % 256); + writer.WriteNumber("ref-width", _referenceWidth); + writer.WriteNumber("ref-height", _referenceHeight); + + writer.WriteStartArray("zones"); + foreach (Int32Rect rect in Zones) + { + writer.WriteStartObject(); + writer.WriteNumber("X", rect.X); + writer.WriteNumber("Y", rect.Y); + writer.WriteNumber("width", rect.Width); + writer.WriteNumber("height", rect.Height); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + + // end info object + writer.WriteEndObject(); + + // end root object + writer.WriteEndObject(); + writer.Flush(); } - return data; + outputStream.Close(); } } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/GridLayoutModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/GridLayoutModel.cs index 54860f2466..9641dbb63f 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/GridLayoutModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/GridLayoutModel.cs @@ -2,19 +2,20 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; - -namespace FancyZonesEditor.Models -{ - // GridLayoutModel - // Grid-styled Layout Model, which specifies rows, columns, percentage sizes, and row/column spans - public class GridLayoutModel : LayoutModel - { - private static readonly ushort _latestVersion = 0; - - // Rows - number of rows in the Grid - public int Rows - { +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace FancyZonesEditor.Models +{ + // GridLayoutModel + // Grid-styled Layout Model, which specifies rows, columns, percentage sizes, and row/column spans + public class GridLayoutModel : LayoutModel + { + // Rows - number of rows in the Grid + public int Rows + { get { return _rows; @@ -22,19 +23,19 @@ namespace FancyZonesEditor.Models set { - if (_rows != value) - { - _rows = value; - FirePropertyChanged("Rows"); + if (_rows != value) + { + _rows = value; + FirePropertyChanged("Rows"); } } - } - - private int _rows = 1; - - // Columns - number of columns in the Grid - public int Columns - { + } + + private int _rows = 1; + + // Columns - number of columns in the Grid + public int Columns + { get { return _cols; @@ -42,19 +43,19 @@ namespace FancyZonesEditor.Models set { - if (_cols != value) - { - _cols = value; - FirePropertyChanged("Columns"); + if (_cols != value) + { + _cols = value; + FirePropertyChanged("Columns"); } } - } - - private int _cols = 1; - - // CellChildMap - represents which "children" belong in which grid cells; - // shows spanning children by the same index appearing in adjacent cells - // TODO: ideally no setter here - this means moving logic like "split" over to model + } + + private int _cols = 1; + + // CellChildMap - represents which "children" belong in which grid cells; + // shows spanning children by the same index appearing in adjacent cells + // TODO: ideally no setter here - this means moving logic like "split" over to model public int[,] CellChildMap { get; set; } // RowPercents - represents the %age height of each row in the grid @@ -69,179 +70,159 @@ namespace FancyZonesEditor.Models public IList FreeZones { get; } = new List(); public GridLayoutModel() - : base() - { - } + : base() + { + } public GridLayoutModel(string name) : base(name) { } - public GridLayoutModel(string name, ushort id) - : base(name, id) - { - } - - public GridLayoutModel(ushort version, string name, ushort id, byte[] data) - : base(name, id) - { - if (version == _latestVersion) - { - Reload(data); - } + public GridLayoutModel(string name, LayoutType type) + : base(name, type) + { } - public void Reload(byte[] data) - { - // Skip version (2 bytes), id (2 bytes), and type (1 bytes) - int i = 5; - - Rows = data[i++]; - Columns = data[i++]; - - RowPercents = new int[Rows]; - for (int row = 0; row < Rows; row++) - { - RowPercents[row] = (data[i++] * 256) + data[i++]; - } - - ColumnPercents = new int[Columns]; - for (int col = 0; col < Columns; col++) - { - ColumnPercents[col] = (data[i++] * 256) + data[i++]; - } - - CellChildMap = new int[Rows, Columns]; - for (int row = 0; row < Rows; row++) - { - for (int col = 0; col < Columns; col++) - { - CellChildMap[row, col] = data[i++]; - } - } - } - - // Clone - // Implements the LayoutModel.Clone abstract method - // Clones the data from this GridLayoutModel to a new GridLayoutModel - public override LayoutModel Clone() - { - GridLayoutModel layout = new GridLayoutModel(Name); - int rows = Rows; - int cols = Columns; - - layout.Rows = rows; - layout.Columns = cols; - - int[,] cellChildMap = new int[rows, cols]; - for (int row = 0; row < rows; row++) - { - for (int col = 0; col < cols; col++) - { - cellChildMap[row, col] = CellChildMap[row, col]; - } - } - - layout.CellChildMap = cellChildMap; - - int[] rowPercents = new int[rows]; - for (int row = 0; row < rows; row++) - { - rowPercents[row] = RowPercents[row]; - } - - layout.RowPercents = rowPercents; - - int[] colPercents = new int[cols]; - for (int col = 0; col < cols; col++) - { - colPercents[col] = ColumnPercents[col]; - } - - layout.ColumnPercents = colPercents; - - return layout; - } - - // GetPersistData - // Implements the LayoutModel.GetPersistData abstract method - // Returns the state of this GridLayoutModel in persisted format - protected override byte[] GetPersistData() - { - int rows = Rows; - int cols = Columns; - - int[,] cellChildMap; - - if (FreeZones.Count == 0) - { - // no unused indices -- so we can just use the _cellChildMap as is - cellChildMap = CellChildMap; - } - else - { - // compress cellChildMap to not have gaps for unused child indices; - List mapping = new List(); - - cellChildMap = new int[rows, cols]; - - for (int row = 0; row < rows; row++) - { - for (int col = 0; col < cols; col++) - { - int source = CellChildMap[row, col]; - - int index = mapping.IndexOf(source); - if (index == -1) - { - index = mapping.Count; - mapping.Add(source); - } - - cellChildMap[row, col] = index; - } - } - } - - byte[] data = new byte[7 + (Rows * 2) + (Columns * 2) + (Rows * Columns)]; - - int i = 0; + public GridLayoutModel(string uuid, string name, LayoutType type, int rows, int cols, int[] rowPercents, int[] colsPercents, int[,] cellChildMap) + : base(uuid, name, type) + { + _rows = rows; + _cols = cols; + RowPercents = rowPercents; + ColumnPercents = colsPercents; + CellChildMap = cellChildMap; + } - // Common persisted values between all layout types - data[i++] = (byte)(_latestVersion / 256); - data[i++] = (byte)(_latestVersion % 256); - data[i++] = 0; // LayoutModelType: 0 == GridLayoutModel - data[i++] = (byte)(Id / 256); - data[i++] = (byte)(Id % 256); - - // End common - data[i++] = (byte)Rows; - data[i++] = (byte)Columns; - - for (int row = 0; row < Rows; row++) - { - int rowPercent = RowPercents[row]; - data[i++] = (byte)(rowPercent / 256); - data[i++] = (byte)(rowPercent % 256); - } - - for (int col = 0; col < Columns; col++) - { - int colPercent = ColumnPercents[col]; - data[i++] = (byte)(colPercent / 256); - data[i++] = (byte)(colPercent % 256); - } - - for (int row = 0; row < Rows; row++) - { - for (int col = 0; col < Columns; col++) - { - data[i++] = (byte)cellChildMap[row, col]; - } - } - - return data; - } - } -} + public void Reload(byte[] data) + { + // Skip version (2 bytes), id (2 bytes), and type (1 bytes) + int i = 5; + + Rows = data[i++]; + Columns = data[i++]; + + RowPercents = new int[Rows]; + for (int row = 0; row < Rows; row++) + { + RowPercents[row] = (data[i++] * 256) + data[i++]; + } + + ColumnPercents = new int[Columns]; + for (int col = 0; col < Columns; col++) + { + ColumnPercents[col] = (data[i++] * 256) + data[i++]; + } + + CellChildMap = new int[Rows, Columns]; + for (int row = 0; row < Rows; row++) + { + for (int col = 0; col < Columns; col++) + { + CellChildMap[row, col] = data[i++]; + } + } + } + + // Clone + // Implements the LayoutModel.Clone abstract method + // Clones the data from this GridLayoutModel to a new GridLayoutModel + public override LayoutModel Clone() + { + GridLayoutModel layout = new GridLayoutModel(Name); + int rows = Rows; + int cols = Columns; + + layout.Rows = rows; + layout.Columns = cols; + + int[,] cellChildMap = new int[rows, cols]; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + cellChildMap[row, col] = CellChildMap[row, col]; + } + } + + layout.CellChildMap = cellChildMap; + + int[] rowPercents = new int[rows]; + for (int row = 0; row < rows; row++) + { + rowPercents[row] = RowPercents[row]; + } + + layout.RowPercents = rowPercents; + + int[] colPercents = new int[cols]; + for (int col = 0; col < cols; col++) + { + colPercents[col] = ColumnPercents[col]; + } + + layout.ColumnPercents = colPercents; + + return layout; + } + + // PersistData + // Implements the LayoutModel.PersistData abstract method + protected override void PersistData() + { + FileStream outputStream = File.Open(Settings.AppliedZoneSetTmpFile, FileMode.Create); + using (var writer = new Utf8JsonWriter(outputStream, options: default)) + { + writer.WriteStartObject(); + writer.WriteString("uuid", "{" + Guid.ToString().ToUpper() + "}"); + writer.WriteString("name", Name); + + writer.WriteString("type", "grid"); + + writer.WriteStartObject("info"); + + writer.WriteNumber("rows", Rows); + writer.WriteNumber("columns", Columns); + + writer.WriteStartArray("rows-percentage"); + for (int row = 0; row < Rows; row++) + { + writer.WriteNumberValue(RowPercents[row]); + } + + writer.WriteEndArray(); + + writer.WriteStartArray("columns-percentage"); + for (int col = 0; col < Columns; col++) + { + writer.WriteNumberValue(ColumnPercents[col]); + } + + writer.WriteEndArray(); + + writer.WriteStartArray("cell-child-map"); + for (int row = 0; row < Rows; row++) + { + writer.WriteStartArray(); + for (int col = 0; col < Columns; col++) + { + writer.WriteNumberValue(CellChildMap[row, col]); + } + + writer.WriteEndArray(); + } + + writer.WriteEndArray(); + + // end info object + writer.WriteEndObject(); + + // end root object + writer.WriteEndObject(); + writer.Flush(); + } + + outputStream.Close(); + } + } +} diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs index 1d431c913f..e8b22bf185 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs @@ -1,15 +1,28 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Runtime.InteropServices; -using Microsoft.Win32; +using System.IO; +using System.Text.Json; +using System.Windows; namespace FancyZonesEditor.Models { + public enum LayoutType + { + Blank = -1, + Focus, + Columns, + Rows, + Grid, + PriorityGrid, + Custom, + } + // Base LayoutModel // Manages common properties and base persistence public abstract class LayoutModel : INotifyPropertyChanged @@ -19,18 +32,30 @@ namespace FancyZonesEditor.Models protected LayoutModel() { + _guid = Guid.NewGuid(); + Type = LayoutType.Custom; } protected LayoutModel(string name) : this() { + _guid = Guid.NewGuid(); Name = name; } - protected LayoutModel(string name, ushort id) + protected LayoutModel(string uuid, string name, LayoutType type) + : this() + { + _guid = Guid.Parse(uuid); + Name = name; + Type = type; + } + + protected LayoutModel(string name, LayoutType type) : this(name) { - _id = id; + _guid = Guid.NewGuid(); + Type = type; } // Name - the display name for this layout model - is also used as the key in the registry @@ -53,22 +78,17 @@ namespace FancyZonesEditor.Models private string _name; - // Id - the unique ID for this layout model - is used to connect fancy zones' ZonesSets with the editor's Layouts - // - note: 0 means this is a new layout, which means it will have its ID auto-assigned on persist - public ushort Id + public LayoutType Type { get; set; } + + public Guid Guid { get { - if (_id == 0) - { - _id = ++_maxId; - } - - return _id; + return _guid; } } - private ushort _id = 0; + private Guid _guid; // IsSelected (not-persisted) - tracks whether or not this LayoutModel is selected in the picker // TODO: once we switch to a picker per monitor, we need to move this state to the view @@ -103,51 +123,99 @@ namespace FancyZonesEditor.Models // Removes this Layout from the registry and the loaded CustomModels list public void Delete() { - RegistryKey key = Registry.CurrentUser.OpenSubKey(_registryPath, true); - if (key != null) - { - key.DeleteValue(Name); - } - int i = _customModels.IndexOf(this); if (i != -1) { _customModels.RemoveAt(i); + _deletedCustomModels.Add(Guid.ToString().ToUpper()); } } - // Loads all the Layouts persisted under the Layouts key in the registry + public static void SerializeDeletedCustomZoneSets() + { + FileStream outputStream = File.Open(Settings.CustomZoneSetsTmpFile, FileMode.Create); + var writer = new Utf8JsonWriter(outputStream, options: default); + writer.WriteStartObject(); + writer.WriteStartArray("deleted-custom-zone-sets"); + foreach (string zoneSet in _deletedCustomModels) + { + writer.WriteStringValue(zoneSet); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + writer.Flush(); + outputStream.Close(); + } + + // Loads all the custom Layouts from tmp file passed by FancuZonesLib public static ObservableCollection LoadCustomModels() { _customModels = new ObservableCollection(); - RegistryKey key = Registry.CurrentUser.OpenSubKey(_registryPath); - if (key != null) + FileStream inputStream = File.Open(Settings.CustomZoneSetsTmpFile, FileMode.Open); + var jsonObject = JsonDocument.Parse(inputStream, options: default); + JsonElement.ArrayEnumerator customZoneSetsEnumerator = jsonObject.RootElement.GetProperty("custom-zone-sets").EnumerateArray(); + while (customZoneSetsEnumerator.MoveNext()) { - foreach (string name in key.GetValueNames()) + var current = customZoneSetsEnumerator.Current; + string name = current.GetProperty("name").GetString(); + string type = current.GetProperty("type").GetString(); + string uuid = current.GetProperty("uuid").GetString(); + var info = current.GetProperty("info"); + if (type.Equals("grid")) { - LayoutModel model = null; - byte[] data = (byte[])Registry.GetValue(_fullRegistryPath, name, null); - - ushort version = (ushort)((data[0] * 256) + data[1]); - byte type = data[2]; - ushort id = (ushort)((data[3] * 256) + data[4]); - - switch (type) + int rows = info.GetProperty("rows").GetInt32(); + int columns = info.GetProperty("columns").GetInt32(); + int[] rowsPercentage = new int[rows]; + JsonElement.ArrayEnumerator rowsPercentageEnumerator = info.GetProperty("rows-percentage").EnumerateArray(); + int i = 0; + while (rowsPercentageEnumerator.MoveNext()) { - case 0: model = new GridLayoutModel(version, name, id, data); break; - case 1: model = new CanvasLayoutModel(version, name, id, data); break; + rowsPercentage[i++] = rowsPercentageEnumerator.Current.GetInt32(); } - if (model != null) + i = 0; + int[] columnsPercentage = new int[columns]; + JsonElement.ArrayEnumerator columnsPercentageEnumerator = info.GetProperty("columns-percentage").EnumerateArray(); + while (columnsPercentageEnumerator.MoveNext()) { - if (_maxId < id) + columnsPercentage[i++] = columnsPercentageEnumerator.Current.GetInt32(); + } + + i = 0; + JsonElement.ArrayEnumerator cellChildMapRows = info.GetProperty("cell-child-map").EnumerateArray(); + int[,] cellChildMap = new int[rows, columns]; + while (cellChildMapRows.MoveNext()) + { + int j = 0; + JsonElement.ArrayEnumerator cellChildMapRowElems = cellChildMapRows.Current.EnumerateArray(); + while (cellChildMapRowElems.MoveNext()) { - _maxId = id; + cellChildMap[i, j++] = cellChildMapRowElems.Current.GetInt32(); } - _customModels.Add(model); + i++; } + + _customModels.Add(new GridLayoutModel(uuid, name, LayoutType.Custom, rows, columns, rowsPercentage, columnsPercentage, cellChildMap)); + } + else if (type.Equals("canvas")) + { + int referenceWidth = info.GetProperty("ref-width").GetInt32(); + int referenceHeight = info.GetProperty("ref-height").GetInt32(); + JsonElement.ArrayEnumerator zonesEnumerator = info.GetProperty("zones").EnumerateArray(); + IList zones = new List(); + while (zonesEnumerator.MoveNext()) + { + int x = zonesEnumerator.Current.GetProperty("X").GetInt32(); + int y = zonesEnumerator.Current.GetProperty("Y").GetInt32(); + int width = zonesEnumerator.Current.GetProperty("width").GetInt32(); + int height = zonesEnumerator.Current.GetProperty("height").GetInt32(); + zones.Add(new Int32Rect(x, y, width, height)); + } + + _customModels.Add(new CanvasLayoutModel(uuid, name, LayoutType.Custom, referenceWidth, referenceHeight, zones)); } } @@ -155,55 +223,63 @@ namespace FancyZonesEditor.Models } private static ObservableCollection _customModels = null; - - private static ushort _maxId = 0; + private static List _deletedCustomModels = new List(); // Callbacks that the base LayoutModel makes to derived types - protected abstract byte[] GetPersistData(); + protected abstract void PersistData(); public abstract LayoutModel Clone(); public void Persist(System.Windows.Int32Rect[] zones) { - // Persist the editor data - Registry.SetValue(_fullRegistryPath, Name, GetPersistData(), Microsoft.Win32.RegistryValueKind.Binary); + PersistData(); Apply(zones); } public void Apply(System.Windows.Int32Rect[] zones) { - // Persist the zone data back into FZ - var module = Native.LoadLibrary("fancyzones.dll"); - if (module == IntPtr.Zero) - { - return; - } - - var pfn = Native.GetProcAddress(module, "PersistZoneSet"); - if (pfn == IntPtr.Zero) - { - return; - } - - // Scale all the zones to the DPI and then pack them up to be marshalled. int zoneCount = zones.Length; - var zoneArray = new int[zoneCount * 4]; - for (int i = 0; i < zones.Length; i++) - { - var left = (int)(zones[i].X * Settings.Dpi); - var top = (int)(zones[i].Y * Settings.Dpi); - var right = left + (int)(zones[i].Width * Settings.Dpi); - var bottom = top + (int)(zones[i].Height * Settings.Dpi); + FileStream outputStream = File.Open(Settings.ActiveZoneSetTmpFile, FileMode.Create); + var writer = new Utf8JsonWriter(outputStream, options: default); - var index = i * 4; - zoneArray[index] = left; - zoneArray[index + 1] = top; - zoneArray[index + 2] = right; - zoneArray[index + 3] = bottom; + writer.WriteStartObject(); + writer.WriteString("device-id", Settings.UniqueKey); + + writer.WriteStartObject("active-zoneset"); + writer.WriteString("uuid", "{" + Guid.ToString().ToUpper() + "}"); + switch (Type) + { + case LayoutType.Focus: + writer.WriteString("type", "focus"); + break; + case LayoutType.Rows: + writer.WriteString("type", "rows"); + break; + case LayoutType.Columns: + writer.WriteString("type", "columns"); + break; + case LayoutType.Grid: + writer.WriteString("type", "grid"); + break; + case LayoutType.PriorityGrid: + writer.WriteString("type", "priority-grid"); + break; + case LayoutType.Custom: + writer.WriteString("type", "custom"); + break; } - var persistZoneSet = Marshal.GetDelegateForFunctionPointer(pfn); - persistZoneSet(Settings.UniqueKey, Settings.WorkAreaKey, Settings.Monitor, _id, zoneCount, zoneArray); + writer.WriteEndObject(); + + Settings settings = ((App)Application.Current).ZoneSettings; + + writer.WriteBoolean("editor-show-spacing", settings.ShowSpacing); + writer.WriteNumber("editor-spacing", settings.Spacing); + writer.WriteNumber("editor-zone-count", settings.ZoneCount); + + writer.WriteEndObject(); + writer.Flush(); + outputStream.Close(); } } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/Settings.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/Settings.cs index d47811bb2b..409792d2d9 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/Settings.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/Settings.cs @@ -7,9 +7,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; +using System.IO; +using System.Text.Json; using System.Windows; using FancyZonesEditor.Models; -using Microsoft.Win32; namespace FancyZonesEditor { @@ -18,20 +19,30 @@ namespace FancyZonesEditor // Other UIs in the editor will subscribe to change events on the properties to stay up to date as these properties change public class Settings : INotifyPropertyChanged { - private readonly CanvasLayoutModel _blankCustomModel; + private enum CmdArgs + { + MonitorHandle = 1, + X_Y_Width_Height, + ResolutionKey, + ActiveZoneSetTmpFile, + AppliedZoneSetTmpFile, + CustomZoneSetsTmpFile, + } + + private static CanvasLayoutModel _blankCustomModel; private readonly CanvasLayoutModel _focusModel; private readonly GridLayoutModel _rowsModel; private readonly GridLayoutModel _columnsModel; private readonly GridLayoutModel _gridModel; private readonly GridLayoutModel _priorityGridModel; - private static readonly ushort _focusModelId = 0xFFFF; - private static readonly ushort _rowsModelId = 0xFFFE; - private static readonly ushort _columnsModelId = 0xFFFD; - private static readonly ushort _gridModelId = 0xFFFC; - private static readonly ushort _priorityGridModelId = 0xFFFB; - private static readonly ushort _blankCustomModelId = 0xFFFA; - private static readonly ushort _lastPrefinedId = _blankCustomModelId; + public const ushort _focusModelId = 0xFFFF; + public const ushort _rowsModelId = 0xFFFE; + public const ushort _columnsModelId = 0xFFFD; + public const ushort _gridModelId = 0xFFFC; + public const ushort _priorityGridModelId = 0xFFFB; + public const ushort _blankCustomModelId = 0xFFFA; + public const ushort _lastPrefinedId = _blankCustomModelId; // hard coded data for all the "Priority Grid" configurations that are unique to "Grid" private static readonly byte[][] _priorityData = new byte[][] @@ -73,34 +84,30 @@ namespace FancyZonesEditor // Initialize the five default layout models: Focus, Columns, Rows, Grid, and PriorityGrid DefaultModels = new List(5); - _focusModel = new CanvasLayoutModel("Focus", _focusModelId, (int)_workArea.Width, (int)_workArea.Height); + _focusModel = new CanvasLayoutModel("Focus", LayoutType.Focus, (int)_workArea.Width, (int)_workArea.Height); DefaultModels.Add(_focusModel); - _columnsModel = new GridLayoutModel("Columns", _columnsModelId) + _columnsModel = new GridLayoutModel("Columns", LayoutType.Columns) { Rows = 1, RowPercents = new int[1] { _multiplier }, }; DefaultModels.Add(_columnsModel); - _rowsModel = new GridLayoutModel("Rows", _rowsModelId) + _rowsModel = new GridLayoutModel("Rows", LayoutType.Rows) { Columns = 1, ColumnPercents = new int[1] { _multiplier }, }; DefaultModels.Add(_rowsModel); - _gridModel = new GridLayoutModel("Grid", _gridModelId); + _gridModel = new GridLayoutModel("Grid", LayoutType.Grid); DefaultModels.Add(_gridModel); - _priorityGridModel = new GridLayoutModel("Priority Grid", _priorityGridModelId); + _priorityGridModel = new GridLayoutModel("Priority Grid", LayoutType.PriorityGrid); DefaultModels.Add(_priorityGridModel); - _blankCustomModel = new CanvasLayoutModel("Create new custom", _blankCustomModelId, (int)_workArea.Width, (int)_workArea.Height); - - _zoneCount = ReadRegistryInt("ZoneCount", 3); - _spacing = ReadRegistryInt("Spacing", 16); - _showSpacing = ReadRegistryInt("ShowSpacing", 1) == 1; + _blankCustomModel = new CanvasLayoutModel("Create new custom", LayoutType.Blank, (int)_workArea.Width, (int)_workArea.Height); UpdateLayoutModels(); } @@ -118,7 +125,6 @@ namespace FancyZonesEditor if (_zoneCount != value) { _zoneCount = value; - Registry.SetValue(_uniqueRegistryPath, "ZoneCount", _zoneCount, RegistryValueKind.DWord); UpdateLayoutModels(); FirePropertyChanged("ZoneCount"); } @@ -140,7 +146,6 @@ namespace FancyZonesEditor if (_spacing != value) { _spacing = value; - Registry.SetValue(_uniqueRegistryPath, "Spacing", _spacing, RegistryValueKind.DWord); FirePropertyChanged("Spacing"); } } @@ -161,7 +166,6 @@ namespace FancyZonesEditor if (_showSpacing != value) { _showSpacing = value; - Registry.SetValue(_uniqueRegistryPath, "ShowSpacing", _showSpacing, RegistryValueKind.DWord); FirePropertyChanged("ShowSpacing"); } } @@ -220,18 +224,33 @@ namespace FancyZonesEditor public static string UniqueKey { get; private set; } - private string _uniqueRegistryPath; + public static string ActiveZoneSetUUid { get; private set; } + + public static LayoutType ActiveZoneSetLayoutType { get; private set; } + + public static string ActiveZoneSetTmpFile + { + get { return _activeZoneSetTmpFile; } + } + + private static string _activeZoneSetTmpFile; + + public static string AppliedZoneSetTmpFile + { + get { return _appliedZoneSetTmpFile; } + } + + private static string _appliedZoneSetTmpFile; + + public static string CustomZoneSetsTmpFile + { + get { return _customZoneSetsTmpFile; } + } + + private static string _customZoneSetsTmpFile; public static string WorkAreaKey { get; private set; } - public static float Dpi { get; private set; } - - private int ReadRegistryInt(string valueName, int defaultValue) - { - object obj = Registry.GetValue(_uniqueRegistryPath, valueName, defaultValue); - return (obj != null) ? (int)obj : defaultValue; - } - // UpdateLayoutModels // Update the five default layouts based on the new ZoneCount private void UpdateLayoutModels() @@ -327,59 +346,87 @@ namespace FancyZonesEditor } } + private void ParseDeviceInfoData() + { + FileStream inputStream = File.Open(Settings.ActiveZoneSetTmpFile, FileMode.Open); + var jsonObject = JsonDocument.Parse(inputStream, options: default).RootElement; + + UniqueKey = jsonObject.GetProperty("device-id").GetString(); + ActiveZoneSetUUid = jsonObject.GetProperty("active-zoneset").GetProperty("uuid").GetString(); + string layoutType = jsonObject.GetProperty("active-zoneset").GetProperty("type").GetString(); + + if (ActiveZoneSetUUid == "null" || layoutType == "blank") + { + // Default selection is Focus + ActiveZoneSetLayoutType = LayoutType.Focus; + _showSpacing = true; + _spacing = 16; + _zoneCount = 3; + } + else + { + switch (layoutType) + { + case "focus": + ActiveZoneSetLayoutType = LayoutType.Focus; + break; + case "columns": + ActiveZoneSetLayoutType = LayoutType.Columns; + break; + case "rows": + ActiveZoneSetLayoutType = LayoutType.Rows; + break; + case "grid": + ActiveZoneSetLayoutType = LayoutType.Grid; + break; + case "priority-grid": + ActiveZoneSetLayoutType = LayoutType.PriorityGrid; + break; + case "custom": + ActiveZoneSetLayoutType = LayoutType.Custom; + break; + } + + _showSpacing = jsonObject.GetProperty("editor-show-spacing").GetBoolean(); + _spacing = jsonObject.GetProperty("editor-spacing").GetInt32(); + _zoneCount = jsonObject.GetProperty("editor-zone-count").GetInt32(); + } + } + private void ParseCommandLineArgs() { _workArea = SystemParameters.WorkArea; Monitor = 0; - _uniqueRegistryPath = FullRegistryPath; - UniqueKey = string.Empty; - Dpi = 1; string[] args = Environment.GetCommandLineArgs(); if (args.Length == 7) { - // 1 = unique key for per-monitor settings - // 2 = layoutid used to generate current layout (used to pick the default layout to show) - // 3 = handle to monitor (passed back to engine to persist data) - // 4 = X_Y_Width_Height in a dpi-scaled-but-unaware coords (where EditorOverlay shows up) - // 5 = resolution key (passed back to engine to persist data) - // 6 = monitor DPI (float) - UniqueKey = args[1]; - _uniqueRegistryPath += "\\" + UniqueKey; + if (uint.TryParse(args[(int)CmdArgs.MonitorHandle], out uint monitor)) + { + Monitor = monitor; + } - var parsedLocation = args[4].Split('_'); + var parsedLocation = args[(int)CmdArgs.X_Y_Width_Height].Split('_'); var x = int.Parse(parsedLocation[0]); var y = int.Parse(parsedLocation[1]); var width = int.Parse(parsedLocation[2]); var height = int.Parse(parsedLocation[3]); - WorkAreaKey = args[5]; - - // Try invariant culture first, caller likely uses invariant i.e. "C" locale to construct parameters - foreach (var cultureInfo in new[] { CultureInfo.InvariantCulture, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture }) - { - try - { - Dpi = float.Parse(args[6], cultureInfo); - break; - } - catch (FormatException) - { - } - } - _workArea = new Rect(x, y, width, height); - if (uint.TryParse(args[4], out uint monitor)) - { - Monitor = monitor; - } + WorkAreaKey = args[(int)CmdArgs.ResolutionKey]; + + _activeZoneSetTmpFile = args[(int)CmdArgs.ActiveZoneSetTmpFile]; + _appliedZoneSetTmpFile = args[(int)CmdArgs.AppliedZoneSetTmpFile]; + _customZoneSetsTmpFile = args[(int)CmdArgs.CustomZoneSetsTmpFile]; + + ParseDeviceInfoData(); } } public IList DefaultModels { get; } - public ObservableCollection CustomModels + public static ObservableCollection CustomModels { get { @@ -393,14 +440,14 @@ namespace FancyZonesEditor } } - private ObservableCollection _customModels; + private static ObservableCollection _customModels; public static readonly string RegistryPath = "SOFTWARE\\SuperFancyZones"; public static readonly string FullRegistryPath = "HKEY_CURRENT_USER\\" + RegistryPath; public static bool IsPredefinedLayout(LayoutModel model) { - return model.Id >= _lastPrefinedId; + return model.Type != LayoutType.Custom; } // implementation of INotifyProeprtyChanged diff --git a/src/modules/fancyzones/lib/FancyZones.cpp b/src/modules/fancyzones/lib/FancyZones.cpp index 489bf2e242..1c8443fdb5 100644 --- a/src/modules/fancyzones/lib/FancyZones.cpp +++ b/src/modules/fancyzones/lib/FancyZones.cpp @@ -6,14 +6,27 @@ #include "lib/Settings.h" #include "lib/ZoneWindow.h" #include "lib/RegistryHelpers.h" +#include "lib/JsonHelpers.h" +#include "lib/ZoneSet.h" #include "trace.h" #include #include +#include + +enum class DisplayChangeType +{ + WorkArea, + DisplayChange, + VirtualDesktop, + Editor, + Initialization +}; namespace std { - template<> struct hash + template<> + struct hash { size_t operator()(const GUID& Value) const { @@ -26,9 +39,9 @@ namespace std struct FancyZones : public winrt::implements { public: - FancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept - : m_hinstance(hinstance) - , m_settings(settings) + FancyZones(HINSTANCE hinstance, const winrt::com_ptr& settings) noexcept : + m_hinstance(hinstance), + m_settings(settings) { m_settings->SetCallback(this); } @@ -38,7 +51,11 @@ public: IFACEMETHODIMP_(void) Destroy() noexcept; // IFancyZonesCallback - IFACEMETHODIMP_(bool) InMoveSize() noexcept { std::shared_lock readLock(m_lock); return m_inMoveSize; } + IFACEMETHODIMP_(bool) InMoveSize() noexcept + { + std::shared_lock readLock(m_lock); + return m_inMoveSize; + } IFACEMETHODIMP_(void) MoveSizeStart(HWND window, HMONITOR monitor, POINT const& ptScreen) noexcept; IFACEMETHODIMP_(void) MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen) noexcept; IFACEMETHODIMP_(void) MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept; @@ -61,12 +78,16 @@ public: const auto nB = (tmp & 0xFF); return RGB(nR, nG, nB); } - IFACEMETHODIMP_(GUID) GetCurrentMonitorZoneSetId(HMONITOR monitor) noexcept + IFACEMETHODIMP_(IZoneWindow*)GetParentZoneWindow(HMONITOR monitor) noexcept { - if (auto it = m_zoneWindowMap.find(monitor); it != m_zoneWindowMap.end() && it->second->ActiveZoneSet()) { - return it->second->ActiveZoneSet()->Id(); + //NOTE: as public method it's unsafe without lock, but it's called from AddZoneWindow through making ZoneWindow that causes deadlock + //TODO: needs refactoring + auto it = m_zoneWindowMap.find(monitor); + if (it != m_zoneWindowMap.end()) + { + return it->second.get(); } - return GUID_NULL; + return nullptr; } IFACEMETHODIMP_(int) GetZoneHighlightOpacity() noexcept { @@ -85,16 +106,25 @@ private: struct require_read_lock { template - require_read_lock(const std::shared_lock& lock) { lock; } + require_read_lock(const std::shared_lock& lock) + { + lock; + } template - require_read_lock(const std::unique_lock& lock) { lock; } + require_read_lock(const std::unique_lock& lock) + { + lock; + } }; struct require_write_lock { template - require_write_lock(const std::unique_lock& lock) { lock; } + require_write_lock(const std::unique_lock& lock) + { + lock; + } }; bool IsInterestingWindow(HWND window) noexcept; @@ -107,6 +137,7 @@ private: void MoveSizeEndInternal(HWND window, POINT const& ptScreen, require_write_lock) noexcept; void MoveSizeUpdateInternal(HMONITOR monitor, POINT const& ptScreen, require_write_lock) noexcept; void HandleVirtualDesktopUpdates(HANDLE fancyZonesDestroyedEvent) noexcept; + void OnEditorExitEvent() noexcept; const HINSTANCE m_hinstance{}; @@ -115,11 +146,11 @@ private: mutable std::shared_mutex m_lock; HWND m_window{}; HWND m_windowMoveSize{}; // The window that is being moved/sized - bool m_inMoveSize{}; // Whether or not a move/size operation is currently active + bool m_inMoveSize{}; // Whether or not a move/size operation is currently active bool m_dragEnabled{}; // True if we should be showing zone hints while dragging std::map> m_zoneWindowMap; // Map of monitor to ZoneWindow (one per monitor) winrt::com_ptr m_zoneWindowMoveSize; // "Active" ZoneWindow, where the move/size is happening. Will update as drag moves between monitors. - IFancyZonesSettings* m_settings{}; + winrt::com_ptr m_settings{}; GUID m_currentVirtualDesktopId{}; // UUID of the current virtual desktop. Is GUID_NULL until first VD switch per session. std::unordered_map m_virtualDesktopIds; wil::unique_handle m_terminateEditorEvent; // Handle of FancyZonesEditor.exe we launch and wait on @@ -159,18 +190,21 @@ IFACEMETHODIMP_(void) FancyZones::Run() noexcept BufferedPaintInit(); m_window = CreateWindowExW(WS_EX_TOOLWINDOW, L"SuperFancyZones", L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, m_hinstance, this); - if (!m_window) return; + if (!m_window) + return; RegisterHotKey(m_window, 1, m_settings->GetSettings().editorHotkey.get_modifiers(), m_settings->GetSettings().editorHotkey.get_code()); VirtualDesktopInitialize(); - m_dpiUnawareThread.submit(OnThreadExecutor::task_t{[]{ - SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE); - SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED); - }}).wait(); + m_dpiUnawareThread.submit(OnThreadExecutor::task_t{ [] { + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE); + SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED); + } }) + .wait(); - if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops", 0, KEY_ALL_ACCESS, &m_virtualDesktopsRegKey) == ERROR_SUCCESS) { + if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops", 0, KEY_ALL_ACCESS, &m_virtualDesktopsRegKey) == ERROR_SUCCESS) + { m_terminateVirtualDesktopTrackerEvent.reset(CreateEvent(nullptr, FALSE, FALSE, nullptr)); m_virtualDesktopTrackerThread.submit( OnThreadExecutor::task_t{ std::bind(&FancyZones::HandleVirtualDesktopUpdates, this, m_terminateVirtualDesktopTrackerEvent.get()) }); @@ -188,10 +222,12 @@ IFACEMETHODIMP_(void) FancyZones::Destroy() noexcept DestroyWindow(m_window); m_window = nullptr; } - if (m_terminateVirtualDesktopTrackerEvent) { + if (m_terminateVirtualDesktopTrackerEvent) + { SetEvent(m_terminateVirtualDesktopTrackerEvent.get()); } - if (m_virtualDesktopsRegKey) { + if (m_virtualDesktopsRegKey) + { RegCloseKey(m_virtualDesktopsRegKey); m_virtualDesktopsRegKey = nullptr; } @@ -229,6 +265,7 @@ IFACEMETHODIMP_(void) FancyZones::VirtualDesktopChanged() noexcept { // VirtualDesktopChanged is called from another thread but results in new windows being created. // Jump over to the UI thread to handle it. + std::shared_lock readLock(m_lock); PostMessage(m_window, WM_PRIV_VDCHANGED, 0, 0); } @@ -243,14 +280,28 @@ IFACEMETHODIMP_(void) FancyZones::WindowCreated(HWND window) noexcept { if (m_settings->GetSettings().appLastZone_moveWindows && IsInterestingWindow(window)) { - auto processPath = get_process_path(window); - if (!processPath.empty()) + auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + if (monitor) { - INT zoneIndex = -1; - LRESULT res = RegistryHelpers::GetAppLastZone(window, processPath.data(), &zoneIndex); - if ((res == ERROR_SUCCESS) && (zoneIndex != -1)) + auto zoneWindow = m_zoneWindowMap.find(monitor); + if (zoneWindow != m_zoneWindowMap.end()) { - MoveWindowIntoZoneByIndex(window, zoneIndex); + const auto& zoneWindowPtr = zoneWindow->second; + const auto activeZoneSet = zoneWindowPtr->ActiveZoneSet(); + if (activeZoneSet) + { + const auto& fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + + wil::unique_cotaskmem_string guidString; + if (SUCCEEDED_LOG(StringFromCLSID(activeZoneSet->Id(), &guidString))) + { + int zoneIndex = fancyZonesData.GetAppLastZoneIndex(window, zoneWindowPtr->UniqueId(), guidString.get()); + if (zoneIndex != -1) + { + MoveWindowIntoZoneByIndex(window, zoneIndex); + } + } + } } } } @@ -346,11 +397,12 @@ void FancyZones::ToggleEditor() noexcept MONITORINFOEX mi; mi.cbSize = sizeof(mi); - m_dpiUnawareThread.submit(OnThreadExecutor::task_t{[&]{ - GetMonitorInfo(monitor, &mi); - }}).wait(); + m_dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] { + GetMonitorInfo(monitor, &mi); + } }) + .wait(); - if(use_cursorpos_editor_startupscreen) + if (use_cursorpos_editor_startupscreen) { DPIAware::GetScreenDPIForPoint(currentCursorPos, dpi_x, dpi_y); } @@ -359,6 +411,11 @@ void FancyZones::ToggleEditor() noexcept DPIAware::GetScreenDPIForWindow(foregroundWindow, dpi_x, dpi_y); } + auto zoneWindow = iter->second; + + const auto& fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + fancyZonesData.CustomZoneSetsToJsonFile(ZoneWindowUtils::GetCustomZoneSetsTmpPath()); + const auto taskbar_x_offset = MulDiv(mi.rcWork.left - mi.rcMonitor.left, DPIAware::DEFAULT_DPI, dpi_x); const auto taskbar_y_offset = MulDiv(mi.rcWork.top - mi.rcMonitor.top, DPIAware::DEFAULT_DPI, dpi_y); @@ -367,22 +424,24 @@ void FancyZones::ToggleEditor() noexcept const auto y = mi.rcMonitor.top + taskbar_y_offset; const auto width = mi.rcWork.right - mi.rcWork.left; const auto height = mi.rcWork.bottom - mi.rcWork.top; - const std::wstring editorLocation = + const std::wstring editorLocation = std::to_wstring(x) + L"_" + std::to_wstring(y) + L"_" + std::to_wstring(width) + L"_" + std::to_wstring(height); - const auto activeZoneSet = iter->second->ActiveZoneSet(); - const std::wstring layoutID = activeZoneSet ? std::to_wstring(activeZoneSet->LayoutId()) : L"0"; + const auto& deviceInfo = fancyZonesData.GetDeviceInfoMap().at(zoneWindow->UniqueId()); + + JSONHelpers::DeviceInfoJSON deviceInfoJson{ zoneWindow->UniqueId(), deviceInfo }; + fancyZonesData.SerializeDeviceInfoToTmpFile(deviceInfoJson, ZoneWindowUtils::GetActiveZoneSetTmpPath()); const std::wstring params = - iter->second->UniqueId() + L" " + - layoutID + L" " + - std::to_wstring(reinterpret_cast(monitor)) + L" " + - editorLocation + L" " + - iter->second->WorkAreaKey() + L" " + - std::to_wstring(static_cast(dpi_x) / DPIAware::DEFAULT_DPI); + /*1*/ std::to_wstring(reinterpret_cast(monitor)) + L" " + + /*2*/ editorLocation + L" " + + /*3*/ zoneWindow->WorkAreaKey() + L" " + + /*4*/ ZoneWindowUtils::GetActiveZoneSetTmpPath() + L" " + + /*5*/ ZoneWindowUtils::GetAppliedZoneSetTmpPath() + L" " + + /*6*/ ZoneWindowUtils::GetCustomZoneSetsTmpPath(); SHELLEXECUTEINFO sei{ sizeof(sei) }; sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; @@ -394,8 +453,7 @@ void FancyZones::ToggleEditor() noexcept // Launch the editor on a background thread // Wait for the editor's process to exit // Post back to the main thread to update - std::thread waitForEditorThread([window = m_window, processHandle = sei.hProcess, terminateEditorEvent = m_terminateEditorEvent.get()]() - { + std::thread waitForEditorThread([window = m_window, processHandle = sei.hProcess, terminateEditorEvent = m_terminateEditorEvent.get()]() { HANDLE waitEvents[2] = { processHandle, terminateEditorEvent }; auto result = WaitForMultipleObjects(2, waitEvents, false, INFINITE); if (result == WAIT_OBJECT_0 + 0) @@ -419,6 +477,7 @@ void FancyZones::ToggleEditor() noexcept void FancyZones::SettingsChanged() noexcept { + std::shared_lock readLock(m_lock); // Update the hotkey UnregisterHotKey(m_window, 1); RegisterHotKey(m_window, 1, m_settings->GetSettings().editorHotkey.get_modifiers(), m_settings->GetSettings().editorHotkey.get_code()); @@ -475,7 +534,7 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa { if (lparam == static_cast(EditorExitKind::Exit)) { - // Don't reload settings if we terminated the editor + OnEditorExitEvent(); OnDisplayChange(DisplayChangeType::Editor); } @@ -504,10 +563,10 @@ void FancyZones::OnDisplayChange(DisplayChangeType changeType) noexcept // the first virtual desktop switch happens. If the user hasn't switched virtual desktops in this session // then this value will be empty. This means loading the first virtual desktop's configuration can be // funky the first time we load up at boot since the user will not have switched virtual desktops yet. - std::shared_lock readLock(m_lock); GUID currentVirtualDesktopId{}; if (SUCCEEDED(RegistryHelpers::GetCurrentVirtualDesktop(¤tVirtualDesktopId))) { + std::unique_lock writeLock(m_lock); m_currentVirtualDesktopId = currentVirtualDesktopId; } else @@ -548,13 +607,19 @@ void FancyZones::AddZoneWindow(HMONITOR monitor, PCWSTR deviceId) noexcept wil::unique_cotaskmem_string virtualDesktopId; if (SUCCEEDED_LOG(StringFromCLSID(m_currentVirtualDesktopId, &virtualDesktopId))) { + std::wstring uniqueId = ZoneWindowUtils::GenerateUniqueId(monitor, deviceId, virtualDesktopId.get()); bool newVirtualDesktop = true; - if (auto it = m_virtualDesktopIds.find(m_currentVirtualDesktopId); it != end(m_virtualDesktopIds)) { - newVirtualDesktop = it->second; - } - const bool flash = m_settings->GetSettings().zoneSetChange_flashZones && newVirtualDesktop; - if (auto zoneWindow = MakeZoneWindow(this, m_hinstance, monitor, deviceId, virtualDesktopId.get(), flash)) + auto it = m_virtualDesktopIds.find(m_currentVirtualDesktopId); + if (it != end(m_virtualDesktopIds)) + { + newVirtualDesktop = it->second; + JSONHelpers::FancyZonesDataInstance().SetActiveDeviceId(uniqueId); + } + + const bool flash = m_settings->GetSettings().zoneSetChange_flashZones && newVirtualDesktop; + auto zoneWindow = MakeZoneWindow(this, m_hinstance, monitor, uniqueId, flash); + if (zoneWindow) { m_zoneWindowMap[monitor] = std::move(zoneWindow); } @@ -567,12 +632,14 @@ void FancyZones::MoveWindowIntoZoneByIndex(HWND window, int index) noexcept std::shared_lock readLock(m_lock); if (window != m_windowMoveSize) { - if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL)) + const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + if (monitor) { auto iter = m_zoneWindowMap.find(monitor); if (iter != m_zoneWindowMap.end()) { - iter->second->MoveWindowIntoZoneByIndex(window, index); + const auto& zoneWindowPtr = iter->second; + zoneWindowPtr->MoveWindowIntoZoneByIndex(window, index); } } } @@ -589,7 +656,7 @@ LRESULT CALLBACK FancyZones::s_WndProc(HWND window, UINT message, WPARAM wparam, } return thisRef ? thisRef->WndProc(window, message, wparam, lparam) : - DefWindowProc(window, message, wparam, lparam); + DefWindowProc(window, message, wparam, lparam); } bool FancyZones::IsInterestingWindow(HWND window) noexcept @@ -616,8 +683,7 @@ bool FancyZones::IsInterestingWindow(HWND window) noexcept void FancyZones::UpdateZoneWindows() noexcept { - auto callback = [](HMONITOR monitor, HDC, RECT *, LPARAM data) -> BOOL - { + auto callback = [](HMONITOR monitor, HDC, RECT*, LPARAM data) -> BOOL { MONITORINFOEX mi; mi.cbSize = sizeof(mi); if (GetMonitorInfo(monitor, &mi)) @@ -643,8 +709,8 @@ void FancyZones::UpdateZoneWindows() noexcept if (!deviceId) { deviceId = GetSystemMetrics(SM_REMOTESESSION) ? - L"\\\\?\\DISPLAY#REMOTEDISPLAY#" : - L"\\\\?\\DISPLAY#LOCALDISPLAY#"; + L"\\\\?\\DISPLAY#REMOTEDISPLAY#" : + L"\\\\?\\DISPLAY#LOCALDISPLAY#"; } auto strongThis = reinterpret_cast(data); @@ -659,14 +725,13 @@ void FancyZones::UpdateZoneWindows() noexcept void FancyZones::MoveWindowsOnDisplayChange() noexcept { - auto callback = [](HWND window, LPARAM data) -> BOOL - { + auto callback = [](HWND window, LPARAM data) -> BOOL { int i = static_cast(reinterpret_cast(::GetProp(window, ZONE_STAMP))); if (i != 0) { // i is off by 1 since 0 is special. auto strongThis = reinterpret_cast(data); - strongThis->MoveWindowIntoZoneByIndex(window, i-1); + strongThis->MoveWindowIntoZoneByIndex(window, i - 1); } return TRUE; }; @@ -683,7 +748,7 @@ void FancyZones::UpdateDragState(require_write_lock) noexcept const bool mouseX2 = GetAsyncKeyState(VK_XBUTTON2) & 0x8000; // Note, Middle, X1 and X2 can also be used in addition to R/L - bool mouse = mouseM | mouseX1 | mouseX2; + bool mouse = mouseM | mouseX1 | mouseX2; // If the user has swapped their Right and Left Buttons, use the "Right" equivalent if (GetSystemMetrics(SM_SWAPBUTTON)) { @@ -709,13 +774,16 @@ void FancyZones::CycleActiveZoneSet(DWORD vkCode) noexcept auto window = GetForegroundWindow(); if (IsInterestingWindow(window)) { - if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL)) + const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + if (monitor) { std::shared_lock readLock(m_lock); + auto iter = m_zoneWindowMap.find(monitor); if (iter != m_zoneWindowMap.end()) { - iter->second->CycleActiveZoneSet(vkCode); + const auto& zoneWindowPtr = iter->second; + zoneWindowPtr->CycleActiveZoneSet(vkCode); } } } @@ -726,13 +794,16 @@ bool FancyZones::OnSnapHotkey(DWORD vkCode) noexcept auto window = GetForegroundWindow(); if (IsInterestingWindow(window)) { - if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL)) + const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + if (monitor) { std::shared_lock readLock(m_lock); + auto iter = m_zoneWindowMap.find(monitor); if (iter != m_zoneWindowMap.end()) { - iter->second->MoveWindowIntoZoneByDirection(window, vkCode); + const auto& zoneWindowPtr = iter->second; + zoneWindowPtr->MoveWindowIntoZoneByDirection(window, vkCode); return true; } } @@ -798,10 +869,23 @@ void FancyZones::MoveSizeEndInternal(HWND window, POINT const& ptScreen, require { ::RemoveProp(window, ZONE_STAMP); - auto processPath = get_process_path(window); - if (!processPath.empty()) + auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + if (monitor) { - RegistryHelpers::SaveAppLastZone(window, processPath.data(), -1); + auto zoneWindow = m_zoneWindowMap.find(monitor); + if (zoneWindow != m_zoneWindowMap.end()) + { + const auto zoneWindowPtr = zoneWindow->second; + const auto activeZoneSet = zoneWindowPtr->ActiveZoneSet(); + if (activeZoneSet) + { + wil::unique_cotaskmem_string guidString; + if (SUCCEEDED_LOG(StringFromCLSID(activeZoneSet->Id(), &guidString))) + { + JSONHelpers::FancyZonesDataInstance().RemoveAppLastZone(window, zoneWindowPtr->UniqueId(), guidString.get()); + } + } + } } } } @@ -853,38 +937,48 @@ void FancyZones::HandleVirtualDesktopUpdates(HANDLE fancyZonesDestroyedEvent) no { HANDLE regKeyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); HANDLE events[2] = { regKeyEvent, fancyZonesDestroyedEvent }; - while (1) { - if (RegNotifyChangeKeyValue(HKEY_CURRENT_USER, TRUE, REG_NOTIFY_CHANGE_LAST_SET, regKeyEvent, TRUE) != ERROR_SUCCESS) { + while (1) + { + if (RegNotifyChangeKeyValue(HKEY_CURRENT_USER, TRUE, REG_NOTIFY_CHANGE_LAST_SET, regKeyEvent, TRUE) != ERROR_SUCCESS) + { return; } - if (WaitForMultipleObjects(2, events, FALSE, INFINITE) != (WAIT_OBJECT_0 + 0)) { + if (WaitForMultipleObjects(2, events, FALSE, INFINITE) != (WAIT_OBJECT_0 + 0)) + { // if fancyZonesDestroyedEvent is signalized or WaitForMultipleObjects failed, terminate thread execution return; } DWORD bufferCapacity; const WCHAR* key = L"VirtualDesktopIDs"; // request regkey binary buffer capacity only - if (RegQueryValueExW(m_virtualDesktopsRegKey, key, 0, nullptr, nullptr, &bufferCapacity) != ERROR_SUCCESS) { + if (RegQueryValueExW(m_virtualDesktopsRegKey, key, 0, nullptr, nullptr, &bufferCapacity) != ERROR_SUCCESS) + { return; } std::unique_ptr buffer = std::make_unique(bufferCapacity); // request regkey binary content - if (RegQueryValueExW(m_virtualDesktopsRegKey, key, 0, nullptr, buffer.get(), &bufferCapacity) != ERROR_SUCCESS) { + if (RegQueryValueExW(m_virtualDesktopsRegKey, key, 0, nullptr, buffer.get(), &bufferCapacity) != ERROR_SUCCESS) + { return; } const int guidSize = sizeof(GUID); std::unordered_map temp; temp.reserve(bufferCapacity / guidSize); - for (size_t i = 0; i < bufferCapacity; i += guidSize) { - GUID *guid = reinterpret_cast(buffer.get() + i); + for (size_t i = 0; i < bufferCapacity; i += guidSize) + { + GUID* guid = reinterpret_cast(buffer.get() + i); temp[*guid] = true; } std::unique_lock writeLock(m_lock); - for (auto it = begin(m_virtualDesktopIds); it != end(m_virtualDesktopIds);) { - if (auto iter = temp.find(it->first); iter == temp.end()) { + for (auto it = begin(m_virtualDesktopIds); it != end(m_virtualDesktopIds);) + { + auto iter = temp.find(it->first); + if (iter == temp.end()) + { it = m_virtualDesktopIds.erase(it); // virtual desktop closed, remove it from map } - else { + else + { temp.erase(it->first); // virtual desktop already in map, skip it ++it; } @@ -894,7 +988,21 @@ void FancyZones::HandleVirtualDesktopUpdates(HANDLE fancyZonesDestroyedEvent) no } } -winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept +void FancyZones::OnEditorExitEvent() noexcept { + // Colect information about changes in zone layout after editor exited. + JSONHelpers::FancyZonesDataInstance().ParseDeviceInfoFromTmpFile(ZoneWindowUtils::GetActiveZoneSetTmpPath()); + JSONHelpers::FancyZonesDataInstance().ParseDeletedCustomZoneSetsFromTmpFile(ZoneWindowUtils::GetCustomZoneSetsTmpPath()); + JSONHelpers::FancyZonesDataInstance().ParseCustomZoneSetFromTmpFile(ZoneWindowUtils::GetAppliedZoneSetTmpPath()); + JSONHelpers::FancyZonesDataInstance().SaveFancyZonesData(); +} + +winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, const winrt::com_ptr& settings) noexcept +{ + if (!settings) + { + return nullptr; + } + return winrt::make_self(hinstance, settings); } \ No newline at end of file diff --git a/src/modules/fancyzones/lib/FancyZones.h b/src/modules/fancyzones/lib/FancyZones.h index 035b2e83a3..47b5b43c85 100644 --- a/src/modules/fancyzones/lib/FancyZones.h +++ b/src/modules/fancyzones/lib/FancyZones.h @@ -2,15 +2,7 @@ interface IZoneWindow; interface IFancyZonesSettings; - -enum class DisplayChangeType -{ - WorkArea, - DisplayChange, - VirtualDesktop, - Editor, - Initialization -}; +interface IZoneSet; interface __declspec(uuid("{50D3F0F5-736E-4186-BDF4-3D6BEE150C3A}")) IFancyZones : public IUnknown { @@ -35,8 +27,8 @@ interface __declspec(uuid("{5C8D99D6-34B2-4F4A-A8E5-7483F6869775}")) IZoneWindow { IFACEMETHOD_(void, MoveWindowsOnActiveZoneSetChange)() = 0; IFACEMETHOD_(COLORREF, GetZoneHighlightColor)() = 0; - IFACEMETHOD_(GUID, GetCurrentMonitorZoneSetId)(HMONITOR monitor) = 0; + IFACEMETHOD_(IZoneWindow*, GetParentZoneWindow) (HMONITOR monitor) = 0; IFACEMETHOD_(int, GetZoneHighlightOpacity)() = 0; }; -winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept; +winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, const winrt::com_ptr& settings) noexcept; diff --git a/src/modules/fancyzones/lib/FancyZonesLib.vcxproj b/src/modules/fancyzones/lib/FancyZonesLib.vcxproj index f52848cb01..6df22f7e2b 100644 --- a/src/modules/fancyzones/lib/FancyZonesLib.vcxproj +++ b/src/modules/fancyzones/lib/FancyZonesLib.vcxproj @@ -92,6 +92,7 @@ + @@ -104,12 +105,14 @@ + Create Create + diff --git a/src/modules/fancyzones/lib/FancyZonesLib.vcxproj.filters b/src/modules/fancyzones/lib/FancyZonesLib.vcxproj.filters index 31731ca959..7478f922f8 100644 --- a/src/modules/fancyzones/lib/FancyZonesLib.vcxproj.filters +++ b/src/modules/fancyzones/lib/FancyZonesLib.vcxproj.filters @@ -45,6 +45,9 @@ Header Files + + Header Files + @@ -68,6 +71,12 @@ Source Files + + Source Files + + + Source Files + @@ -76,6 +85,5 @@ - \ No newline at end of file diff --git a/src/modules/fancyzones/lib/JsonHelpers.cpp b/src/modules/fancyzones/lib/JsonHelpers.cpp new file mode 100644 index 0000000000..05bcab097e --- /dev/null +++ b/src/modules/fancyzones/lib/JsonHelpers.cpp @@ -0,0 +1,950 @@ +#include "pch.h" +#include "JsonHelpers.h" +#include "RegistryHelpers.h" +#include "ZoneSet.h" + +#include + +#include +#include +#include +#include + +namespace +{ + // From Settings.cs + constexpr int c_focusModelId = 0xFFFF; + constexpr int c_rowsModelId = 0xFFFE; + constexpr int c_columnsModelId = 0xFFFD; + constexpr int c_gridModelId = 0xFFFC; + constexpr int c_priorityGridModelId = 0xFFFB; + constexpr int c_blankCustomModelId = 0xFFFA; + + const wchar_t* FANCY_ZONES_DATA_FILE = L"zones-settings.json"; +} + +namespace JSONHelpers +{ + json::JsonArray NumVecToJsonArray(const std::vector& vec) + { + json::JsonArray arr; + for (const auto& val : vec) + { + arr.Append(json::JsonValue::CreateNumberValue(val)); + } + + return arr; + } + + std::vector JsonArrayToNumVec(const json::JsonArray& arr) + { + std::vector vec; + for (const auto& val : arr) + { + vec.emplace_back(static_cast(val.GetNumber())); + } + + return vec; + } + + ZoneSetLayoutType TypeFromLayoutId(int layoutID) + { + switch (layoutID) + { + case c_focusModelId: + return ZoneSetLayoutType::Focus; + case c_columnsModelId: + return ZoneSetLayoutType::Columns; + case c_rowsModelId: + return ZoneSetLayoutType::Rows; + case c_gridModelId: + return ZoneSetLayoutType::Grid; + case c_priorityGridModelId: + return ZoneSetLayoutType::PriorityGrid; + case c_blankCustomModelId: + return ZoneSetLayoutType::Blank; + default: + return ZoneSetLayoutType::Custom; + } + } + + std::wstring TypeToString(ZoneSetLayoutType type) + { + switch (type) + { + case ZoneSetLayoutType::Blank: + return L"blank"; + case ZoneSetLayoutType::Focus: + return L"focus"; + case ZoneSetLayoutType::Columns: + return L"columns"; + case ZoneSetLayoutType::Rows: + return L"rows"; + case ZoneSetLayoutType::Grid: + return L"grid"; + case ZoneSetLayoutType::PriorityGrid: + return L"priority-grid"; + case ZoneSetLayoutType::Custom: + return L"custom"; + default: + return L"TypeToString_ERROR"; + } + } + + ZoneSetLayoutType TypeFromString(const std::wstring& typeStr) + { + if (typeStr == L"focus") + { + return JSONHelpers::ZoneSetLayoutType::Focus; + } + else if (typeStr == L"columns") + { + return JSONHelpers::ZoneSetLayoutType::Columns; + } + else if (typeStr == L"rows") + { + return JSONHelpers::ZoneSetLayoutType::Rows; + } + else if (typeStr == L"grid") + { + return JSONHelpers::ZoneSetLayoutType::Grid; + } + else if (typeStr == L"priority-grid") + { + return JSONHelpers::ZoneSetLayoutType::PriorityGrid; + } + else if (typeStr == L"custom") + { + return JSONHelpers::ZoneSetLayoutType::Custom; + } + else + { + return JSONHelpers::ZoneSetLayoutType::Blank; + } + } + + FancyZonesData& FancyZonesDataInstance() + { + static FancyZonesData instance; + return instance; + } + + FancyZonesData::FancyZonesData() + { + std::wstring result = PTSettingsHelper::get_module_save_folder_location(L"FancyZones"); + jsonFilePath = result + L"\\" + std::wstring(FANCY_ZONES_DATA_FILE); + } + + const std::wstring& FancyZonesData::GetPersistFancyZonesJSONPath() const + { + return jsonFilePath; + } + + json::JsonObject FancyZonesData::GetPersistFancyZonesJSON() + { + std::wstring save_file_path = GetPersistFancyZonesJSONPath(); + + auto result = json::from_file(save_file_path); + if (result) + { + return *result; + } + else + { + return json::JsonObject(); + } + } + + void FancyZonesData::AddDevice(const std::wstring& deviceId) + { + if (!deviceInfoMap.contains(deviceId)) + { + // Creates default entry in map when ZoneWindow is created + deviceInfoMap[deviceId] = DeviceInfoData{ ZoneSetData{ L"null", ZoneSetLayoutType::Blank } }; + + MigrateDeviceInfoFromRegistry(deviceId); + } + } + + void FancyZonesData::CloneDeviceInfo(const std::wstring& source, const std::wstring& destination) + { + // Clone information from source device if destination device is uninitialized (Blank). + auto& destInfo = deviceInfoMap[destination]; + if (destInfo.activeZoneSet.type == ZoneSetLayoutType::Blank) + { + destInfo = deviceInfoMap[source]; + } + } + + int FancyZonesData::GetAppLastZoneIndex(HWND window, const std::wstring_view& deviceId, const std::wstring_view& zoneSetId) const + { + auto processPath = get_process_path(window); + if (!processPath.empty()) + { + auto history = appZoneHistoryMap.find(processPath); + if (history != appZoneHistoryMap.end()) + { + const auto& data = history->second; + if (data.zoneSetUuid == zoneSetId && data.deviceId == deviceId) + { + return history->second.zoneIndex; + } + } + } + + return -1; + } + + bool FancyZonesData::RemoveAppLastZone(HWND window, const std::wstring_view& deviceId, const std::wstring_view& zoneSetId) + { + auto processPath = get_process_path(window); + if (!processPath.empty()) + { + auto history = appZoneHistoryMap.find(processPath); + if (history != appZoneHistoryMap.end()) + { + const auto& data = history->second; + if (data.zoneSetUuid == zoneSetId && data.deviceId == deviceId) + { + appZoneHistoryMap.erase(processPath); + return true; + } + } + } + + return false; + } + + bool FancyZonesData::SetAppLastZone(HWND window, const std::wstring& deviceId, const std::wstring& zoneSetId, int zoneIndex) + { + auto processPath = get_process_path(window); + if (processPath.empty()) + { + return false; + } + + appZoneHistoryMap[processPath] = AppZoneHistoryData{ .zoneSetUuid = zoneSetId, .deviceId = deviceId, .zoneIndex = zoneIndex }; + return true; + } + + void FancyZonesData::SetActiveZoneSet(const std::wstring& deviceId, const ZoneSetData& data) + { + auto it = deviceInfoMap.find(deviceId); + if (it != deviceInfoMap.end()) + { + it->second.activeZoneSet = data; + } + } + + void FancyZonesData::SerializeDeviceInfoToTmpFile(const DeviceInfoJSON& deviceInfo, std::wstring_view tmpFilePath) const + { + json::JsonObject deviceInfoJson = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(tmpFilePath, deviceInfoJson); + } + + void FancyZonesData::ParseDeviceInfoFromTmpFile(std::wstring_view tmpFilePath) + { + if (std::filesystem::exists(tmpFilePath)) + { + if (auto zoneSetJson = json::from_file(tmpFilePath); zoneSetJson.has_value()) + { + if (auto deviceInfo = DeviceInfoJSON::FromJson(zoneSetJson.value()); deviceInfo.has_value()) + { + activeDeviceId = deviceInfo->deviceId; + deviceInfoMap[activeDeviceId] = std::move(deviceInfo->data); + DeleteTmpFile(tmpFilePath); + } + } + } + else + { + activeDeviceId.clear(); + } + } + + bool FancyZonesData::ParseCustomZoneSetFromTmpFile(std::wstring_view tmpFilePath) + { + bool res = true; + if (std::filesystem::exists(tmpFilePath)) + { + try + { + if (auto customZoneSetJson = json::from_file(tmpFilePath); customZoneSetJson.has_value()) + { + if (auto customZoneSet = CustomZoneSetJSON::FromJson(customZoneSetJson.value()); customZoneSet.has_value()) + { + customZoneSetsMap[customZoneSet->uuid] = std::move(customZoneSet->data); + } + } + } + catch (const winrt::hresult_error&) + { + res = false; + } + + DeleteTmpFile(tmpFilePath); + } + return res; + } + + bool FancyZonesData::ParseDeletedCustomZoneSetsFromTmpFile(std::wstring_view tmpFilePath) + { + bool res = true; + if (std::filesystem::exists(tmpFilePath)) + { + auto deletedZoneSetsJson = json::from_file(tmpFilePath); + try + { + auto deletedCustomZoneSets = deletedZoneSetsJson->GetNamedArray(L"deleted-custom-zone-sets"); + for (auto zoneSet : deletedCustomZoneSets) + { + std::wstring uuid = L"{" + std::wstring{ zoneSet.GetString() } + L"}"; + customZoneSetsMap.erase(std::wstring{ uuid }); + } + } + catch (const winrt::hresult_error&) + { + res = false; + } + + DeleteTmpFile(tmpFilePath); + } + + return res; + } + + bool FancyZonesData::ParseAppZoneHistory(const json::JsonObject& fancyZonesDataJSON) + { + try + { + auto appLastZones = fancyZonesDataJSON.GetNamedArray(L"app-zone-history"); + + for (uint32_t i = 0; i < appLastZones.Size(); ++i) + { + json::JsonObject appLastZone = appLastZones.GetObjectAt(i); + if (auto appZoneHistory = AppZoneHistoryJSON::FromJson(appLastZone); appZoneHistory.has_value()) + { + appZoneHistoryMap[appZoneHistory->appPath] = std::move(appZoneHistory->data); + } + else + { + return false; + } + } + + return true; + } + catch (const winrt::hresult_error&) + { + return false; + } + } + + json::JsonArray FancyZonesData::SerializeAppZoneHistory() const + { + json::JsonArray appHistoryArray; + + for (const auto& [appPath, appZoneHistoryData] : appZoneHistoryMap) + { + appHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, appZoneHistoryData })); + } + + return appHistoryArray; + } + + bool FancyZonesData::ParseDeviceInfos(const json::JsonObject& fancyZonesDataJSON) + { + try + { + auto devices = fancyZonesDataJSON.GetNamedArray(L"devices"); + + for (uint32_t i = 0; i < devices.Size(); ++i) + { + if (auto device = DeviceInfoJSON::DeviceInfoJSON::FromJson(devices.GetObjectAt(i)); device.has_value()) + { + deviceInfoMap[device->deviceId] = std::move(device->data); + } + else + { + return false; + } + } + + return true; + } + catch (const winrt::hresult_error&) + { + return false; + } + } + + json::JsonArray FancyZonesData::SerializeDeviceInfos() const + { + json::JsonArray DeviceInfosJSON{}; + + for (const auto& [deviceID, deviceData] : deviceInfoMap) + { + if (deviceData.activeZoneSet.type != ZoneSetLayoutType::Blank) { + DeviceInfosJSON.Append(DeviceInfoJSON::DeviceInfoJSON::ToJson(DeviceInfoJSON{ deviceID, deviceData })); + } + } + + return DeviceInfosJSON; + } + + bool FancyZonesData::ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON) + { + try + { + auto customZoneSets = fancyZonesDataJSON.GetNamedArray(L"custom-zone-sets"); + + for (uint32_t i = 0; i < customZoneSets.Size(); ++i) + { + if (auto zoneSet = CustomZoneSetJSON::FromJson(customZoneSets.GetObjectAt(i)); zoneSet.has_value()) + { + customZoneSetsMap[zoneSet->uuid] = std::move(zoneSet->data); + } + } + + return true; + } + catch (const winrt::hresult_error&) + { + return false; + } + } + + json::JsonArray FancyZonesData::SerializeCustomZoneSets() const + { + json::JsonArray customZoneSetsJSON{}; + + for (const auto& [zoneSetId, zoneSetData] : customZoneSetsMap) + { + customZoneSetsJSON.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ zoneSetId, zoneSetData })); + } + + return customZoneSetsJSON; + } + + void FancyZonesData::CustomZoneSetsToJsonFile(std::wstring_view filePath) const + { + const auto& customZoneSetsJson = SerializeCustomZoneSets(); + json::JsonObject root{}; + root.SetNamedValue(L"custom-zone-sets", customZoneSetsJson); + json::to_file(filePath, root); + } + + void FancyZonesData::LoadFancyZonesData() + { + std::wstring jsonFilePath = GetPersistFancyZonesJSONPath(); + + if (!std::filesystem::exists(jsonFilePath)) + { + TmpMigrateAppliedZoneSetsFromRegistry(); + + // Custom zone sets have to be migrated after applied zone sets! + MigrateCustomZoneSetsFromRegistry(); + + SaveFancyZonesData(); + } + else + { + json::JsonObject fancyZonesDataJSON = GetPersistFancyZonesJSON(); + + ParseAppZoneHistory(fancyZonesDataJSON); + ParseDeviceInfos(fancyZonesDataJSON); + ParseCustomZoneSets(fancyZonesDataJSON); + } + } + + void FancyZonesData::SaveFancyZonesData() const + { + json::JsonObject root{}; + + root.SetNamedValue(L"app-zone-history", SerializeAppZoneHistory()); + root.SetNamedValue(L"devices", SerializeDeviceInfos()); + root.SetNamedValue(L"custom-zone-sets", SerializeCustomZoneSets()); + + json::to_file(jsonFilePath, root); + } + + void FancyZonesData::TmpMigrateAppliedZoneSetsFromRegistry() + { + std::wregex ex(L"^[0-9]{3,4}_[0-9]{3,4}$"); + + wchar_t key[256]; + StringCchPrintf(key, ARRAYSIZE(key), L"%s", RegistryHelpers::REG_SETTINGS); + HKEY hkey; + if (RegOpenKeyExW(HKEY_CURRENT_USER, key, 0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS) + { + wchar_t resolutionKey[256]{}; + DWORD resolutionKeyLength = ARRAYSIZE(resolutionKey); + DWORD i = 0; + while (RegEnumKeyW(hkey, i++, resolutionKey, resolutionKeyLength) == ERROR_SUCCESS) + { + std::wstring resolution{ resolutionKey }; + wchar_t appliedZoneSetskey[256]; + StringCchPrintf(appliedZoneSetskey, ARRAYSIZE(appliedZoneSetskey), L"%s\\%s", RegistryHelpers::REG_SETTINGS, resolutionKey); + HKEY appliedZoneSetsHkey; + if (std::regex_match(resolution, ex) && RegOpenKeyExW(HKEY_CURRENT_USER, appliedZoneSetskey, 0, KEY_ALL_ACCESS, &appliedZoneSetsHkey) == ERROR_SUCCESS) + { + ZoneSetPersistedDataOLD data; + DWORD dataSize = sizeof(data); + wchar_t value[256]{}; + DWORD valueLength = ARRAYSIZE(value); + DWORD i = 0; + + while (RegEnumValueW(appliedZoneSetsHkey, i++, value, &valueLength, nullptr, nullptr, reinterpret_cast(&data), &dataSize) == ERROR_SUCCESS) + { + ZoneSetData appliedZoneSetData; + appliedZoneSetData.type = TypeFromLayoutId(data.LayoutId); + if (appliedZoneSetData.type != ZoneSetLayoutType::Custom) + { + appliedZoneSetData.uuid = std::wstring{ value }; + } + else + { + // uuid is changed later to actual uuid when migrating custom zone sets + appliedZoneSetData.uuid = std::to_wstring(data.LayoutId); + } + appliedZoneSetsMap[value] = appliedZoneSetData; + dataSize = sizeof(data); + valueLength = ARRAYSIZE(value); + } + } + resolutionKeyLength = ARRAYSIZE(resolutionKey); + } + } + } + + void FancyZonesData::MigrateDeviceInfoFromRegistry(const std::wstring& deviceId) + { + wchar_t key[256]; + StringCchPrintf(key, ARRAYSIZE(key), L"%s\\%s", RegistryHelpers::REG_SETTINGS, deviceId.c_str()); + + wchar_t activeZoneSetId[256]; + activeZoneSetId[0] = '\0'; + DWORD bufferSize = sizeof(activeZoneSetId); + DWORD showSpacing = 1; + DWORD spacing = 16; + DWORD zoneCount = 3; + DWORD size = sizeof(DWORD); + + SHRegGetUSValueW(key, L"ActiveZoneSetId", nullptr, &activeZoneSetId, &bufferSize, FALSE, nullptr, 0); + SHRegGetUSValueW(key, L"ShowSpacing", nullptr, &showSpacing, &size, FALSE, nullptr, 0); + SHRegGetUSValueW(key, L"Spacing", nullptr, &spacing, &size, FALSE, nullptr, 0); + SHRegGetUSValueW(key, L"ZoneCount", nullptr, &zoneCount, &size, FALSE, nullptr, 0); + + if (appliedZoneSetsMap.contains(std::wstring{ activeZoneSetId })) + { + deviceInfoMap[deviceId] = DeviceInfoData{ appliedZoneSetsMap.at(std::wstring{ activeZoneSetId }), static_cast(showSpacing), static_cast(spacing), static_cast(zoneCount) }; + } + } + + void FancyZonesData::MigrateCustomZoneSetsFromRegistry() + { + wchar_t key[256]; + StringCchPrintf(key, ARRAYSIZE(key), L"%s\\%s", RegistryHelpers::REG_SETTINGS, L"Layouts"); + HKEY hkey; + if (RegOpenKeyExW(HKEY_CURRENT_USER, key, 0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS) + { + BYTE data[256]; + DWORD dataSize = ARRAYSIZE(data); + wchar_t value[256]{}; + DWORD valueLength = ARRAYSIZE(value); + DWORD i = 0; + while (RegEnumValueW(hkey, i++, value, &valueLength, nullptr, nullptr, reinterpret_cast(&data), &dataSize) == ERROR_SUCCESS) + { + CustomZoneSetData zoneSetData; + zoneSetData.name = std::wstring{ value }; + zoneSetData.type = static_cast(data[2]); + // int version = data[0] * 256 + data[1]; - Not used anymore + + std::wstring uuid = std::to_wstring(data[3] * 256 + data[4]); + auto it = std::find_if(appliedZoneSetsMap.begin(), appliedZoneSetsMap.end(), [&uuid](std::pair zoneSetMap) { + return zoneSetMap.second.uuid.compare(uuid) == 0; + }); + + if (it != appliedZoneSetsMap.end()) + { + it->second.uuid = uuid = it->first; + } + switch (zoneSetData.type) + { + case CustomLayoutType::Grid: + { + int j = 5; + GridLayoutInfo zoneSetInfo(GridLayoutInfo::Minimal{ .rows = data[j++], .columns = data[j++] }); + + for (int row = 0; row < zoneSetInfo.rows(); row++) + { + zoneSetInfo.rowsPercents()[row] = data[j++] * 256 + data[j++]; + } + + for (int col = 0; col < zoneSetInfo.columns(); col++) + { + zoneSetInfo.columnsPercents()[col] = data[j++] * 256 + data[j++]; + } + + for (int row = 0; row < zoneSetInfo.rows(); row++) + { + for (int col = 0; col < zoneSetInfo.columns(); col++) + { + zoneSetInfo.cellChildMap()[row][col] = data[j++]; + } + } + zoneSetData.info = zoneSetInfo; + break; + } + case CustomLayoutType::Canvas: + { + CanvasLayoutInfo info; + + int j = 5; + info.referenceWidth = data[j] * 256 + data[j + 1]; + j += 2; + info.referenceHeight = data[j] * 256 + data[j + 1]; + j += 2; + + int count = data[j++]; + info.zones.reserve(count); + while (count-- > 0) + { + int x = data[j] * 256 + data[j + 1]; + j += 2; + int y = data[j] * 256 + data[j + 1]; + j += 2; + int width = data[j] * 256 + data[j + 1]; + j += 2; + int height = data[j] * 256 + data[j + 1]; + j += 2; + info.zones.push_back(CanvasLayoutInfo::Rect{ + x, y, width, height }); + } + zoneSetData.info = info; + break; + } + default: + abort(); // TODO(stefan): Exception safety + } + customZoneSetsMap[uuid] = zoneSetData; + + valueLength = ARRAYSIZE(value); + dataSize = ARRAYSIZE(data); + } + } + } + + json::JsonObject ZoneSetData::ToJson(const ZoneSetData& zoneSet) + { + json::JsonObject result{}; + + result.SetNamedValue(L"uuid", json::value(zoneSet.uuid)); + result.SetNamedValue(L"type", json::value(TypeToString(zoneSet.type))); + + return result; + } + + std::optional ZoneSetData::FromJson(const json::JsonObject& zoneSet) + { + try + { + ZoneSetData zoneSetData; + + zoneSetData.uuid = zoneSet.GetNamedString(L"uuid"); + zoneSetData.type = TypeFromString(std::wstring{ zoneSet.GetNamedString(L"type") }); + + return zoneSetData; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } + + json::JsonObject AppZoneHistoryJSON::ToJson(const AppZoneHistoryJSON& appZoneHistory) + { + json::JsonObject result{}; + + result.SetNamedValue(L"app-path", json::value(appZoneHistory.appPath)); + result.SetNamedValue(L"zone-index", json::value(appZoneHistory.data.zoneIndex)); + result.SetNamedValue(L"device-id", json::value(appZoneHistory.data.deviceId)); + result.SetNamedValue(L"zoneset-uuid", json::value(appZoneHistory.data.zoneSetUuid)); + + return result; + } + + std::optional AppZoneHistoryJSON::FromJson(const json::JsonObject& zoneSet) + { + try + { + AppZoneHistoryJSON result; + + result.appPath = zoneSet.GetNamedString(L"app-path"); + result.data.zoneIndex = static_cast(zoneSet.GetNamedNumber(L"zone-index")); + result.data.deviceId = zoneSet.GetNamedString(L"device-id"); + result.data.zoneSetUuid = zoneSet.GetNamedString(L"zoneset-uuid"); + + return result; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } + + json::JsonObject DeviceInfoJSON::ToJson(const DeviceInfoJSON& device) + { + json::JsonObject result{}; + + result.SetNamedValue(L"device-id", json::value(device.deviceId)); + result.SetNamedValue(L"active-zoneset", ZoneSetData::ToJson(device.data.activeZoneSet)); + result.SetNamedValue(L"editor-show-spacing", json::value(device.data.showSpacing)); + result.SetNamedValue(L"editor-spacing", json::value(device.data.spacing)); + result.SetNamedValue(L"editor-zone-count", json::value(device.data.zoneCount)); + + return result; + } + + std::optional DeviceInfoJSON::FromJson(const json::JsonObject& device) + { + try + { + DeviceInfoJSON result; + + result.deviceId = device.GetNamedString(L"device-id"); + + if (auto zoneSet = ZoneSetData::FromJson(device.GetNamedObject(L"active-zoneset")); zoneSet.has_value()) + { + result.data.activeZoneSet = std::move(zoneSet.value()); + } + else + { + return std::nullopt; + } + + result.data.showSpacing = device.GetNamedBoolean(L"editor-show-spacing"); + result.data.spacing = static_cast(device.GetNamedNumber(L"editor-spacing")); + result.data.zoneCount = static_cast( + device.GetNamedNumber(L"editor-zone-count")); + + return result; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } + + json::JsonObject CanvasLayoutInfo::ToJson(const CanvasLayoutInfo& canvasInfo) + { + json::JsonObject infoJson{}; + infoJson.SetNamedValue(L"ref-width", json::value(canvasInfo.referenceWidth)); + infoJson.SetNamedValue(L"ref-height", json::value(canvasInfo.referenceHeight)); + json::JsonArray zonesJson; + + for (const auto& [x, y, width, height] : canvasInfo.zones) + { + json::JsonObject zoneJson; + zoneJson.SetNamedValue(L"X", json::value(x)); + zoneJson.SetNamedValue(L"Y", json::value(y)); + zoneJson.SetNamedValue(L"width", json::value(width)); + zoneJson.SetNamedValue(L"height", json::value(height)); + zonesJson.Append(zoneJson); + } + infoJson.SetNamedValue(L"zones", zonesJson); + return infoJson; + } + + std::optional CanvasLayoutInfo::FromJson(const json::JsonObject& infoJson) + { + try + { + CanvasLayoutInfo info; + info.referenceWidth = static_cast(infoJson.GetNamedNumber(L"ref-width")); + info.referenceHeight = static_cast(infoJson.GetNamedNumber(L"ref-height")); + json::JsonArray zonesJson = infoJson.GetNamedArray(L"zones"); + uint32_t size = zonesJson.Size(); + info.zones.reserve(size); + for (uint32_t i = 0; i < size; ++i) + { + json::JsonObject zoneJson = zonesJson.GetObjectAt(i); + const int x = static_cast(zoneJson.GetNamedNumber(L"X")); + const int y = static_cast(zoneJson.GetNamedNumber(L"Y")); + const int width = static_cast(zoneJson.GetNamedNumber(L"width")); + const int height = static_cast(zoneJson.GetNamedNumber(L"height")); + CanvasLayoutInfo::Rect zone{ x, y, width, height }; + info.zones.push_back(zone); + } + return info; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } + + GridLayoutInfo::GridLayoutInfo(const Minimal& info) : + m_rows(info.rows), + m_columns(info.columns) + { + m_rowsPercents.resize(m_rows, 0); + m_columnsPercents.resize(m_columns, 0); + m_cellChildMap.resize(m_rows, {}); + for (auto& cellRow : m_cellChildMap) + { + cellRow.resize(m_columns, 0); + } + } + + GridLayoutInfo::GridLayoutInfo(const Full& info) : + m_rows(info.rows), + m_columns(info.columns), + m_rowsPercents(info.rowsPercents), + m_columnsPercents(info.columnsPercents), + m_cellChildMap(info.cellChildMap) + { + m_rowsPercents.resize(m_rows, 0); + m_columnsPercents.resize(m_columns, 0); + m_cellChildMap.resize(m_rows, {}); + for (auto& cellRow : m_cellChildMap) + { + cellRow.resize(m_columns, 0); + } + } + + json::JsonObject GridLayoutInfo::ToJson(const GridLayoutInfo& gridInfo) + { + json::JsonObject infoJson; + infoJson.SetNamedValue(L"rows", json::value(gridInfo.m_rows)); + infoJson.SetNamedValue(L"columns", json::value(gridInfo.m_columns)); + infoJson.SetNamedValue(L"rows-percentage", NumVecToJsonArray(gridInfo.m_rowsPercents)); + infoJson.SetNamedValue(L"columns-percentage", NumVecToJsonArray(gridInfo.m_columnsPercents)); + + json::JsonArray cellChildMapJson; + for (int i = 0; i < gridInfo.m_cellChildMap.size(); ++i) + { + cellChildMapJson.Append(NumVecToJsonArray(gridInfo.m_cellChildMap[i])); + } + infoJson.SetNamedValue(L"cell-child-map", cellChildMapJson); + + return infoJson; + } + + std::optional GridLayoutInfo::FromJson(const json::JsonObject& infoJson) + { + try + { + GridLayoutInfo info(GridLayoutInfo::Minimal{}); + + info.m_rows = static_cast(infoJson.GetNamedNumber(L"rows")); + info.m_columns = static_cast(infoJson.GetNamedNumber(L"columns")); + + json::JsonArray rowsPercentage = infoJson.GetNamedArray(L"rows-percentage"); + json::JsonArray columnsPercentage = infoJson.GetNamedArray(L"columns-percentage"); + json::JsonArray cellChildMap = infoJson.GetNamedArray(L"cell-child-map"); + + if (rowsPercentage.Size() != info.m_rows || columnsPercentage.Size() != info.m_columns || cellChildMap.Size() != info.m_rows) + { + return std::nullopt; + } + + info.m_rowsPercents = JsonArrayToNumVec(rowsPercentage); + info.m_columnsPercents = JsonArrayToNumVec(columnsPercentage); + for (const auto& cellsRow : cellChildMap) + { + const auto cellsArray = cellsRow.GetArray(); + if (cellsArray.Size() != info.m_columns) + { + return std::nullopt; + } + info.cellChildMap().push_back(JsonArrayToNumVec(cellsArray)); + } + + return info; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } + + json::JsonObject CustomZoneSetJSON::ToJson(const CustomZoneSetJSON& customZoneSet) + { + json::JsonObject result{}; + + result.SetNamedValue(L"uuid", json::value(customZoneSet.uuid)); + result.SetNamedValue(L"name", json::value(customZoneSet.data.name)); + switch (customZoneSet.data.type) + { + case CustomLayoutType::Canvas: + { + result.SetNamedValue(L"type", json::value(L"canvas")); + + CanvasLayoutInfo info = std::get(customZoneSet.data.info); + result.SetNamedValue(L"info", CanvasLayoutInfo::ToJson(info)); + + break; + } + case CustomLayoutType::Grid: + { + result.SetNamedValue(L"type", json::value(L"grid")); + + GridLayoutInfo gridInfo = std::get(customZoneSet.data.info); + result.SetNamedValue(L"info", GridLayoutInfo::ToJson(gridInfo)); + + break; + } + } + + return result; + } + + std::optional CustomZoneSetJSON::FromJson(const json::JsonObject& customZoneSet) + { + try + { + CustomZoneSetJSON result; + + result.uuid = customZoneSet.GetNamedString(L"uuid"); + result.data.name = customZoneSet.GetNamedString(L"name"); + + json::JsonObject infoJson = customZoneSet.GetNamedObject(L"info"); + std::wstring zoneSetType = std::wstring{ customZoneSet.GetNamedString(L"type") }; + if (zoneSetType.compare(L"canvas") == 0) + { + if (auto info = CanvasLayoutInfo::FromJson(infoJson); info.has_value()) + { + result.data.type = CustomLayoutType::Canvas; + result.data.info = std::move(info.value()); + } + else + { + return std::nullopt; + } + } + else if (zoneSetType.compare(L"grid") == 0) + { + if (auto info = GridLayoutInfo::FromJson(infoJson); info.has_value()) + { + result.data.type = CustomLayoutType::Grid; + result.data.info = std::move(info.value()); + } + else + { + return std::nullopt; + } + } + else + { + return std::nullopt; + } + + return result; + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + } +} diff --git a/src/modules/fancyzones/lib/JsonHelpers.h b/src/modules/fancyzones/lib/JsonHelpers.h new file mode 100644 index 0000000000..5ec6b461fb --- /dev/null +++ b/src/modules/fancyzones/lib/JsonHelpers.h @@ -0,0 +1,240 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace JSONHelpers +{ + constexpr int MAX_ZONE_COUNT = 50; + + enum class ZoneSetLayoutType : int + { + Blank = -1, + Focus, + Columns, + Rows, + Grid, + PriorityGrid, + Custom + }; + + enum class CustomLayoutType : int + { + Grid = 0, + Canvas + }; + + std::wstring TypeToString(ZoneSetLayoutType type); + ZoneSetLayoutType TypeFromString(const std::wstring& typeStr); + + ZoneSetLayoutType TypeFromLayoutId(int layoutID); + + struct CanvasLayoutInfo + { + int referenceWidth; + int referenceHeight; + struct Rect + { + int x; + int y; + int width; + int height; + }; + std::vector zones; + + static json::JsonObject ToJson(const CanvasLayoutInfo& canvasInfo); + static std::optional FromJson(const json::JsonObject& infoJson); + }; + + class GridLayoutInfo + { + public: + struct Minimal + { + int rows; + int columns; + }; + + struct Full + { + int rows; + int columns; + const std::vector& rowsPercents; + const std::vector& columnsPercents; + const std::vector>& cellChildMap; + }; + + GridLayoutInfo(const Minimal& info); + GridLayoutInfo(const Full& info); + ~GridLayoutInfo() = default; + + static json::JsonObject ToJson(const GridLayoutInfo& gridInfo); + static std::optional FromJson(const json::JsonObject& infoJson); + + inline std::vector& rowsPercents() { return m_rowsPercents; }; + inline std::vector& columnsPercents() { return m_columnsPercents; }; + inline std::vector>& cellChildMap() { return m_cellChildMap; }; + + inline int rows() const { return m_rows; } + inline int columns() const { return m_columns; } + + inline const std::vector& rowsPercents() const { return m_rowsPercents; }; + inline const std::vector& columnsPercents() const { return m_columnsPercents; }; + inline const std::vector>& cellChildMap() const { return m_cellChildMap; }; + + private: + int m_rows; + int m_columns; + std::vector m_rowsPercents; + std::vector m_columnsPercents; + std::vector> m_cellChildMap; + }; + + struct CustomZoneSetData + { + std::wstring name; + CustomLayoutType type; + std::variant info; + }; + + struct CustomZoneSetJSON + { + std::wstring uuid; + CustomZoneSetData data; + + static json::JsonObject ToJson(const CustomZoneSetJSON& device); + static std::optional FromJson(const json::JsonObject& customZoneSet); + }; + + // TODO(stefan): This needs to be moved to ZoneSet.h (probably) + struct ZoneSetData + { + std::wstring uuid; + ZoneSetLayoutType type; + + static json::JsonObject ToJson(const ZoneSetData& zoneSet); + static std::optional FromJson(const json::JsonObject& zoneSet); + }; + + struct AppZoneHistoryData + { + std::wstring zoneSetUuid; + std::wstring deviceId; + int zoneIndex; + }; + + struct AppZoneHistoryJSON + { + std::wstring appPath; + AppZoneHistoryData data; + + static json::JsonObject ToJson(const AppZoneHistoryJSON& appZoneHistory); + static std::optional FromJson(const json::JsonObject& zoneSet); + }; + + struct DeviceInfoData + { + ZoneSetData activeZoneSet; + bool showSpacing; + int spacing; + int zoneCount; + }; + + struct DeviceInfoJSON + { + std::wstring deviceId; + DeviceInfoData data; + + static json::JsonObject ToJson(const DeviceInfoJSON& device); + static std::optional FromJson(const json::JsonObject& device); + }; + + class FancyZonesData + { + public: + FancyZonesData(); + + const std::wstring& GetPersistFancyZonesJSONPath() const; + json::JsonObject GetPersistFancyZonesJSON(); + + inline const std::unordered_map& GetDeviceInfoMap() const + { + return deviceInfoMap; + } + + inline const std::unordered_map& GetCustomZoneSetsMap() const + { + return customZoneSetsMap; + } + + inline const std::unordered_map& GetAppZoneHistoryMap() const + { + return appZoneHistoryMap; + } + + inline const std::wstring GetActiveDeviceId() const + { + return activeDeviceId; + } + + void SetActiveDeviceId(const std::wstring& deviceId) + { + activeDeviceId = deviceId; + } + + inline bool DeleteTmpFile(std::wstring_view tmpFilePath) const + { + return DeleteFileW(tmpFilePath.data()); + } + + void AddDevice(const std::wstring& deviceId); + void CloneDeviceInfo(const std::wstring& source, const std::wstring& destination); + + int GetAppLastZoneIndex(HWND window, const std::wstring_view& deviceId, const std::wstring_view& zoneSetId) const; + bool RemoveAppLastZone(HWND window, const std::wstring_view& deviceId, const std::wstring_view& zoneSetId); + bool SetAppLastZone(HWND window, const std::wstring& deviceId, const std::wstring& zoneSetId, int zoneIndex); + + void SetActiveZoneSet(const std::wstring& deviceId, const ZoneSetData& zoneSet); + + void SerializeDeviceInfoToTmpFile(const DeviceInfoJSON& deviceInfo, std::wstring_view tmpFilePath) const; + + void ParseDeviceInfoFromTmpFile(std::wstring_view tmpFilePath); + bool ParseCustomZoneSetFromTmpFile(std::wstring_view tmpFilePath); + bool ParseDeletedCustomZoneSetsFromTmpFile(std::wstring_view tmpFilePath); + + bool ParseAppZoneHistory(const json::JsonObject& fancyZonesDataJSON); + json::JsonArray SerializeAppZoneHistory() const; + bool ParseDeviceInfos(const json::JsonObject& fancyZonesDataJSON); + json::JsonArray SerializeDeviceInfos() const; + bool ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON); + json::JsonArray SerializeCustomZoneSets() const; + void CustomZoneSetsToJsonFile(std::wstring_view filePath) const; + + void LoadFancyZonesData(); + void SaveFancyZonesData() const; + + void MigrateDeviceInfoFromRegistry(const std::wstring& deviceId); + + private: + void TmpMigrateAppliedZoneSetsFromRegistry(); + void MigrateCustomZoneSetsFromRegistry(); + + std::unordered_map appliedZoneSetsMap{}; + std::unordered_map appZoneHistoryMap{}; + std::unordered_map deviceInfoMap{}; + std::unordered_map customZoneSetsMap{}; + + std::wstring activeDeviceId; + std::wstring jsonFilePath; + }; + + FancyZonesData& FancyZonesDataInstance(); +} diff --git a/src/modules/fancyzones/lib/RegistryHelpers.h b/src/modules/fancyzones/lib/RegistryHelpers.h index 627254a5c1..e16213e4a6 100644 --- a/src/modules/fancyzones/lib/RegistryHelpers.h +++ b/src/modules/fancyzones/lib/RegistryHelpers.h @@ -7,132 +7,6 @@ namespace RegistryHelpers static PCWSTR REG_SETTINGS = L"Software\\SuperFancyZones"; static PCWSTR APP_ZONE_HISTORY_SUBKEY = L"AppZoneHistory"; - inline PCWSTR GetKey(_In_opt_ PCWSTR monitorId, PWSTR key, size_t keyLength) - { - if (monitorId) - { - StringCchPrintf(key, keyLength, L"%s\\%s", REG_SETTINGS, monitorId); - } - else - { - StringCchPrintf(key, keyLength, L"%s", REG_SETTINGS); - } - return key; - } - - inline HKEY OpenKey(_In_opt_ PCWSTR monitorId) - { - HKEY hkey; - wchar_t key[256]; - GetKey(monitorId, key, ARRAYSIZE(key)); - if (RegOpenKeyExW(HKEY_CURRENT_USER, key, 0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS) - { - return hkey; - } - return nullptr; - } - - inline HKEY CreateKey(PCWSTR monitorId) - { - HKEY hkey; - wchar_t key[256]{}; - GetKey(monitorId, key, ARRAYSIZE(key)); - if (RegCreateKeyExW(HKEY_CURRENT_USER, key, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &hkey, nullptr) == ERROR_SUCCESS) - { - return hkey; - } - return nullptr; - } - - inline LSTATUS GetAppLastZone(HWND window, PCWSTR appPath, _Out_ PINT iZoneIndex) - { - *iZoneIndex = -1; - - LSTATUS res{}; - if (auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL)) - { - wchar_t keyPath[256]{}; - StringCchPrintf(keyPath, ARRAYSIZE(keyPath), L"%s\\%s\\%x", REG_SETTINGS, APP_ZONE_HISTORY_SUBKEY, monitor); - - DWORD zoneIndex; - DWORD dataType = REG_DWORD; - DWORD dataSize = sizeof(DWORD); - res = SHRegGetUSValueW(keyPath, appPath, &dataType, &zoneIndex, &dataSize, FALSE, nullptr, 0); - if (res == ERROR_SUCCESS) - { - *iZoneIndex = static_cast(zoneIndex); - } - } - return res; - } - - // Pass -1 for the zoneIndex to delete the entry from the registry - inline void SaveAppLastZone(HWND window, PCWSTR appPath, DWORD zoneIndex) - { - LSTATUS res{}; - if (auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL)) - { - wchar_t keyPath[256]{}; - StringCchPrintf(keyPath, ARRAYSIZE(keyPath), L"%s\\%s\\%x", REG_SETTINGS, APP_ZONE_HISTORY_SUBKEY, monitor); - if (zoneIndex == -1) - { - SHDeleteValueW(HKEY_CURRENT_USER, keyPath, appPath); - } - else - { - SHRegSetUSValueW(keyPath, appPath, REG_DWORD, &zoneIndex, sizeof(zoneIndex), SHREGSET_FORCE_HKCU); - } - } - } - - inline void GetString(PCWSTR uniqueId, PCWSTR setting, PWSTR value, DWORD cbValue) - { - wchar_t key[256]{}; - GetKey(uniqueId, key, ARRAYSIZE(key)); - SHRegGetUSValueW(key, setting, nullptr, value, &cbValue, FALSE, nullptr, 0); - } - - inline void SetString(PCWSTR uniqueId, PCWSTR setting, PCWSTR value) - { - wchar_t key[256]{}; - GetKey(uniqueId, key, ARRAYSIZE(key)); - SHRegSetUSValueW(key, setting, REG_SZ, value, sizeof(value) * static_cast(wcslen(value)), SHREGSET_FORCE_HKCU); - } - - template - inline void GetValue(PCWSTR monitorId, PCWSTR setting, t* value, DWORD size) - { - wchar_t key[256]{}; - GetKey(monitorId, key, ARRAYSIZE(key)); - SHRegGetUSValueW(key, setting, nullptr, value, &size, FALSE, nullptr, 0); - } - - template - inline void SetValue(PCWSTR monitorId, PCWSTR setting, t value, DWORD size) - { - wchar_t key[256]{}; - GetKey(monitorId, key, ARRAYSIZE(key)); - SHRegSetUSValueW(key, setting, REG_BINARY, &value, size, SHREGSET_FORCE_HKCU); - } - - inline void DeleteZoneSet(PCWSTR monitorId, GUID guid) - { - wil::unique_cotaskmem_string zoneSetId; - if (SUCCEEDED_LOG(StringFromCLSID(guid, &zoneSetId))) - { - wchar_t key[256]{}; - GetKey(monitorId, key, ARRAYSIZE(key)); - SHDeleteValueW(HKEY_CURRENT_USER, key, zoneSetId.get()); - } - } - - inline void DeleteAllZoneSets(PCWSTR monitorId) - { - wchar_t key[256]{}; - GetKey(monitorId, key, ARRAYSIZE(key)); - SHDeleteKey(HKEY_CURRENT_USER, key); - } - inline HRESULT GetCurrentVirtualDesktop(_Out_ GUID* id) { *id = GUID_NULL; diff --git a/src/modules/fancyzones/lib/Settings.cpp b/src/modules/fancyzones/lib/Settings.cpp index 09ded242de..f1bd179fa8 100644 --- a/src/modules/fancyzones/lib/Settings.cpp +++ b/src/modules/fancyzones/lib/Settings.cpp @@ -9,11 +9,11 @@ struct FancyZonesSettings : winrt::implements MakeZone(RECT zoneRect) noexcept +winrt::com_ptr MakeZone(const RECT& zoneRect) noexcept { return winrt::make_self(zoneRect); } diff --git a/src/modules/fancyzones/lib/Zone.h b/src/modules/fancyzones/lib/Zone.h index da4b954509..f2c7794897 100644 --- a/src/modules/fancyzones/lib/Zone.h +++ b/src/modules/fancyzones/lib/Zone.h @@ -11,4 +11,4 @@ interface __declspec(uuid("{8228E934-B6EF-402A-9892-15A1441BF8B0}")) IZone : pub IFACEMETHOD_(size_t, Id)() = 0; }; -winrt::com_ptr MakeZone(RECT zoneRect) noexcept; +winrt::com_ptr MakeZone(const RECT& zoneRect) noexcept; diff --git a/src/modules/fancyzones/lib/ZoneSet.cpp b/src/modules/fancyzones/lib/ZoneSet.cpp index 6b4b90ec59..cce49c61e3 100644 --- a/src/modules/fancyzones/lib/ZoneSet.cpp +++ b/src/modules/fancyzones/lib/ZoneSet.cpp @@ -1,12 +1,113 @@ #include "pch.h" +#include "util.h" #include "lib/ZoneSet.h" #include "lib/RegistryHelpers.h" +#include + +namespace +{ + constexpr int C_MULTIPLIER = 10000; + + /* + struct GridLayoutInfo { + int rows; + int columns; + int rowsPercents[MAX_ZONE_COUNT]; + int columnsPercents[MAX_ZONE_COUNT]; + int cellChildMap[MAX_ZONE_COUNT][MAX_ZONE_COUNT]; + }; + */ + + auto l = JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Minimal{ .rows = 1, .columns = 1 }); + // PriorityGrid layout is unique for zoneCount <= 11. For zoneCount > 11 PriorityGrid is same as Grid + JSONHelpers::GridLayoutInfo predefinedPriorityGridLayouts[11] = { + /* 1 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 1, + .rowsPercents = { 10000 }, + .columnsPercents = { 10000 }, + .cellChildMap = { { 0 } } }), + /* 2 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 2, + .rowsPercents = { 10000 }, + .columnsPercents = { 6667, 3333 }, + .cellChildMap = { { 0, 1 } } }), + /* 3 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } }), + /* 4 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 2, + .columns = 3, + .rowsPercents = { 5000, 5000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 }, { 0, 1, 3 } } }), + /* 5 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 2, + .columns = 3, + .rowsPercents = { 5000, 5000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 }, { 3, 1, 4 } } }), + /* 6 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 3, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 }, { 0, 1, 3 }, { 4, 1, 5 } } }), + /* 7 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 3, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 }, { 3, 1, 4 }, { 5, 1, 6 } } }), + /* 8 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 4, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 2500, 2500, 2500 }, + .cellChildMap = { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 2, 7 } } }), + /* 9 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 4, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 2500, 2500, 2500 }, + .cellChildMap = { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 7, 8 } } }), + /* 10 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 4, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 2500, 2500, 2500 }, + .cellChildMap = { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 1, 8, 9 } } }), + /* 11 */ + JSONHelpers::GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 3, + .columns = 4, + .rowsPercents = { 3333, 3334, 3333 }, + .columnsPercents = { 2500, 2500, 2500, 2500 }, + .cellChildMap = { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 8, 9, 10 } } }), + }; +} + struct ZoneSet : winrt::implements { public: - ZoneSet(ZoneSetConfig const& config) : m_config(config) + ZoneSet(ZoneSetConfig const& config) : + m_config(config) { } @@ -16,18 +117,35 @@ public: { } - IFACEMETHODIMP_(GUID) Id() noexcept { return m_config.Id; } - IFACEMETHODIMP_(WORD) LayoutId() noexcept { return m_config.LayoutId; } + IFACEMETHODIMP_(GUID) + Id() noexcept { return m_config.Id; } + IFACEMETHODIMP_(JSONHelpers::ZoneSetLayoutType) + LayoutType() noexcept { return m_config.LayoutType; } IFACEMETHODIMP AddZone(winrt::com_ptr zone) noexcept; - IFACEMETHODIMP_(winrt::com_ptr) ZoneFromPoint(POINT pt) noexcept; - IFACEMETHODIMP_(int) GetZoneIndexFromWindow(HWND window) noexcept; - IFACEMETHODIMP_(std::vector>) GetZones() noexcept { return m_zones; } - IFACEMETHODIMP_(void) Save() noexcept; - IFACEMETHODIMP_(void) MoveWindowIntoZoneByIndex(HWND window, HWND zoneWindow, int index) noexcept; - IFACEMETHODIMP_(void) MoveWindowIntoZoneByDirection(HWND window, HWND zoneWindow, DWORD vkCode) noexcept; - IFACEMETHODIMP_(void) MoveSizeEnd(HWND window, HWND zoneWindow, POINT ptClient) noexcept; + IFACEMETHODIMP_(winrt::com_ptr) + ZoneFromPoint(POINT pt) noexcept; + IFACEMETHODIMP_(int) + GetZoneIndexFromWindow(HWND window) noexcept; + IFACEMETHODIMP_(std::vector>) + GetZones() noexcept { return m_zones; } + IFACEMETHODIMP_(void) + MoveWindowIntoZoneByIndex(HWND window, HWND zoneWindow, int index) noexcept; + IFACEMETHODIMP_(void) + MoveWindowIntoZoneByDirection(HWND window, HWND zoneWindow, DWORD vkCode) noexcept; + IFACEMETHODIMP_(void) + MoveWindowIntoZoneByPoint(HWND window, HWND zoneWindow, POINT ptClient) noexcept; + IFACEMETHODIMP_(bool) + CalculateZones(MONITORINFO monitorInfo, int zoneCount, int spacing) noexcept; private: + bool CalculateFocusLayout(Rect workArea, int zoneCount) noexcept; + bool CalculateColumnsAndRowsLayout(Rect workArea, JSONHelpers::ZoneSetLayoutType type, int zoneCount, int spacing) noexcept; + bool CalculateGridLayout(Rect workArea, JSONHelpers::ZoneSetLayoutType type, int zoneCount, int spacing) noexcept; + bool CalculateUniquePriorityGridLayout(Rect workArea, int zoneCount, int spacing) noexcept; + bool CalculateCustomLayout(Rect workArea, int spacing) noexcept; + + bool CalculateGridZones(Rect workArea, JSONHelpers::GridLayoutInfo gridLayoutInfo, int spacing); + winrt::com_ptr ZoneFromWindow(HWND window) noexcept; std::vector> m_zones; @@ -44,7 +162,8 @@ IFACEMETHODIMP ZoneSet::AddZone(winrt::com_ptr zone) noexcept return S_OK; } -IFACEMETHODIMP_(winrt::com_ptr) ZoneSet::ZoneFromPoint(POINT pt) noexcept +IFACEMETHODIMP_(winrt::com_ptr) +ZoneSet::ZoneFromPoint(POINT pt) noexcept { winrt::com_ptr smallestKnownZone = nullptr; // To reduce redundant calculations, we will store the last known zones area. @@ -61,16 +180,16 @@ IFACEMETHODIMP_(winrt::com_ptr) ZoneSet::ZoneFromPoint(POINT pt) noexcept smallestKnownZone = zone; RECT* r = &smallestKnownZone->GetZoneRect(); - smallestKnownZoneArea = (r->right-r->left)*(r->bottom-r->top); + smallestKnownZoneArea = (r->right - r->left) * (r->bottom - r->top); } else { - int newZoneArea = (newZoneRect->right-newZoneRect->left)*(newZoneRect->bottom-newZoneRect->top); + int newZoneArea = (newZoneRect->right - newZoneRect->left) * (newZoneRect->bottom - newZoneRect->top); - if (newZoneArea) ZoneSet::ZoneFromPoint(POINT pt) noexcept return smallestKnownZone; } -IFACEMETHODIMP_(void) ZoneSet::Save() noexcept -{ - size_t const zoneCount = m_zones.size(); - if (zoneCount == 0) - { - RegistryHelpers::DeleteZoneSet(m_config.ResolutionKey, m_config.Id); - } - else - { - ZoneSetPersistedData data{}; - data.LayoutId = m_config.LayoutId; - data.ZoneCount = static_cast(zoneCount); - - int i = 0; - for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++) - { - winrt::com_ptr zone = iter->as(); - CopyRect(&data.Zones[i++], &zone->GetZoneRect()); - } - - wil::unique_cotaskmem_string guid; - if (SUCCEEDED_LOG(StringFromCLSID(m_config.Id, &guid))) - { - if (wil::unique_hkey hkey{ RegistryHelpers::CreateKey(m_config.ResolutionKey) }) - { - RegSetValueExW(hkey.get(), guid.get(), 0, REG_BINARY, reinterpret_cast(&data), sizeof(data)); - } - } - } -} - -IFACEMETHODIMP_(int) ZoneSet::GetZoneIndexFromWindow(HWND window) noexcept +IFACEMETHODIMP_(int) +ZoneSet::GetZoneIndexFromWindow(HWND window) noexcept { int zoneIndex = 0; for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++, zoneIndex++) @@ -127,26 +216,40 @@ IFACEMETHODIMP_(int) ZoneSet::GetZoneIndexFromWindow(HWND window) noexcept return -1; } -IFACEMETHODIMP_(void) ZoneSet::MoveWindowIntoZoneByIndex(HWND window, HWND windowZone, int index) noexcept +IFACEMETHODIMP_(void) +ZoneSet::MoveWindowIntoZoneByIndex(HWND window, HWND windowZone, int index) noexcept { - if (index >= static_cast(m_zones.size())) + if (m_zones.empty()) + { + return; + } + + if (index >= int(m_zones.size())) { index = 0; } - if (index < m_zones.size()) + while (auto zoneDrop = ZoneFromWindow(window)) { - if (auto zone = m_zones.at(index)) - { - zone->AddWindowToZone(window, windowZone, false); - } + zoneDrop->RemoveWindowFromZone(window, !IsZoomed(window)); + } + + if (auto zone = m_zones.at(index)) + { + zone->AddWindowToZone(window, windowZone, false); } } -IFACEMETHODIMP_(void) ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCode) noexcept +IFACEMETHODIMP_(void) +ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCode) noexcept { - winrt::com_ptr oldZone; - winrt::com_ptr newZone; + if (m_zones.empty()) + { + return; + } + + winrt::com_ptr oldZone = nullptr; + winrt::com_ptr newZone = nullptr; auto iter = std::find(m_zones.begin(), m_zones.end(), ZoneFromWindow(window)); if (iter == m_zones.end()) @@ -183,9 +286,10 @@ IFACEMETHODIMP_(void) ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND w } } -IFACEMETHODIMP_(void) ZoneSet::MoveSizeEnd(HWND window, HWND zoneWindow, POINT ptClient) noexcept +IFACEMETHODIMP_(void) +ZoneSet::MoveWindowIntoZoneByPoint(HWND window, HWND zoneWindow, POINT ptClient) noexcept { - if (auto zoneDrop = ZoneFromWindow(window)) + while (auto zoneDrop = ZoneFromWindow(window)) { zoneDrop->RemoveWindowFromZone(window, !IsZoomed(window)); } @@ -196,6 +300,314 @@ IFACEMETHODIMP_(void) ZoneSet::MoveSizeEnd(HWND window, HWND zoneWindow, POINT p } } +IFACEMETHODIMP_(bool) +ZoneSet::CalculateZones(MONITORINFO monitorInfo, int zoneCount, int spacing) noexcept +{ + Rect const workArea(monitorInfo.rcWork); + //invalid work area + if (workArea.width() == 0 || workArea.height() == 0) + { + return false; + } + + //invalid zoneCount, may cause division by zero + if (zoneCount <= 0 && m_config.LayoutType != JSONHelpers::ZoneSetLayoutType::Custom) + { + return false; + } + + bool success = true; + switch (m_config.LayoutType) + { + case JSONHelpers::ZoneSetLayoutType::Focus: + success = CalculateFocusLayout(workArea, zoneCount); + break; + case JSONHelpers::ZoneSetLayoutType::Columns: + case JSONHelpers::ZoneSetLayoutType::Rows: + success = CalculateColumnsAndRowsLayout(workArea, m_config.LayoutType, zoneCount, spacing); + break; + case JSONHelpers::ZoneSetLayoutType::Grid: + case JSONHelpers::ZoneSetLayoutType::PriorityGrid: + success = CalculateGridLayout(workArea, m_config.LayoutType, zoneCount, spacing); + break; + case JSONHelpers::ZoneSetLayoutType::Custom: + success = CalculateCustomLayout(workArea, spacing); + break; + } + + return success; +} + +bool ZoneSet::CalculateFocusLayout(Rect workArea, int zoneCount) noexcept +{ + bool success = true; + + long left{ long(workArea.width() * 0.1) }; + long top{ long(workArea.height() * 0.1) }; + long right{ long(workArea.width() * 0.6) }; + long bottom{ long(workArea.height() * 0.6) }; + + RECT focusZoneRect{ left, top, right, bottom }; + + long focusRectXIncrement = (zoneCount <= 1) ? 0 : (int)(workArea.width() * 0.2) / (zoneCount - 1); + long focusRectYIncrement = (zoneCount <= 1) ? 0 : (int)(workArea.height() * 0.2) / (zoneCount - 1); + + if (left >= right || top >= bottom || left < 0 || right < 0 || top < 0 || bottom < 0) + { + success = false; + } + + for (int i = 0; i < zoneCount; i++) + { + AddZone(MakeZone(focusZoneRect)); + focusZoneRect.left += focusRectXIncrement; + focusZoneRect.right += focusRectXIncrement; + focusZoneRect.bottom += focusRectYIncrement; + focusZoneRect.top += focusRectYIncrement; + } + + return success; +} + +bool ZoneSet::CalculateColumnsAndRowsLayout(Rect workArea, JSONHelpers::ZoneSetLayoutType type, int zoneCount, int spacing) noexcept +{ + bool success = true; + + int zonePercent = C_MULTIPLIER / zoneCount; + + long totalWidth; + long totalHeight; + + long cellWidth; + long cellHeight; + + if (type == JSONHelpers::ZoneSetLayoutType::Columns) + { + totalWidth = workArea.width() - (spacing * (zoneCount + 1)); + totalHeight = workArea.height() - (spacing * 2); + cellWidth = totalWidth * zonePercent / C_MULTIPLIER; + cellHeight = totalHeight; + } + else + { //Rows + totalWidth = workArea.width() - (spacing * 2); + totalHeight = workArea.height() - (spacing * (zoneCount + 1)); + cellWidth = totalWidth; + cellHeight = totalHeight * zonePercent / C_MULTIPLIER; + } + + long top = spacing; + long left = spacing; + long bottom = top + cellHeight; + long right = left + cellWidth; + + for (int zone = 0; zone < zoneCount; zone++) + { + if (left >= right || top >= bottom || left < 0 || right < 0 || top < 0 || bottom < 0) + { + success = false; + } + + RECT focusZoneRect{ left, top, right, bottom }; + AddZone(MakeZone(focusZoneRect)); + + if (type == JSONHelpers::ZoneSetLayoutType::Columns) + { + left += cellWidth + spacing; + right = left + cellWidth; + } + else + { //Rows + top += cellHeight + spacing; + bottom = top + cellHeight; + } + } + + return success; +} + +bool ZoneSet::CalculateGridLayout(Rect workArea, JSONHelpers::ZoneSetLayoutType type, int zoneCount, int spacing) noexcept +{ + const auto count = sizeof(predefinedPriorityGridLayouts) / sizeof(JSONHelpers::GridLayoutInfo); + if (type == JSONHelpers::ZoneSetLayoutType::PriorityGrid && zoneCount < count) + { + return CalculateUniquePriorityGridLayout(workArea, zoneCount, spacing); + } + + int rows = 1, columns = 1; + while (zoneCount / rows >= rows) + { + rows++; + } + rows--; + columns = zoneCount / rows; + if (zoneCount % rows == 0) + { + // even grid + } + else + { + columns++; + } + + JSONHelpers::GridLayoutInfo gridLayoutInfo(JSONHelpers::GridLayoutInfo::Minimal{ .rows = rows, .columns = columns }); + + for (int row = 0; row < rows; row++) + { + gridLayoutInfo.rowsPercents()[row] = C_MULTIPLIER / rows; + } + for (int col = 0; col < columns; col++) + { + gridLayoutInfo.columnsPercents()[col] = C_MULTIPLIER / columns; + } + + for (int i = 0; i < rows; ++i) + { + gridLayoutInfo.cellChildMap()[i] = std::vector(columns); + } + + int index = 0; + for (int col = columns - 1; col >= 0; col--) + { + for (int row = rows - 1; row >= 0; row--) + { + gridLayoutInfo.cellChildMap()[row][col] = index++; + if (index == zoneCount) + { + index--; + } + } + } + return CalculateGridZones(workArea, gridLayoutInfo, spacing); +} + +bool ZoneSet::CalculateUniquePriorityGridLayout(Rect workArea, int zoneCount, int spacing) noexcept +{ + if (zoneCount <= 0 || zoneCount >= sizeof(predefinedPriorityGridLayouts)) + { + return false; + } + + return CalculateGridZones(workArea, predefinedPriorityGridLayouts[zoneCount - 1], spacing); +} + +bool ZoneSet::CalculateCustomLayout(Rect workArea, int spacing) noexcept +{ + wil::unique_cotaskmem_string guuidStr; + if (SUCCEEDED_LOG(StringFromCLSID(m_config.Id, &guuidStr))) + { + const std::wstring guuid = guuidStr.get(); + const auto& customZoneSets = JSONHelpers::FancyZonesDataInstance().GetCustomZoneSetsMap(); + if (!customZoneSets.contains(guuid)) + { + return false; + } + + const auto& zoneSet = customZoneSets.at(guuid); + if (zoneSet.type == JSONHelpers::CustomLayoutType::Canvas && std::holds_alternative(zoneSet.info)) + { + const auto& zoneSetInfo = std::get(zoneSet.info); + for (const auto& zone : zoneSetInfo.zones) + { + int x = zone.x; + int y = zone.y; + int width = zone.width; + int height = zone.height; + + if (x < 0 || y < 0 || width < 0 || height < 0) + { + return false; + } + + DPIAware::Convert(m_config.Monitor, x, y); + DPIAware::Convert(m_config.Monitor, width, height); + + AddZone(MakeZone(RECT{ x, y, x + width, y + height })); + } + + return true; + } + else if (zoneSet.type == JSONHelpers::CustomLayoutType::Grid && std::holds_alternative(zoneSet.info)) + { + const auto& info = std::get(zoneSet.info); + return CalculateGridZones(workArea, info, spacing); + } + } + + return false; +} + +bool ZoneSet::CalculateGridZones(Rect workArea, JSONHelpers::GridLayoutInfo gridLayoutInfo, int spacing) +{ + bool success = true; + + long totalWidth = workArea.width() - (spacing * (gridLayoutInfo.columns() + 1)); + long totalHeight = workArea.height() - (spacing * (gridLayoutInfo.rows() + 1)); + struct Info + { + long Extent; + long Start; + long End; + }; + Info rowInfo[JSONHelpers::MAX_ZONE_COUNT]; + Info columnInfo[JSONHelpers::MAX_ZONE_COUNT]; + + long top = spacing; + for (int row = 0; row < gridLayoutInfo.rows(); row++) + { + rowInfo[row].Start = top; + rowInfo[row].Extent = totalHeight * gridLayoutInfo.rowsPercents()[row] / C_MULTIPLIER; + rowInfo[row].End = rowInfo[row].Start + rowInfo[row].Extent; + top += rowInfo[row].Extent + spacing; + } + + long left = spacing; + for (int col = 0; col < gridLayoutInfo.columns(); col++) + { + columnInfo[col].Start = left; + columnInfo[col].Extent = totalWidth * gridLayoutInfo.columnsPercents()[col] / C_MULTIPLIER; + columnInfo[col].End = columnInfo[col].Start + columnInfo[col].Extent; + left += columnInfo[col].Extent + spacing; + } + + for (int row = 0; row < gridLayoutInfo.rows(); row++) + { + for (int col = 0; col < gridLayoutInfo.columns(); col++) + { + int i = gridLayoutInfo.cellChildMap()[row][col]; + if (((row == 0) || (gridLayoutInfo.cellChildMap()[row - 1][col] != i)) && + ((col == 0) || (gridLayoutInfo.cellChildMap()[row][col - 1] != i))) + { + left = columnInfo[col].Start; + top = rowInfo[row].Start; + + int maxRow = row; + while (((maxRow + 1) < gridLayoutInfo.rows()) && (gridLayoutInfo.cellChildMap()[maxRow + 1][col] == i)) + { + maxRow++; + } + int maxCol = col; + while (((maxCol + 1) < gridLayoutInfo.columns()) && (gridLayoutInfo.cellChildMap()[row][maxCol + 1] == i)) + { + maxCol++; + } + + long right = columnInfo[maxCol].End; + long bottom = rowInfo[maxRow].End; + + if (left >= right || top >= bottom || left < 0 || right < 0 || top < 0 || bottom < 0) + { + success = false; + } + + AddZone(MakeZone(RECT{ left, top, right, bottom })); + } + } + } + + return success; +} + winrt::com_ptr ZoneSet::ZoneFromWindow(HWND window) noexcept { for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++) diff --git a/src/modules/fancyzones/lib/ZoneSet.h b/src/modules/fancyzones/lib/ZoneSet.h index 2871dd6bd9..c500d4b11d 100644 --- a/src/modules/fancyzones/lib/ZoneSet.h +++ b/src/modules/fancyzones/lib/ZoneSet.h @@ -1,27 +1,21 @@ #pragma once #include "Zone.h" +#include "JsonHelpers.h" -enum class ZoneSetLayout -{ - Grid, - Row, - Focus, - Custom -}; interface __declspec(uuid("{E4839EB7-669D-49CF-84A9-71A2DFD851A3}")) IZoneSet : public IUnknown { IFACEMETHOD_(GUID, Id)() = 0; - IFACEMETHOD_(WORD, LayoutId)() = 0; + IFACEMETHOD_(JSONHelpers::ZoneSetLayoutType, LayoutType)() = 0; IFACEMETHOD(AddZone)(winrt::com_ptr zone) = 0; IFACEMETHOD_(winrt::com_ptr, ZoneFromPoint)(POINT pt) = 0; IFACEMETHOD_(int, GetZoneIndexFromWindow)(HWND window) = 0; IFACEMETHOD_(std::vector>, GetZones)() = 0; - IFACEMETHOD_(void, Save)() = 0; IFACEMETHOD_(void, MoveWindowIntoZoneByIndex)(HWND window, HWND zoneWindow, int index) = 0; IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, HWND zoneWindow, DWORD vkCode) = 0; - IFACEMETHOD_(void, MoveSizeEnd)(HWND window, HWND zoneWindow, POINT ptClient) = 0; + IFACEMETHOD_(void, MoveWindowIntoZoneByPoint)(HWND window, HWND zoneWindow, POINT ptClient) = 0; + IFACEMETHOD_(bool, CalculateZones)(MONITORINFO monitorInfo, int zoneCount, int spacing) = 0; }; #define VERSION_PERSISTEDDATA 0x0000F00D @@ -32,28 +26,39 @@ struct ZoneSetPersistedData DWORD Version{VERSION_PERSISTEDDATA}; WORD LayoutId{}; DWORD ZoneCount{}; - ZoneSetLayout Layout{}; + JSONHelpers::ZoneSetLayoutType Layout{}; + RECT Zones[MAX_ZONES]{}; +}; + +struct ZoneSetPersistedDataOLD +{ + static constexpr inline size_t MAX_ZONES = 40; + DWORD Version{ VERSION_PERSISTEDDATA }; + WORD LayoutId{}; + DWORD ZoneCount{}; + JSONHelpers::ZoneSetLayoutType Layout{}; DWORD PaddingInner{}; DWORD PaddingOuter{}; RECT Zones[MAX_ZONES]{}; }; + struct ZoneSetConfig { ZoneSetConfig( GUID id, - WORD layoutId, + JSONHelpers::ZoneSetLayoutType layoutType, HMONITOR monitor, PCWSTR resolutionKey) noexcept : Id(id), - LayoutId(layoutId), + LayoutType(layoutType), Monitor(monitor), ResolutionKey(resolutionKey) { } GUID Id{}; - WORD LayoutId{}; + JSONHelpers::ZoneSetLayoutType LayoutType{}; HMONITOR Monitor{}; PCWSTR ResolutionKey{}; }; diff --git a/src/modules/fancyzones/lib/ZoneWindow.cpp b/src/modules/fancyzones/lib/ZoneWindow.cpp index 7bf747c764..a70ffc5dfa 100644 --- a/src/modules/fancyzones/lib/ZoneWindow.cpp +++ b/src/modules/fancyzones/lib/ZoneWindow.cpp @@ -1,765 +1,778 @@ -#include "pch.h" - -#include -#include - -#include "ZoneWindow.h" -#include "trace.h" -#include "util.h" -#include "RegistryHelpers.h" - -#include - -struct ZoneWindow : public winrt::implements -{ -public: - ZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, PCWSTR deviceId, PCWSTR virtualDesktopId, bool flashZones); - - IFACEMETHODIMP MoveSizeEnter(HWND window, bool dragEnabled) noexcept; - IFACEMETHODIMP MoveSizeUpdate(POINT const& ptScreen, bool dragEnabled) noexcept; - IFACEMETHODIMP MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept; - IFACEMETHODIMP MoveSizeCancel() noexcept; - IFACEMETHODIMP_(bool) IsDragEnabled() noexcept { return m_dragEnabled; } - IFACEMETHODIMP_(void) MoveWindowIntoZoneByIndex(HWND window, int index) noexcept; - IFACEMETHODIMP_(void) MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept; - IFACEMETHODIMP_(void) CycleActiveZoneSet(DWORD vkCode) noexcept; - IFACEMETHODIMP_(std::wstring) DeviceId() noexcept { return { m_deviceId.get() }; } - IFACEMETHODIMP_(std::wstring) UniqueId() noexcept { return { m_uniqueId }; } - IFACEMETHODIMP_(std::wstring) WorkAreaKey() noexcept { return { m_workArea }; } - IFACEMETHODIMP_(void) SaveWindowProcessToZoneIndex(HWND window) noexcept; - IFACEMETHODIMP_(IZoneSet*) ActiveZoneSet() noexcept { return m_activeZoneSet.get(); } - -protected: - static LRESULT CALLBACK s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; - -private: - struct ColorSetting - { - BYTE fillAlpha{}; - COLORREF fill{}; - BYTE borderAlpha{}; - COLORREF border{}; - int thickness{}; - }; - - void ShowZoneWindow() noexcept; - void HideZoneWindow() noexcept; - void InitializeId(PCWSTR deviceId, PCWSTR virtualDesktopId) noexcept; - void LoadSettings() noexcept; - void InitializeZoneSets(MONITORINFO const& mi) noexcept; - void LoadZoneSetsFromRegistry() noexcept; - void UpdateActiveZoneSet(_In_opt_ IZoneSet* zoneSet) noexcept; - LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept; - void DrawBackdrop(wil::unique_hdc& hdc, RECT const& clientRect) noexcept; - void DrawZone(wil::unique_hdc& hdc, ColorSetting const& colorSetting, winrt::com_ptr zone) noexcept; - void DrawIndex(wil::unique_hdc& hdc, POINT offset, size_t index, int padding, int size, bool flipX, bool flipY, COLORREF colorFill); - void DrawActiveZoneSet(wil::unique_hdc& hdc, RECT const& clientRect) noexcept; - void OnPaint(wil::unique_hdc& hdc) noexcept; - void OnKeyUp(WPARAM wparam) noexcept; - winrt::com_ptr ZoneFromPoint(POINT pt) noexcept; - void ChooseDefaultActiveZoneSet() noexcept; - bool IsOccluded(POINT pt, size_t index) noexcept; - void CycleActiveZoneSetInternal(DWORD wparam, Trace::ZoneWindow::InputMode mode) noexcept; - void FlashZones() noexcept; - UINT GetDpiForMonitor() noexcept; - - winrt::com_ptr m_host; - HMONITOR m_monitor{}; - wchar_t m_uniqueId[256]{}; // Parsed deviceId + resolution + virtualDesktopId - wchar_t m_workArea[256]{}; - wil::unique_cotaskmem_string m_deviceId{}; - wil::unique_hwnd m_window{}; - HWND m_windowMoveSize{}; - bool m_drawHints{}; - bool m_flashMode{}; - bool m_dragEnabled{}; - winrt::com_ptr m_activeZoneSet; - GUID m_activeZoneSetId{}; - std::vector> m_zoneSets; - winrt::com_ptr m_highlightZone; - WPARAM m_keyLast{}; - size_t m_keyCycle{}; - static const UINT m_showAnimationDuration = 200; // ms - static const UINT m_flashDuration = 700; // ms -}; - -ZoneWindow::ZoneWindow( - IZoneWindowHost* host, - HINSTANCE hinstance, - HMONITOR monitor, - PCWSTR deviceId, - PCWSTR virtualDesktopId, - bool flashZones) - : m_monitor(monitor) -{ - m_host.copy_from(host); - - MONITORINFO mi{}; - mi.cbSize = sizeof(mi); - if (!GetMonitorInfoW(m_monitor, &mi)) - { - return; - } - const UINT dpi = GetDpiForMonitor(); - const Rect monitorRect(mi.rcMonitor); - const Rect workAreaRect(mi.rcWork, dpi); - - StringCchPrintf(m_workArea, ARRAYSIZE(m_workArea), L"%d_%d", monitorRect.width(), monitorRect.height()); - - InitializeId(deviceId, virtualDesktopId); - LoadSettings(); - InitializeZoneSets(mi); - - WNDCLASSEXW wcex{}; - wcex.cbSize = sizeof(WNDCLASSEX); - wcex.lpfnWndProc = s_WndProc; - wcex.hInstance = hinstance; - wcex.lpszClassName = L"SuperFancyZones_ZoneWindow"; - wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); - RegisterClassExW(&wcex); - - m_window = wil::unique_hwnd { - CreateWindowExW(WS_EX_TOOLWINDOW, L"SuperFancyZones_ZoneWindow", L"", WS_POPUP, - workAreaRect.left(), workAreaRect.top(), workAreaRect.width(), workAreaRect.height(), - nullptr, nullptr, hinstance, this) - }; - - if (m_window) - { - MakeWindowTransparent(m_window.get()); - if (flashZones) - { - // Don't flash if the foreground window is in full screen mode - RECT windowRect; - if (GetWindowRect(GetForegroundWindow(), &windowRect) && - windowRect.left == mi.rcMonitor.left && - windowRect.top == mi.rcMonitor.top && - windowRect.right == mi.rcMonitor.right && - windowRect.bottom == mi.rcMonitor.bottom) +#include "pch.h" + +#include + +#include "ZoneWindow.h" +#include "trace.h" +#include "util.h" +#include "RegistryHelpers.h" + +#include +#include + +namespace ZoneWindowUtils +{ + const std::wstring& GetActiveZoneSetTmpPath() + { + static std::wstring activeZoneSetTmpFileName; + static std::once_flag flag; + + std::call_once(flag, []() { + wchar_t fileName[L_tmpnam_s]; + + if (_wtmpnam_s(fileName, L_tmpnam_s) != 0) + abort(); + + activeZoneSetTmpFileName = std::wstring{ fileName }; + }); + + return activeZoneSetTmpFileName; + } + + const std::wstring& GetAppliedZoneSetTmpPath() + { + static std::wstring appliedZoneSetTmpFileName; + static std::once_flag flag; + + std::call_once(flag, []() { + wchar_t fileName[L_tmpnam_s]; + + if (_wtmpnam_s(fileName, L_tmpnam_s) != 0) + abort(); + + appliedZoneSetTmpFileName = std::wstring{ fileName }; + }); + + return appliedZoneSetTmpFileName; + } + + const std::wstring& GetCustomZoneSetsTmpPath() + { + static std::wstring customZoneSetsTmpFileName; + static std::once_flag flag; + + std::call_once(flag, []() { + wchar_t fileName[L_tmpnam_s]; + + if (_wtmpnam_s(fileName, L_tmpnam_s) != 0) + abort(); + + customZoneSetsTmpFileName = std::wstring{ fileName }; + }); + + return customZoneSetsTmpFileName; + } + + std::wstring GenerateUniqueId(HMONITOR monitor, PCWSTR deviceId, PCWSTR virtualDesktopId) + { + wchar_t uniqueId[256]{}; // Parsed deviceId + resolution + virtualDesktopId + + MONITORINFOEXW mi; + mi.cbSize = sizeof(mi); + if (virtualDesktopId && GetMonitorInfo(monitor, &mi)) + { + wchar_t parsedId[256]{}; + ParseDeviceId(deviceId, parsedId, 256); + + Rect const monitorRect(mi.rcMonitor); + StringCchPrintf(uniqueId, ARRAYSIZE(uniqueId), L"%s_%d_%d_%s", parsedId, monitorRect.width(), monitorRect.height(), virtualDesktopId); + } + return std::wstring{ uniqueId }; + } +} + +namespace ZoneWindowDrawUtils +{ + struct ColorSetting + { + BYTE fillAlpha{}; + COLORREF fill{}; + BYTE borderAlpha{}; + COLORREF border{}; + int thickness{}; + }; + + bool IsOccluded(const std::vector>& zones, POINT pt, size_t index) noexcept + { + size_t i = 1; + + for (auto iter = zones.begin(); iter != zones.end(); iter++) + { + if (winrt::com_ptr zone = iter->try_as()) { - return; - } - FlashZones(); - } - } -} - -IFACEMETHODIMP ZoneWindow::MoveSizeEnter(HWND window, bool dragEnabled) noexcept -{ - if (m_windowMoveSize) - { - return E_INVALIDARG; - } - - m_dragEnabled = dragEnabled; - m_windowMoveSize = window; - m_drawHints = true; - m_highlightZone = nullptr; - ShowZoneWindow(); - return S_OK; -} - -IFACEMETHODIMP ZoneWindow::MoveSizeUpdate(POINT const& ptScreen, bool dragEnabled) noexcept -{ - bool redraw = false; - POINT ptClient = ptScreen; - MapWindowPoints(nullptr, m_window.get(), &ptClient, 1); - - m_dragEnabled = dragEnabled; - - if (dragEnabled) - { - auto highlightZone = ZoneFromPoint(ptClient); - redraw = (highlightZone != m_highlightZone); - m_highlightZone = std::move(highlightZone); - } - else if (m_highlightZone) - { - m_highlightZone = nullptr; - redraw = true; - } - - if (redraw) - { - InvalidateRect(m_window.get(), nullptr, true); - } - return S_OK; -} - -IFACEMETHODIMP ZoneWindow::MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept -{ - if (m_windowMoveSize != window) - { - return E_INVALIDARG; - } - - if (m_activeZoneSet) - { - POINT ptClient = ptScreen; - MapWindowPoints(nullptr, m_window.get(), &ptClient, 1); - m_activeZoneSet->MoveSizeEnd(window, m_window.get(), ptClient); - - SaveWindowProcessToZoneIndex(window); - } - Trace::ZoneWindow::MoveSizeEnd(m_activeZoneSet); - - HideZoneWindow(); - m_windowMoveSize = nullptr; - return S_OK; -} - -IFACEMETHODIMP ZoneWindow::MoveSizeCancel() noexcept -{ - HideZoneWindow(); - return S_OK; -} - -IFACEMETHODIMP_(void) ZoneWindow::MoveWindowIntoZoneByIndex(HWND window, int index) noexcept -{ - if (m_activeZoneSet) - { - m_activeZoneSet->MoveWindowIntoZoneByIndex(window, m_window.get(), index); - } -} - -IFACEMETHODIMP_(void) ZoneWindow::MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept -{ - if (m_activeZoneSet) - { - m_activeZoneSet->MoveWindowIntoZoneByDirection(window, m_window.get(), vkCode); - SaveWindowProcessToZoneIndex(window); - } -} - -IFACEMETHODIMP_(void) ZoneWindow::CycleActiveZoneSet(DWORD wparam) noexcept -{ - CycleActiveZoneSetInternal(wparam, Trace::ZoneWindow::InputMode::Keyboard); - - if (m_windowMoveSize) - { - InvalidateRect(m_window.get(), nullptr, true); - } - else - { - FlashZones(); - } -} - -IFACEMETHODIMP_(void) ZoneWindow::SaveWindowProcessToZoneIndex(HWND window) noexcept -{ - auto processPath = get_process_path(window); - if (!processPath.empty()) + if (i < index) + { + if (PtInRect(&zone->GetZoneRect(), pt)) + { + return true; + } + } + } + i++; + } + return false; + } + + void DrawBackdrop(wil::unique_hdc& hdc, RECT const& clientRect) noexcept + { + FillRectARGB(hdc, &clientRect, 0, RGB(0, 0, 0), false); + } + + void DrawIndex(wil::unique_hdc& hdc, POINT offset, size_t index, int padding, int size, bool flipX, bool flipY, COLORREF colorFill) + { + RECT rect = { offset.x, offset.y, offset.x + size, offset.y + size }; + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < 3; x++) + { + RECT useRect = rect; + if (flipX) + { + if (x == 0) + useRect.left += (size + padding + size + padding); + else if (x == 2) + useRect.left -= (size + padding + size + padding); + useRect.right = useRect.left + size; + } + + if (flipY) + { + if (y == 0) + useRect.top += (size + padding + size + padding); + else if (y == 2) + useRect.top -= (size + padding + size + padding); + useRect.bottom = useRect.top + size; + } + + FillRectARGB(hdc, &useRect, 200, RGB(50, 50, 50), true); + + RECT inside = useRect; + InflateRect(&inside, -2, -2); + + FillRectARGB(hdc, &inside, 100, colorFill, true); + + rect.left += (size + padding); + rect.right = rect.left + size; + + if (--index == 0) + { + return; + } + } + rect.left = offset.x; + rect.right = rect.left + size; + rect.top += (size + padding); + rect.bottom = rect.top + size; + } + } + + void DrawZone(wil::unique_hdc& hdc, ColorSetting const& colorSetting, winrt::com_ptr zone, const std::vector>& zones, bool flashMode) noexcept + { + RECT zoneRect = zone->GetZoneRect(); + if (colorSetting.borderAlpha > 0) + { + FillRectARGB(hdc, &zoneRect, colorSetting.borderAlpha, colorSetting.border, false); + InflateRect(&zoneRect, colorSetting.thickness, colorSetting.thickness); + } + FillRectARGB(hdc, &zoneRect, colorSetting.fillAlpha, colorSetting.fill, false); + + if (flashMode) + { + return; + } + COLORREF const colorFill = RGB(255, 255, 255); + + size_t const index = zone->Id(); + int const padding = 5; + int const size = 10; + POINT offset = { zoneRect.left + padding, zoneRect.top + padding }; + if (!IsOccluded(zones, offset, index)) + { + DrawIndex(hdc, offset, index, padding, size, false, false, colorFill); // top left + return; + } + + offset.x = zoneRect.right - ((padding + size) * 3); + if (!IsOccluded(zones, offset, index)) + { + DrawIndex(hdc, offset, index, padding, size, true, false, colorFill); // top right + return; + } + + offset.y = zoneRect.bottom - ((padding + size) * 3); + if (!IsOccluded(zones, offset, index)) + { + DrawIndex(hdc, offset, index, padding, size, true, true, colorFill); // bottom right + return; + } + + offset.x = zoneRect.left + padding; + DrawIndex(hdc, offset, index, padding, size, false, true, colorFill); // bottom left + } + + void DrawActiveZoneSet(wil::unique_hdc& hdc, COLORREF highlightColor, int highlightOpacity, const std::vector>& zones, const winrt::com_ptr& highlightZone, bool flashMode, bool drawHints) noexcept + { + static constexpr std::array colors{ + RGB(75, 75, 85), + RGB(150, 150, 160), + RGB(100, 100, 110), + RGB(125, 125, 135), + RGB(225, 225, 235), + RGB(25, 25, 35), + RGB(200, 200, 210), + RGB(50, 50, 60), + RGB(175, 175, 185), + }; + + // { fillAlpha, fill, borderAlpha, border, thickness } + ColorSetting const colorHints{ 225, RGB(81, 92, 107), 255, RGB(104, 118, 138), -2 }; + ColorSetting colorViewer{ OpacitySettingToAlpha(highlightOpacity), 0, 255, RGB(40, 50, 60), -2 }; + ColorSetting colorHighlight{ OpacitySettingToAlpha(highlightOpacity), 0, 255, 0, -2 }; + ColorSetting const colorFlash{ 200, RGB(81, 92, 107), 200, RGB(104, 118, 138), -2 }; + + const size_t maxColorIndex = min(size(zones) - 1, size(colors) - 1); + size_t colorIndex = maxColorIndex; + for (auto iter = zones.begin(); iter != zones.end(); iter++) + { + winrt::com_ptr zone = iter->try_as(); + if (!zone) + { + continue; + } + + if (zone != highlightZone) + { + if (flashMode) + { + DrawZone(hdc, colorFlash, zone, zones, flashMode); + } + else if (drawHints) + { + DrawZone(hdc, colorHints, zone, zones, flashMode); + } + { + colorViewer.fill = colors[colorIndex]; + DrawZone(hdc, colorViewer, zone, zones, flashMode); + } + } + colorIndex = colorIndex != 0 ? colorIndex - 1 : maxColorIndex; + } + + if (highlightZone) + { + colorHighlight.fill = highlightColor; + colorHighlight.border = RGB( + max(0, GetRValue(colorHighlight.fill) - 25), + max(0, GetGValue(colorHighlight.fill) - 25), + max(0, GetBValue(colorHighlight.fill) - 25)); + DrawZone(hdc, colorHighlight, highlightZone, zones, flashMode); + } + } +} + +struct ZoneWindow : public winrt::implements +{ +public: + ZoneWindow(HINSTANCE hinstance); + bool Init(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, const std::wstring& uniqueId, bool flashZones); + + IFACEMETHODIMP MoveSizeEnter(HWND window, bool dragEnabled) noexcept; + IFACEMETHODIMP MoveSizeUpdate(POINT const& ptScreen, bool dragEnabled) noexcept; + IFACEMETHODIMP MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept; + IFACEMETHODIMP MoveSizeCancel() noexcept; + IFACEMETHODIMP_(bool) IsDragEnabled() noexcept { return m_dragEnabled; } + IFACEMETHODIMP_(void) MoveWindowIntoZoneByIndex(HWND window, int index) noexcept; + IFACEMETHODIMP_(void) MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept; + IFACEMETHODIMP_(void) CycleActiveZoneSet(DWORD vkCode) noexcept; + IFACEMETHODIMP_(std::wstring) UniqueId() noexcept { return { m_uniqueId }; } + IFACEMETHODIMP_(std::wstring) WorkAreaKey() noexcept { return { m_workArea }; } + IFACEMETHODIMP_(void) SaveWindowProcessToZoneIndex(HWND window) noexcept; + IFACEMETHODIMP_(IZoneSet*) ActiveZoneSet() noexcept { return m_activeZoneSet.get(); } + +protected: + static LRESULT CALLBACK s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; + +private: + void ShowZoneWindow() noexcept; + void HideZoneWindow() noexcept; + void LoadSettings() noexcept; + void InitializeZoneSets(MONITORINFO const& mi) noexcept; + void CalculateZoneSet() noexcept; + void UpdateActiveZoneSet(_In_opt_ IZoneSet* zoneSet) noexcept; + LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept; + void OnPaint(wil::unique_hdc& hdc) noexcept; + void OnKeyUp(WPARAM wparam) noexcept; + winrt::com_ptr ZoneFromPoint(POINT pt) noexcept; + void CycleActiveZoneSetInternal(DWORD wparam, Trace::ZoneWindow::InputMode mode) noexcept; + void FlashZones() noexcept; + + winrt::com_ptr m_host; + HMONITOR m_monitor{}; + std::wstring m_uniqueId; // Parsed deviceId + resolution + virtualDesktopId + wchar_t m_workArea[256]{}; + wil::unique_hwnd m_window{}; + HWND m_windowMoveSize{}; + bool m_drawHints{}; + bool m_flashMode{}; + bool m_dragEnabled{}; + winrt::com_ptr m_activeZoneSet; + std::vector> m_zoneSets; + winrt::com_ptr m_highlightZone; + WPARAM m_keyLast{}; + size_t m_keyCycle{}; + static const UINT m_showAnimationDuration = 200; // ms + static const UINT m_flashDuration = 700; // ms +}; + +ZoneWindow::ZoneWindow(HINSTANCE hinstance) +{ + WNDCLASSEXW wcex{}; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.lpfnWndProc = s_WndProc; + wcex.hInstance = hinstance; + wcex.lpszClassName = L"SuperFancyZones_ZoneWindow"; + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + RegisterClassExW(&wcex); +} + +bool ZoneWindow::Init(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, const std::wstring& uniqueId, bool flashZones) +{ + m_host.copy_from(host); + + MONITORINFO mi{}; + mi.cbSize = sizeof(mi); + if (!GetMonitorInfoW(monitor, &mi)) + { + return false; + } + + m_monitor = monitor; + const UINT dpi = GetDpiForMonitor(m_monitor); + const Rect monitorRect(mi.rcMonitor); + const Rect workAreaRect(mi.rcWork, dpi); + StringCchPrintf(m_workArea, ARRAYSIZE(m_workArea), L"%d_%d", monitorRect.width(), monitorRect.height()); + + m_uniqueId = uniqueId; + LoadSettings(); + InitializeZoneSets(mi); + + m_window = wil::unique_hwnd{ + CreateWindowExW(WS_EX_TOOLWINDOW, L"SuperFancyZones_ZoneWindow", L"", WS_POPUP, workAreaRect.left(), workAreaRect.top(), workAreaRect.width(), workAreaRect.height(), nullptr, nullptr, hinstance, this) + }; + + if (!m_window) + { + return false; + } + + MakeWindowTransparent(m_window.get()); + if (flashZones) + { + // Don't flash if the foreground window is in full screen mode + RECT windowRect; + if (!(GetWindowRect(GetForegroundWindow(), &windowRect) && + windowRect.left == mi.rcMonitor.left && + windowRect.top == mi.rcMonitor.top && + windowRect.right == mi.rcMonitor.right && + windowRect.bottom == mi.rcMonitor.bottom)) + { + FlashZones(); + } + } + + return true; +} + +IFACEMETHODIMP ZoneWindow::MoveSizeEnter(HWND window, bool dragEnabled) noexcept +{ + if (m_windowMoveSize) + { + return E_INVALIDARG; + } + + m_dragEnabled = dragEnabled; + m_windowMoveSize = window; + m_drawHints = true; + m_highlightZone = nullptr; + ShowZoneWindow(); + return S_OK; +} + +IFACEMETHODIMP ZoneWindow::MoveSizeUpdate(POINT const& ptScreen, bool dragEnabled) noexcept +{ + bool redraw = false; + POINT ptClient = ptScreen; + MapWindowPoints(nullptr, m_window.get(), &ptClient, 1); + + m_dragEnabled = dragEnabled; + + if (dragEnabled) + { + auto highlightZone = ZoneFromPoint(ptClient); + redraw = (highlightZone != m_highlightZone); + m_highlightZone = std::move(highlightZone); + } + else if (m_highlightZone) + { + m_highlightZone = nullptr; + redraw = true; + } + + if (redraw) + { + InvalidateRect(m_window.get(), nullptr, true); + } + return S_OK; +} + +IFACEMETHODIMP ZoneWindow::MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept +{ + if (m_windowMoveSize != window) + { + return E_INVALIDARG; + } + + if (m_activeZoneSet) + { + POINT ptClient = ptScreen; + MapWindowPoints(nullptr, m_window.get(), &ptClient, 1); + m_activeZoneSet->MoveWindowIntoZoneByPoint(window, m_window.get(), ptClient); + + SaveWindowProcessToZoneIndex(window); + } + Trace::ZoneWindow::MoveSizeEnd(m_activeZoneSet); + + HideZoneWindow(); + m_windowMoveSize = nullptr; + return S_OK; +} + +IFACEMETHODIMP ZoneWindow::MoveSizeCancel() noexcept +{ + HideZoneWindow(); + return S_OK; +} + +IFACEMETHODIMP_(void) +ZoneWindow::MoveWindowIntoZoneByIndex(HWND window, int index) noexcept +{ + if (m_activeZoneSet) + { + m_activeZoneSet->MoveWindowIntoZoneByIndex(window, m_window.get(), index); + } +} + +IFACEMETHODIMP_(void) +ZoneWindow::MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept +{ + if (m_activeZoneSet) + { + m_activeZoneSet->MoveWindowIntoZoneByDirection(window, m_window.get(), vkCode); + SaveWindowProcessToZoneIndex(window); + } +} + +IFACEMETHODIMP_(void) +ZoneWindow::CycleActiveZoneSet(DWORD wparam) noexcept +{ + CycleActiveZoneSetInternal(wparam, Trace::ZoneWindow::InputMode::Keyboard); + + if (m_windowMoveSize) + { + InvalidateRect(m_window.get(), nullptr, true); + } + else + { + FlashZones(); + } +} + +IFACEMETHODIMP_(void) +ZoneWindow::SaveWindowProcessToZoneIndex(HWND window) noexcept +{ + if (m_activeZoneSet) { DWORD zoneIndex = static_cast(m_activeZoneSet->GetZoneIndexFromWindow(window)); if (zoneIndex != -1) { - RegistryHelpers::SaveAppLastZone(window, processPath.data(), zoneIndex); - } - } -} - -#pragma region private -void ZoneWindow::ShowZoneWindow() noexcept -{ - if (m_window) - { - m_flashMode = false; - - UINT flags = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE; - - HWND windowInsertAfter = m_windowMoveSize; - if (windowInsertAfter == nullptr) - { - windowInsertAfter = HWND_TOPMOST; - } - - SetWindowPos(m_window.get(), windowInsertAfter, 0, 0, 0, 0, flags); - - AnimateWindow(m_window.get(), m_showAnimationDuration, AW_BLEND); - InvalidateRect(m_window.get(), nullptr, true); - } -} - -void ZoneWindow::HideZoneWindow() noexcept -{ - if (m_window) - { - ShowWindow(m_window.get(), SW_HIDE); - m_keyLast = 0; - m_windowMoveSize = nullptr; - m_drawHints = false; - m_highlightZone = nullptr; - } -} - -void ZoneWindow::InitializeId(PCWSTR deviceId, PCWSTR virtualDesktopId) noexcept -{ - SHStrDup(deviceId, &m_deviceId); - - MONITORINFOEXW mi; - mi.cbSize = sizeof(mi); - if (GetMonitorInfo(m_monitor, &mi)) - { - wchar_t parsedId[256]{}; - ParseDeviceId(m_deviceId.get(), parsedId, 256); - - Rect const monitorRect(mi.rcMonitor); - StringCchPrintf(m_uniqueId, ARRAYSIZE(m_uniqueId), L"%s_%d_%d_%s", - parsedId, monitorRect.width(), monitorRect.height(), virtualDesktopId); - } -} - -void ZoneWindow::LoadSettings() noexcept -{ - wchar_t activeZoneSetId[256]; - RegistryHelpers::GetString(m_uniqueId, L"ActiveZoneSetId", activeZoneSetId, sizeof(activeZoneSetId)); - CLSIDFromString(activeZoneSetId, &m_activeZoneSetId); -} - -void ZoneWindow::InitializeZoneSets(MONITORINFO const& mi) noexcept -{ - LoadZoneSetsFromRegistry(); - - if (!m_activeZoneSet) - { - ChooseDefaultActiveZoneSet(); - } -} - -void ZoneWindow::LoadZoneSetsFromRegistry() noexcept -{ - wil::unique_hkey key{RegistryHelpers::OpenKey(m_workArea)}; - if (!key) - { - return; - } - - ZoneSetPersistedData data{}; - DWORD dataSize = sizeof(data); - wchar_t value[256]{}; - DWORD valueLength = ARRAYSIZE(value); - DWORD i = 0; - while (RegEnumValueW(key.get(), i++, value, &valueLength, nullptr, nullptr, reinterpret_cast(&data), &dataSize) == ERROR_SUCCESS) - { - if (data.Version == VERSION_PERSISTEDDATA) - { - GUID zoneSetId; - if (SUCCEEDED_LOG(CLSIDFromString(value, &zoneSetId))) - { - auto zoneSet = MakeZoneSet(ZoneSetConfig( - zoneSetId, - data.LayoutId, - m_monitor, - m_workArea)); - - if (zoneSet) - { - for (UINT j = 0; j < data.ZoneCount; j++) - { - zoneSet->AddZone(MakeZone(data.Zones[j])); - } - - if (zoneSetId == m_activeZoneSetId) - { - UpdateActiveZoneSet(zoneSet.get()); - } - - m_zoneSets.emplace_back(std::move(zoneSet)); - } - } - } - else - { - // Migrate from older settings format - } - - valueLength = ARRAYSIZE(value); - dataSize = sizeof(data); - } -} - -void ZoneWindow::UpdateActiveZoneSet(_In_opt_ IZoneSet* zoneSet) noexcept -{ - m_activeZoneSet.copy_from(zoneSet); - - if (m_activeZoneSet) - { - wil::unique_cotaskmem_string zoneSetId; - if (SUCCEEDED_LOG(StringFromCLSID(m_activeZoneSet->Id(), &zoneSetId))) - { - RegistryHelpers::SetString(m_uniqueId, L"ActiveZoneSetId", zoneSetId.get()); - } - } -} - -LRESULT ZoneWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept -{ - switch (message) - { - case WM_NCDESTROY: - { - ::DefWindowProc(m_window.get(), message, wparam, lparam); - SetWindowLongPtr(m_window.get(), GWLP_USERDATA, 0); - } - break; - - case WM_ERASEBKGND: - return 1; - - case WM_PRINTCLIENT: - case WM_PAINT: - { - PAINTSTRUCT ps; - wil::unique_hdc hdc{ reinterpret_cast(wparam) }; - if (!hdc) - { - hdc.reset(BeginPaint(m_window.get(), &ps)); - } - - OnPaint(hdc); - - if (wparam == 0) - { - EndPaint(m_window.get(), &ps); - } - - hdc.release(); - } - break; - - default: - { - return DefWindowProc(m_window.get(), message, wparam, lparam); - } - } - return 0; -} - -void ZoneWindow::DrawBackdrop(wil::unique_hdc& hdc, RECT const& clientRect) noexcept -{ - FillRectARGB(hdc, &clientRect, 0, RGB(0, 0, 0), false); -} - -void ZoneWindow::DrawZone(wil::unique_hdc& hdc, ColorSetting const& colorSetting, winrt::com_ptr zone) noexcept -{ - RECT zoneRect = zone->GetZoneRect(); - if (colorSetting.borderAlpha > 0) - { - FillRectARGB(hdc, &zoneRect, colorSetting.borderAlpha, colorSetting.border, false); - InflateRect(&zoneRect, colorSetting.thickness, colorSetting.thickness); - } - FillRectARGB(hdc, &zoneRect, colorSetting.fillAlpha, colorSetting.fill, false); - - if (m_flashMode) - { - return; - } - COLORREF const colorFill = RGB(255, 255, 255); - - size_t const index = zone->Id(); - int const padding = 5; - int const size = 10; - POINT offset = { zoneRect.left + padding, zoneRect.top + padding }; - if (!IsOccluded(offset, index)) - { - DrawIndex(hdc, offset, index, padding, size, false, false, colorFill); // top left - return; - } - - offset.x = zoneRect.right - ((padding + size) * 3); - if (!IsOccluded(offset, index)) - { - DrawIndex(hdc, offset, index, padding, size, true, false, colorFill); // top right - return; - } - - offset.y = zoneRect.bottom - ((padding + size) * 3); - if (!IsOccluded(offset, index)) - { - DrawIndex(hdc, offset, index, padding, size, true, true, colorFill); // bottom right - return; - } - - offset.x = zoneRect.left + padding; - DrawIndex(hdc, offset, index, padding, size, false, true, colorFill); // bottom left -} - -void ZoneWindow::DrawIndex(wil::unique_hdc& hdc, POINT offset, size_t index, int padding, int size, bool flipX, bool flipY, COLORREF colorFill) -{ - RECT rect = { offset.x, offset.y, offset.x + size, offset.y + size }; - for (int y = 0; y < 3; y++) - { - for (int x = 0; x < 3; x++) - { - RECT useRect = rect; - if (flipX) - { - if (x == 0) useRect.left += (size + padding + size + padding); - else if (x == 2) useRect.left -= (size + padding + size + padding); - useRect.right = useRect.left + size; - } - - if (flipY) - { - if (y == 0) useRect.top += (size + padding + size + padding); - else if (y == 2) useRect.top -= (size + padding + size + padding); - useRect.bottom = useRect.top + size; - } - - FillRectARGB(hdc, &useRect, 200, RGB(50, 50, 50), true); - - RECT inside = useRect; - InflateRect(&inside, -2, -2); - - FillRectARGB(hdc, &inside, 100, colorFill, true); - - rect.left += (size + padding); - rect.right = rect.left + size; - - if (--index == 0) - { - return; - } - } - rect.left = offset.x; - rect.right = rect.left + size; - rect.top += (size + padding); - rect.bottom = rect.top + size; - } -} - -void ZoneWindow::DrawActiveZoneSet(wil::unique_hdc& hdc, RECT const& clientRect) noexcept -{ - if (m_activeZoneSet) - { - static constexpr std::array colors{ - RGB(75, 75, 85), - RGB(150, 150, 160), - RGB(100, 100, 110), - RGB(125, 125, 135), - RGB(225, 225, 235), - RGB(25, 25, 35), - RGB(200, 200, 210), - RGB(50, 50, 60), - RGB(175, 175, 185), - }; - - // { fillAlpha, fill, borderAlpha, border, thickness } - ColorSetting const colorHints { 225, RGB(81, 92, 107), 255, RGB(104, 118, 138), -2 }; - ColorSetting colorViewer { OpacitySettingToAlpha(m_host->GetZoneHighlightOpacity()), 0, 255, RGB(40, 50, 60), -2 }; - ColorSetting colorHighlight { OpacitySettingToAlpha(m_host->GetZoneHighlightOpacity()), 0, 255, 0, -2 }; - ColorSetting const colorFlash { 200, RGB(81, 92, 107), 200, RGB(104, 118, 138), -2 }; - - auto zones = m_activeZoneSet->GetZones(); - const size_t maxColorIndex = min(size(zones) - 1, size(colors) - 1); - size_t colorIndex = maxColorIndex; - for (auto iter = zones.begin(); iter != zones.end(); iter++) - { - winrt::com_ptr zone = iter->try_as(); - if (!zone) - { - continue; - } - - if (zone != m_highlightZone) - { - if (m_flashMode) - { - DrawZone(hdc, colorFlash, zone); - } - else if (m_drawHints) - { - DrawZone(hdc, colorHints, zone); - } - { - colorViewer.fill = colors[colorIndex]; - DrawZone(hdc, colorViewer, zone); - } - } - colorIndex = colorIndex != 0 ? colorIndex - 1 : maxColorIndex; - } - - if (m_highlightZone) - { - colorHighlight.fill = m_host->GetZoneHighlightColor(); - colorHighlight.border = RGB( - max(0, GetRValue(colorHighlight.fill) - 25), - max(0, GetGValue(colorHighlight.fill) - 25), - max(0, GetBValue(colorHighlight.fill) - 25) - ); - DrawZone(hdc, colorHighlight, m_highlightZone); - } - } -} - -void ZoneWindow::OnPaint(wil::unique_hdc& hdc) noexcept -{ - RECT clientRect; - GetClientRect(m_window.get(), &clientRect); - - wil::unique_hdc hdcMem; - HPAINTBUFFER bufferedPaint = BeginBufferedPaint(hdc.get(), &clientRect, BPBF_TOPDOWNDIB, nullptr, &hdcMem); - if (bufferedPaint) - { - DrawBackdrop(hdcMem, clientRect); - DrawActiveZoneSet(hdcMem, clientRect); - EndBufferedPaint(bufferedPaint, TRUE); - } -} - -void ZoneWindow::OnKeyUp(WPARAM wparam) noexcept -{ - bool fRedraw = false; - Trace::ZoneWindow::KeyUp(wparam); - - if ((wparam >= '0') && (wparam <= '9')) - { - CycleActiveZoneSetInternal(static_cast(wparam), Trace::ZoneWindow::InputMode::Keyboard); - InvalidateRect(m_window.get(), nullptr, true); - } -} - -winrt::com_ptr ZoneWindow::ZoneFromPoint(POINT pt) noexcept -{ - if (m_activeZoneSet) - { - return m_activeZoneSet->ZoneFromPoint(pt); - } - return nullptr; -} - -void ZoneWindow::ChooseDefaultActiveZoneSet() noexcept -{ - // Default zone set can be empty (no fancyzones layout), or it can be layout from virtual - // desktop from which this virtual desktop is created. - if (GUID id{ m_host->GetCurrentMonitorZoneSetId(m_monitor) }; id != GUID_NULL) { - for (const auto& zoneSet : m_zoneSets) { - if (id == zoneSet->Id()) { - UpdateActiveZoneSet(zoneSet.get()); - return; + OLECHAR* guidString; + if (StringFromCLSID(m_activeZoneSet->Id(), &guidString) == S_OK) + { + JSONHelpers::FancyZonesDataInstance().SetAppLastZone(window, m_uniqueId, guidString, zoneIndex); } - } - } -} - -bool ZoneWindow::IsOccluded(POINT pt, size_t index) noexcept -{ - auto zones = m_activeZoneSet->GetZones(); - size_t i = 1; - - for (auto iter = zones.begin(); iter != zones.end(); iter++) - { - if (winrt::com_ptr zone = iter->try_as()) - { - if (i < index) - { - if (PtInRect(&zone->GetZoneRect(), pt)) - { - return true; - } - } - } - i++; - } - return false; -} - -void ZoneWindow::CycleActiveZoneSetInternal(DWORD wparam, Trace::ZoneWindow::InputMode mode) noexcept -{ - Trace::ZoneWindow::CycleActiveZoneSet(m_activeZoneSet, mode); - if (m_keyLast != wparam) - { - m_keyCycle = 0; - } - - m_keyLast = wparam; - - bool loopAround = true; - size_t const val = static_cast(wparam - L'0'); - size_t i = 0; - for (auto zoneSet : m_zoneSets) - { - if (zoneSet->GetZones().size() == val) - { - if (i < m_keyCycle) - { - i++; - } - else - { - UpdateActiveZoneSet(zoneSet.get()); - loopAround = false; - break; - } - } - } - - if ((m_keyCycle > 0) && loopAround) - { - // Cycling through a non-empty group and hit the end - m_keyCycle = 0; - OnKeyUp(wparam); - } - else - { - m_keyCycle++; - } - - m_host->MoveWindowsOnActiveZoneSetChange(); - m_highlightZone = nullptr; -} - -void ZoneWindow::FlashZones() noexcept -{ - m_flashMode = true; - - ShowWindow(m_window.get(), SW_SHOWNA); - std::thread([window = m_window.get()]() - { - AnimateWindow(window, m_flashDuration, AW_HIDE | AW_BLEND); - }).detach(); -} - -typedef BOOL(WINAPI *GetDpiForMonitorInternalFunc)(HMONITOR, UINT, UINT*, UINT*); -UINT ZoneWindow::GetDpiForMonitor() noexcept -{ - UINT dpi{}; - if (wil::unique_hmodule user32{ LoadLibrary(L"user32.dll") }) - { - if (auto func = reinterpret_cast(GetProcAddress(user32.get(), "GetDpiForMonitorInternal"))) - { - func(m_monitor, 0, &dpi, &dpi); - } - } - - if (dpi == 0) - { - if (wil::unique_hdc hdc{ GetDC(nullptr) }) - { - dpi = GetDeviceCaps(hdc.get(), LOGPIXELSX); - } - } - - return (dpi == 0) ? DPIAware::DEFAULT_DPI : dpi; -} -#pragma endregion - -LRESULT CALLBACK ZoneWindow::s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept -{ - auto thisRef = reinterpret_cast(GetWindowLongPtr(window, GWLP_USERDATA)); - if ((thisRef == nullptr) && (message == WM_CREATE)) - { - auto createStruct = reinterpret_cast(lparam); - thisRef = reinterpret_cast(createStruct->lpCreateParams); - SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(thisRef)); - } - - return (thisRef != nullptr) ? thisRef->WndProc(message, wparam, lparam) : - DefWindowProc(window, message, wparam, lparam); -} - -winrt::com_ptr MakeZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, - PCWSTR deviceId, PCWSTR virtualDesktopId, bool flashZones) noexcept -{ - return winrt::make_self(host, hinstance, monitor, deviceId, virtualDesktopId, flashZones); -} + + CoTaskMemFree(guidString); + } + } +} + +#pragma region private +void ZoneWindow::ShowZoneWindow() noexcept +{ + if (m_window) + { + m_flashMode = false; + + UINT flags = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE; + + HWND windowInsertAfter = m_windowMoveSize; + if (windowInsertAfter == nullptr) + { + windowInsertAfter = HWND_TOPMOST; + } + + SetWindowPos(m_window.get(), windowInsertAfter, 0, 0, 0, 0, flags); + + AnimateWindow(m_window.get(), m_showAnimationDuration, AW_BLEND); + InvalidateRect(m_window.get(), nullptr, true); + } +} + +void ZoneWindow::HideZoneWindow() noexcept +{ + if (m_window) + { + ShowWindow(m_window.get(), SW_HIDE); + m_keyLast = 0; + m_windowMoveSize = nullptr; + m_drawHints = false; + m_highlightZone = nullptr; + } +} + +void ZoneWindow::LoadSettings() noexcept +{ + JSONHelpers::FancyZonesDataInstance().AddDevice(m_uniqueId); +} + +void ZoneWindow::InitializeZoneSets(MONITORINFO const& mi) noexcept +{ + auto parent = m_host->GetParentZoneWindow(m_monitor); + if (parent) + { + // Update device info with device info from parent virtual desktop (if empty). + JSONHelpers::FancyZonesDataInstance().CloneDeviceInfo(parent->UniqueId(), m_uniqueId); + } + CalculateZoneSet(); +} + +void ZoneWindow::CalculateZoneSet() noexcept +{ + const auto& fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + const auto& deviceInfoMap = fancyZonesData.GetDeviceInfoMap(); + const auto& activeDeviceId = fancyZonesData.GetActiveDeviceId(); + const auto& activeZoneSet = deviceInfoMap.at(m_uniqueId).activeZoneSet; + + if (!activeDeviceId.empty() && activeDeviceId != m_uniqueId) + { + return; + } + + if (activeZoneSet.uuid.empty() || activeZoneSet.type == JSONHelpers::ZoneSetLayoutType::Blank) + { + return; + } + + GUID zoneSetId; + if (SUCCEEDED_LOG(CLSIDFromString(activeZoneSet.uuid.c_str(), &zoneSetId))) + { + auto zoneSet = MakeZoneSet(ZoneSetConfig( + zoneSetId, + activeZoneSet.type, + m_monitor, + m_workArea)); + MONITORINFO monitorInfo{}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (GetMonitorInfoW(m_monitor, &monitorInfo)) + { + bool showSpacing = deviceInfoMap.at(m_uniqueId).showSpacing; + int spacing = showSpacing ? deviceInfoMap.at(m_uniqueId).spacing : 0; + int zoneCount = deviceInfoMap.at(m_uniqueId).zoneCount; + zoneSet->CalculateZones(monitorInfo, zoneCount, spacing); + UpdateActiveZoneSet(zoneSet.get()); + } + } +} + +void ZoneWindow::UpdateActiveZoneSet(_In_opt_ IZoneSet* zoneSet) noexcept +{ + m_activeZoneSet.copy_from(zoneSet); + + if (m_activeZoneSet) + { + wil::unique_cotaskmem_string zoneSetId; + if (SUCCEEDED_LOG(StringFromCLSID(m_activeZoneSet->Id(), &zoneSetId))) + { + JSONHelpers::ZoneSetData data{ + .uuid = zoneSetId.get(), + .type = m_activeZoneSet->LayoutType() + }; + JSONHelpers::FancyZonesDataInstance().SetActiveZoneSet(m_uniqueId, data); + } + } +} + +LRESULT ZoneWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept +{ + switch (message) + { + case WM_NCDESTROY: { + ::DefWindowProc(m_window.get(), message, wparam, lparam); + SetWindowLongPtr(m_window.get(), GWLP_USERDATA, 0); + } + break; + + case WM_ERASEBKGND: + return 1; + + case WM_PRINTCLIENT: + case WM_PAINT: { + PAINTSTRUCT ps; + wil::unique_hdc hdc{ reinterpret_cast(wparam) }; + if (!hdc) + { + hdc.reset(BeginPaint(m_window.get(), &ps)); + } + + OnPaint(hdc); + + if (wparam == 0) + { + EndPaint(m_window.get(), &ps); + } + + hdc.release(); + } + break; + + default: + { + return DefWindowProc(m_window.get(), message, wparam, lparam); + } + } + return 0; +} + +void ZoneWindow::OnPaint(wil::unique_hdc& hdc) noexcept +{ + RECT clientRect; + GetClientRect(m_window.get(), &clientRect); + + wil::unique_hdc hdcMem; + HPAINTBUFFER bufferedPaint = BeginBufferedPaint(hdc.get(), &clientRect, BPBF_TOPDOWNDIB, nullptr, &hdcMem); + if (bufferedPaint) + { + ZoneWindowDrawUtils::DrawBackdrop(hdcMem, clientRect); + if (m_activeZoneSet && m_host) + { + ZoneWindowDrawUtils::DrawActiveZoneSet(hdcMem, m_host->GetZoneHighlightColor(), m_host->GetZoneHighlightOpacity(), m_activeZoneSet->GetZones(), m_highlightZone, m_flashMode, m_drawHints); + } + + EndBufferedPaint(bufferedPaint, TRUE); + } +} + +void ZoneWindow::OnKeyUp(WPARAM wparam) noexcept +{ + bool fRedraw = false; + Trace::ZoneWindow::KeyUp(wparam); + + if ((wparam >= '0') && (wparam <= '9')) + { + CycleActiveZoneSetInternal(static_cast(wparam), Trace::ZoneWindow::InputMode::Keyboard); + InvalidateRect(m_window.get(), nullptr, true); + } +} + +winrt::com_ptr ZoneWindow::ZoneFromPoint(POINT pt) noexcept +{ + if (m_activeZoneSet) + { + return m_activeZoneSet->ZoneFromPoint(pt); + } + return nullptr; +} + +void ZoneWindow::CycleActiveZoneSetInternal(DWORD wparam, Trace::ZoneWindow::InputMode mode) noexcept +{ + Trace::ZoneWindow::CycleActiveZoneSet(m_activeZoneSet, mode); + if (m_keyLast != wparam) + { + m_keyCycle = 0; + } + + m_keyLast = wparam; + + bool loopAround = true; + size_t const val = static_cast(wparam - L'0'); + size_t i = 0; + for (auto zoneSet : m_zoneSets) + { + if (zoneSet->GetZones().size() == val) + { + if (i < m_keyCycle) + { + i++; + } + else + { + UpdateActiveZoneSet(zoneSet.get()); + loopAround = false; + break; + } + } + } + + if ((m_keyCycle > 0) && loopAround) + { + // Cycling through a non-empty group and hit the end + m_keyCycle = 0; + OnKeyUp(wparam); + } + else + { + m_keyCycle++; + } + + if (m_host) + { + m_host->MoveWindowsOnActiveZoneSetChange(); + } + m_highlightZone = nullptr; +} + +void ZoneWindow::FlashZones() noexcept +{ + m_flashMode = true; + + ShowWindow(m_window.get(), SW_SHOWNA); + std::thread([window = m_window.get()]() { + AnimateWindow(window, m_flashDuration, AW_HIDE | AW_BLEND); + }).detach(); +} +#pragma endregion + +LRESULT CALLBACK ZoneWindow::s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept +{ + auto thisRef = reinterpret_cast(GetWindowLongPtr(window, GWLP_USERDATA)); + if ((thisRef == nullptr) && (message == WM_CREATE)) + { + auto createStruct = reinterpret_cast(lparam); + thisRef = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(thisRef)); + } + + return (thisRef != nullptr) ? thisRef->WndProc(message, wparam, lparam) : + DefWindowProc(window, message, wparam, lparam); +} + +winrt::com_ptr MakeZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, const std::wstring& uniqueId, bool flashZones) noexcept +{ + auto self = winrt::make_self(hinstance); + if (self->Init(host, hinstance, monitor, uniqueId, flashZones)) + { + return self; + } + + return nullptr; +} diff --git a/src/modules/fancyzones/lib/ZoneWindow.h b/src/modules/fancyzones/lib/ZoneWindow.h index 0341e12f3b..93a195ae65 100644 --- a/src/modules/fancyzones/lib/ZoneWindow.h +++ b/src/modules/fancyzones/lib/ZoneWindow.h @@ -2,6 +2,14 @@ #include "FancyZones.h" #include "lib/ZoneSet.h" +namespace ZoneWindowUtils +{ + const std::wstring& GetActiveZoneSetTmpPath(); + const std::wstring& GetAppliedZoneSetTmpPath(); + const std::wstring& GetCustomZoneSetsTmpPath(); + std::wstring GenerateUniqueId(HMONITOR monitor, PCWSTR deviceId, PCWSTR virtualDesktopId); +} + interface __declspec(uuid("{7F017528-8110-4FB3-BE41-F472969C2560}")) IZoneWindow : public IUnknown { IFACEMETHOD(MoveSizeEnter)(HWND window, bool dragEnabled) = 0; @@ -13,11 +21,10 @@ interface __declspec(uuid("{7F017528-8110-4FB3-BE41-F472969C2560}")) IZoneWindow IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, DWORD vkCode) = 0; IFACEMETHOD_(void, CycleActiveZoneSet)(DWORD vkCode) = 0; IFACEMETHOD_(void, SaveWindowProcessToZoneIndex)(HWND window) = 0; - IFACEMETHOD_(std::wstring, DeviceId)() = 0; IFACEMETHOD_(std::wstring, UniqueId)() = 0; IFACEMETHOD_(std::wstring, WorkAreaKey)() = 0; IFACEMETHOD_(IZoneSet*, ActiveZoneSet)() = 0; }; winrt::com_ptr MakeZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor, - PCWSTR deviceId, PCWSTR virtualDesktopId, bool flashZones) noexcept; + const std::wstring& uniqueId, bool flashZones) noexcept; diff --git a/src/modules/fancyzones/lib/util.cpp b/src/modules/fancyzones/lib/util.cpp new file mode 100644 index 0000000000..aee018f067 --- /dev/null +++ b/src/modules/fancyzones/lib/util.cpp @@ -0,0 +1,27 @@ +#include "pch.h" +#include "util.h" + +#include + +typedef BOOL(WINAPI* GetDpiForMonitorInternalFunc)(HMONITOR, UINT, UINT*, UINT*); +UINT GetDpiForMonitor(HMONITOR monitor) noexcept +{ + UINT dpi{}; + if (wil::unique_hmodule user32{ LoadLibrary(L"user32.dll") }) + { + if (auto func = reinterpret_cast(GetProcAddress(user32.get(), "GetDpiForMonitorInternal"))) + { + func(monitor, 0, &dpi, &dpi); + } + } + + if (dpi == 0) + { + if (wil::unique_hdc hdc{ GetDC(nullptr) }) + { + dpi = GetDeviceCaps(hdc.get(), LOGPIXELSX); + } + } + + return (dpi == 0) ? DPIAware::DEFAULT_DPI : dpi; +} diff --git a/src/modules/fancyzones/lib/util.h b/src/modules/fancyzones/lib/util.h index 9f59f21121..171a789298 100644 --- a/src/modules/fancyzones/lib/util.h +++ b/src/modules/fancyzones/lib/util.h @@ -117,6 +117,12 @@ inline void ParseDeviceId(PCWSTR deviceId, PWSTR parsedId, size_t size) // We're interested in the unique part between the first and last #'s // Example input: \\?\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} // Example output: DELA026#5&10a58c63&0&UID16777488 + const std::wstring defaultDeviceId = L"FallbackDevice"; + if (!deviceId) + { + StringCchCopy(parsedId, size, defaultDeviceId.c_str()); + return; + } wchar_t buffer[256]; StringCchCopy(buffer, 256, deviceId); @@ -130,12 +136,14 @@ inline void ParseDeviceId(PCWSTR deviceId, PWSTR parsedId, size_t size) } else { - StringCchCopy(parsedId, size, L"FallbackDevice"); + StringCchCopy(parsedId, size, defaultDeviceId.c_str()); } } inline int OpacitySettingToAlpha(int opacity) { // convert percentage to a 0-255 alpha value - return opacity * 2.55; + return static_cast(opacity * 2.55); } + +UINT GetDpiForMonitor(HMONITOR monitor) noexcept; \ No newline at end of file diff --git a/src/modules/fancyzones/tests/UnitTests/FancyZones.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/FancyZones.Spec.cpp new file mode 100644 index 0000000000..e2f5ff1d54 --- /dev/null +++ b/src/modules/fancyzones/tests/UnitTests/FancyZones.Spec.cpp @@ -0,0 +1,475 @@ +#include "pch.h" + +#include + +#include +#include +#include + +#include "util.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace FancyZonesUnitTests +{ + TEST_CLASS(FancyZonesUnitTests) + { + HINSTANCE m_hInst; + winrt::com_ptr m_settings; + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + m_settings = MakeFancyZonesSettings(m_hInst, L"FancyZonesUnitTests"); + Assert::IsTrue(m_settings != nullptr); + } + + TEST_METHOD(Create) + { + auto actual = MakeFancyZones(m_hInst, m_settings); + Assert::IsNotNull(actual.get()); + } + TEST_METHOD(CreateWithEmptyHinstance) + { + auto actual = MakeFancyZones({}, m_settings); + Assert::IsNotNull(actual.get()); + } + + TEST_METHOD(CreateWithNullHinstance) + { + auto actual = MakeFancyZones(nullptr, m_settings); + Assert::IsNotNull(actual.get()); + } + + TEST_METHOD(CreateWithNullSettings) + { + auto actual = MakeFancyZones(m_hInst, nullptr); + Assert::IsNull(actual.get()); + } + + TEST_METHOD(Run) + { + auto actual = MakeFancyZones(m_hInst, m_settings); + + std::vector threads; + std::atomic counter = 0; + const int expectedCount = 10; + + auto runFunc = [&]() { + actual->Run(); + counter++; + }; + + for (int i = 0; i < expectedCount; i++) + { + threads.push_back(std::thread(runFunc)); + } + + for (auto& thread : threads) + { + thread.join(); + } + + Assert::AreEqual(expectedCount, counter.load()); + } + + TEST_METHOD(Destroy) + { + auto actual = MakeFancyZones(m_hInst, m_settings); + + std::vector threads; + std::atomic counter = 0; + const int expectedCount = 10; + + auto destroyFunc = [&]() { + actual->Destroy(); + counter++; + }; + + for (int i = 0; i < expectedCount; i++) + { + threads.push_back(std::thread(destroyFunc)); + } + + for (auto& thread : threads) + { + thread.join(); + } + + Assert::AreEqual(expectedCount, counter.load()); + } + + TEST_METHOD(RunDestroy) + { + auto actual = MakeFancyZones(m_hInst, m_settings); + + std::vector threads; + std::atomic counter = 0; + const int expectedCount = 20; + + auto func = [&]() { + auto idHash = std::hash()(std::this_thread::get_id()); + bool run = (idHash % 2 == 0); + run ? actual->Run() : actual->Destroy(); + counter++; + }; + + for (int i = 0; i < expectedCount; i++) + { + threads.push_back(std::thread(func)); + } + + for (auto& thread : threads) + { + thread.join(); + } + + Assert::AreEqual(expectedCount, counter.load()); + } + }; + + TEST_CLASS(FancyZonesIZoneWindowHostUnitTests) + { + HINSTANCE m_hInst{}; + winrt::com_ptr m_settings = nullptr; + winrt::com_ptr m_zoneWindowHost = nullptr; + + std::wstring serializedPowerToySettings(const Settings& settings) + { + PowerToysSettings::Settings ptSettings(HINSTANCE{}, L"FancyZonesUnitTests"); + + ptSettings.add_hotkey(L"fancyzones_editor_hotkey", IDS_SETTING_LAUNCH_EDITOR_HOTKEY_LABEL, settings.editorHotkey); + ptSettings.add_bool_toogle(L"fancyzones_shiftDrag", IDS_SETTING_DESCRIPTION_SHIFTDRAG, settings.shiftDrag); + ptSettings.add_bool_toogle(L"fancyzones_overrideSnapHotkeys", IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS, settings.overrideSnapHotkeys); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_flashZones", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES, settings.zoneSetChange_flashZones); + ptSettings.add_bool_toogle(L"fancyzones_displayChange_moveWindows", IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS, settings.displayChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_moveWindows", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS, settings.zoneSetChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_virtualDesktopChange_moveWindows", IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS, settings.virtualDesktopChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_appLastZone_moveWindows", IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS, settings.appLastZone_moveWindows); + ptSettings.add_bool_toogle(L"use_cursorpos_editor_startupscreen", IDS_SETTING_DESCRIPTION_USE_CURSORPOS_EDITOR_STARTUPSCREEN, settings.use_cursorpos_editor_startupscreen); + ptSettings.add_int_spinner(L"fancyzones_highlight_opacity", IDS_SETTINGS_HIGHLIGHT_OPACITY, settings.zoneHighlightOpacity, 0, 100, 1); + ptSettings.add_color_picker(L"fancyzones_zoneHighlightColor", IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR, settings.zoneHightlightColor); + ptSettings.add_multiline_string(L"fancyzones_excluded_apps", IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION, settings.excludedApps); + + return ptSettings.serialize(); + } + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + m_settings = MakeFancyZonesSettings(m_hInst, L"FancyZonesUnitTests"); + Assert::IsTrue(m_settings != nullptr); + + auto fancyZones = MakeFancyZones(m_hInst, m_settings); + Assert::IsTrue(fancyZones != nullptr); + + m_zoneWindowHost = fancyZones.as(); + Assert::IsTrue(m_zoneWindowHost != nullptr); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + const auto settingsFile = PTSettingsHelper::get_module_save_folder_location(L"FancyZonesUnitTests") + L"\\settings.json"; + std::filesystem::remove(settingsFile); + } + + TEST_METHOD(GetZoneHighlightColor) + { + const auto expected = RGB(171, 175, 238); + const Settings settings{ + .shiftDrag = true, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = false, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = true, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#abafee", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, false, false, false, VK_OEM_3), + .excludedApps = L"app\r\napp2", + .excludedAppsArray = { L"APP", L"APP2" }, + }; + + auto config = serializedPowerToySettings(settings); + m_settings->SetConfig(config.c_str()); + + const auto actual = m_zoneWindowHost->GetZoneHighlightColor(); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(GetZoneHighlightOpacity) + { + const auto expected = 88; + const Settings settings{ + .shiftDrag = true, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = false, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = true, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#abafee", + .zoneHighlightOpacity = expected, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, false, false, false, VK_OEM_3), + .excludedApps = L"app\r\napp2", + .excludedAppsArray = { L"APP", L"APP2" }, + }; + + auto config = serializedPowerToySettings(settings); + m_settings->SetConfig(config.c_str()); + + const auto actual = m_zoneWindowHost->GetZoneHighlightOpacity(); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(GetCurrentMonitorZoneSetEmpty) + { + const auto* actual = m_zoneWindowHost->GetParentZoneWindow(Mocks::Monitor()); + Assert::IsNull(actual); + } + + TEST_METHOD(GetCurrentMonitorZoneSetNullMonitor) + { + const auto* actual = m_zoneWindowHost->GetParentZoneWindow(nullptr); + Assert::IsNull(actual); + } + }; + + TEST_CLASS(FancyZonesIFancyZonesCallbackUnitTests) + { + HINSTANCE m_hInst{}; + winrt::com_ptr m_settings = nullptr; + winrt::com_ptr m_fzCallback = nullptr; + + JSONHelpers::FancyZonesData& m_fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + + std::wstring serializedPowerToySettings(const Settings& settings) + { + PowerToysSettings::Settings ptSettings(HINSTANCE{}, L"FancyZonesUnitTests"); + + ptSettings.add_hotkey(L"fancyzones_editor_hotkey", IDS_SETTING_LAUNCH_EDITOR_HOTKEY_LABEL, settings.editorHotkey); + ptSettings.add_bool_toogle(L"fancyzones_shiftDrag", IDS_SETTING_DESCRIPTION_SHIFTDRAG, settings.shiftDrag); + ptSettings.add_bool_toogle(L"fancyzones_overrideSnapHotkeys", IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS, settings.overrideSnapHotkeys); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_flashZones", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES, settings.zoneSetChange_flashZones); + ptSettings.add_bool_toogle(L"fancyzones_displayChange_moveWindows", IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS, settings.displayChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_moveWindows", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS, settings.zoneSetChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_virtualDesktopChange_moveWindows", IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS, settings.virtualDesktopChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_appLastZone_moveWindows", IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS, settings.appLastZone_moveWindows); + ptSettings.add_bool_toogle(L"use_cursorpos_editor_startupscreen", IDS_SETTING_DESCRIPTION_USE_CURSORPOS_EDITOR_STARTUPSCREEN, settings.use_cursorpos_editor_startupscreen); + ptSettings.add_int_spinner(L"fancyzones_highlight_opacity", IDS_SETTINGS_HIGHLIGHT_OPACITY, settings.zoneHighlightOpacity, 0, 100, 1); + ptSettings.add_color_picker(L"fancyzones_zoneHighlightColor", IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR, settings.zoneHightlightColor); + ptSettings.add_multiline_string(L"fancyzones_excluded_apps", IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION, settings.excludedApps); + + return ptSettings.serialize(); + } + + void sendKeyboardInput(WORD code, bool release = false) + { + INPUT ip; + ip.type = INPUT_KEYBOARD; + ip.ki.wScan = 0; // hardware scan code for key + ip.ki.time = 0; + ip.ki.dwExtraInfo = 0; + ip.ki.wVk = code; + ip.ki.dwFlags = release ? KEYEVENTF_KEYUP : 0; + SendInput(1, &ip, sizeof(INPUT)); + } + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + m_settings = MakeFancyZonesSettings(m_hInst, L"FancyZonesUnitTests"); + Assert::IsTrue(m_settings != nullptr); + + auto fancyZones = MakeFancyZones(m_hInst, m_settings); + Assert::IsTrue(fancyZones != nullptr); + + m_fzCallback = fancyZones.as(); + Assert::IsTrue(m_fzCallback != nullptr); + + m_fancyZonesData = JSONHelpers::FancyZonesData(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + sendKeyboardInput(VK_SHIFT, true); + sendKeyboardInput(VK_LWIN, true); + sendKeyboardInput(VK_CONTROL, true); + + const auto settingsFile = PTSettingsHelper::get_module_save_folder_location(L"FancyZonesUnitTests") + L"\\settings.json"; + std::filesystem::remove(settingsFile); + } + + TEST_METHOD(InMoveSizeTest) + { + Assert::IsFalse(m_fzCallback->InMoveSize()); + + m_fzCallback->MoveSizeStart(Mocks::Window(), Mocks::Monitor(), POINT{ 0, 0 }); + Assert::IsFalse(m_fzCallback->InMoveSize()); //point outside of window rect + + const auto window = Mocks::WindowCreate(m_hInst); + const int paddingX = 8, paddingY = 6; + RECT windowRect{}; + ::GetWindowRect(window, &windowRect); + m_fzCallback->MoveSizeStart(window, Mocks::Monitor(), POINT{ windowRect.left + paddingX, windowRect.top + paddingY }); + Assert::IsTrue(m_fzCallback->InMoveSize()); + + m_fzCallback->MoveSizeEnd(Mocks::Window(), POINT{ 0, 0 }); + Assert::IsFalse(m_fzCallback->InMoveSize()); + } + + TEST_METHOD(OnKeyDownNothingPressed) + { + for (DWORD code = '0'; code <= '9'; code++) + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = code; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + } + + TEST_METHOD(OnKeyDownShiftPressed) + { + sendKeyboardInput(VK_SHIFT); + + for (DWORD code = '0'; code <= '9'; code++) + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = code; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + } + + TEST_METHOD(OnKeyDownWinPressed) + { + sendKeyboardInput(VK_LWIN); + + for (DWORD code = '0'; code <= '9'; code++) + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = code; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + } + + TEST_METHOD(OnKeyDownWinShiftPressed) + { + sendKeyboardInput(VK_LWIN); + sendKeyboardInput(VK_SHIFT); + + for (DWORD code = '0'; code <= '9'; code++) + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = code; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + } + + TEST_METHOD(OnKeyDownWinCtrlPressed) + { + sendKeyboardInput(VK_LWIN); + sendKeyboardInput(VK_CONTROL); + + const Settings settings{ + .overrideSnapHotkeys = false, + }; + + auto config = serializedPowerToySettings(settings); + m_settings->SetConfig(config.c_str()); + + for (DWORD code = '0'; code <= '9'; code++) + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = code; + Assert::IsTrue(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsFalse(m_fzCallback->OnKeyDown(&input)); + } + } + + TEST_METHOD(OnKeyDownWinPressedOverride) + { + sendKeyboardInput(VK_LWIN); + + const Settings settings{ + .overrideSnapHotkeys = true, + }; + + auto config = serializedPowerToySettings(settings); + m_settings->SetConfig(config.c_str()); + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_LEFT; + Assert::IsTrue(m_fzCallback->OnKeyDown(&input)); + } + + { + tagKBDLLHOOKSTRUCT input{}; + input.vkCode = VK_RIGHT; + Assert::IsTrue(m_fzCallback->OnKeyDown(&input)); + } + } + }; +} \ No newline at end of file diff --git a/src/modules/fancyzones/tests/UnitTests/FancyZonesSettings.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/FancyZonesSettings.Spec.cpp new file mode 100644 index 0000000000..528156b0db --- /dev/null +++ b/src/modules/fancyzones/tests/UnitTests/FancyZonesSettings.Spec.cpp @@ -0,0 +1,714 @@ +#include "pch.h" +#include + +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace FancyZonesUnitTests +{ + void compareHotkeyObjects(const PowerToysSettings::HotkeyObject& expected, const PowerToysSettings::HotkeyObject& actual) + { + Assert::AreEqual(expected.alt_pressed(), actual.alt_pressed()); + Assert::AreEqual(expected.ctrl_pressed(), actual.ctrl_pressed()); + Assert::AreEqual(expected.shift_pressed(), actual.shift_pressed()); + Assert::AreEqual(expected.win_pressed(), actual.win_pressed()); + + //NOTE: key_from_code may create different values + //Assert::AreEqual(expected.get_key(), actual.get_key()); + Assert::AreEqual(expected.get_code(), actual.get_code()); + Assert::AreEqual(expected.get_modifiers(), actual.get_modifiers()); + Assert::AreEqual(expected.get_modifiers_repeat(), actual.get_modifiers_repeat()); + } + + void compareSettings(const Settings& expected, const Settings& actual) + { + Assert::AreEqual(expected.shiftDrag, actual.shiftDrag); + Assert::AreEqual(expected.displayChange_moveWindows, actual.displayChange_moveWindows); + Assert::AreEqual(expected.virtualDesktopChange_moveWindows, actual.virtualDesktopChange_moveWindows); + Assert::AreEqual(expected.zoneSetChange_flashZones, actual.zoneSetChange_flashZones); + Assert::AreEqual(expected.zoneSetChange_moveWindows, actual.zoneSetChange_moveWindows); + Assert::AreEqual(expected.overrideSnapHotkeys, actual.overrideSnapHotkeys); + Assert::AreEqual(expected.appLastZone_moveWindows, actual.appLastZone_moveWindows); + Assert::AreEqual(expected.use_cursorpos_editor_startupscreen, actual.use_cursorpos_editor_startupscreen); + Assert::AreEqual(expected.zoneHightlightColor.c_str(), actual.zoneHightlightColor.c_str()); + Assert::AreEqual(expected.zoneHighlightOpacity, actual.zoneHighlightOpacity); + Assert::AreEqual(expected.excludedApps.c_str(), actual.excludedApps.c_str()); + Assert::AreEqual(expected.excludedAppsArray.size(), actual.excludedAppsArray.size()); + for (int i = 0; i < expected.excludedAppsArray.size(); i++) + { + Assert::AreEqual(expected.excludedAppsArray[i].c_str(), actual.excludedAppsArray[i].c_str()); + } + + compareHotkeyObjects(expected.editorHotkey, actual.editorHotkey); + } + + TEST_CLASS(FancyZonesSettingsCreationUnitTest) + { + HINSTANCE m_hInst; + PCWSTR m_moduleName = L"FancyZonesTest"; + std::wstring m_tmpName; + + const PowerToysSettings::HotkeyObject m_defaultHotkeyObject = PowerToysSettings::HotkeyObject::from_settings(true, false, false, false, VK_OEM_3); + const Settings m_defaultSettings; + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + m_tmpName = PTSettingsHelper::get_module_save_folder_location(m_moduleName) + L"\\settings.json"; + } + + TEST_METHOD_CLEANUP(Cleanup) + { + std::filesystem::remove(m_tmpName); + } + + TEST_METHOD(CreateWithHinstanceDefault) + { + auto actual = MakeFancyZonesSettings({}, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(m_defaultSettings, actualSettings); + } + + TEST_METHOD(CreateWithHinstanceNullptr) + { + auto actual = MakeFancyZonesSettings(nullptr, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(m_defaultSettings, actualSettings); + } + + TEST_METHOD(CreateWithNameEmpty) + { + auto actual = MakeFancyZonesSettings(m_hInst, L""); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(m_defaultSettings, actualSettings); + } + + TEST_METHOD(Create) + { + //prepare data + const Settings expected { + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateWithMultipleApps) + { + //prepare data + const Settings expected { + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app\r\napp1\r\napp2\r\nanother app", + .excludedAppsArray = { L"APP", L"APP1", L"APP2", L"ANOTHER APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateWithBoolValuesMissed) + { + const Settings expected { + .shiftDrag = m_defaultSettings.shiftDrag, + .displayChange_moveWindows = m_defaultSettings.displayChange_moveWindows, + .virtualDesktopChange_moveWindows = m_defaultSettings.virtualDesktopChange_moveWindows, + .zoneSetChange_flashZones = m_defaultSettings.zoneSetChange_flashZones, + .zoneSetChange_moveWindows = m_defaultSettings.zoneSetChange_moveWindows, + .overrideSnapHotkeys = m_defaultSettings.overrideSnapHotkeys, + .appLastZone_moveWindows = m_defaultSettings.appLastZone_moveWindows, + .use_cursorpos_editor_startupscreen = m_defaultSettings.use_cursorpos_editor_startupscreen, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateColorMissed) + { + //prepare data + const Settings expected { + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = m_defaultSettings.zoneHightlightColor, + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateOpacityMissed) + { + //prepare data + const Settings expected { + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = m_defaultSettings.zoneHighlightOpacity, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateHotkeyMissed) + { + //prepare data + const Settings expected = Settings{ + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = m_defaultSettings.editorHotkey, + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateAppsMissed) + { + //prepare data + const Settings expected = Settings{ + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = m_defaultSettings.excludedApps, + .excludedAppsArray = m_defaultSettings.excludedAppsArray, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + + values.save_to_settings_file(); + + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(expected, actualSettings); + } + + TEST_METHOD(CreateWithEmptyJson) + { + json::to_file(m_tmpName, json::JsonObject()); + auto actual = MakeFancyZonesSettings(m_hInst, m_moduleName); + Assert::IsTrue(actual != nullptr); + + auto actualSettings = actual->GetSettings(); + compareSettings(m_defaultSettings, actualSettings); + } + }; + + TEST_CLASS(FancyZonesSettingsCallbackUnitTests) + { + winrt::com_ptr m_settings = nullptr; + PCWSTR m_moduleName = L"FancyZonesTest"; + + struct FZCallback : public winrt::implements + { + public: + FZCallback(bool* callFlag) : + m_callFlag(callFlag) + { + *m_callFlag = false; + } + + IFACEMETHODIMP_(bool) InMoveSize() noexcept { return false; } + IFACEMETHODIMP_(void) MoveSizeStart(HWND window, HMONITOR monitor, POINT const& ptScreen) noexcept {} + IFACEMETHODIMP_(void) MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen) noexcept {} + IFACEMETHODIMP_(void) MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept {} + IFACEMETHODIMP_(void) VirtualDesktopChanged() noexcept {} + IFACEMETHODIMP_(void) VirtualDesktopInitialize() noexcept {} + IFACEMETHODIMP_(void) WindowCreated(HWND window) noexcept {} + IFACEMETHODIMP_(bool) OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept { return false; } + + IFACEMETHODIMP_(void) ToggleEditor() noexcept + { + Assert::IsNotNull(m_callFlag); + *m_callFlag = true; + } + + IFACEMETHODIMP_(void) SettingsChanged() noexcept + { + Assert::IsNotNull(m_callFlag); + *m_callFlag = true; + } + + private: + bool* m_callFlag = nullptr; + }; + + TEST_METHOD_INITIALIZE(Init) + { + HINSTANCE hInst = (HINSTANCE)GetModuleHandleW(nullptr); + const Settings expected{ + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + m_settings = MakeFancyZonesSettings(hInst, m_moduleName); + Assert::IsTrue(m_settings != nullptr); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + const auto settingsFile = PTSettingsHelper::get_module_save_folder_location(m_moduleName) + L"\\settings.json"; + std::filesystem::remove(settingsFile); + } + + TEST_METHOD(CallbackSetConfig) + { + bool flag = false; + FZCallback callback(&flag); + + json::JsonObject json{}; + json.SetNamedValue(L"name", json::JsonValue::CreateStringValue(L"name")); + + m_settings->SetCallback(&callback); + m_settings->SetConfig(json.Stringify().c_str()); + + Assert::IsTrue(flag); + } + + TEST_METHOD(CallbackCallCustomAction) + { + bool flag = false; + FZCallback callback(&flag); + + json::JsonObject action{}; + action.SetNamedValue(L"action_name", json::JsonValue::CreateStringValue(L"ToggledFZEditor")); + + m_settings->SetCallback(&callback); + m_settings->CallCustomAction(action.Stringify().c_str()); + + Assert::IsTrue(flag); + } + + TEST_METHOD(CallbackCallCustomActionNotToggle) + { + bool flag = false; + FZCallback callback(&flag); + + json::JsonObject action{}; + action.SetNamedValue(L"action_name", json::JsonValue::CreateStringValue(L"NOT_ToggledFZEditor")); + + m_settings->SetCallback(&callback); + m_settings->CallCustomAction(action.Stringify().c_str()); + + Assert::IsFalse(flag); + } + + TEST_METHOD(CallbackGetConfig) + { + bool flag = false; + FZCallback callback(&flag); + + m_settings->SetCallback(&callback); + + int bufSize = 0; + m_settings->GetConfig(L"", &bufSize); + + Assert::IsFalse(flag); + } + + TEST_METHOD(CallbackGetSettings) + { + bool flag = false; + FZCallback callback(&flag); + + m_settings->SetCallback(&callback); + m_settings->GetSettings(); + + Assert::IsFalse(flag); + } + }; + + TEST_CLASS(FancyZonesSettingsUnitTests) + { + winrt::com_ptr m_settings = nullptr; + PowerToysSettings::Settings* m_ptSettings = nullptr; + PCWSTR m_moduleName = L"FancyZonesTest"; + + std::wstring serializedPowerToySettings(const Settings& settings) + { + PowerToysSettings::Settings ptSettings(HINSTANCE{}, m_moduleName); + ptSettings.set_description(IDS_SETTING_DESCRIPTION); + ptSettings.set_icon_key(L"pt-fancy-zones"); + ptSettings.set_overview_link(L"https://github.com/microsoft/PowerToys/blob/master/src/modules/fancyzones/README.md"); + ptSettings.set_video_link(L"https://youtu.be/rTtGzZYAXgY"); + + ptSettings.add_custom_action( + L"ToggledFZEditor", // action name. + IDS_SETTING_LAUNCH_EDITOR_LABEL, + IDS_SETTING_LAUNCH_EDITOR_BUTTON, + IDS_SETTING_LAUNCH_EDITOR_DESCRIPTION); + ptSettings.add_hotkey(L"fancyzones_editor_hotkey", IDS_SETTING_LAUNCH_EDITOR_HOTKEY_LABEL, settings.editorHotkey); + ptSettings.add_bool_toogle(L"fancyzones_shiftDrag", IDS_SETTING_DESCRIPTION_SHIFTDRAG, settings.shiftDrag); + ptSettings.add_bool_toogle(L"fancyzones_overrideSnapHotkeys", IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS, settings.overrideSnapHotkeys); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_flashZones", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES, settings.zoneSetChange_flashZones); + ptSettings.add_bool_toogle(L"fancyzones_displayChange_moveWindows", IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS, settings.displayChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_zoneSetChange_moveWindows", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS, settings.zoneSetChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_virtualDesktopChange_moveWindows", IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS, settings.virtualDesktopChange_moveWindows); + ptSettings.add_bool_toogle(L"fancyzones_appLastZone_moveWindows", IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS, settings.appLastZone_moveWindows); + ptSettings.add_bool_toogle(L"use_cursorpos_editor_startupscreen", IDS_SETTING_DESCRIPTION_USE_CURSORPOS_EDITOR_STARTUPSCREEN, settings.use_cursorpos_editor_startupscreen); + ptSettings.add_int_spinner(L"fancyzones_highlight_opacity", IDS_SETTINGS_HIGHLIGHT_OPACITY, settings.zoneHighlightOpacity, 0, 100, 1); + ptSettings.add_color_picker(L"fancyzones_zoneHighlightColor", IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR, settings.zoneHightlightColor); + ptSettings.add_multiline_string(L"fancyzones_excluded_apps", IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION, settings.excludedApps); + + return ptSettings.serialize(); + } + + TEST_METHOD_INITIALIZE(Init) + { + HINSTANCE hInst = (HINSTANCE)GetModuleHandleW(nullptr); + + //init m_settings + const Settings expected{ + .shiftDrag = false, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = true, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = false, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00FFD7", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, true, true, false, VK_OEM_3), + .excludedApps = L"app", + .excludedAppsArray = { L"APP" }, + }; + + PowerToysSettings::PowerToyValues values(m_moduleName); + values.add_property(L"fancyzones_shiftDrag", expected.shiftDrag); + values.add_property(L"fancyzones_displayChange_moveWindows", expected.displayChange_moveWindows); + values.add_property(L"fancyzones_virtualDesktopChange_moveWindows", expected.virtualDesktopChange_moveWindows); + values.add_property(L"fancyzones_zoneSetChange_flashZones", expected.zoneSetChange_flashZones); + values.add_property(L"fancyzones_zoneSetChange_moveWindows", expected.zoneSetChange_moveWindows); + values.add_property(L"fancyzones_overrideSnapHotkeys", expected.overrideSnapHotkeys); + values.add_property(L"fancyzones_appLastZone_moveWindows", expected.appLastZone_moveWindows); + values.add_property(L"use_cursorpos_editor_startupscreen", expected.use_cursorpos_editor_startupscreen); + values.add_property(L"fancyzones_zoneHighlightColor", expected.zoneHightlightColor); + values.add_property(L"fancyzones_highlight_opacity", expected.zoneHighlightOpacity); + values.add_property(L"fancyzones_editor_hotkey", expected.editorHotkey.get_json()); + values.add_property(L"fancyzones_excluded_apps", expected.excludedApps); + + values.save_to_settings_file(); + + m_settings = MakeFancyZonesSettings(hInst, m_moduleName); + Assert::IsTrue(m_settings != nullptr); + + //init m_ptSettings + m_ptSettings = new PowerToysSettings::Settings(hInst, m_moduleName); + m_ptSettings->set_description(IDS_SETTING_DESCRIPTION); + m_ptSettings->set_icon_key(L"pt-fancy-zones"); + m_ptSettings->set_overview_link(L"https://github.com/microsoft/PowerToys/blob/master/src/modules/fancyzones/README.md"); + m_ptSettings->set_video_link(L"https://youtu.be/rTtGzZYAXgY"); + + m_ptSettings->add_custom_action( + L"ToggledFZEditor", // action name. + IDS_SETTING_LAUNCH_EDITOR_LABEL, + IDS_SETTING_LAUNCH_EDITOR_BUTTON, + IDS_SETTING_LAUNCH_EDITOR_DESCRIPTION); + m_ptSettings->add_hotkey(L"fancyzones_editor_hotkey", IDS_SETTING_LAUNCH_EDITOR_HOTKEY_LABEL, expected.editorHotkey); + m_ptSettings->add_bool_toogle(L"fancyzones_shiftDrag", IDS_SETTING_DESCRIPTION_SHIFTDRAG, expected.shiftDrag); + m_ptSettings->add_bool_toogle(L"fancyzones_overrideSnapHotkeys", IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS, expected.overrideSnapHotkeys); + m_ptSettings->add_bool_toogle(L"fancyzones_zoneSetChange_flashZones", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES, expected.zoneSetChange_flashZones); + m_ptSettings->add_bool_toogle(L"fancyzones_displayChange_moveWindows", IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS, expected.displayChange_moveWindows); + m_ptSettings->add_bool_toogle(L"fancyzones_zoneSetChange_moveWindows", IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS, expected.zoneSetChange_moveWindows); + m_ptSettings->add_bool_toogle(L"fancyzones_virtualDesktopChange_moveWindows", IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS, expected.virtualDesktopChange_moveWindows); + m_ptSettings->add_bool_toogle(L"fancyzones_appLastZone_moveWindows", IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS, expected.appLastZone_moveWindows); + m_ptSettings->add_bool_toogle(L"use_cursorpos_editor_startupscreen", IDS_SETTING_DESCRIPTION_USE_CURSORPOS_EDITOR_STARTUPSCREEN, expected.use_cursorpos_editor_startupscreen); + m_ptSettings->add_int_spinner(L"fancyzones_highlight_opacity", IDS_SETTINGS_HIGHLIGHT_OPACITY, expected.zoneHighlightOpacity, 0, 100, 1); + m_ptSettings->add_color_picker(L"fancyzones_zoneHighlightColor", IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR, expected.zoneHightlightColor); + m_ptSettings->add_multiline_string(L"fancyzones_excluded_apps", IDS_SETTING_EXCLCUDED_APPS_DESCRIPTION, expected.excludedApps); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + const auto settingsFile = PTSettingsHelper::get_module_save_folder_location(m_moduleName) + L"\\settings.json"; + std::filesystem::remove(settingsFile); + } + + TEST_METHOD(GetConfig) + { + const int expectedSize = m_ptSettings->serialize().size() + 1; + + int actualBufferSize = expectedSize; + PWSTR actualBuffer = new wchar_t[actualBufferSize]; + + Assert::IsTrue(m_settings->GetConfig(actualBuffer, &actualBufferSize)); + Assert::AreEqual(expectedSize, actualBufferSize); + + Assert::AreEqual(m_ptSettings->serialize().c_str(), actualBuffer); + } + + TEST_METHOD(GetConfigSmallBuffer) + { + const auto serialized = m_ptSettings->serialize(); + const int expectedSize = serialized.size() + 1; + + int actualBufferSize = m_ptSettings->serialize().size() - 1; + PWSTR actualBuffer = new wchar_t[actualBufferSize]; + + Assert::IsFalse(m_settings->GetConfig(actualBuffer, &actualBufferSize)); + Assert::AreEqual(expectedSize, actualBufferSize); + Assert::AreNotEqual(serialized.c_str(), actualBuffer); + } + + TEST_METHOD(GetConfigNullBuffer) + { + const auto serialized = m_ptSettings->serialize(); + const int expectedSize = serialized.size() + 1; + + int actualBufferSize = 0; + PWSTR actualBuffer = nullptr; + + Assert::IsFalse(m_settings->GetConfig(actualBuffer, &actualBufferSize)); + Assert::AreEqual(expectedSize, actualBufferSize); + } + + TEST_METHOD(SetConfig) + { + //cleanup file before call set config + const auto settingsFile = PTSettingsHelper::get_module_save_folder_location(m_moduleName) + L"\\settings.json"; + std::filesystem::remove(settingsFile); + + const Settings expected { + .shiftDrag = true, + .displayChange_moveWindows = true, + .virtualDesktopChange_moveWindows = true, + .zoneSetChange_flashZones = false, + .zoneSetChange_moveWindows = true, + .overrideSnapHotkeys = false, + .appLastZone_moveWindows = true, + .use_cursorpos_editor_startupscreen = true, + .zoneHightlightColor = L"#00AABB", + .zoneHighlightOpacity = 45, + .editorHotkey = PowerToysSettings::HotkeyObject::from_settings(false, false, false, false, VK_OEM_3), + .excludedApps = L"app\r\napp2", + .excludedAppsArray = { L"APP", L"APP2" }, + }; + + auto config = serializedPowerToySettings(expected); + m_settings->SetConfig(config.c_str()); + + auto actual = m_settings->GetSettings(); + compareSettings(expected, actual); + + Assert::IsTrue(std::filesystem::exists(settingsFile)); + } + }; +} \ No newline at end of file diff --git a/src/modules/fancyzones/tests/UnitTests/JsonHelpers.Tests.cpp b/src/modules/fancyzones/tests/UnitTests/JsonHelpers.Tests.cpp new file mode 100644 index 0000000000..ce161a2c58 --- /dev/null +++ b/src/modules/fancyzones/tests/UnitTests/JsonHelpers.Tests.cpp @@ -0,0 +1,1659 @@ +#include "pch.h" +#include + +#include +#include "util.h" + +#include + +using namespace JSONHelpers; +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace FancyZonesUnitTests +{ + void compareJsonObjects(const json::JsonObject& expected, const json::JsonObject& actual, bool recursive = true) + { + auto iter = expected.First(); + while (iter.HasCurrent()) + { + const auto key = iter.Current().Key(); + Assert::IsTrue(actual.HasKey(key), key.c_str()); + + const std::wstring expectedStringified = iter.Current().Value().Stringify().c_str(); + const std::wstring actualStringified = actual.GetNamedValue(key).Stringify().c_str(); + + if (recursive) + { + json::JsonObject expectedJson; + if (json::JsonObject::TryParse(expectedStringified, expectedJson)) + { + json::JsonObject actualJson; + if (json::JsonObject::TryParse(actualStringified, actualJson)) + { + compareJsonObjects(expectedJson, actualJson, true); + } + else + { + Assert::IsTrue(false, key.c_str()); + } + } + else + { + Assert::AreEqual(expectedStringified, actualStringified, key.c_str()); + } + } + else + { + Assert::AreEqual(expectedStringified, actualStringified, key.c_str()); + } + + iter.MoveNext(); + } + } + + TEST_CLASS(ZoneSetLayoutTypeUnitTest) + { + TEST_METHOD(ZoneSetLayoutTypeToString){ + std::map expectedMap = { + std::make_pair(-2, L"TypeToString_ERROR"), + std::make_pair(-1, L"blank"), + std::make_pair(0, L"focus"), + std::make_pair(1, L"columns"), + std::make_pair(2, L"rows"), + std::make_pair(3, L"grid"), + std::make_pair(4, L"priority-grid"), + std::make_pair(5, L"custom"), + std::make_pair(6, L"TypeToString_ERROR"), + }; + + for (const auto& expected : expectedMap) + { + auto actual = JSONHelpers::TypeToString(static_cast(expected.first)); + Assert::AreEqual(expected.second, actual); + } + } + + TEST_METHOD(ZoneSetLayoutTypeFromString) + { + std::map expectedMap = { + std::make_pair(ZoneSetLayoutType::Focus, L"focus"), + std::make_pair(ZoneSetLayoutType::Columns, L"columns"), + std::make_pair(ZoneSetLayoutType::Rows, L"rows"), + std::make_pair(ZoneSetLayoutType::Grid, L"grid"), + std::make_pair(ZoneSetLayoutType::PriorityGrid, L"priority-grid"), + std::make_pair(ZoneSetLayoutType::Custom, L"custom"), + }; + + for (const auto& expected : expectedMap) + { + auto actual = JSONHelpers::TypeFromString(expected.second); + Assert::AreEqual(static_cast(expected.first), static_cast(actual)); + } + } + + TEST_METHOD(ZoneSetLayoutTypeFromLayoutId) + { + std::map expectedMap = { + std::make_pair(ZoneSetLayoutType::Focus, 0xFFFF), + std::make_pair(ZoneSetLayoutType::Columns, 0xFFFD), + std::make_pair(ZoneSetLayoutType::Rows, 0xFFFE), + std::make_pair(ZoneSetLayoutType::Grid, 0xFFFC), + std::make_pair(ZoneSetLayoutType::PriorityGrid, 0xFFFB), + std::make_pair(ZoneSetLayoutType::Blank, 0xFFFA), + std::make_pair(ZoneSetLayoutType::Custom, 0), + std::make_pair(ZoneSetLayoutType::Custom, 1), + std::make_pair(ZoneSetLayoutType::Custom, -1), + }; + + for (const auto& expected : expectedMap) + { + auto actual = JSONHelpers::TypeFromLayoutId(expected.second); + Assert::AreEqual(static_cast(expected.first), static_cast(actual)); + } + } + }; + + TEST_CLASS(CanvasLayoutInfoUnitTests) + { + json::JsonObject m_json = json::JsonObject::Parse(L"{\"ref-width\": 123, \"ref-height\": 321, \"zones\": [{\"X\": 11, \"Y\": 22, \"width\": 33, \"height\": 44}, {\"X\": 55, \"Y\": 66, \"width\": 77, \"height\": 88}]}"); + + TEST_METHOD(ToJson) + { + CanvasLayoutInfo info; + info.referenceWidth = 123; + info.referenceHeight = 321; + info.zones = { CanvasLayoutInfo::Rect{ 11, 22, 33, 44 }, CanvasLayoutInfo::Rect{ 55, 66, 77, 88 } }; + + auto actual = CanvasLayoutInfo::ToJson(info); + compareJsonObjects(m_json, actual); + } + + TEST_METHOD(FromJson) + { + CanvasLayoutInfo expected; + expected.referenceWidth = 123; + expected.referenceHeight = 321; + expected.zones = { CanvasLayoutInfo::Rect{ 11, 22, 33, 44 }, CanvasLayoutInfo::Rect{ 55, 66, 77, 88 } }; + + auto actual = CanvasLayoutInfo::FromJson(m_json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.referenceHeight, actual->referenceHeight); + Assert::AreEqual(expected.referenceWidth, actual->referenceWidth); + Assert::AreEqual(expected.zones.size(), actual->zones.size()); + for (int i = 0; i < expected.zones.size(); i++) + { + Assert::AreEqual(expected.zones[i].x, actual->zones[i].x); + Assert::AreEqual(expected.zones[i].y, actual->zones[i].y); + Assert::AreEqual(expected.zones[i].width, actual->zones[i].width); + Assert::AreEqual(expected.zones[i].height, actual->zones[i].height); + } + } + + TEST_METHOD(FromJsonMissingKeys) + { + CanvasLayoutInfo info{ 123, 321, { CanvasLayoutInfo::Rect{ 11, 22, 33, 44 }, CanvasLayoutInfo::Rect{ 55, 66, 77, 88 } } }; + const auto json = CanvasLayoutInfo::ToJson(info); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = CanvasLayoutInfo::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(GridLayoutInfoUnitTests) + { + private: + GridLayoutInfo m_info = GridLayoutInfo(GridLayoutInfo::Minimal{ .rows = 3, .columns = 4 }); + json::JsonObject m_gridJson = json::JsonObject(); + json::JsonArray m_rowsArray, m_columnsArray, m_cells; + + void compareSizes(int expectedRows, int expectedColumns, const GridLayoutInfo& actual) + { + Assert::AreEqual(expectedRows, actual.rows()); + Assert::AreEqual(expectedColumns, actual.columns()); + Assert::AreEqual((size_t)expectedRows, actual.rowsPercents().size()); + Assert::AreEqual((size_t)expectedColumns, actual.columnsPercents().size()); + Assert::AreEqual((size_t)expectedRows, actual.cellChildMap().size()); + + for (int i = 0; i < expectedRows; i++) + { + Assert::AreEqual((size_t)expectedColumns, actual.cellChildMap()[i].size()); + } + } + + void compareVectors(const std::vector& expected, const std::vector& actual) + { + Assert::AreEqual(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) + { + Assert::AreEqual(expected[i], actual[i]); + } + } + + void compareGridInfos(const GridLayoutInfo& expected, const GridLayoutInfo& actual) + { + compareSizes(expected.rows(), expected.columns(), actual); + + compareVectors(expected.rowsPercents(), actual.rowsPercents()); + compareVectors(expected.columnsPercents(), actual.columnsPercents()); + for (int i = 0; i < expected.cellChildMap().size(); i++) + { + compareVectors(expected.cellChildMap()[i], actual.cellChildMap()[i]); + } + } + + TEST_METHOD_INITIALIZE(Init) + { + m_info = GridLayoutInfo(GridLayoutInfo::Minimal{ .rows = 3, .columns = 4 }); + for (int i = 0; i < m_info.rows(); i++) + { + int row = rand() % 100; + m_rowsArray.Append(json::JsonValue::CreateNumberValue(row)); + m_info.rowsPercents()[i] = row; + } + + for (int i = 0; i < m_info.columns(); i++) + { + int column = rand() % 100; + m_columnsArray.Append(json::JsonValue::CreateNumberValue(column)); + m_info.columnsPercents()[i] = column; + } + + for (int i = 0; i < m_info.rows(); i++) + { + json::JsonArray cellsArray; + for (int j = 0; j < m_info.columns(); j++) + { + int cell = rand() % 100; + m_info.cellChildMap()[i][j] = cell; + cellsArray.Append(json::JsonValue::CreateNumberValue(cell)); + } + m_cells.Append(cellsArray); + } + + m_gridJson = json::JsonObject::Parse(L"{\"rows\": 3, \"columns\": 4}"); + m_gridJson.SetNamedValue(L"rows-percentage", m_rowsArray); + m_gridJson.SetNamedValue(L"columns-percentage", m_columnsArray); + m_gridJson.SetNamedValue(L"cell-child-map", m_cells); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + m_rowsArray.Clear(); + m_cells.Clear(); + m_columnsArray.Clear(); + m_gridJson.Clear(); + m_info = GridLayoutInfo(GridLayoutInfo::Minimal{ .rows = 3, .columns = 4 }); + } + + public: + TEST_METHOD(CreationZero) + { + const int expectedRows = 0, expectedColumns = 0; + GridLayoutInfo info(GridLayoutInfo::Minimal{ .rows = expectedRows, .columns = expectedColumns }); + compareSizes(expectedRows, expectedColumns, info); + } + + TEST_METHOD(Creation) + { + const int expectedRows = 3, expectedColumns = 4; + const std::vector expectedRowsPercents = { 0, 0, 0 }; + const std::vector expectedColumnsPercents = { 0, 0, 0, 0 }; + + GridLayoutInfo info(GridLayoutInfo::Minimal{ .rows = expectedRows, .columns = expectedColumns }); + compareSizes(expectedRows, expectedColumns, info); + + compareVectors(expectedRowsPercents, info.rowsPercents()); + compareVectors(expectedColumnsPercents, info.columnsPercents()); + for (int i = 0; i < info.cellChildMap().size(); i++) + { + compareVectors({ 0, 0, 0, 0 }, info.cellChildMap()[i]); + } + } + + TEST_METHOD(CreationFull) + { + const int expectedRows = 3, expectedColumns = 4; + const std::vector expectedRowsPercents = { 1, 2, 3 }; + const std::vector expectedColumnsPercents = { 4, 3, 2, 1 }; + const std::vector> expectedCells = { expectedColumnsPercents, expectedColumnsPercents, expectedColumnsPercents }; + + GridLayoutInfo info(GridLayoutInfo::Full{ + .rows = expectedRows, + .columns = expectedColumns , + .rowsPercents = expectedRowsPercents, + .columnsPercents = expectedColumnsPercents, + .cellChildMap = expectedCells }); + compareSizes(expectedRows, expectedColumns, info); + + compareVectors(expectedRowsPercents, info.rowsPercents()); + compareVectors(expectedColumnsPercents, info.columnsPercents()); + for (int i = 0; i < info.cellChildMap().size(); i++) + { + compareVectors(expectedCells[i], info.cellChildMap()[i]); + } + } + + TEST_METHOD(CreationFullVectorsSmaller) + { + const int expectedRows = 3, expectedColumns = 4; + const std::vector expectedRowsPercents = { 1, 2, 0 }; + const std::vector expectedColumnsPercents = { 4, 3, 0, 0 }; + const std::vector> expectedCells = { { 0, 0, 0, 0 }, { 1, 0, 0, 0 }, { 1, 2, 0, 0 } }; + + GridLayoutInfo info(GridLayoutInfo::Full{ + .rows = expectedRows, + .columns = expectedColumns, + .rowsPercents = { 1, 2 }, + .columnsPercents = { 4, 3 }, + .cellChildMap = { {}, { 1 }, { 1, 2 } } }); + compareSizes(expectedRows, expectedColumns, info); + + compareVectors(expectedRowsPercents, info.rowsPercents()); + compareVectors(expectedColumnsPercents, info.columnsPercents()); + for (int i = 0; i < info.cellChildMap().size(); i++) + { + compareVectors(expectedCells[i], info.cellChildMap()[i]); + } + } + + TEST_METHOD(CreationFullVectorsBigger) + { + const int expectedRows = 3, expectedColumns = 4; + const std::vector expectedRowsPercents = { 1, 2, 3 }; + const std::vector expectedColumnsPercents = { 4, 3, 2, 1 }; + const std::vector> expectedCells = { expectedColumnsPercents, expectedColumnsPercents, expectedColumnsPercents }; + + GridLayoutInfo info(GridLayoutInfo::Full{ + .rows = expectedRows, + .columns = expectedColumns, + .rowsPercents = { 1, 2, 3, 4, 5 }, + .columnsPercents = { 4, 3, 2, 1, 0, -1 }, + .cellChildMap = { { 4, 3, 2, 1, 0, -1 }, { 4, 3, 2, 1, 0, -1 }, { 4, 3, 2, 1, 0, -1 } } }); + compareSizes(expectedRows, expectedColumns, info); + + compareVectors(expectedRowsPercents, info.rowsPercents()); + compareVectors(expectedColumnsPercents, info.columnsPercents()); + for (int i = 0; i < info.cellChildMap().size(); i++) + { + compareVectors(expectedCells[i], info.cellChildMap()[i]); + } + } + + TEST_METHOD(ToJson) + { + json::JsonObject expected = json::JsonObject(m_gridJson); + GridLayoutInfo info = m_info; + + auto actual = GridLayoutInfo::ToJson(info); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(FromJson) + { + json::JsonObject json = json::JsonObject(m_gridJson); + GridLayoutInfo expected = m_info; + + auto actual = GridLayoutInfo::FromJson(json); + Assert::IsTrue(actual.has_value()); + compareGridInfos(expected, *actual); + } + + TEST_METHOD(FromJsonEmptyArray) + { + json::JsonObject json = json::JsonObject::Parse(L"{\"rows\": 0, \"columns\": 0}"); + GridLayoutInfo expected(GridLayoutInfo::Minimal{ 0, 0 }); + + json.SetNamedValue(L"rows-percentage", json::JsonArray()); + json.SetNamedValue(L"columns-percentage", json::JsonArray()); + json.SetNamedValue(L"cell-child-map", json::JsonArray()); + + auto actual = GridLayoutInfo::FromJson(json); + Assert::IsTrue(actual.has_value()); + compareGridInfos(expected, *actual); + } + + TEST_METHOD(FromJsonSmallerArray) + { + GridLayoutInfo expected = m_info; + expected.rowsPercents().pop_back(); + expected.columnsPercents().pop_back(); + expected.cellChildMap().pop_back(); + expected.cellChildMap()[0].pop_back(); + json::JsonObject json = GridLayoutInfo::ToJson(expected); + + auto actual = GridLayoutInfo::FromJson(json); + Assert::IsFalse(actual.has_value()); + } + + TEST_METHOD(FromJsonBiggerArray) + { + GridLayoutInfo expected = m_info; + + //extra + for (int i = 0; i < 5; i++) + { + expected.rowsPercents().push_back(rand() % 100); + expected.columnsPercents().push_back(rand() % 100); + expected.cellChildMap().push_back({}); + + for (int j = 0; j < 5; j++) + { + expected.cellChildMap()[i].push_back(rand() % 100); + } + } + + auto json = GridLayoutInfo::ToJson(expected); + + auto actual = GridLayoutInfo::FromJson(json); + Assert::IsFalse(actual.has_value()); + } + + TEST_METHOD(FromJsonMissingKeys) + { + GridLayoutInfo info = m_info; + const auto json = json::JsonObject(m_gridJson); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = GridLayoutInfo::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(CustomZoneSetUnitTests) + { + TEST_METHOD(ToJsonGrid) + { + CustomZoneSetJSON zoneSet{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Grid, GridLayoutInfo(GridLayoutInfo::Minimal{}) } }; + + json::JsonObject expected = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"name\": \"name\", \"type\": \"grid\"}"); + expected.SetNamedValue(L"info", GridLayoutInfo::ToJson(std::get(zoneSet.data.info))); + + auto actual = CustomZoneSetJSON::ToJson(zoneSet); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(ToJsonCanvas) + { + CustomZoneSetJSON zoneSet{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{} } }; + + json::JsonObject expected = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"name\": \"name\", \"type\": \"canvas\"}"); + expected.SetNamedValue(L"info", CanvasLayoutInfo::ToJson(std::get(zoneSet.data.info))); + + auto actual = CustomZoneSetJSON::ToJson(zoneSet); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(FromJsonGrid) + { + const auto grid = GridLayoutInfo(GridLayoutInfo::Full{ 1, 3, { 10000 }, { 2500, 5000, 2500 }, { { 0, 1, 2 } } }); + CustomZoneSetJSON expected{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + + json::JsonObject json = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"name\": \"name\", \"type\": \"grid\"}"); + json.SetNamedValue(L"info", GridLayoutInfo::ToJson(std::get(expected.data.info))); + + auto actual = CustomZoneSetJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.uuid.c_str(), actual->uuid.c_str()); + Assert::AreEqual(expected.data.name.c_str(), actual->data.name.c_str()); + Assert::AreEqual((int)expected.data.type, (int)actual->data.type); + + auto expectedGrid = std::get(expected.data.info); + auto actualGrid = std::get(actual->data.info); + Assert::AreEqual(expectedGrid.rows(), actualGrid.rows()); + Assert::AreEqual(expectedGrid.columns(), actualGrid.columns()); + } + + TEST_METHOD(FromJsonCanvas) + { + CustomZoneSetJSON expected{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 2, 1 } } }; + + json::JsonObject json = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"name\": \"name\", \"type\": \"canvas\"}"); + json.SetNamedValue(L"info", CanvasLayoutInfo::ToJson(std::get(expected.data.info))); + + auto actual = CustomZoneSetJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.uuid.c_str(), actual->uuid.c_str()); + Assert::AreEqual(expected.data.name.c_str(), actual->data.name.c_str()); + Assert::AreEqual((int)expected.data.type, (int)actual->data.type); + + auto expectedGrid = std::get(expected.data.info); + auto actualGrid = std::get(actual->data.info); + Assert::AreEqual(expectedGrid.referenceWidth, actualGrid.referenceWidth); + Assert::AreEqual(expectedGrid.referenceHeight, actualGrid.referenceHeight); + } + + TEST_METHOD(FromJsonMissingKeys) + { + CustomZoneSetJSON zoneSet{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 2, 1 } } }; + const auto json = CustomZoneSetJSON::ToJson(zoneSet); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = CustomZoneSetJSON::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(ZoneSetDataUnitTest){ + TEST_METHOD(ToJsonGeneral) + { + json::JsonObject expected = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"type\": \"rows\"}"); + ZoneSetData data{ L"uuid", ZoneSetLayoutType::Rows }; + const auto actual = ZoneSetData::ToJson(data); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(FromJsonGeneral) + { + ZoneSetData expected{ L"uuid", ZoneSetLayoutType::Columns }; + + json::JsonObject json = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"type\": \"columns\"}"); + auto actual = ZoneSetData::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.uuid.c_str(), actual->uuid.c_str()); + Assert::AreEqual((int)expected.type, (int)actual->type); + } + + TEST_METHOD(FromJsonTypeInvalid) + { + ZoneSetData expected{ L"uuid", ZoneSetLayoutType::Blank }; + + json::JsonObject json = json::JsonObject::Parse(L"{\"uuid\": \"uuid\", \"type\": \"invalid_type\"}"); + auto actual = ZoneSetData::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.uuid.c_str(), actual->uuid.c_str()); + Assert::AreEqual((int)expected.type, (int)actual->type); + } + + TEST_METHOD(FromJsonMissingKeys) + { + ZoneSetData data{ L"uuid", ZoneSetLayoutType::Columns }; + const auto json = ZoneSetData::ToJson(data); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = ZoneSetData::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(AppZoneHistoryUnitTests) + { + TEST_METHOD(ToJson) + { + AppZoneHistoryJSON appZoneHistory{ L"appPath", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } }; + json::JsonObject expected = json::JsonObject::Parse(L"{\"app-path\": \"appPath\", \"device-id\": \"device-id\", \"zoneset-uuid\": \"zoneset-uuid\", \"zone-index\": 54321}"); + + auto actual = AppZoneHistoryJSON::ToJson(appZoneHistory); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(FromJson) + { + AppZoneHistoryJSON expected{ L"appPath", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } }; + json::JsonObject json = json::JsonObject::Parse(L"{\"app-path\": \"appPath\", \"device-id\": \"device-id\", \"zoneset-uuid\": \"zoneset-uuid\", \"zone-index\": 54321}"); + + auto actual = AppZoneHistoryJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.appPath.c_str(), actual->appPath.c_str()); + Assert::AreEqual(expected.data.zoneIndex, actual->data.zoneIndex); + Assert::AreEqual(expected.data.deviceId.c_str(), actual->data.deviceId.c_str()); + Assert::AreEqual(expected.data.zoneSetUuid.c_str(), actual->data.zoneSetUuid.c_str()); + } + + TEST_METHOD(FromJsonMissingKeys) + { + AppZoneHistoryJSON appZoneHistory{ L"appPath", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } }; + const auto json = AppZoneHistoryJSON::ToJson(appZoneHistory); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = AppZoneHistoryJSON::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(DeviceInfoUnitTests) + { + private: + DeviceInfoJSON m_defaultDeviceInfo = DeviceInfoJSON{ L"default_device_id", DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + json::JsonObject m_defaultJson = json::JsonObject::Parse(L"{\"device-id\": \"default_device_id\", \"active-zoneset\": {\"type\": \"custom\", \"uuid\": \"uuid\"}, \"editor-show-spacing\": true, \"editor-spacing\": 16, \"editor-zone-count\": 3}"); + + public: + TEST_METHOD(ToJson) + { + DeviceInfoJSON deviceInfo = m_defaultDeviceInfo; + json::JsonObject expected = m_defaultJson; + + auto actual = DeviceInfoJSON::ToJson(deviceInfo); + compareJsonObjects(expected, actual); + } + + TEST_METHOD(FromJson) + { + DeviceInfoJSON expected = m_defaultDeviceInfo; + expected.data.spacing = true; + + json::JsonObject json = DeviceInfoJSON::ToJson(expected); + auto actual = DeviceInfoJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.deviceId.c_str(), actual->deviceId.c_str(), L"device id"); + Assert::AreEqual(expected.data.zoneCount, actual->data.zoneCount, L"zone count"); + Assert::AreEqual((int)expected.data.activeZoneSet.type, (int)actual->data.activeZoneSet.type, L"zone set type"); + Assert::AreEqual(expected.data.activeZoneSet.uuid.c_str(), actual->data.activeZoneSet.uuid.c_str(), L"zone set uuid"); + } + + TEST_METHOD(FromJsonSpacingTrue) + { + DeviceInfoJSON expected = m_defaultDeviceInfo; + expected.data.spacing = true; + + json::JsonObject json = DeviceInfoJSON::ToJson(expected); + auto actual = DeviceInfoJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.data.spacing, actual->data.spacing); + } + + TEST_METHOD(FromJsonSpacingFalse) + { + DeviceInfoJSON expected = m_defaultDeviceInfo; + expected.data.activeZoneSet.type = ZoneSetLayoutType::Custom; + + json::JsonObject json = DeviceInfoJSON::ToJson(expected); + auto actual = DeviceInfoJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual(expected.data.spacing, actual->data.spacing); + } + + TEST_METHOD(FromJsonZoneGeneral) + { + DeviceInfoJSON expected = m_defaultDeviceInfo; + expected.data.activeZoneSet.type = ZoneSetLayoutType::PriorityGrid; + + json::JsonObject json = DeviceInfoJSON::ToJson(expected); + auto actual = DeviceInfoJSON::FromJson(json); + Assert::IsTrue(actual.has_value()); + + Assert::AreEqual((int)expected.data.activeZoneSet.type, (int)actual->data.activeZoneSet.type, L"zone set type"); + } + + TEST_METHOD(FromJsonMissingKeys) + { + DeviceInfoJSON deviceInfo{ L"default_device_id", DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + + auto iter = json.First(); + while (iter.HasCurrent()) + { + json::JsonObject modifiedJson = json::JsonObject::Parse(json.Stringify()); + modifiedJson.Remove(iter.Current().Key()); + + auto actual = DeviceInfoJSON::FromJson(modifiedJson); + Assert::IsFalse(actual.has_value()); + + iter.MoveNext(); + } + } + }; + + TEST_CLASS(FancyZonesDataUnitTests) + { + private: + const std::wstring m_defaultCustomDeviceStr = L"{\"device-id\": \"default_device_id\", \"active-zoneset\": {\"type\": \"custom\", \"uuid\": \"uuid\"}, \"editor-show-spacing\": true, \"editor-spacing\": 16, \"editor-zone-count\": 3}"; + const json::JsonValue m_defaultCustomDeviceValue = json::JsonValue::Parse(m_defaultCustomDeviceStr); + const json::JsonObject m_defaultCustomDeviceObj = json::JsonObject::Parse(m_defaultCustomDeviceStr); + + HINSTANCE m_hInst{}; + FancyZonesData& m_fzData = FancyZonesDataInstance(); + + void compareJsonArrays(const json::JsonArray& expected, const json::JsonArray& actual) + { + Assert::AreEqual(expected.Size(), actual.Size()); + for (uint32_t i = 0; i < expected.Size(); i++) + { + compareJsonObjects(expected.GetObjectAt(i), actual.GetObjectAt(i)); + } + } + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + m_fzData = FancyZonesData(); + } + + public: + TEST_METHOD(FancyZonesDataPath) + { + FancyZonesData data; + Assert::IsFalse(data.GetPersistFancyZonesJSONPath().empty()); + } + + TEST_METHOD(FancyZonesDataJsonEmpty) + { + FancyZonesData data; + const auto jsonPath = data.GetPersistFancyZonesJSONPath(); + auto savedJson = json::from_file(jsonPath); + + if (std::filesystem::exists(jsonPath)) + { + std::filesystem::remove(jsonPath); + } + + json::JsonObject expected; + auto actual = data.GetPersistFancyZonesJSON(); + + Assert::AreEqual(expected.Stringify().c_str(), actual.Stringify().c_str()); + + if (savedJson) + { + json::to_file(jsonPath, *savedJson); + } + } + + TEST_METHOD(FancyZonesDataJson) + { + FancyZonesData data; + const auto jsonPath = data.GetPersistFancyZonesJSONPath(); + auto savedJson = json::from_file(jsonPath); + + if (std::filesystem::exists(jsonPath)) + { + std::filesystem::remove(jsonPath); + } + + json::JsonObject expected = json::JsonObject::Parse(L"{\"fancy-zones\":{\"custom-zonesets \":[{\"uuid\":\"uuid1\",\"name\":\"Custom1\",\"type\":\"custom\" }] } }"); + json::to_file(jsonPath, expected); + + auto actual = data.GetPersistFancyZonesJSON(); + Assert::AreEqual(expected.Stringify().c_str(), actual.Stringify().c_str()); + + if (savedJson) + { + json::to_file(jsonPath, *savedJson); + } + else + { + std::filesystem::remove(jsonPath); + } + } + + TEST_METHOD(FancyZonesDataDeviceInfoMap) + { + FancyZonesData data; + const auto actual = data.GetDeviceInfoMap(); + Assert::IsTrue(actual.empty()); + } + + TEST_METHOD(FancyZonesDataDeviceInfoMapParseEmpty) + { + FancyZonesData data; + + json::JsonObject json; + data.ParseDeviceInfos(json); + + const auto actual = data.GetDeviceInfoMap(); + Assert::IsTrue(actual.empty()); + } + + TEST_METHOD(FancyZonesDataDeviceInfoMapParseValidEmpty) + { + FancyZonesData data; + + json::JsonObject expected; + json::JsonArray zoneSets; + expected.SetNamedValue(L"devices", zoneSets); + + data.ParseDeviceInfos(expected); + + const auto actual = data.GetDeviceInfoMap(); + Assert::IsTrue(actual.empty()); + } + + TEST_METHOD(FancyZonesDataDeviceInfoMapParseInvalid) + { + json::JsonArray devices; + devices.Append(json::JsonObject::Parse(m_defaultCustomDeviceStr)); + devices.Append(json::JsonObject::Parse(L"{\"device-id\": \"device_id\"}")); + + json::JsonObject expected; + expected.SetNamedValue(L"devices", devices); + + FancyZonesData data; + auto actual = data.ParseDeviceInfos(expected); + + Assert::IsFalse(actual); + } + + TEST_METHOD(FancyZonesDataDeviceInfoMapParseSingle) + { + json::JsonArray devices; + devices.Append(m_defaultCustomDeviceValue); + json::JsonObject expected; + expected.SetNamedValue(L"devices", devices); + + FancyZonesData data; + data.ParseDeviceInfos(expected); + + const auto actualMap = data.GetDeviceInfoMap(); + Assert::AreEqual((size_t)1, actualMap.size()); + } + + TEST_METHOD(FancyZonesDataDeviceInfoMapParseMany) + { + json::JsonArray devices; + for (int i = 0; i < 10; i++) + { + json::JsonObject obj = json::JsonObject::Parse(m_defaultCustomDeviceStr); + obj.SetNamedValue(L"device-id", json::JsonValue::CreateStringValue(std::to_wstring(i))); + + Logger::WriteMessage(obj.Stringify().c_str()); + Logger::WriteMessage("\n"); + + devices.Append(obj); + } + + json::JsonObject expected; + expected.SetNamedValue(L"devices", devices); + Logger::WriteMessage(expected.Stringify().c_str()); + Logger::WriteMessage("\n"); + + FancyZonesData data; + data.ParseDeviceInfos(expected); + + const auto actualMap = data.GetDeviceInfoMap(); + Assert::AreEqual((size_t)10, actualMap.size()); + } + + TEST_METHOD(FancyZonesDataSerialize) + { + json::JsonArray expectedDevices; + expectedDevices.Append(m_defaultCustomDeviceObj); + json::JsonObject expected; + expected.SetNamedValue(L"devices", expectedDevices); + + FancyZonesData data; + data.ParseDeviceInfos(expected); + + auto actual = data.SerializeDeviceInfos(); + compareJsonArrays(expectedDevices, actual); + } + + TEST_METHOD(DeviceInfoSaveTemp) + { + FancyZonesData data; + DeviceInfoJSON deviceInfo{ L"default_device_id", DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + + const std::wstring path = data.GetPersistFancyZonesJSONPath() + L".test_tmp"; + data.SerializeDeviceInfoToTmpFile(deviceInfo, path); + + bool actualFileExists = std::filesystem::exists(path); + Assert::IsTrue(actualFileExists); + + auto expectedData = DeviceInfoJSON::ToJson(deviceInfo); + auto actualSavedData = json::from_file(path); + std::filesystem::remove(path); //clean up before compare asserts + + Assert::IsTrue(actualSavedData.has_value()); + compareJsonObjects(expectedData, *actualSavedData); + } + + TEST_METHOD(DeviceInfoReadTemp) + { + FancyZonesData data; + const std::wstring zoneUuid = L"default_device_id"; + DeviceInfoJSON expected{ zoneUuid, DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + const std::wstring path = data.GetPersistFancyZonesJSONPath() + L".test_tmp"; + data.SerializeDeviceInfoToTmpFile(expected, path); + + data.ParseDeviceInfoFromTmpFile(path); + + bool actualFileExists = std::filesystem::exists(path); + if (actualFileExists) + { + std::filesystem::remove(path); //clean up before compare asserts + } + Assert::IsFalse(actualFileExists); + + auto devices = data.GetDeviceInfoMap(); + Assert::AreEqual((size_t)1, devices.size()); + + auto actual = devices.find(zoneUuid)->second; + Assert::AreEqual(expected.data.showSpacing, actual.showSpacing); + Assert::AreEqual(expected.data.spacing, actual.spacing); + Assert::AreEqual(expected.data.zoneCount, actual.zoneCount); + Assert::AreEqual((int)expected.data.activeZoneSet.type, (int)actual.activeZoneSet.type); + Assert::AreEqual(expected.data.activeZoneSet.uuid.c_str(), actual.activeZoneSet.uuid.c_str()); + } + + TEST_METHOD(DeviceInfoReadTempUnexsisted) + { + FancyZonesData data; + const std::wstring path = data.GetPersistFancyZonesJSONPath() + L".test_tmp"; + data.ParseDeviceInfoFromTmpFile(path); + + auto devices = data.GetDeviceInfoMap(); + Assert::AreEqual((size_t)0, devices.size()); + } + + TEST_METHOD(AppZoneHistoryParseSingle) + { + const std::wstring expectedAppPath = L"appPath"; + const std::wstring expectedDeviceId = L"device-id"; + const std::wstring expectedZoneSetId = L"zone-set-id"; + const int expectedIndex = 54321; + + json::JsonObject json; + AppZoneHistoryJSON expected{ expectedAppPath, AppZoneHistoryData{ .zoneSetUuid = expectedZoneSetId, .deviceId = expectedDeviceId, .zoneIndex = expectedIndex } }; + json::JsonArray zoneHistoryArray; + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(expected)); + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(zoneHistoryArray.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + const auto actualProcessHistoryMap = data.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)zoneHistoryArray.Size(), actualProcessHistoryMap.size()); + + const auto actualProcessHistory = actualProcessHistoryMap.begin(); + Assert::AreEqual(expectedAppPath.c_str(), actualProcessHistory->first.c_str()); + + const auto actualAppZoneHistory = actualProcessHistory->second; + Assert::AreEqual(expectedZoneSetId.c_str(), actualAppZoneHistory.zoneSetUuid.c_str()); + Assert::AreEqual(expectedDeviceId.c_str(), actualAppZoneHistory.deviceId.c_str()); + Assert::AreEqual(expectedIndex, actualAppZoneHistory.zoneIndex); + } + + TEST_METHOD(AppZoneHistoryParseManyApps) + { + json::JsonObject json; + json::JsonArray zoneHistoryArray; + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-1", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-1", .deviceId = L"device-id-1", .zoneIndex = 1 } })); + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-2", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-2", .deviceId = L"device-id-2", .zoneIndex = 2 } })); + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-3", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-3", .deviceId = L"device-id-3", .zoneIndex = 3 } })); + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-4", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-4", .deviceId = L"device-id-4", .zoneIndex = 4 } })); + + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(zoneHistoryArray.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + auto actualMap = data.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)zoneHistoryArray.Size(), actualMap.size()); + + const auto actualProcessHistoryMap = data.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)zoneHistoryArray.Size(), actualProcessHistoryMap.size()); + + auto iter = zoneHistoryArray.First(); + while (iter.HasCurrent()) + { + auto expected = AppZoneHistoryJSON::FromJson(json::JsonObject::Parse(iter.Current().Stringify())); + + const auto actual = actualProcessHistoryMap.at(expected->appPath); + Assert::AreEqual(expected->data.deviceId.c_str(), actual.deviceId.c_str()); + Assert::AreEqual(expected->data.zoneSetUuid.c_str(), actual.zoneSetUuid.c_str()); + Assert::AreEqual(expected->data.zoneIndex, actual.zoneIndex); + + iter.MoveNext(); + } + } + + TEST_METHOD(AppZoneHistoryParseManyZonesForSingleApp) + { + json::JsonObject json; + json::JsonArray zoneHistoryArray; + + const auto appPath = L"app-path"; + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-1", .deviceId = L"device-id-1", .zoneIndex = 1 } })); + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-2", .deviceId = L"device-id-2", .zoneIndex = 2 } })); + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-3", .deviceId = L"device-id-3", .zoneIndex = 3 } })); + const auto expected = AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid-4", .deviceId = L"device-id-4", .zoneIndex = 4 }; + zoneHistoryArray.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, expected })); + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(zoneHistoryArray.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + const auto actualProcessHistoryMap = data.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)1, actualProcessHistoryMap.size()); + + const auto actual = actualProcessHistoryMap.at(appPath); + Assert::AreEqual(expected.deviceId.c_str(), actual.deviceId.c_str()); + Assert::AreEqual(expected.zoneSetUuid.c_str(), actual.zoneSetUuid.c_str()); + Assert::AreEqual(expected.zoneIndex, actual.zoneIndex); + } + + TEST_METHOD(AppZoneHistoryParseEmpty) + { + FancyZonesData data; + data.ParseAppZoneHistory(json::JsonObject()); + + auto actual = data.GetAppZoneHistoryMap(); + Assert::IsTrue(actual.empty()); + } + + TEST_METHOD(AppZoneHistoryParseInvalid) + { + const std::wstring appPath = L"appPath"; + json::JsonObject json; + AppZoneHistoryJSON expected{ appPath, AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } }; + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(AppZoneHistoryJSON::ToJson(expected).Stringify())); + + FancyZonesData data; + bool actual = data.ParseAppZoneHistory(json); + + Assert::IsFalse(actual); + } + + TEST_METHOD(AppZoneHistorySerializeSingle) + { + const std::wstring appPath = L"appPath"; + json::JsonArray expected; + expected.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ appPath, AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } })); + json::JsonObject json; + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + auto actual = data.SerializeAppZoneHistory(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(AppZoneHistorySerializeMany) + { + json::JsonObject json; + json::JsonArray expected; + expected.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-1", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } })); + expected.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-2", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } })); + expected.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-3", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } })); + expected.Append(AppZoneHistoryJSON::ToJson(AppZoneHistoryJSON{ L"app-path-4", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } })); + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + auto actual = data.SerializeAppZoneHistory(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(AppZoneHistorySerializeEmpty) + { + json::JsonArray expected; + json::JsonObject json; + json.SetNamedValue(L"app-zone-history", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseAppZoneHistory(json); + + auto actual = data.SerializeAppZoneHistory(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(CustomZoneSetsParseSingle) + { + const std::wstring zoneUuid = L"uuid"; + GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + + json::JsonObject json; + CustomZoneSetJSON expected{ zoneUuid, CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + json::JsonArray array; + array.Append(CustomZoneSetJSON::ToJson(expected)); + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(array.Stringify())); + + FancyZonesData data; + data.ParseCustomZoneSets(json); + + auto actualMap = data.GetCustomZoneSetsMap(); + Assert::AreEqual((size_t)array.Size(), actualMap.size()); + + auto actual = actualMap.find(zoneUuid)->second; + Assert::AreEqual(expected.data.name.c_str(), actual.name.c_str()); + Assert::AreEqual((int)expected.data.type, (int)actual.type); + + auto expectedGrid = std::get(expected.data.info); + auto actualGrid = std::get(actual.info); + Assert::AreEqual(expectedGrid.rows(), actualGrid.rows()); + Assert::AreEqual(expectedGrid.columns(), actualGrid.columns()); + } + + TEST_METHOD(CustomZoneSetsParseMany) + { + json::JsonObject json; + json::JsonArray array; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + array.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-1", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } })); + array.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-2", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 1, 2 } } })); + array.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-3", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } })); + array.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-4", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 1, 2 } } })); + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(array.Stringify())); + + FancyZonesData data; + data.ParseCustomZoneSets(json); + + auto actualMap = data.GetCustomZoneSetsMap(); + Assert::AreEqual((size_t)array.Size(), actualMap.size()); + + auto iter = array.First(); + while (iter.HasCurrent()) + { + auto expected = CustomZoneSetJSON::FromJson(json::JsonObject::Parse(iter.Current().Stringify())); + auto actual = actualMap.find(expected->uuid)->second; + Assert::AreEqual(expected->data.name.c_str(), actual.name.c_str(), L"name"); + Assert::AreEqual((int)expected->data.type, (int)actual.type, L"type"); + + if (expected->data.type == CustomLayoutType::Grid) + { + auto expectedInfo = std::get(expected->data.info); + auto actualInfo = std::get(actual.info); + Assert::AreEqual(expectedInfo.rows(), actualInfo.rows(), L"grid rows"); + Assert::AreEqual(expectedInfo.columns(), actualInfo.columns(), L"grid columns"); + } + else + { + auto expectedInfo = std::get(expected->data.info); + auto actualInfo = std::get(actual.info); + Assert::AreEqual(expectedInfo.referenceWidth, actualInfo.referenceWidth, L"canvas width"); + Assert::AreEqual(expectedInfo.referenceHeight, actualInfo.referenceHeight, L"canvas height"); + } + + iter.MoveNext(); + } + } + + TEST_METHOD(CustomZoneSetsParseEmpty) + { + FancyZonesData data; + data.ParseCustomZoneSets(json::JsonObject()); + + auto actual = data.GetCustomZoneSetsMap(); + Assert::IsTrue(actual.empty()); + } + + TEST_METHOD(CustomZoneSetsParseInvalid) + { + json::JsonObject json; + CustomZoneSetJSON expected{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Grid, GridLayoutInfo(GridLayoutInfo::Minimal{ 1, 2 }) } }; + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(CustomZoneSetJSON::ToJson(expected).Stringify())); + + FancyZonesData data; + auto actual = data.ParseCustomZoneSets(json); + + Assert::IsFalse(actual); + } + + TEST_METHOD(CustomZoneSetsSerializeSingle) + { + json::JsonArray expected; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + expected.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"uuid", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } })); + json::JsonObject json; + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseCustomZoneSets(json); + + auto actual = data.SerializeCustomZoneSets(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(CustomZoneSetsSerializeMany) + { + json::JsonObject json; + json::JsonArray expected; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + + expected.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-1", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } })); + expected.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-2", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 1, 2 } } })); + expected.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-3", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } })); + expected.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ L"zone-uuid-4", CustomZoneSetData{ L"name", CustomLayoutType::Canvas, CanvasLayoutInfo{ 1, 2 } } })); + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseCustomZoneSets(json); + + auto actual = data.SerializeCustomZoneSets(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(CustomZoneSetsSerializeEmpty) + { + json::JsonArray expected; + json::JsonObject json; + json.SetNamedValue(L"custom-zone-sets", json::JsonValue::Parse(expected.Stringify())); + + FancyZonesData data; + data.ParseCustomZoneSets(json); + + auto actual = data.SerializeCustomZoneSets(); + compareJsonArrays(expected, actual); + } + + TEST_METHOD(CustomZoneSetsReadTemp) + { + //prepare device data + const std::wstring deviceId = L"default_device_id"; + + { + DeviceInfoJSON deviceInfo{ deviceId, DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + const std::wstring deviceInfoPath = m_fzData.GetPersistFancyZonesJSONPath() + L".device_info_tmp"; + m_fzData.SerializeDeviceInfoToTmpFile(deviceInfo, deviceInfoPath); + + m_fzData.ParseDeviceInfoFromTmpFile(deviceInfoPath); + std::filesystem::remove(deviceInfoPath); + } + + const std::wstring uuid = L"uuid"; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + CustomZoneSetJSON expected{ uuid, CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + + FancyZonesData data; + const std::wstring path = data.GetPersistFancyZonesJSONPath() + L".test_tmp"; + json::to_file(path, CustomZoneSetJSON::ToJson(expected)); + m_fzData.ParseCustomZoneSetFromTmpFile(path); + + bool actualFileExists = std::filesystem::exists(path); + if (actualFileExists) + { + std::filesystem::remove(path); //clean up before compare asserts + } + Assert::IsFalse(actualFileExists); + + auto devices = m_fzData.GetCustomZoneSetsMap(); + Assert::AreEqual((size_t)1, devices.size()); + + auto actual = devices.find(uuid)->second; + Assert::AreEqual((int)expected.data.type, (int)actual.type); + Assert::AreEqual(expected.data.name.c_str(), actual.name.c_str()); + auto expectedGrid = std::get(expected.data.info); + auto actualGrid = std::get(actual.info); + Assert::AreEqual(expectedGrid.rows(), actualGrid.rows()); + Assert::AreEqual(expectedGrid.columns(), actualGrid.columns()); + } + + TEST_METHOD(CustomZoneSetsReadTempUnexsisted) + { + const std::wstring path = m_fzData.GetPersistFancyZonesJSONPath() + L".test_tmp"; + const std::wstring deviceId = L"default_device_id"; + + m_fzData.ParseCustomZoneSetFromTmpFile(path); + auto devices = m_fzData.GetDeviceInfoMap(); + Assert::AreEqual((size_t)0, devices.size()); + } + + TEST_METHOD(SetActiveZoneSet) + { + FancyZonesData data; + const std::wstring uniqueId = L"default_device_id"; + + json::JsonArray devices; + devices.Append(m_defaultCustomDeviceValue); + json::JsonObject json; + json.SetNamedValue(L"devices", devices); + data.ParseDeviceInfos(json); + + JSONHelpers::ZoneSetData expectedZoneSetData{ + .uuid = L"uuid", + .type = ZoneSetLayoutType::Focus + }; + + data.SetActiveZoneSet(uniqueId, expectedZoneSetData); + + auto actual = data.GetDeviceInfoMap().find(uniqueId)->second; + Assert::AreEqual(expectedZoneSetData.uuid.c_str(), actual.activeZoneSet.uuid.c_str()); + Assert::IsTrue(expectedZoneSetData.type == actual.activeZoneSet.type); + } + + TEST_METHOD(SetActiveZoneSetUuidEmpty) + { + FancyZonesData data; + const std::wstring expected = L"uuid"; + const std::wstring uniqueId = L"default_device_id"; + + json::JsonArray devices; + devices.Append(m_defaultCustomDeviceValue); + json::JsonObject json; + json.SetNamedValue(L"devices", devices); + data.ParseDeviceInfos(json); + + JSONHelpers::ZoneSetData expectedZoneSetData{ + .uuid = L"", + .type = ZoneSetLayoutType::Focus + }; + + data.SetActiveZoneSet(uniqueId, expectedZoneSetData); + + auto actual = data.GetDeviceInfoMap().find(uniqueId)->second; + Assert::AreEqual(expectedZoneSetData.uuid.c_str(), actual.activeZoneSet.uuid.c_str()); + Assert::IsTrue(expectedZoneSetData.type == actual.activeZoneSet.type); + } + + TEST_METHOD(SetActiveZoneSetUniqueIdInvalid) + { + FancyZonesData data; + const std::wstring expected = L"uuid"; + const std::wstring uniqueId = L"id_not_contained_by_device_info_map"; + + json::JsonArray devices; + devices.Append(m_defaultCustomDeviceValue); + json::JsonObject json; + json.SetNamedValue(L"devices", devices); + bool parseRes = data.ParseDeviceInfos(json); + Assert::IsTrue(parseRes); + + JSONHelpers::ZoneSetData zoneSetData{ + .uuid = L"new_uuid", + .type = ZoneSetLayoutType::Focus + }; + + data.SetActiveZoneSet(uniqueId, zoneSetData); + + const auto& deviceInfoMap = data.GetDeviceInfoMap(); + auto actual = deviceInfoMap.find(L"default_device_id")->second; + Assert::AreEqual(expected.c_str(), actual.activeZoneSet.uuid.c_str()); + Assert::IsTrue(deviceInfoMap.end() == deviceInfoMap.find(uniqueId), L"new device info should not be added"); + } + + TEST_METHOD(LoadFancyZonesDataFromJson) + { + FancyZonesData data; + const auto jsonPath = data.GetPersistFancyZonesJSONPath(); + auto savedJson = json::from_file(jsonPath); + + if (std::filesystem::exists(jsonPath)) + { + std::filesystem::remove(jsonPath); + } + + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + CustomZoneSetJSON zoneSets{ L"zone-set-uuid", CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + AppZoneHistoryJSON appZoneHistory{ L"app-path", AppZoneHistoryData{ .zoneSetUuid = L"zoneset-uuid", .deviceId = L"device-id", .zoneIndex = 54321 } }; + DeviceInfoJSON deviceInfo{ L"uuid", DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + json::JsonArray zoneSetsArray, appZonesArray, deviceInfoArray; + zoneSetsArray.Append(CustomZoneSetJSON::ToJson(zoneSets)); + appZonesArray.Append(AppZoneHistoryJSON::ToJson(appZoneHistory)); + deviceInfoArray.Append(DeviceInfoJSON::ToJson(deviceInfo)); + json::JsonObject fancyZones; + fancyZones.SetNamedValue(L"custom-zone-sets", zoneSetsArray); + fancyZones.SetNamedValue(L"app-zone-history", appZonesArray); + fancyZones.SetNamedValue(L"devices", deviceInfoArray); + + json::to_file(jsonPath, fancyZones); + + data.LoadFancyZonesData(); + if (savedJson) + { + json::to_file(jsonPath, *savedJson); + } + else + { + std::filesystem::remove(jsonPath); + } + + Assert::IsFalse(data.GetCustomZoneSetsMap().empty()); + Assert::IsFalse(data.GetCustomZoneSetsMap().empty()); + Assert::IsFalse(data.GetCustomZoneSetsMap().empty()); + } + + TEST_METHOD(LoadFancyZonesDataFromRegistry) + { + FancyZonesData data; + const auto jsonPath = data.GetPersistFancyZonesJSONPath(); + auto savedJson = json::from_file(jsonPath); + + if (std::filesystem::exists(jsonPath)) + { + std::filesystem::remove(jsonPath); + } + + data.LoadFancyZonesData(); + bool actual = std::filesystem::exists(jsonPath); + if (savedJson) + { + json::to_file(jsonPath, *savedJson); + } + else + { + std::filesystem::remove(jsonPath); + } + + Assert::IsTrue(actual); + } + + TEST_METHOD(SaveFancyZonesData) + { + FancyZonesData data; + const auto jsonPath = data.GetPersistFancyZonesJSONPath(); + auto savedJson = json::from_file(jsonPath); + + if (std::filesystem::exists(jsonPath)) + { + std::filesystem::remove(jsonPath); + } + + data.SaveFancyZonesData(); + bool actual = std::filesystem::exists(jsonPath); + + if (savedJson) + { + json::to_file(jsonPath, *savedJson); + } + else + { + std::filesystem::remove(jsonPath); + } + + Assert::IsTrue(actual); + } + + TEST_METHOD(AppLastZoneIndex) + { + const std::wstring deviceId = L"device-id"; + const std::wstring zoneSetId = L"zoneset-uuid"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + + const int expectedZoneIndex = 10; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneIndexZero) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const int expectedZoneIndex = 0; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneIndexNegative) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const int expectedZoneIndex = -1; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneIndexOverflow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const long expectedZoneIndex = LONG_MAX; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(static_cast(expectedZoneIndex), data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneIndexOverride) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const int expectedZoneIndex = 3; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, 1)); + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, 2)); + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneInvalidWindow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::Window(); + FancyZonesData data; + + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + + const int expectedZoneIndex = 1; + Assert::IsFalse(data.SetAppLastZone(window, deviceId, zoneSetId, expectedZoneIndex)); + } + + TEST_METHOD(AppLastZoneNullWindow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const auto window = nullptr; + FancyZonesData data; + + const int expectedZoneIndex = 1; + Assert::IsFalse(data.SetAppLastZone(window, L"device-id", zoneSetId, expectedZoneIndex)); + } + + TEST_METHOD(AppLastdeviceIdTest) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId1 = L"device-id-1"; + const std::wstring deviceId2 = L"device-id-2"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const int expectedZoneIndex = 10; + Assert::IsTrue(data.SetAppLastZone(window, deviceId1, zoneSetId, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId1, zoneSetId)); + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId2, zoneSetId)); + } + + TEST_METHOD(AppLastZoneSetIdTest) + { + const std::wstring zoneSetId1 = L"zoneset-uuid-1"; + const std::wstring zoneSetId2 = L"zoneset-uuid-2"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + const int expectedZoneIndex = 10; + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId1, expectedZoneIndex)); + Assert::AreEqual(expectedZoneIndex, data.GetAppLastZoneIndex(window, deviceId, zoneSetId1)); + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId, zoneSetId2)); + } + + TEST_METHOD(AppLastZoneRemoveWindow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetId, 1)); + Assert::IsTrue(data.RemoveAppLastZone(window, deviceId, zoneSetId)); + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneRemoveUnknownWindow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::IsFalse(data.RemoveAppLastZone(window, deviceId, zoneSetId)); + Assert::AreEqual(-1, data.GetAppLastZoneIndex(window, deviceId, zoneSetId)); + } + + TEST_METHOD(AppLastZoneRemoveUnknownZoneSetId) + { + const std::wstring zoneSetIdToInsert = L"zoneset-uuid-to-insert"; + const std::wstring zoneSetIdToRemove = L"zoneset-uuid-to-remove"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::IsTrue(data.SetAppLastZone(window, deviceId, zoneSetIdToInsert, 1)); + Assert::IsFalse(data.RemoveAppLastZone(window, deviceId, zoneSetIdToRemove)); + Assert::AreEqual(1, data.GetAppLastZoneIndex(window, deviceId, zoneSetIdToInsert)); + } + + TEST_METHOD(AppLastZoneRemoveUnknownWindowId) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceIdToInsert = L"device-id-insert"; + const std::wstring deviceIdToRemove = L"device-id-remove"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::IsTrue(data.SetAppLastZone(window, deviceIdToInsert, zoneSetId, 1)); + Assert::IsFalse(data.RemoveAppLastZone(window, deviceIdToRemove, zoneSetId)); + Assert::AreEqual(1, data.GetAppLastZoneIndex(window, deviceIdToInsert, zoneSetId)); + } + + TEST_METHOD(AppLastZoneRemoveNullWindow) + { + const std::wstring zoneSetId = L"zoneset-uuid"; + const std::wstring deviceId = L"device-id"; + const auto window = Mocks::WindowCreate(m_hInst); + FancyZonesData data; + + Assert::IsFalse(data.RemoveAppLastZone(nullptr, deviceId, zoneSetId)); + } + }; +} \ No newline at end of file diff --git a/src/modules/fancyzones/tests/UnitTests/RegistryHelpers.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/RegistryHelpers.Spec.cpp deleted file mode 100644 index ecfbe1d3f0..0000000000 --- a/src/modules/fancyzones/tests/UnitTests/RegistryHelpers.Spec.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "pch.h" -#include "lib\RegistryHelpers.h" - -using namespace Microsoft::VisualStudio::CppUnitTestFramework; - -namespace FancyZonesUnitTests -{ - TEST_CLASS(RegistryHelpersUnitTests){ - public: - TEST_METHOD(GetDefaultKey){ - // Test the path to the key is the same string. - wchar_t key[256]; - Assert::AreEqual(0, wcscmp(RegistryHelpers::GetKey(nullptr, key, ARRAYSIZE(key)), L"Software\\SuperFancyZones")); -} - -TEST_METHOD(GetKeyWithMonitor) -{ - // Test the path to the key is the same string. - wchar_t key[256]; - Assert::AreEqual(0, wcscmp(RegistryHelpers::GetKey(L"Monitor1", key, ARRAYSIZE(key)), L"Software\\SuperFancyZones\\Monitor1")); -} - -TEST_METHOD(OpenKey) -{ - // The default key should exist. - wil::unique_hkey key{ RegistryHelpers::OpenKey({}) }; - Assert::IsNotNull(key.get()); - - // The Monitor1 key shouldn't exist. - wil::unique_hkey key2{ RegistryHelpers::OpenKey(L"Monitor1") }; - Assert::IsNull(key2.get()); -} -} -; -} diff --git a/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj b/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj index 15ec6d2ee0..c42116b8f0 100644 --- a/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj +++ b/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj @@ -96,12 +96,15 @@ + + + Create Create - + diff --git a/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj.filters b/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj.filters index fe17c4c652..71de598915 100644 --- a/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj.filters +++ b/src/modules/fancyzones/tests/UnitTests/UnitTests.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -24,15 +24,24 @@ Source Files - - Source Files - Source Files Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/src/modules/fancyzones/tests/UnitTests/Util.cpp b/src/modules/fancyzones/tests/UnitTests/Util.cpp new file mode 100644 index 0000000000..b0657288d6 --- /dev/null +++ b/src/modules/fancyzones/tests/UnitTests/Util.cpp @@ -0,0 +1,149 @@ +#include "pch.h" +#include "Util.h" + +static int s_classId = 0; + +namespace Mocks +{ + class HwndCreator + { + public: + HwndCreator(const std::wstring& title = L""); + + ~HwndCreator(); + + HWND operator()(HINSTANCE hInst); + + void setHwnd(HWND val); + void setCondition(bool cond); + + inline HINSTANCE getHInstance() const { return m_hInst; } + inline const std::wstring& getTitle() const { return m_windowTitle; } + inline const std::wstring& getWindowClassName() const { return m_windowClassName; } + + private: + std::wstring m_windowTitle; + std::wstring m_windowClassName; + + std::mutex m_mutex; + std::condition_variable m_conditionVar; + bool m_conditionFlag; + HANDLE m_thread; + + HINSTANCE m_hInst; + HWND m_hWnd; + }; + + HWND WindowCreate(HINSTANCE hInst) + { + return HwndCreator()(hInst); + } +} + +LRESULT CALLBACK DLLWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_DESTROY) + { + PostQuitMessage(0); + return 0; + } + + return DefWindowProc(hwnd, message, wParam, lParam); +} + +BOOL RegisterDLLWindowClass(LPCWSTR szClassName, Mocks::HwndCreator* creator) +{ + if (!creator) + return false; + + WNDCLASSEX wc; + + wc.hInstance = creator->getHInstance(); + wc.lpszClassName = szClassName; + wc.lpfnWndProc = DLLWindowProc; + wc.cbSize = sizeof(WNDCLASSEX); + + wc.style = CS_DBLCLKS; + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszMenuName = NULL; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND; + + auto regRes = RegisterClassEx(&wc); + return regRes; +} + +DWORD WINAPI ThreadProc(LPVOID lpParam) +{ + MSG messages; + Mocks::HwndCreator* creator = reinterpret_cast(lpParam); + if (!creator) + return -1; + + if (RegisterDLLWindowClass((LPCWSTR)creator->getWindowClassName().c_str(), creator) != 0) + { + auto hWnd = CreateWindowEx(0, (LPCWSTR)creator->getWindowClassName().c_str(), (LPCWSTR)creator->getTitle().c_str(), WS_EX_APPWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 10, 10, nullptr, nullptr, creator->getHInstance(), NULL); + creator->setHwnd(hWnd); + creator->setCondition(true); + + while (GetMessage(&messages, NULL, 0, 0)) + { + TranslateMessage(&messages); + DispatchMessage(&messages); + } + + creator->setHwnd(hWnd); + } + else + { + creator->setCondition(true); + } + + return 1; +} + +namespace Mocks +{ + HwndCreator::HwndCreator(const std::wstring& title) : + m_windowTitle(title), m_windowClassName(std::to_wstring(++s_classId)), m_conditionFlag(false), m_thread(nullptr), m_hInst(HINSTANCE{}), m_hWnd(nullptr) + { + } + + HwndCreator::~HwndCreator() + { + std::unique_lock lock(m_mutex); + m_conditionVar.wait(lock, [this] { return m_conditionFlag; }); + + if (m_thread) + { + CloseHandle(m_thread); + } + } + + HWND HwndCreator::operator()(HINSTANCE hInst) + { + m_hInst = hInst; + m_conditionFlag = false; + std::unique_lock lock(m_mutex); + + m_thread = CreateThread(0, NULL, ThreadProc, (LPVOID)this, NULL, NULL); + m_conditionVar.wait(lock, [this] { return m_conditionFlag; }); + + return m_hWnd; + } + + void HwndCreator::setHwnd(HWND val) + { + m_hWnd = val; + } + + void HwndCreator::setCondition(bool cond) + { + m_conditionFlag = cond; + m_conditionVar.notify_one(); + } + +} \ No newline at end of file diff --git a/src/modules/fancyzones/tests/UnitTests/Util.h b/src/modules/fancyzones/tests/UnitTests/Util.h index 6d5ca961b8..b66e51381e 100644 --- a/src/modules/fancyzones/tests/UnitTests/Util.h +++ b/src/modules/fancyzones/tests/UnitTests/Util.h @@ -1,5 +1,7 @@ #pragma once +#include "lib/JsonHelpers.h" + namespace CustomAssert { static void AreEqual(const RECT& r1, const RECT& r2) @@ -13,9 +15,9 @@ namespace CustomAssert Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(g1 == g2); } - static void AreEqual(WORD w1, WORD w2) + static void AreEqual(JSONHelpers::ZoneSetLayoutType t1, JSONHelpers::ZoneSetLayoutType t2) { - Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(w1 == w2); + Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(t1 == t2); } } @@ -39,4 +41,5 @@ namespace Mocks return reinterpret_cast(++s_nextInstance); } -} \ No newline at end of file + HWND WindowCreate(HINSTANCE hInst); +} diff --git a/src/modules/fancyzones/tests/UnitTests/Zone.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/Zone.Spec.cpp index 561e9dd67c..ae5be7c313 100644 --- a/src/modules/fancyzones/tests/UnitTests/Zone.Spec.cpp +++ b/src/modules/fancyzones/tests/UnitTests/Zone.Spec.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "lib\Zone.h" +#include "lib\Settings.h" #include "Util.h" @@ -7,48 +8,419 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace FancyZonesUnitTests { - TEST_CLASS(ZoneUnitTests){ - public: - TEST_METHOD(TestCreateZone){ - RECT zoneRect{ 10, 10, 200, 200 }; - winrt::com_ptr zone = MakeZone(zoneRect); - Assert::IsNotNull(&zone); - CustomAssert::AreEqual(zoneRect, zone->GetZoneRect()); + TEST_CLASS(ZoneUnitTests) + { + private: + RECT m_zoneRect{ 10, 10, 200, 200 }; + HINSTANCE m_hInst{}; - constexpr size_t id = 10; - zone->SetId(id); - Assert::AreEqual(zone->Id(), id); -} + HWND addWindow(const winrt::com_ptr& zone, bool stamp) + { + HWND window = Mocks::WindowCreate(m_hInst); + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + zone->AddWindowToZone(window, zoneWindow, stamp); -TEST_METHOD(ContainsWindow) -{ - RECT zoneRect{ 10, 10, 200, 200 }; - winrt::com_ptr zone = MakeZone(zoneRect); - HWND newWindow = Mocks::Window(); - Assert::IsFalse(zone->ContainsWindow(newWindow)); -} + return window; + } -TEST_METHOD(TestAddRemoveWindow) -{ - RECT zoneRect{ 10, 10, 200, 200 }; - winrt::com_ptr zone = MakeZone(zoneRect); - HWND newWindow = Mocks::Window(); + void addMany(const winrt::com_ptr& zone) + { + for (int i = 0; i < 10; i++) + { + addWindow(zone, i % 2 == 0); + } + } - Assert::IsFalse(zone->ContainsWindow(newWindow)); - zone->AddWindowToZone(newWindow, Mocks::Window(), true); - Assert::IsTrue(zone->ContainsWindow(newWindow)); + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + } - zone->RemoveWindowFromZone(newWindow, false); - Assert::IsFalse(zone->ContainsWindow(newWindow)); -} + public: + TEST_METHOD(TestCreateZone) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + Assert::IsNotNull(&zone); + CustomAssert::AreEqual(m_zoneRect, zone->GetZoneRect()); + } -TEST_METHOD(TestRemoveInvalidWindow) -{ - RECT zoneRect{ 10, 10, 200, 200 }; - winrt::com_ptr zone = MakeZone(zoneRect); - HWND newWindow = Mocks::Window(); - zone->RemoveWindowFromZone(newWindow, false); -} -} -; + TEST_METHOD(TestCreateZoneZeroRect) + { + RECT zoneRect{ 0, 0, 0, 0 }; + winrt::com_ptr zone = MakeZone(zoneRect); + Assert::IsNotNull(&zone); + CustomAssert::AreEqual(zoneRect, zone->GetZoneRect()); + } + + TEST_METHOD(GetSetId) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + constexpr size_t id = 10; + zone->SetId(id); + Assert::AreEqual(zone->Id(), id); + } + + TEST_METHOD(IsEmpty) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + Assert::IsTrue(zone->IsEmpty()); + } + + TEST_METHOD(IsNonEmptyStampTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + addWindow(zone, true); + + Assert::IsFalse(zone->IsEmpty()); + } + + TEST_METHOD(IsNonEmptyStampFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + addWindow(zone, false); + + Assert::IsFalse(zone->IsEmpty()); + } + + TEST_METHOD(IsNonEmptyManyWindows) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + for (int i = 0; i < 10; i++) + { + HWND window = Mocks::WindowCreate(m_hInst); + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + Assert::IsFalse(zone->IsEmpty()); + } + + TEST_METHOD(IsNonEmptyManyZoneWindows) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = Mocks::WindowCreate(m_hInst); + for (int i = 0; i < 10; i++) + { + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + Assert::IsFalse(zone->IsEmpty()); + } + + TEST_METHOD(IsNonEmptyMany) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + addMany(zone); + + Assert::IsFalse(zone->IsEmpty()); + } + + TEST_METHOD(ContainsWindowEmpty) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = Mocks::WindowCreate(m_hInst); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(ContainsWindowNot) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + addMany(zone); + + HWND newWindow = Mocks::WindowCreate(m_hInst); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(ContainsWindowStampTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND window = addWindow(zone, true); + + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(ContainsWindowStampFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND window = addWindow(zone, false); + + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(ContainsWindowManyWindows) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + std::vector windowVec{}; + for (int i = 0; i < 10; i++) + { + HWND window = Mocks::WindowCreate(m_hInst); + windowVec.push_back(window); + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + for (auto wnd : windowVec) + { + Assert::IsTrue(zone->ContainsWindow(wnd)); + } + } + + TEST_METHOD(ContainsWindowManyZoneWindows) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = Mocks::WindowCreate(m_hInst); + std::vector windowVec{}; + for (int i = 0; i < 10; i++) + { + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + windowVec.push_back(window); + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + for (auto wnd : windowVec) + { + Assert::IsTrue(zone->ContainsWindow(wnd)); + } + } + + TEST_METHOD(ContainsWindowMany) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + std::vector windowVec{}; + for (int i = 0; i < 10; i++) + { + HWND window = addWindow(zone, i % 2 == 0); + windowVec.push_back(window); + } + + for (auto wnd : windowVec) + { + Assert::IsTrue(zone->ContainsWindow(wnd)); + } + } + + TEST_METHOD(AddWindowNullptr) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = nullptr; + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + zone->AddWindowToZone(window, zoneWindow, true); + + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(AddWindowZoneNullptr) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = Mocks::WindowCreate(m_hInst); + HWND zoneWindow = nullptr; + zone->AddWindowToZone(window, zoneWindow, true); + + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(AddManySame) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + HWND window = Mocks::WindowCreate(m_hInst); + for (int i = 0; i < 10; i++) + { + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(AddManySameNullptr) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = nullptr; + HWND window = nullptr; + for (int i = 0; i < 10; i++) + { + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + Assert::IsTrue(zone->ContainsWindow(window)); + } + + TEST_METHOD(RemoveWindowRestoreSizeTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = Mocks::WindowCreate(m_hInst); + + zone->AddWindowToZone(newWindow, Mocks::WindowCreate(m_hInst), true); + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(newWindow)); + + zone->RemoveWindowFromZone(newWindow, true); + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveWindowRestoreSizeFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = Mocks::WindowCreate(m_hInst); + + zone->AddWindowToZone(newWindow, Mocks::WindowCreate(m_hInst), true); + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(newWindow)); + + zone->RemoveWindowFromZone(newWindow, false); + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveInvalidWindowRestoreSizeTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = Mocks::WindowCreate(m_hInst); + zone->RemoveWindowFromZone(newWindow, true); + + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveInvalidWindowRestoreSizeFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = Mocks::WindowCreate(m_hInst); + zone->RemoveWindowFromZone(newWindow, false); + + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveNullptrWindowRestoreSizeTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = nullptr; + + zone->AddWindowToZone(newWindow, Mocks::WindowCreate(m_hInst), true); + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(newWindow)); + + zone->RemoveWindowFromZone(newWindow, true); + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveNullptrWindowRestoreSizeFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + HWND newWindow = nullptr; + + zone->AddWindowToZone(newWindow, Mocks::WindowCreate(m_hInst), true); + Assert::IsFalse(zone->IsEmpty()); + Assert::IsTrue(zone->ContainsWindow(newWindow)); + + zone->RemoveWindowFromZone(newWindow, false); + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(newWindow)); + } + + TEST_METHOD(RemoveMany) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + std::vector windowVec{}; + for (int i = 0; i < 10; i++) + { + HWND window = addWindow(zone, i % 2 == 0); + windowVec.push_back(window); + } + + for (auto wnd : windowVec) + { + zone->RemoveWindowFromZone(wnd, true); + } + + Assert::IsTrue(zone->IsEmpty()); + } + + TEST_METHOD(RemoveManySame) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + HWND window = Mocks::WindowCreate(m_hInst); + for (int i = 0; i < 10; i++) + { + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + zone->RemoveWindowFromZone(window, true); + + Assert::IsTrue(zone->IsEmpty()); + Assert::IsFalse(zone->ContainsWindow(window)); + } + + TEST_METHOD(RemoveDouble) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND zoneWindow = Mocks::WindowCreate(m_hInst); + HWND window = Mocks::WindowCreate(m_hInst); + for (int i = 0; i < 10; i++) + { + zone->AddWindowToZone(window, zoneWindow, i % 2 == 0); + } + + zone->RemoveWindowFromZone(window, true); + zone->RemoveWindowFromZone(window, true); + + Assert::IsTrue(zone->IsEmpty()); + } + + TEST_METHOD(StampTrue) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + size_t expected = 123456; + zone->SetId(expected); + + HWND window = addWindow(zone, true); + + HANDLE actual = GetProp(window, ZONE_STAMP); + Assert::IsNotNull(actual); + + size_t actualVal = HandleToLong(actual); + Assert::AreEqual(expected, actualVal); + } + + TEST_METHOD(StampTrueNoId) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = addWindow(zone, true); + + HANDLE actual = GetProp(window, ZONE_STAMP); + Assert::IsNull(actual); + } + + TEST_METHOD(StampFalse) + { + winrt::com_ptr zone = MakeZone(m_zoneRect); + + HWND window = addWindow(zone, false); + + HANDLE actual = GetProp(window, ZONE_STAMP); + Assert::IsNull(actual); + } + }; } diff --git a/src/modules/fancyzones/tests/UnitTests/ZoneSet.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/ZoneSet.Spec.cpp index 1affea8413..fd00c24ff8 100644 --- a/src/modules/fancyzones/tests/UnitTests/ZoneSet.Spec.cpp +++ b/src/modules/fancyzones/tests/UnitTests/ZoneSet.Spec.cpp @@ -1,192 +1,1198 @@ #include "pch.h" +#include "lib\JsonHelpers.h" #include "lib\ZoneSet.h" +#include + #include "Util.h" +#include using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using TZoneSetLayoutType = JSONHelpers::ZoneSetLayoutType; namespace FancyZonesUnitTests { - TEST_CLASS(ZoneSetUnitTests){ - public: - TEST_METHOD(TestCreateZoneSet){ - GUID zoneSetId{}; - CoCreateGuid(&zoneSetId); - constexpr WORD layoutId = 0xFFFF; + TEST_CLASS(ZoneSetUnitTests) + { + GUID m_id; + const TZoneSetLayoutType m_layoutType = TZoneSetLayoutType::Custom; + const PCWSTR m_resolutionKey = L"WorkAreaIn"; - ZoneSetConfig config(zoneSetId, layoutId, Mocks::Monitor(), L"WorkAreaIn"); - winrt::com_ptr set = MakeZoneSet(config); - Assert::IsNotNull(&set); - CustomAssert::AreEqual(set->Id(), zoneSetId); - CustomAssert::AreEqual(set->LayoutId(), layoutId); -} - -TEST_METHOD(TestAddZone) -{ - ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn"); - winrt::com_ptr set = MakeZoneSet(config); - - // Add a zone - { - winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); - set->AddZone(zone); - auto zones = set->GetZones(); - Assert::IsTrue(zones.size() == 1); - Assert::IsTrue(zones[0] == zone); - Assert::IsTrue(zone->Id() == 1); - } - - // Add a second zone at the back. - { - winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); - set->AddZone(zone); - auto zones = set->GetZones(); - Assert::IsTrue(zones.size() == 2); - Assert::IsTrue(zones[1] == zone); - Assert::IsTrue(zone->Id() == 2); - } -} - -TEST_METHOD(TestMoveWindowIntoZoneByIndex) -{ - ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn"); - winrt::com_ptr set = MakeZoneSet(config); - - // Add a couple of zones. - winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); - winrt::com_ptr zone2 = MakeZone({ 0, 0, 100, 100 }); - winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); - set->AddZone(zone1); - set->AddZone(zone2); - set->AddZone(zone3); - - HWND window = Mocks::Window(); - set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 1); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsTrue(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); -} - -TEST_METHOD(TestMoveWindowIntoZoneByIndexWithNoZones) -{ - ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn"); - winrt::com_ptr set = MakeZoneSet(config); - - // Add a couple of zones. - HWND window = Mocks::Window(); - set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); -} - -TEST_METHOD(TestMoveWindowIntoZoneByIndexWithInvalidIndex) -{ - ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn"); - winrt::com_ptr set = MakeZoneSet(config); - - // Add a couple of zones. - winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); - winrt::com_ptr zone2 = MakeZone({ 0, 0, 100, 100 }); - winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); - set->AddZone(zone1); - set->AddZone(zone2); - set->AddZone(zone3); - - HWND window = Mocks::Window(); - set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 100); - Assert::IsTrue(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); -} -} -; - -// MoveWindowIntoZoneByDirection is complicated enough to warrant it's own test class -TEST_CLASS(MoveWindowIntoZoneByDirectionUnitTests) -{ - winrt::com_ptr set; - winrt::com_ptr zone1; - winrt::com_ptr zone2; - winrt::com_ptr zone3; - - TEST_METHOD_INITIALIZE(Initialize) - { - ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn"); - set = MakeZoneSet(config); - - // Add a couple of zones. - zone1 = MakeZone({ 0, 0, 100, 100 }); - zone2 = MakeZone({ 0, 0, 100, 100 }); - zone3 = MakeZone({ 0, 0, 100, 100 }); - set->AddZone(zone1); - set->AddZone(zone2); - set->AddZone(zone3); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionRightNoZones) - { - HWND window = Mocks::Window(); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); - Assert::IsTrue(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionLeftNoZones) - { - HWND window = Mocks::Window(); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsTrue(zone3->ContainsWindow(window)); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionRight) - { - HWND window = Mocks::Window(); - zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsTrue(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); - - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsTrue(zone3->ContainsWindow(window)); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionLeft) - { - HWND window = Mocks::Window(); - zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsTrue(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); - - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); - Assert::IsTrue(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundRight) - { - HWND window = Mocks::Window(); - zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); - Assert::IsTrue(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsFalse(zone3->ContainsWindow(window)); - } - - TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundLeft) - { - HWND window = Mocks::Window(); - zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); - set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); - Assert::IsFalse(zone1->ContainsWindow(window)); - Assert::IsFalse(zone2->ContainsWindow(window)); - Assert::IsTrue(zone3->ContainsWindow(window)); - } -}; + winrt::com_ptr m_set; + + TEST_METHOD_INITIALIZE(Init) + { + auto hres = CoCreateGuid(&m_id); + Assert::AreEqual(S_OK, hres); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, m_layoutType, Mocks::Monitor(), m_resolutionKey); + m_set = MakeZoneSet(m_config); + } + + void compareZones(const winrt::com_ptr& expected, const winrt::com_ptr& actual) + { + Assert::AreEqual(expected->Id(), actual->Id()); + Assert::AreEqual(expected->GetZoneRect().left, actual->GetZoneRect().left); + Assert::AreEqual(expected->GetZoneRect().right, actual->GetZoneRect().right); + Assert::AreEqual(expected->GetZoneRect().top, actual->GetZoneRect().top); + Assert::AreEqual(expected->GetZoneRect().bottom, actual->GetZoneRect().bottom); + } + + public: + TEST_METHOD(TestCreateZoneSet) + { + Assert::IsNotNull(&m_set); + CustomAssert::AreEqual(m_set->Id(), m_id); + CustomAssert::AreEqual(m_set->LayoutType(), m_layoutType); + } + + TEST_METHOD(TestCreateZoneSetGuidEmpty) + { + GUID zoneSetId{}; + ZoneSetConfig config(zoneSetId, m_layoutType, Mocks::Monitor(), m_resolutionKey); + winrt::com_ptr set = MakeZoneSet(config); + + Assert::IsNotNull(&set); + CustomAssert::AreEqual(set->Id(), zoneSetId); + CustomAssert::AreEqual(set->LayoutType(), m_layoutType); + } + + TEST_METHOD(TestCreateZoneSetMonitorEmpty) + { + ZoneSetConfig config(m_id, m_layoutType, nullptr, m_resolutionKey); + winrt::com_ptr set = MakeZoneSet(config); + Assert::IsNotNull(&set); + CustomAssert::AreEqual(set->Id(), m_id); + CustomAssert::AreEqual(set->LayoutType(), m_layoutType); + } + + TEST_METHOD(TestCreateZoneSetKeyEmpty) + { + ZoneSetConfig config(m_id, m_layoutType, Mocks::Monitor(), nullptr); + winrt::com_ptr set = MakeZoneSet(config); + Assert::IsNotNull(&set); + CustomAssert::AreEqual(set->Id(), m_id); + CustomAssert::AreEqual(set->LayoutType(), m_layoutType); + } + + TEST_METHOD(EmptyZones) + { + auto zones = m_set->GetZones(); + Assert::AreEqual((size_t)0, zones.size()); + } + + TEST_METHOD(AddOne) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone); + auto zones = m_set->GetZones(); + Assert::AreEqual((size_t)1, zones.size()); + compareZones(zone, zones[0]); + Assert::AreEqual((size_t)1, zones[0]->Id()); + } + + TEST_METHOD(AddManySame) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); + for (size_t i = 0; i < 1024; i++) + { + m_set->AddZone(zone); + auto zones = m_set->GetZones(); + Assert::AreEqual(i + 1, zones.size()); + compareZones(zone, zones[i]); + Assert::AreEqual(i + 1, zones[i]->Id()); + } + } + + TEST_METHOD(AddManyEqual) + { + for (size_t i = 0; i < 1024; i++) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone); + auto zones = m_set->GetZones(); + Assert::AreEqual(i + 1, zones.size()); + compareZones(zone, zones[i]); + Assert::AreEqual(i + 1, zones[i]->Id()); + } + } + + TEST_METHOD(AddManyDifferent) + { + for (size_t i = 0; i < 1024; i++) + { + winrt::com_ptr zone = MakeZone({ rand() % 10, rand() % 10, rand() % 100, rand() % 100 }); + m_set->AddZone(zone); + auto zones = m_set->GetZones(); + Assert::AreEqual(i + 1, zones.size()); + compareZones(zone, zones[i]); + Assert::AreEqual(i + 1, zones[i]->Id()); + } + } + + TEST_METHOD(ZoneFromPointEmpty) + { + auto actual = m_set->ZoneFromPoint(POINT{ 0, 0 }); + Assert::IsTrue(nullptr == actual); + } + + TEST_METHOD(ZoneFromPointInner) + { + const int left = 0, top = 0, right = 100, bottom = 100; + winrt::com_ptr expected = MakeZone({ left, top, right, bottom }); + m_set->AddZone(expected); + + for (int i = left + 1; i < right; i++) + { + for (int j = top + 1; j < bottom; j++) + { + auto actual = m_set->ZoneFromPoint(POINT{ i, j }); + Assert::IsTrue(actual != nullptr); + compareZones(expected, actual); + } + } + } + + TEST_METHOD(ZoneFromPointBorder) + { + const int left = 0, top = 0, right = 100, bottom = 100; + winrt::com_ptr expected = MakeZone({ left, top, right, bottom }); + m_set->AddZone(expected); + + for (int i = left; i < right; i++) + { + auto actual = m_set->ZoneFromPoint(POINT{ i, top }); + Assert::IsTrue(actual != nullptr); + compareZones(expected, actual); + } + + for (int i = top; i < bottom; i++) + { + auto actual = m_set->ZoneFromPoint(POINT{ left, i }); + Assert::IsTrue(actual != nullptr); + compareZones(expected, actual); + } + + //bottom and right borders considered to be outside + for (int i = left; i < right; i++) + { + auto actual = m_set->ZoneFromPoint(POINT{ i, bottom }); + Assert::IsTrue(nullptr == actual); + } + + for (int i = top; i < bottom; i++) + { + auto actual = m_set->ZoneFromPoint(POINT{ right, i }); + Assert::IsTrue(nullptr == actual); + } + } + + TEST_METHOD(ZoneFromPointOuter) + { + const int left = 0, top = 0, right = 100, bottom = 100; + winrt::com_ptr zone = MakeZone({ left, top, right, bottom }); + m_set->AddZone(zone); + + auto actual = m_set->ZoneFromPoint(POINT{ 101, 101 }); + Assert::IsTrue(actual == nullptr); + } + + TEST_METHOD(ZoneFromPointOverlapping) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone1); + winrt::com_ptr zone2 = MakeZone({ 10, 10, 90, 90 }); + m_set->AddZone(zone2); + winrt::com_ptr zone3 = MakeZone({ 10, 10, 150, 150 }); + m_set->AddZone(zone3); + winrt::com_ptr zone4 = MakeZone({ 10, 10, 50, 50 }); + m_set->AddZone(zone4); + + auto actual = m_set->ZoneFromPoint(POINT{ 50, 50 }); + Assert::IsTrue(actual != nullptr); + compareZones(zone2, actual); + } + + TEST_METHOD(ZoneFromPointWithNotNormalizedRect) + { + winrt::com_ptr zone = MakeZone({ 100, 100, 0, 0 }); + m_set->AddZone(zone); + + auto actual = m_set->ZoneFromPoint(POINT{ 50, 50 }); + Assert::IsTrue(actual == nullptr); + } + + TEST_METHOD(ZoneFromPointWithZeroRect) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 0, 0 }); + m_set->AddZone(zone); + + auto actual = m_set->ZoneFromPoint(POINT{ 0, 0 }); + Assert::IsTrue(actual == nullptr); + } + + TEST_METHOD(ZoneIndexFromWindow) + { + HWND window = Mocks::Window(); + HWND zoneWindow = Mocks::Window(); + + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 20, 20, 200, 200 }); + winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone4 = MakeZone({ 10, 10, 100, 100 }); + winrt::com_ptr zone5 = MakeZone({ 20, 20, 100, 100 }); + + zone3->AddWindowToZone(window, zoneWindow, true); + + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + m_set->AddZone(zone4); + m_set->AddZone(zone5); + + const int expected = 2; + auto actual = m_set->GetZoneIndexFromWindow(window); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(ZoneIndexFromWindowWithEqualWindows) + { + HWND window = Mocks::Window(); + HWND zoneWindow = Mocks::Window(); + + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 20, 20, 200, 200 }); + winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone4 = MakeZone({ 10, 10, 100, 100 }); + winrt::com_ptr zone5 = MakeZone({ 20, 20, 100, 100 }); + + zone3->AddWindowToZone(window, zoneWindow, true); + zone4->AddWindowToZone(window, zoneWindow, true); + + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + m_set->AddZone(zone4); + m_set->AddZone(zone5); + + const int expected = 2; + auto actual = m_set->GetZoneIndexFromWindow(window); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(ZoneIndexFromWindowUnknown) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); + HWND window = Mocks::Window(); + HWND zoneWindow = Mocks::Window(); + zone->AddWindowToZone(window, zoneWindow, true); + m_set->AddZone(zone); + + const int expected = -1; + auto actual = m_set->GetZoneIndexFromWindow(Mocks::Window()); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(ZoneIndexFromWindowNull) + { + winrt::com_ptr zone = MakeZone({ 0, 0, 100, 100 }); + HWND window = Mocks::Window(); + HWND zoneWindow = Mocks::Window(); + zone->AddWindowToZone(window, zoneWindow, true); + m_set->AddZone(zone); + + const int expected = -1; + auto actual = m_set->GetZoneIndexFromWindow(nullptr); + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(MoveWindowIntoZoneByIndex) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 1); + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsTrue(zone2->ContainsWindow(window)); + Assert::IsFalse(zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByIndexWithNoZones) + { + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); + } + + TEST_METHOD(MoveWindowIntoZoneByIndexWithInvalidIndex) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone3 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 100); + Assert::IsTrue(zone1->ContainsWindow(window)); + Assert::IsFalse(zone2->ContainsWindow(window)); + Assert::IsFalse(zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByIndexSeveralTimesSameWindow) + { + // Add a couple of zones. + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 1, 1, 101, 101 }); + winrt::com_ptr zone3 = MakeZone({ 2, 2, 102, 102 }); + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); + Assert::IsTrue(zone1->ContainsWindow(window)); + Assert::IsFalse(zone2->ContainsWindow(window)); + Assert::IsFalse(zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 1); + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsTrue(zone2->ContainsWindow(window)); + Assert::IsFalse(zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 2); + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsFalse(zone2->ContainsWindow(window)); + Assert::IsTrue(zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByIndexSeveralTimesSameIndex) + { + // Add a couple of zones. + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 1, 1, 101, 101 }); + winrt::com_ptr zone3 = MakeZone({ 2, 2, 102, 102 }); + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); + m_set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0); + Assert::IsTrue(zone1->ContainsWindow(window)); + Assert::IsFalse(zone2->ContainsWindow(window)); + Assert::IsFalse(zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointEmpty) + { + m_set->MoveWindowIntoZoneByPoint(Mocks::Window(), Mocks::Window(), POINT{ 0, 0 }); + } + + TEST_METHOD(MoveWindowIntoZoneByPointOuterPoint) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone1); + + auto window = Mocks::Window(); + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 101, 101 }); + + Assert::IsFalse(zone1->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointInnerPoint) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(zone1); + + auto window = Mocks::Window(); + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 50, 50 }); + + Assert::IsTrue(zone1->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointInnerPointOverlappingZones) + { + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 10, 10, 90, 90 }); + m_set->AddZone(zone1); + m_set->AddZone(zone2); + + auto window = Mocks::Window(); + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 50, 50 }); + + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsTrue(zone2->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointDropAddWindow) + { + const auto window = Mocks::Window(); + const auto zoneWindow = Mocks::Window(); + + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 10, 10, 90, 90 }); + + zone1->AddWindowToZone(window, zoneWindow, false); + + m_set->AddZone(zone1); + m_set->AddZone(zone2); + + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 50, 50 }); + + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsTrue(zone2->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointDropAddWindowToSameZone) + { + const auto window = Mocks::Window(); + const auto zoneWindow = Mocks::Window(); + + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 10, 10, 90, 90 }); + + zone2->AddWindowToZone(window, zoneWindow, false); + + m_set->AddZone(zone1); + m_set->AddZone(zone2); + + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 50, 50 }); + + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsTrue(zone2->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByPointSeveralZonesWithSameWindow) + { + const auto window = Mocks::Window(); + const auto zoneWindow = Mocks::Window(); + + winrt::com_ptr zone1 = MakeZone({ 0, 0, 100, 100 }); + winrt::com_ptr zone2 = MakeZone({ 10, 10, 90, 90 }); + winrt::com_ptr zone3 = MakeZone({ 20, 20, 80, 80 }); + + zone1->AddWindowToZone(window, zoneWindow, false); + zone2->AddWindowToZone(window, zoneWindow, false); + zone3->AddWindowToZone(window, zoneWindow, false); + + m_set->AddZone(zone1); + m_set->AddZone(zone2); + m_set->AddZone(zone3); + + m_set->MoveWindowIntoZoneByPoint(window, Mocks::Window(), POINT{ 50, 50 }); + + Assert::IsFalse(zone1->ContainsWindow(window)); + Assert::IsFalse(zone2->ContainsWindow(window)); + Assert::IsTrue(zone3->ContainsWindow(window)); + } + }; + + // MoveWindowIntoZoneByDirection is complicated enough to warrant it's own test class + TEST_CLASS(ZoneSetsMoveWindowIntoZoneByDirectionUnitTests) + { + winrt::com_ptr m_set; + winrt::com_ptr m_zone1; + winrt::com_ptr m_zone2; + winrt::com_ptr m_zone3; + + TEST_METHOD_INITIALIZE(Initialize) + { + ZoneSetConfig config({}, TZoneSetLayoutType::Custom, Mocks::Monitor(), L"WorkAreaIn"); + m_set = MakeZoneSet(config); + + // Add a couple of zones. + m_zone1 = MakeZone({ 0, 0, 100, 100 }); + m_zone2 = MakeZone({ 0, 0, 100, 100 }); + m_zone3 = MakeZone({ 0, 0, 100, 100 }); + m_set->AddZone(m_zone1); + m_set->AddZone(m_zone2); + m_set->AddZone(m_zone3); + } + + TEST_METHOD(EmptyZonesLeft) + { + ZoneSetConfig config({}, TZoneSetLayoutType::Custom, Mocks::Monitor(), L"WorkAreaIn"); + auto set = MakeZoneSet(config); + + set->MoveWindowIntoZoneByDirection(Mocks::Window(), Mocks::Window(), VK_LEFT); + } + + TEST_METHOD(EmptyZonesRight) + { + ZoneSetConfig config({}, TZoneSetLayoutType::Custom, Mocks::Monitor(), L"WorkAreaIn"); + auto set = MakeZoneSet(config); + + set->MoveWindowIntoZoneByDirection(Mocks::Window(), Mocks::Window(), VK_RIGHT); + } + + TEST_METHOD(MoveRightNoZones) + { + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveLeftNoZones) + { + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveRightTwice) + { + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveLeftTwice) + { + HWND window = Mocks::Window(); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveRightMoreThanZonesCount) + { + HWND window = Mocks::Window(); + for (int i = 0; i <= m_set->GetZones().size(); i++) + { + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + } + + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveLeftMoreThanZonesCount) + { + HWND window = Mocks::Window(); + for (int i = 0; i <= m_set->GetZones().size(); i++) + { + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + } + + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionRight) + { + HWND window = Mocks::Window(); + m_zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveRightWithSameWindowAdded) + { + HWND window = Mocks::Window(); + m_zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_zone2->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveRightWithDifferentWindowsAdded) + { + HWND window1 = Mocks::Window(); + HWND window2 = Mocks::Window(); + m_zone1->AddWindowToZone(window1, Mocks::Window(), false /*stampZone*/); + m_zone2->AddWindowToZone(window2, Mocks::Window(), false /*stampZone*/); + + Assert::IsTrue(m_zone1->ContainsWindow(window1)); + Assert::IsFalse(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + Assert::IsFalse(m_zone1->ContainsWindow(window2)); + Assert::IsTrue(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + + m_set->MoveWindowIntoZoneByDirection(window1, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window1)); + Assert::IsTrue(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + Assert::IsFalse(m_zone1->ContainsWindow(window2)); + Assert::IsTrue(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + + m_set->MoveWindowIntoZoneByDirection(window1, Mocks::Window(), VK_RIGHT); + Assert::IsFalse(m_zone1->ContainsWindow(window1)); + Assert::IsFalse(m_zone2->ContainsWindow(window1)); + Assert::IsTrue(m_zone3->ContainsWindow(window1)); + Assert::IsFalse(m_zone1->ContainsWindow(window2)); + Assert::IsTrue(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionLeft) + { + HWND window = Mocks::Window(); + m_zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveLeftWithSameWindowAdded) + { + HWND window = Mocks::Window(); + m_zone2->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsTrue(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveLeftWithDifferentWindowsAdded) + { + HWND window1 = Mocks::Window(); + HWND window2 = Mocks::Window(); + m_zone2->AddWindowToZone(window1, Mocks::Window(), false /*stampZone*/); + m_zone3->AddWindowToZone(window2, Mocks::Window(), false /*stampZone*/); + + Assert::IsFalse(m_zone1->ContainsWindow(window1)); + Assert::IsTrue(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + Assert::IsFalse(m_zone1->ContainsWindow(window2)); + Assert::IsFalse(m_zone2->ContainsWindow(window2)); + Assert::IsTrue(m_zone3->ContainsWindow(window2)); + + m_set->MoveWindowIntoZoneByDirection(window2, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window1)); + Assert::IsTrue(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + Assert::IsFalse(m_zone1->ContainsWindow(window2)); + Assert::IsTrue(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + + m_set->MoveWindowIntoZoneByDirection(window2, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window1)); + Assert::IsTrue(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + Assert::IsTrue(m_zone1->ContainsWindow(window2)); + Assert::IsFalse(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundRight) + { + HWND window = Mocks::Window(); + m_zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT); + Assert::IsTrue(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsFalse(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundLeft) + { + HWND window = Mocks::Window(); + m_zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/); + m_set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT); + Assert::IsFalse(m_zone1->ContainsWindow(window)); + Assert::IsFalse(m_zone2->ContainsWindow(window)); + Assert::IsTrue(m_zone3->ContainsWindow(window)); + } + + TEST_METHOD(MoveSecondWindowIntoSameZone) + { + HWND window1 = Mocks::Window(); + m_zone1->AddWindowToZone(window1, Mocks::Window(), false /*stampZone*/); + + HWND window2 = Mocks::Window(); + m_set->MoveWindowIntoZoneByDirection(window2, Mocks::Window(), VK_RIGHT); + + Assert::IsTrue(m_zone1->ContainsWindow(window1)); + Assert::IsFalse(m_zone2->ContainsWindow(window1)); + Assert::IsFalse(m_zone3->ContainsWindow(window1)); + + Assert::IsTrue(m_zone1->ContainsWindow(window2)); + Assert::IsFalse(m_zone2->ContainsWindow(window2)); + Assert::IsFalse(m_zone3->ContainsWindow(window2)); + } + }; + + TEST_CLASS(ZoneSetCalculateZonesUnitTests) + { + GUID m_id; + const TZoneSetLayoutType m_layoutType = TZoneSetLayoutType::Custom; + const PCWSTR m_resolutionKey = L"WorkAreaIn"; + winrt::com_ptr m_set; + + HMONITOR m_monitor; + const std::array m_popularMonitors { + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1024, .bottom = 768 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1280, .bottom = 720 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1280, .bottom = 800 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1280, .bottom = 1024 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1366, .bottom = 768 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1440, .bottom = 900 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1536, .bottom = 864 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1600, .bottom = 900 } }, + MONITORINFO{ .cbSize = sizeof(MONITORINFO), .rcWork{ .left = 0, .top = 0, .right = 1920, .bottom = 1080 } } + }; + + MONITORINFO m_monitorInfo; + + const std::wstring m_path = PTSettingsHelper::get_module_save_folder_location(L"FancyZones") + L"\\" + std::wstring(L"testzones.json"); + + TEST_METHOD_INITIALIZE(Init) + { + auto hres = CoCreateGuid(&m_id); + Assert::AreEqual(S_OK, hres); + + m_monitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, m_layoutType, m_monitor, m_resolutionKey); + m_set = MakeZoneSet(m_config); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + std::filesystem::remove(m_path); + } + + void checkZones(const winrt::com_ptr& set, size_t expectedCount, MONITORINFO monitorInfo) + { + auto zones = set->GetZones(); + Assert::AreEqual(expectedCount, zones.size()); + + for (const auto& zone : zones) + { + Assert::IsTrue(zone->IsEmpty()); + + const auto& zoneRect = zone->GetZoneRect(); + Assert::IsTrue(zoneRect.left >= 0, L"left border is less than zero"); + Assert::IsTrue(zoneRect.top >= 0, L"top border is less than zero"); + + Assert::IsTrue(zoneRect.left < zoneRect.right, L"rect.left >= rect.right"); + Assert::IsTrue(zoneRect.top < zoneRect.bottom, L"rect.top >= rect.bottom"); + + Assert::IsTrue(zoneRect.right <= monitorInfo.rcWork.right, L"right border is bigger than monitor work space"); + Assert::IsTrue(zoneRect.bottom <= monitorInfo.rcWork.bottom, L"bottom border is bigger than monitor work space"); + } + } + + public: + TEST_METHOD(ValidValues) + { + const int spacing = 10; + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto set = MakeZoneSet(m_config); + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsTrue(result); + checkZones(set, zoneCount, monitorInfo); + } + } + } + TEST_METHOD(InvalidMonitorInfo) + { + const int spacing = 10; + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + MONITORINFO info{}; + auto result = set->CalculateZones(info, zoneCount, spacing); + Assert::IsFalse(result); + } + } + + TEST_METHOD(ZeroSpacing) + { + const int spacing = 0; + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto set = MakeZoneSet(m_config); + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsTrue(result); + checkZones(set, zoneCount, monitorInfo); + } + } + } + + TEST_METHOD(NegativeSpacing) + { + const int spacing = -1; + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + if (type == static_cast(JSONHelpers::ZoneSetLayoutType::Focus)) + { + //Focus doesn't depends on spacing + Assert::IsTrue(result); + } + else + { + Assert::IsFalse(result); + } + } + } + } + + TEST_METHOD(HorizontallyBigSpacing) + { + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + const int spacing = monitorInfo.rcWork.right; + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + if (type == static_cast(JSONHelpers::ZoneSetLayoutType::Focus)) + { + //Focus doesn't depends on spacing + Assert::IsTrue(result); + } + else + { + Assert::IsFalse(result); + } + } + } + } + + TEST_METHOD(VerticallyBigSpacing) + { + const int zoneCount = 10; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + const int spacing = monitorInfo.rcWork.bottom; + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + if (type == static_cast(JSONHelpers::ZoneSetLayoutType::Focus)) + { + //Focus doesn't depends on spacing + Assert::IsTrue(result); + } + else + { + Assert::IsFalse(result); + } + } + } + } + + TEST_METHOD(ZeroZoneCount) + { + const int spacing = 10; + const int zoneCount = 0; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + } + + TEST_METHOD(BigZoneCount) + { + const int spacing = 1; + + for (int type = static_cast(JSONHelpers::ZoneSetLayoutType::Focus); type < static_cast(JSONHelpers::ZoneSetLayoutType::Custom); type++) + { + const int spacing = 10; + const int zoneCount = 40; //editor limit + + ZoneSetConfig m_config = ZoneSetConfig(m_id, static_cast(type), m_monitor, m_resolutionKey); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto set = MakeZoneSet(m_config); + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsTrue(result); + checkZones(set, zoneCount, monitorInfo); + } + } + } + + TEST_METHOD(CustomZonesFromUnexistedFile) + { + const int spacing = 10; + const int zoneCount = 0; + + //be sure that file does not exist + if (std::filesystem::exists(m_path)) + { + std::filesystem::remove(m_path); + } + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + + TEST_METHOD(CustomZoneFromEmptyFile) + { + const int spacing = 10; + const int zoneCount = 0; + + Assert::IsTrue(std::filesystem::create_directories(m_path)); + Assert::IsTrue(std::filesystem::exists(m_path)); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + + TEST_METHOD(CustomZoneFromInvalidCanvasLayoutInfo) + { + using namespace JSONHelpers; + + const std::wstring uuid = L"uuid"; + const CanvasLayoutInfo info{ -1, 100, { CanvasLayoutInfo::Rect{ -10, -10, 100, 100 }, CanvasLayoutInfo::Rect{ 50, 50, 150, 150 } } }; + CustomZoneSetJSON expected{ uuid, CustomZoneSetData{ L"name", CustomLayoutType::Canvas, info } }; + json::to_file(m_path, CustomZoneSetJSON::ToJson(expected)); + Assert::IsTrue(std::filesystem::exists(m_path)); + + const int spacing = 10; + const auto zoneCount = info.zones.size(); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + + TEST_METHOD(CustomZoneFromInvalidGridLayoutInfo) + { + using namespace JSONHelpers; + + const std::wstring uuid = L"uuid"; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { -100 }, //rows percents are negative + .columnsPercents = { 2500, 2500 }, //column percents count is invalid + .cellChildMap = { { 0, 1, 2 } } })); + CustomZoneSetJSON expected{ uuid, CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + json::to_file(m_path, CustomZoneSetJSON::ToJson(expected)); + Assert::IsTrue(std::filesystem::exists(m_path)); + + const int spacing = 0; + const int zoneCount = grid.rows() * grid.columns(); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + + TEST_METHOD(CustomZoneFromValidCanvasLayoutInfo) + { + using namespace JSONHelpers; + + //prepare device data + { + const std::wstring zoneUuid = L"default_device_id"; + DeviceInfoJSON deviceInfo{ zoneUuid, DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + const std::wstring deviceInfoPath = FancyZonesDataInstance().GetPersistFancyZonesJSONPath() + L".device_info_tmp"; + FancyZonesDataInstance().SerializeDeviceInfoToTmpFile(deviceInfo, deviceInfoPath); + + FancyZonesDataInstance().ParseDeviceInfoFromTmpFile(deviceInfoPath); + std::filesystem::remove(deviceInfoPath); + } + + //prepare expected data + wil::unique_cotaskmem_string uuid; + Assert::AreEqual(S_OK, StringFromCLSID(m_id, &uuid)); + const CanvasLayoutInfo info{ 123, 321, { CanvasLayoutInfo::Rect{ 0, 0, 100, 100 }, CanvasLayoutInfo::Rect{ 50, 50, 150, 150 } } }; + CustomZoneSetJSON expected{ uuid.get(), CustomZoneSetData{ L"name", CustomLayoutType::Canvas, info } }; + json::to_file(m_path, CustomZoneSetJSON::ToJson(expected)); + Assert::IsTrue(std::filesystem::exists(m_path)); + FancyZonesDataInstance().ParseCustomZoneSetFromTmpFile(m_path); + + //test + const int spacing = 10; + const auto zoneCount = info.zones.size(); + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + for (const auto& monitorInfo : m_popularMonitors) + { + auto set = MakeZoneSet(m_config); + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsTrue(result); + checkZones(set, zoneCount, monitorInfo); + } + } + + TEST_METHOD(CustomZoneFromValidGridFullLayoutInfo) + { + using namespace JSONHelpers; + + //prepare device data + { + const std::wstring zoneUuid = L"default_device_id"; + DeviceInfoJSON deviceInfo{ zoneUuid, DeviceInfoData{ ZoneSetData{ L"uuid", ZoneSetLayoutType::Custom }, true, 16, 3 } }; + const std::wstring deviceInfoPath = FancyZonesDataInstance().GetPersistFancyZonesJSONPath() + L".device_info_tmp"; + FancyZonesDataInstance().SerializeDeviceInfoToTmpFile(deviceInfo, deviceInfoPath); + + FancyZonesDataInstance().ParseDeviceInfoFromTmpFile(deviceInfoPath); + std::filesystem::remove(deviceInfoPath); + } + + //prepare expected data + wil::unique_cotaskmem_string uuid; + Assert::AreEqual(S_OK, StringFromCLSID(m_id, &uuid)); + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Full{ + .rows = 1, + .columns = 3, + .rowsPercents = { 10000 }, + .columnsPercents = { 2500, 5000, 2500 }, + .cellChildMap = { { 0, 1, 2 } } })); + CustomZoneSetJSON expected{ uuid.get(), CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + json::to_file(m_path, CustomZoneSetJSON::ToJson(expected)); + Assert::IsTrue(std::filesystem::exists(m_path)); + FancyZonesDataInstance().ParseCustomZoneSetFromTmpFile(m_path); + + const int spacing = 10; + const int zoneCount = grid.rows() * grid.columns(); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto set = MakeZoneSet(m_config); + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsTrue(result); + checkZones(set, zoneCount, monitorInfo); + } + } + + TEST_METHOD(CustomZoneFromValidGridMinimalLayoutInfo) + { + using namespace JSONHelpers; + + const std::wstring uuid = L"uuid"; + const GridLayoutInfo grid(GridLayoutInfo(JSONHelpers::GridLayoutInfo::Minimal{ + .rows = 1, + .columns = 3 })); + CustomZoneSetJSON expected{ uuid, CustomZoneSetData{ L"name", CustomLayoutType::Grid, grid } }; + json::to_file(m_path, CustomZoneSetJSON::ToJson(expected)); + Assert::IsTrue(std::filesystem::exists(m_path)); + + const int spacing = 0; + const int zoneCount = grid.rows() * grid.columns(); + + ZoneSetConfig m_config = ZoneSetConfig(m_id, TZoneSetLayoutType::Custom, m_monitor, m_resolutionKey); + auto set = MakeZoneSet(m_config); + + for (const auto& monitorInfo : m_popularMonitors) + { + auto result = set->CalculateZones(monitorInfo, zoneCount, spacing); + Assert::IsFalse(result); + } + } + }; } diff --git a/src/modules/fancyzones/tests/UnitTests/ZoneWindow.Spec.cpp b/src/modules/fancyzones/tests/UnitTests/ZoneWindow.Spec.cpp index 82dac19251..8a5418bede 100644 --- a/src/modules/fancyzones/tests/UnitTests/ZoneWindow.Spec.cpp +++ b/src/modules/fancyzones/tests/UnitTests/ZoneWindow.Spec.cpp @@ -1,9 +1,14 @@ #include "pch.h" +#include + +#include #include #include #include +#include #include "Util.h" + using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace FancyZonesUnitTests @@ -17,10 +22,10 @@ namespace FancyZonesUnitTests { return RGB(0xFF, 0xFF, 0xFF); } - IFACEMETHODIMP_(GUID) - GetCurrentMonitorZoneSetId(HMONITOR monitor) noexcept + IFACEMETHODIMP_(IZoneWindow*) + GetParentZoneWindow(HMONITOR monitor) noexcept { - return m_guid; + return m_zoneWindow; } IFACEMETHODIMP_(int) GetZoneHighlightOpacity() noexcept @@ -28,47 +33,636 @@ namespace FancyZonesUnitTests return 100; } - GUID m_guid; + IZoneWindow* m_zoneWindow; }; - TEST_CLASS(ZoneWindowUnitTests){ - public: + TEST_CLASS(ZoneWindowUnitTests) + { + const std::wstring m_deviceId = L"\\\\?\\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"; + const std::wstring m_virtualDesktopId = L"MyVirtualDesktopId"; + std::wstringstream m_uniqueId; - TEST_METHOD(TestCreateZoneWindow){ - winrt::com_ptr zoneWindow = MakeZoneWindow(nullptr, Mocks::Instance(), Mocks::Monitor(), L"DeviceId", L"MyVirtualDesktopId", false); - Assert::IsNotNull(zoneWindow.get()); -} - -TEST_METHOD(TestDeviceId) -{ - // Window initialization requires a valid HMONITOR - just use the primary for now. - HMONITOR pimaryMonitor = MonitorFromWindow(HWND(), MONITOR_DEFAULTTOPRIMARY); - MockZoneWindowHost host; - std::wstring expectedDeviceId = L"SomeRandomValue"; - winrt::com_ptr zoneWindow = MakeZoneWindow(dynamic_cast(&host), Mocks::Instance(), pimaryMonitor, expectedDeviceId.c_str(), L"MyVirtualDesktopId", false); - - Assert::AreEqual(expectedDeviceId, zoneWindow->DeviceId()); -} - -TEST_METHOD(TestUniqueId) -{ - // Unique id of the format "ParsedMonitorDeviceId_MonitorWidth_MonitorHeight_VirtualDesktopId - // Example: "DELA026#5&10a58c63&0&UID16777488_1024_768_MyVirtualDesktopId" - std::wstring deviceId(L"\\\\?\\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"); - // Window initialization requires a valid HMONITOR - just use the primary for now. - HMONITOR pimaryMonitor = MonitorFromWindow(HWND(), MONITOR_DEFAULTTOPRIMARY); - MONITORINFO info; - info.cbSize = sizeof(info); - Assert::IsTrue(GetMonitorInfo(pimaryMonitor, &info)); - - Rect monitorRect = Rect(info.rcMonitor); - std::wstringstream ss; - ss << L"DELA026#5&10a58c63&0&UID16777488_" << monitorRect.width() << "_" << monitorRect.height() << "_MyVirtualDesktopId"; - - MockZoneWindowHost host; - winrt::com_ptr zoneWindow = MakeZoneWindow(dynamic_cast(&host), Mocks::Instance(), pimaryMonitor, deviceId.c_str(), L"MyVirtualDesktopId", false); - Assert::AreEqual(zoneWindow->UniqueId().compare(ss.str()), 0); -} -} -; + HINSTANCE m_hInst{}; + HMONITOR m_monitor{}; + MONITORINFO m_monitorInfo{}; + MockZoneWindowHost m_zoneWindowHost{}; + IZoneWindowHost* m_hostPtr = m_zoneWindowHost.get_strong().get(); + + winrt::com_ptr m_zoneWindow; + + JSONHelpers::FancyZonesData& m_fancyZonesData = JSONHelpers::FancyZonesDataInstance(); + + std::wstring GuidString(const GUID& guid) + { + OLECHAR* guidString; + Assert::AreEqual(S_OK, StringFromCLSID(guid, &guidString)); + + std::wstring guidStr{ guidString }; + CoTaskMemFree(guidString); + + return guidStr; + } + + std::wstring CreateGuidString() + { + GUID guid; + Assert::AreEqual(S_OK, CoCreateGuid(&guid)); + + return GuidString(guid); + } + + TEST_METHOD_INITIALIZE(Init) + { + m_hInst = (HINSTANCE)GetModuleHandleW(nullptr); + + m_monitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY); + m_monitorInfo.cbSize = sizeof(m_monitorInfo); + Assert::AreNotEqual(0, GetMonitorInfoW(m_monitor, &m_monitorInfo)); + + m_uniqueId << L"DELA026#5&10a58c63&0&UID16777488_" << m_monitorInfo.rcMonitor.right << "_" << m_monitorInfo.rcMonitor.bottom << "_MyVirtualDesktopId"; + + Assert::IsFalse(ZoneWindowUtils::GetActiveZoneSetTmpPath().empty()); + Assert::IsFalse(ZoneWindowUtils::GetAppliedZoneSetTmpPath().empty()); + Assert::IsFalse(ZoneWindowUtils::GetCustomZoneSetsTmpPath().empty()); + + Assert::IsFalse(std::filesystem::exists(ZoneWindowUtils::GetActiveZoneSetTmpPath())); + Assert::IsFalse(std::filesystem::exists(ZoneWindowUtils::GetAppliedZoneSetTmpPath())); + Assert::IsFalse(std::filesystem::exists(ZoneWindowUtils::GetCustomZoneSetsTmpPath())); + + m_fancyZonesData = JSONHelpers::FancyZonesData(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + //cleanup temp files if were created + std::filesystem::remove(ZoneWindowUtils::GetActiveZoneSetTmpPath()); + std::filesystem::remove(ZoneWindowUtils::GetAppliedZoneSetTmpPath()); + std::filesystem::remove(ZoneWindowUtils::GetCustomZoneSetsTmpPath()); + + m_zoneWindow = nullptr; + } + + winrt::com_ptr InitZoneWindowWithActiveZoneSet() + { + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + Assert::IsFalse(std::filesystem::exists(activeZoneSetTempPath)); + + const auto type = JSONHelpers::ZoneSetLayoutType::Columns; + const auto expectedZoneSet = JSONHelpers::ZoneSetData{ CreateGuidString(), type }; + const auto data = JSONHelpers::DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = JSONHelpers::DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = JSONHelpers::DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + Assert::IsTrue(std::filesystem::exists(activeZoneSetTempPath)); + + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + + return MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + } + + void testZoneWindow(winrt::com_ptr zoneWindow) + { + const std::wstring expectedWorkArea = std::to_wstring(m_monitorInfo.rcMonitor.right) + L"_" + std::to_wstring(m_monitorInfo.rcMonitor.bottom); + + Assert::IsNotNull(zoneWindow.get()); + Assert::IsFalse(zoneWindow->IsDragEnabled()); + Assert::AreEqual(m_uniqueId.str().c_str(), zoneWindow->UniqueId().c_str()); + Assert::AreEqual(expectedWorkArea, zoneWindow->WorkAreaKey()); + } + + public: + TEST_METHOD(CreateZoneWindow) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + testZoneWindow(m_zoneWindow); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + } + + TEST_METHOD(CreateZoneWindowNoHinst) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, {}, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(m_zoneWindow); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + } + + TEST_METHOD(CreateZoneWindowNoHinstFlashZones) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, {}, m_monitor, m_uniqueId.str(), true); + + testZoneWindow(m_zoneWindow); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + } + + TEST_METHOD(CreateZoneWindowNoMonitor) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, {}, m_uniqueId.str(), false); + + Assert::IsNull(m_zoneWindow.get()); + Assert::IsNotNull(m_hostPtr); + } + + TEST_METHOD(CreateZoneWindowNoMonitorFlashZones) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, {}, m_uniqueId.str(), true); + + Assert::IsNull(m_zoneWindow.get()); + Assert::IsNotNull(m_hostPtr); + } + + TEST_METHOD(CreateZoneWindowNoDeviceId) + { + // Generate unique id without device id + std::wstring uniqueId = ZoneWindowUtils::GenerateUniqueId(m_monitor, nullptr, m_virtualDesktopId.c_str()); + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, uniqueId, false); + + const std::wstring expectedWorkArea = std::to_wstring(m_monitorInfo.rcMonitor.right) + L"_" + std::to_wstring(m_monitorInfo.rcMonitor.bottom); + const std::wstring expectedUniqueId = L"FallbackDevice_" + std::to_wstring(m_monitorInfo.rcMonitor.right) + L"_" + std::to_wstring(m_monitorInfo.rcMonitor.bottom) + L"_" + m_virtualDesktopId; + + Assert::IsNotNull(m_zoneWindow.get()); + Assert::IsFalse(m_zoneWindow->IsDragEnabled()); + Assert::AreEqual(expectedUniqueId.c_str(), m_zoneWindow->UniqueId().c_str()); + Assert::AreEqual(expectedWorkArea, m_zoneWindow->WorkAreaKey()); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + } + + TEST_METHOD(CreateZoneWindowNoDesktopId) + { + // Generate unique id without virtual desktop id + std::wstring uniqueId = ZoneWindowUtils::GenerateUniqueId(m_monitor, m_deviceId.c_str(), nullptr); + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, uniqueId, false); + + const std::wstring expectedWorkArea = std::to_wstring(m_monitorInfo.rcMonitor.right) + L"_" + std::to_wstring(m_monitorInfo.rcMonitor.bottom); + Assert::IsNotNull(m_zoneWindow.get()); + Assert::IsFalse(m_zoneWindow->IsDragEnabled()); + Assert::IsTrue(m_zoneWindow->UniqueId().empty()); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + } + + TEST_METHOD(CreateZoneWindowWithActiveZoneTmpFile) + { + using namespace JSONHelpers; + + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + + for (int type = static_cast(ZoneSetLayoutType::Focus); type < static_cast(ZoneSetLayoutType::Custom); type++) + { + const auto expectedZoneSet = ZoneSetData{ CreateGuidString(), static_cast(type) }; + const auto data = DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + + //temp file read on initialization + auto actual = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(actual); + + Assert::IsNotNull(actual->ActiveZoneSet()); + } + } + + TEST_METHOD(CreateZoneWindowWithActiveCustomZoneTmpFile) + { + using namespace JSONHelpers; + + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + + const ZoneSetLayoutType type = ZoneSetLayoutType::Custom; + const auto expectedZoneSet = ZoneSetData{ CreateGuidString(), type }; + const auto data = DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + + //temp file read on initialization + auto actual = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(actual); + + //custom zone needs temp file for applied zone + Assert::IsNotNull(actual->ActiveZoneSet()); + const auto actualZoneSet = actual->ActiveZoneSet()->GetZones(); + Assert::AreEqual((size_t)0, actualZoneSet.size()); + } + + TEST_METHOD(CreateZoneWindowWithActiveCustomZoneAppliedTmpFile) + { + using namespace JSONHelpers; + + //save required data + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + const auto appliedZoneSetTempPath = ZoneWindowUtils::GetAppliedZoneSetTmpPath(); + + const ZoneSetLayoutType type = ZoneSetLayoutType::Custom; + const auto customSetGuid = CreateGuidString(); + const auto expectedZoneSet = ZoneSetData{ customSetGuid, type }; + const auto data = DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + + const auto info = CanvasLayoutInfo{ + 100, 100, std::vector{ CanvasLayoutInfo::Rect{ 0, 0, 100, 100 } } + }; + const auto customZoneData = CustomZoneSetData{ L"name", CustomLayoutType::Canvas, info }; + auto customZoneJson = CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ customSetGuid, customZoneData }); + json::to_file(appliedZoneSetTempPath, customZoneJson); + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + m_fancyZonesData.ParseCustomZoneSetFromTmpFile(appliedZoneSetTempPath); + + //temp file read on initialization + auto actual = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(actual); + + //custom zone needs temp file for applied zone + Assert::IsNotNull(actual->ActiveZoneSet()); + const auto actualZoneSet = actual->ActiveZoneSet()->GetZones(); + Assert::AreEqual((size_t)1, actualZoneSet.size()); + } + + TEST_METHOD(CreateZoneWindowWithActiveCustomZoneAppliedTmpFileWithDeletedCustomZones) + { + using namespace JSONHelpers; + + //save required data + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + const auto appliedZoneSetTempPath = ZoneWindowUtils::GetAppliedZoneSetTmpPath(); + const auto deletedZonesTempPath = ZoneWindowUtils::GetCustomZoneSetsTmpPath(); + + const ZoneSetLayoutType type = ZoneSetLayoutType::Custom; + const auto customSetGuid = CreateGuidString(); + const auto expectedZoneSet = ZoneSetData{ customSetGuid, type }; + const auto data = DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + + const auto info = CanvasLayoutInfo{ + 100, 100, std::vector{ CanvasLayoutInfo::Rect{ 0, 0, 100, 100 } } + }; + const auto customZoneData = CustomZoneSetData{ L"name", CustomLayoutType::Canvas, info }; + const auto customZoneSet = CustomZoneSetJSON{ customSetGuid, customZoneData }; + auto customZoneJson = CustomZoneSetJSON::ToJson(customZoneSet); + json::to_file(appliedZoneSetTempPath, customZoneJson); + + //save same zone as deleted + json::JsonObject deletedCustomZoneSets = {}; + json::JsonArray zonesArray{}; + zonesArray.Append(json::JsonValue::CreateStringValue(customZoneSet.uuid.substr(1, customZoneSet.uuid.size() - 2).c_str())); + deletedCustomZoneSets.SetNamedValue(L"deleted-custom-zone-sets", zonesArray); + json::to_file(deletedZonesTempPath, deletedCustomZoneSets); + + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + m_fancyZonesData.ParseDeletedCustomZoneSetsFromTmpFile(deletedZonesTempPath); + m_fancyZonesData.ParseCustomZoneSetFromTmpFile(appliedZoneSetTempPath); + + //temp file read on initialization + auto actual = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(actual); + + Assert::IsNotNull(actual->ActiveZoneSet()); + const auto actualZoneSet = actual->ActiveZoneSet()->GetZones(); + Assert::AreEqual((size_t)1, actualZoneSet.size()); + } + + TEST_METHOD(CreateZoneWindowWithActiveCustomZoneAppliedTmpFileWithUnusedDeletedCustomZones) + { + using namespace JSONHelpers; + + //save required data + const auto activeZoneSetTempPath = ZoneWindowUtils::GetActiveZoneSetTmpPath(); + const auto appliedZoneSetTempPath = ZoneWindowUtils::GetAppliedZoneSetTmpPath(); + const auto deletedZonesTempPath = ZoneWindowUtils::GetCustomZoneSetsTmpPath(); + + const ZoneSetLayoutType type = ZoneSetLayoutType::Custom; + const auto customSetGuid = CreateGuidString(); + const auto expectedZoneSet = ZoneSetData{ customSetGuid, type }; + const auto data = DeviceInfoData{ expectedZoneSet, true, 16, 3 }; + const auto deviceInfo = DeviceInfoJSON{ m_uniqueId.str(), data }; + const auto json = DeviceInfoJSON::ToJson(deviceInfo); + json::to_file(activeZoneSetTempPath, json); + + const auto info = CanvasLayoutInfo{ + 100, 100, std::vector{ CanvasLayoutInfo::Rect{ 0, 0, 100, 100 } } + }; + const auto customZoneData = CustomZoneSetData{ L"name", CustomLayoutType::Canvas, info }; + const auto customZoneSet = CustomZoneSetJSON{ customSetGuid, customZoneData }; + auto customZoneJson = CustomZoneSetJSON::ToJson(customZoneSet); + json::to_file(appliedZoneSetTempPath, customZoneJson); + + //save different zone as deleted + json::JsonObject deletedCustomZoneSets = {}; + json::JsonArray zonesArray{}; + const auto uuid = CreateGuidString(); + zonesArray.Append(json::JsonValue::CreateStringValue(uuid.substr(1, uuid.size() - 2).c_str())); + deletedCustomZoneSets.SetNamedValue(L"deleted-custom-zone-sets", zonesArray); + json::to_file(deletedZonesTempPath, deletedCustomZoneSets); + + m_fancyZonesData.ParseDeviceInfoFromTmpFile(activeZoneSetTempPath); + m_fancyZonesData.ParseDeletedCustomZoneSetsFromTmpFile(deletedZonesTempPath); + m_fancyZonesData.ParseCustomZoneSetFromTmpFile(appliedZoneSetTempPath); + + //temp file read on initialization + auto actual = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + testZoneWindow(actual); + + Assert::IsNotNull(actual->ActiveZoneSet()); + const auto actualZoneSet = actual->ActiveZoneSet()->GetZones(); + Assert::AreEqual((size_t)1, actualZoneSet.size()); + } + + TEST_METHOD(MoveSizeEnter) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = S_OK; + const auto actual = m_zoneWindow->MoveSizeEnter(Mocks::Window(), true); + + Assert::AreEqual(expected, actual); + Assert::IsTrue(m_zoneWindow->IsDragEnabled()); + } + + TEST_METHOD(MoveSizeEnterTwice) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = E_INVALIDARG; + + m_zoneWindow->MoveSizeEnter(Mocks::Window(), true); + const auto actual = m_zoneWindow->MoveSizeEnter(Mocks::Window(), false); + + Assert::AreEqual(expected, actual); + Assert::IsTrue(m_zoneWindow->IsDragEnabled()); + } + + TEST_METHOD(MoveSizeUpdate) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = S_OK; + const auto actual = m_zoneWindow->MoveSizeUpdate(POINT{ 0, 0 }, true); + + Assert::AreEqual(expected, actual); + Assert::IsTrue(m_zoneWindow->IsDragEnabled()); + } + + TEST_METHOD(MoveSizeUpdatePointNegativeCoordinates) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = S_OK; + const auto actual = m_zoneWindow->MoveSizeUpdate(POINT{ -10, -10 }, true); + + Assert::AreEqual(expected, actual); + Assert::IsTrue(m_zoneWindow->IsDragEnabled()); + } + + TEST_METHOD(MoveSizeUpdatePointBigCoordinates) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = S_OK; + const auto actual = m_zoneWindow->MoveSizeUpdate(POINT{ m_monitorInfo.rcMonitor.right + 1, m_monitorInfo.rcMonitor.bottom + 1 }, true); + + Assert::AreEqual(expected, actual); + Assert::IsTrue(m_zoneWindow->IsDragEnabled()); + } + + TEST_METHOD(MoveSizeEnd) + { + auto zoneWindow = InitZoneWindowWithActiveZoneSet(); + + const auto window = Mocks::Window(); + zoneWindow->MoveSizeEnter(window, true); + + const auto expected = S_OK; + const auto actual = zoneWindow->MoveSizeEnd(window, POINT{ 0, 0 }); + Assert::AreEqual(expected, actual); + + const auto zoneSet = zoneWindow->ActiveZoneSet(); + zoneSet->MoveWindowIntoZoneByIndex(window, Mocks::Window(), false); + const auto actualZoneIndex = zoneSet->GetZoneIndexFromWindow(window); + Assert::AreNotEqual(-1, actualZoneIndex); + } + + TEST_METHOD(MoveSizeEndWindowNotAdded) + { + auto zoneWindow = InitZoneWindowWithActiveZoneSet(); + + const auto window = Mocks::Window(); + zoneWindow->MoveSizeEnter(window, true); + + const auto expected = S_OK; + const auto actual = zoneWindow->MoveSizeEnd(window, POINT{ 0, 0 }); + Assert::AreEqual(expected, actual); + + const auto zoneSet = zoneWindow->ActiveZoneSet(); + const auto actualZoneIndex = zoneSet->GetZoneIndexFromWindow(window); + Assert::AreEqual(-1, actualZoneIndex); + } + + TEST_METHOD(MoveSizeEndDifferentWindows) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto window = Mocks::Window(); + m_zoneWindow->MoveSizeEnter(window, true); + + const auto expected = E_INVALIDARG; + const auto actual = m_zoneWindow->MoveSizeEnd(Mocks::Window(), POINT{ 0, 0 }); + + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(MoveSizeEndWindowNotSet) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = E_INVALIDARG; + const auto actual = m_zoneWindow->MoveSizeEnd(Mocks::Window(), POINT{ 0, 0 }); + + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(MoveSizeEndInvalidPoint) + { + auto zoneWindow = InitZoneWindowWithActiveZoneSet(); + + const auto window = Mocks::Window(); + zoneWindow->MoveSizeEnter(window, true); + + const auto expected = S_OK; + const auto actual = zoneWindow->MoveSizeEnd(window, POINT{ -1, -1 }); + Assert::AreEqual(expected, actual); + + const auto zoneSet = zoneWindow->ActiveZoneSet(); + zoneSet->MoveWindowIntoZoneByIndex(window, Mocks::Window(), false); + const auto actualZoneIndex = zoneSet->GetZoneIndexFromWindow(window); + Assert::AreNotEqual(-1, actualZoneIndex); //with invalid point zone remains the same + } + + TEST_METHOD(MoveSizeCancel) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + + const auto expected = S_OK; + const auto actual = m_zoneWindow->MoveSizeCancel(); + + Assert::AreEqual(expected, actual); + } + + TEST_METHOD(MoveWindowIntoZoneByIndexNoActiveZoneSet) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + + m_zoneWindow->MoveWindowIntoZoneByIndex(Mocks::Window(), 0); + } + + TEST_METHOD(MoveWindowIntoZoneByIndex) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + m_zoneWindow->MoveWindowIntoZoneByIndex(Mocks::Window(), 0); + + const auto actual = m_zoneWindow->ActiveZoneSet(); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionNoActiveZoneSet) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + + m_zoneWindow->MoveWindowIntoZoneByIndex(Mocks::Window(), 0); + } + + TEST_METHOD(MoveWindowIntoZoneByDirection) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + const auto window = Mocks::WindowCreate(m_hInst); + m_zoneWindow->MoveWindowIntoZoneByDirection(window, VK_RIGHT); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)1, actualAppZoneHistory.size()); + const auto actual = actualAppZoneHistory.begin()->second; + Assert::AreEqual(0, actual.zoneIndex); + } + + TEST_METHOD(MoveWindowIntoZoneByDirectionManyTimes) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + const auto window = Mocks::WindowCreate(m_hInst); + m_zoneWindow->MoveWindowIntoZoneByDirection(window, VK_RIGHT); + m_zoneWindow->MoveWindowIntoZoneByDirection(window, VK_RIGHT); + m_zoneWindow->MoveWindowIntoZoneByDirection(window, VK_RIGHT); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)1, actualAppZoneHistory.size()); + const auto actual = actualAppZoneHistory.begin()->second; + Assert::AreEqual(2, actual.zoneIndex); + } + + TEST_METHOD(SaveWindowProcessToZoneIndexNoActiveZoneSet) + { + m_zoneWindow = MakeZoneWindow(m_hostPtr, m_hInst, m_monitor, m_uniqueId.str(), false); + Assert::IsNull(m_zoneWindow->ActiveZoneSet()); + + m_zoneWindow->SaveWindowProcessToZoneIndex(Mocks::Window()); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::IsTrue(actualAppZoneHistory.empty()); + } + + TEST_METHOD(SaveWindowProcessToZoneIndexNullptrWindow) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + m_zoneWindow->SaveWindowProcessToZoneIndex(nullptr); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::IsTrue(actualAppZoneHistory.empty()); + } + + TEST_METHOD(SaveWindowProcessToZoneIndexNoWindowAdded) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + auto window = Mocks::WindowCreate(m_hInst); + auto zone = MakeZone(RECT{ 0, 0, 100, 100 }); + m_zoneWindow->ActiveZoneSet()->AddZone(zone); + + m_zoneWindow->SaveWindowProcessToZoneIndex(window); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::IsTrue(actualAppZoneHistory.empty()); + } + + TEST_METHOD(SaveWindowProcessToZoneIndexNoWindowAddedWithFilledAppZoneHistory) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + const auto window = Mocks::WindowCreate(m_hInst); + const auto processPath = get_process_path(window); + const auto deviceId = m_zoneWindow->UniqueId(); + const auto zoneSetId = m_zoneWindow->ActiveZoneSet()->Id(); + + //fill app zone history map + Assert::IsTrue(m_fancyZonesData.SetAppLastZone(window, deviceId, GuidString(zoneSetId), 0)); + Assert::AreEqual((size_t)1, m_fancyZonesData.GetAppZoneHistoryMap().size()); + Assert::AreEqual(0, m_fancyZonesData.GetAppZoneHistoryMap().at(processPath).zoneIndex); + + //add zone without window + const auto zone = MakeZone(RECT{ 0, 0, 100, 100 }); + m_zoneWindow->ActiveZoneSet()->AddZone(zone); + + m_zoneWindow->SaveWindowProcessToZoneIndex(window); + Assert::AreEqual((size_t)1, m_fancyZonesData.GetAppZoneHistoryMap().size()); + Assert::AreEqual(0, m_fancyZonesData.GetAppZoneHistoryMap().at(processPath).zoneIndex); + } + + TEST_METHOD(SaveWindowProcessToZoneIndexWindowAdded) + { + m_zoneWindow = InitZoneWindowWithActiveZoneSet(); + Assert::IsNotNull(m_zoneWindow->ActiveZoneSet()); + + auto window = Mocks::WindowCreate(m_hInst); + const auto processPath = get_process_path(window); + const auto deviceId = m_zoneWindow->UniqueId(); + const auto zoneSetId = m_zoneWindow->ActiveZoneSet()->Id(); + + auto zone = MakeZone(RECT{ 0, 0, 100, 100 }); + zone->AddWindowToZone(window, Mocks::Window(), false); + m_zoneWindow->ActiveZoneSet()->AddZone(zone); + + //fill app zone history map + Assert::IsTrue(m_fancyZonesData.SetAppLastZone(window, deviceId, GuidString(zoneSetId), 2)); + Assert::AreEqual((size_t)1, m_fancyZonesData.GetAppZoneHistoryMap().size()); + Assert::AreEqual(2, m_fancyZonesData.GetAppZoneHistoryMap().at(processPath).zoneIndex); + + m_zoneWindow->SaveWindowProcessToZoneIndex(window); + + const auto actualAppZoneHistory = m_fancyZonesData.GetAppZoneHistoryMap(); + Assert::AreEqual((size_t)1, actualAppZoneHistory.size()); + const auto expected = m_zoneWindow->ActiveZoneSet()->GetZoneIndexFromWindow(window); + const auto actual = m_fancyZonesData.GetAppZoneHistoryMap().at(processPath).zoneIndex; + Assert::AreEqual(expected, actual); + } + }; }