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 <zykovas91@gmail.com>
This commit is contained in:
stefansjfw 2020-02-10 14:59:51 +01:00 committed by GitHub
parent a5e3207715
commit 53f830bb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 8496 additions and 1905 deletions

View File

@ -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);

View File

@ -71,7 +71,6 @@
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>fancyzones.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@ -96,7 +95,6 @@
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>fancyzones.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
@ -121,7 +119,6 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="fancyzones.def" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>

View File

@ -26,7 +26,6 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="fancyzones.def" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\lib\fancyzones.rc" />

View File

@ -5,7 +5,6 @@
#include <interface/lowlevel_keyboard_event_data.h>
#include <interface/win_hook_event_data.h>
#include <lib/ZoneSet.h>
#include <lib/RegistryHelpers.h>
#include <lib/resource.h>
#include <lib/trace.h>
@ -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<BYTE*>(&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<IZoneSet> 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<HINSTANCE>(&__ImageBase), m_settings.get());
m_app = MakeFancyZones(reinterpret_cast<HINSTANCE>(&__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<HINSTANCE>(&__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);

View File

@ -1,4 +0,0 @@
LIBRARY fancyzones.dll
EXPORTS
PersistZoneSet PRIVATE

View File

@ -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)

View File

@ -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

View File

@ -47,6 +47,7 @@
<Reference Include="System.Configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
@ -194,6 +195,9 @@
<PackageReference Include="MahApps.Metro">
<Version>2.0.0-alpha0455</Version>
</PackageReference>
<PackageReference Include="System.Text.Json">
<Version>4.6.0</Version>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers">
<Version>1.1.118</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -208,4 +212,4 @@
<Resource Include="images\Merge.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@ -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)
{

View File

@ -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<Int32Rect> 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();
}
}
}

View File

@ -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<int> FreeZones { get; } = new List<int>();
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<int> mapping = new List<int>();
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();
}
}
}

View File

@ -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<LayoutModel> LoadCustomModels()
{
_customModels = new ObservableCollection<LayoutModel>();
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<Int32Rect> zones = new List<Int32Rect>();
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<LayoutModel> _customModels = null;
private static ushort _maxId = 0;
private static List<string> _deletedCustomModels = new List<string>();
// 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<Native.PersistZoneSet>(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();
}
}
}

View File

@ -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<LayoutModel>(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<LayoutModel> DefaultModels { get; }
public ObservableCollection<LayoutModel> CustomModels
public static ObservableCollection<LayoutModel> CustomModels
{
get
{
@ -393,14 +440,14 @@ namespace FancyZonesEditor
}
}
private ObservableCollection<LayoutModel> _customModels;
private static ObservableCollection<LayoutModel> _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

View File

@ -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 <functional>
#include <common/common.h>
#include <lib\util.h>
enum class DisplayChangeType
{
WorkArea,
DisplayChange,
VirtualDesktop,
Editor,
Initialization
};
namespace std
{
template<> struct hash<GUID>
template<>
struct hash<GUID>
{
size_t operator()(const GUID& Value) const
{
@ -26,9 +39,9 @@ namespace std
struct FancyZones : public winrt::implements<FancyZones, IFancyZones, IFancyZonesCallback, IZoneWindowHost>
{
public:
FancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept
: m_hinstance(hinstance)
, m_settings(settings)
FancyZones(HINSTANCE hinstance, const winrt::com_ptr<IFancyZonesSettings>& 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<typename T>
require_read_lock(const std::shared_lock<T>& lock) { lock; }
require_read_lock(const std::shared_lock<T>& lock)
{
lock;
}
template<typename T>
require_read_lock(const std::unique_lock<T>& lock) { lock; }
require_read_lock(const std::unique_lock<T>& lock)
{
lock;
}
};
struct require_write_lock
{
template<typename T>
require_write_lock(const std::unique_lock<T>& lock) { lock; }
require_write_lock(const std::unique_lock<T>& 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<HMONITOR, winrt::com_ptr<IZoneWindow>> m_zoneWindowMap; // Map of monitor to ZoneWindow (one per monitor)
winrt::com_ptr<IZoneWindow> m_zoneWindowMoveSize; // "Active" ZoneWindow, where the move/size is happening. Will update as drag moves between monitors.
IFancyZonesSettings* m_settings{};
winrt::com_ptr<IFancyZonesSettings> m_settings{};
GUID m_currentVirtualDesktopId{}; // UUID of the current virtual desktop. Is GUID_NULL until first VD switch per session.
std::unordered_map<GUID, bool> 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<UINT_PTR>(monitor)) + L" " +
editorLocation + L" " +
iter->second->WorkAreaKey() + L" " +
std::to_wstring(static_cast<float>(dpi_x) / DPIAware::DEFAULT_DPI);
/*1*/ std::to_wstring(reinterpret_cast<UINT_PTR>(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<LPARAM>(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(&currentVirtualDesktopId)))
{
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<FancyZones*>(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<int>(reinterpret_cast<UINT_PTR>(::GetProp(window, ZONE_STAMP)));
if (i != 0)
{
// i is off by 1 since 0 is special.
auto strongThis = reinterpret_cast<FancyZones*>(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<BYTE[]> buffer = std::make_unique<BYTE[]>(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<GUID, bool> temp;
temp.reserve(bufferCapacity / guidSize);
for (size_t i = 0; i < bufferCapacity; i += guidSize) {
GUID *guid = reinterpret_cast<GUID*>(buffer.get() + i);
for (size_t i = 0; i < bufferCapacity; i += guidSize)
{
GUID* guid = reinterpret_cast<GUID*>(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<IFancyZones> 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<IFancyZones> MakeFancyZones(HINSTANCE hinstance, const winrt::com_ptr<IFancyZonesSettings>& settings) noexcept
{
if (!settings)
{
return nullptr;
}
return winrt::make_self<FancyZones>(hinstance, settings);
}

View File

@ -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<IFancyZones> MakeFancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept;
winrt::com_ptr<IFancyZones> MakeFancyZones(HINSTANCE hinstance, const winrt::com_ptr<IFancyZonesSettings>& settings) noexcept;

View File

@ -92,6 +92,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="FancyZones.h" />
<ClInclude Include="JsonHelpers.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="RegistryHelpers.h" />
<ClInclude Include="resource.h" />
@ -104,12 +105,14 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="FancyZones.cpp" />
<ClCompile Include="JsonHelpers.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Settings.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="util.cpp" />
<ClCompile Include="Zone.cpp" />
<ClCompile Include="ZoneSet.cpp" />
<ClCompile Include="ZoneWindow.cpp" />

View File

@ -45,6 +45,9 @@
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonHelpers.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@ -68,6 +71,12 @@
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="JsonHelpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="util.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="fancyzones.rc">
@ -76,6 +85,5 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="fancyzones.def" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,950 @@
#include "pch.h"
#include "JsonHelpers.h"
#include "RegistryHelpers.h"
#include "ZoneSet.h"
#include <common/common.h>
#include <shlwapi.h>
#include <filesystem>
#include <fstream>
#include <regex>
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<int>& vec)
{
json::JsonArray arr;
for (const auto& val : vec)
{
arr.Append(json::JsonValue::CreateNumberValue(val));
}
return arr;
}
std::vector<int> JsonArrayToNumVec(const json::JsonArray& arr)
{
std::vector<int> vec;
for (const auto& val : arr)
{
vec.emplace_back(static_cast<int>(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<BYTE*>(&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<bool>(showSpacing), static_cast<int>(spacing), static_cast<int>(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<BYTE*>(&data), &dataSize) == ERROR_SUCCESS)
{
CustomZoneSetData zoneSetData;
zoneSetData.name = std::wstring{ value };
zoneSetData.type = static_cast<CustomLayoutType>(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<std::wstring, ZoneSetData> 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> 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> AppZoneHistoryJSON::FromJson(const json::JsonObject& zoneSet)
{
try
{
AppZoneHistoryJSON result;
result.appPath = zoneSet.GetNamedString(L"app-path");
result.data.zoneIndex = static_cast<int>(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> 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<int>(device.GetNamedNumber(L"editor-spacing"));
result.data.zoneCount = static_cast<int>(
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> CanvasLayoutInfo::FromJson(const json::JsonObject& infoJson)
{
try
{
CanvasLayoutInfo info;
info.referenceWidth = static_cast<int>(infoJson.GetNamedNumber(L"ref-width"));
info.referenceHeight = static_cast<int>(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<int>(zoneJson.GetNamedNumber(L"X"));
const int y = static_cast<int>(zoneJson.GetNamedNumber(L"Y"));
const int width = static_cast<int>(zoneJson.GetNamedNumber(L"width"));
const int height = static_cast<int>(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> GridLayoutInfo::FromJson(const json::JsonObject& infoJson)
{
try
{
GridLayoutInfo info(GridLayoutInfo::Minimal{});
info.m_rows = static_cast<int>(infoJson.GetNamedNumber(L"rows"));
info.m_columns = static_cast<int>(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<CanvasLayoutInfo>(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<GridLayoutInfo>(customZoneSet.data.info);
result.SetNamedValue(L"info", GridLayoutInfo::ToJson(gridInfo));
break;
}
}
return result;
}
std::optional<CustomZoneSetJSON> 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;
}
}
}

View File

@ -0,0 +1,240 @@
#pragma once
#include <common/settings_helpers.h>
#include <common/json.h>
#include <string>
#include <strsafe.h>
#include <unordered_map>
#include <variant>
#include <optional>
#include <vector>
#include <winnt.h>
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<CanvasLayoutInfo::Rect> zones;
static json::JsonObject ToJson(const CanvasLayoutInfo& canvasInfo);
static std::optional<CanvasLayoutInfo> FromJson(const json::JsonObject& infoJson);
};
class GridLayoutInfo
{
public:
struct Minimal
{
int rows;
int columns;
};
struct Full
{
int rows;
int columns;
const std::vector<int>& rowsPercents;
const std::vector<int>& columnsPercents;
const std::vector<std::vector<int>>& cellChildMap;
};
GridLayoutInfo(const Minimal& info);
GridLayoutInfo(const Full& info);
~GridLayoutInfo() = default;
static json::JsonObject ToJson(const GridLayoutInfo& gridInfo);
static std::optional<GridLayoutInfo> FromJson(const json::JsonObject& infoJson);
inline std::vector<int>& rowsPercents() { return m_rowsPercents; };
inline std::vector<int>& columnsPercents() { return m_columnsPercents; };
inline std::vector<std::vector<int>>& cellChildMap() { return m_cellChildMap; };
inline int rows() const { return m_rows; }
inline int columns() const { return m_columns; }
inline const std::vector<int>& rowsPercents() const { return m_rowsPercents; };
inline const std::vector<int>& columnsPercents() const { return m_columnsPercents; };
inline const std::vector<std::vector<int>>& cellChildMap() const { return m_cellChildMap; };
private:
int m_rows;
int m_columns;
std::vector<int> m_rowsPercents;
std::vector<int> m_columnsPercents;
std::vector<std::vector<int>> m_cellChildMap;
};
struct CustomZoneSetData
{
std::wstring name;
CustomLayoutType type;
std::variant<CanvasLayoutInfo, GridLayoutInfo> info;
};
struct CustomZoneSetJSON
{
std::wstring uuid;
CustomZoneSetData data;
static json::JsonObject ToJson(const CustomZoneSetJSON& device);
static std::optional<CustomZoneSetJSON> 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<ZoneSetData> 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<AppZoneHistoryJSON> 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<DeviceInfoJSON> FromJson(const json::JsonObject& device);
};
class FancyZonesData
{
public:
FancyZonesData();
const std::wstring& GetPersistFancyZonesJSONPath() const;
json::JsonObject GetPersistFancyZonesJSON();
inline const std::unordered_map<std::wstring, DeviceInfoData>& GetDeviceInfoMap() const
{
return deviceInfoMap;
}
inline const std::unordered_map<std::wstring, CustomZoneSetData>& GetCustomZoneSetsMap() const
{
return customZoneSetsMap;
}
inline const std::unordered_map<std::wstring, AppZoneHistoryData>& 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<std::wstring, ZoneSetData> appliedZoneSetsMap{};
std::unordered_map<std::wstring, AppZoneHistoryData> appZoneHistoryMap{};
std::unordered_map<std::wstring, DeviceInfoData> deviceInfoMap{};
std::unordered_map<std::wstring, CustomZoneSetData> customZoneSetsMap{};
std::wstring activeDeviceId;
std::wstring jsonFilePath;
};
FancyZonesData& FancyZonesDataInstance();
}

View File

@ -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<INT>(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<DWORD>(wcslen(value)), SHREGSET_FORCE_HKCU);
}
template<typename t>
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<typename t>
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;

View File

@ -9,11 +9,11 @@ struct FancyZonesSettings : winrt::implements<FancyZonesSettings, IFancyZonesSet
public:
FancyZonesSettings(HINSTANCE hinstance, PCWSTR name)
: m_hinstance(hinstance)
, m_name(name)
, m_moduleName(name)
{
LoadSettings(name, true /*fromFile*/);
LoadSettings(name, true);
}
IFACEMETHODIMP_(void) SetCallback(IFancyZonesCallback* callback) { m_callback = callback; }
IFACEMETHODIMP_(bool) GetConfig(_Out_ PWSTR buffer, _Out_ int *buffer_sizeg) noexcept;
IFACEMETHODIMP_(void) SetConfig(PCWSTR config) noexcept;
@ -26,7 +26,7 @@ private:
IFancyZonesCallback* m_callback{};
const HINSTANCE m_hinstance;
PCWSTR m_name{};
PCWSTR m_moduleName{};
Settings m_settings;
@ -54,7 +54,7 @@ private:
IFACEMETHODIMP_(bool) FancyZonesSettings::GetConfig(_Out_ PWSTR buffer, _Out_ int *buffer_size) noexcept
{
PowerToysSettings::Settings settings(m_hinstance, m_name);
PowerToysSettings::Settings settings(m_hinstance, m_moduleName);
// Pass a string literal or a resource id to Settings::set_description().
settings.set_description(IDS_SETTING_DESCRIPTION);
@ -84,9 +84,9 @@ IFACEMETHODIMP_(bool) FancyZonesSettings::GetConfig(_Out_ PWSTR buffer, _Out_ in
return settings.serialize_to_buffer(buffer, buffer_size);
}
IFACEMETHODIMP_(void) FancyZonesSettings::SetConfig(PCWSTR config) noexcept try
IFACEMETHODIMP_(void) FancyZonesSettings::SetConfig(PCWSTR serializedPowerToysSettingsJson) noexcept try
{
LoadSettings(config, false /*fromFile*/);
LoadSettings(serializedPowerToysSettingsJson, false /*fromFile*/);
SaveSettings();
if (m_callback)
{
@ -112,7 +112,7 @@ CATCH_LOG();
void FancyZonesSettings::LoadSettings(PCWSTR config, bool fromFile) noexcept try
{
PowerToysSettings::PowerToyValues values = fromFile ?
PowerToysSettings::PowerToyValues::load_from_settings_file(m_name) :
PowerToysSettings::PowerToyValues::load_from_settings_file(m_moduleName) :
PowerToysSettings::PowerToyValues::from_json_string(config);
for (auto const& setting : m_configBools)
@ -165,7 +165,7 @@ CATCH_LOG();
void FancyZonesSettings::SaveSettings() noexcept try
{
PowerToysSettings::PowerToyValues values(m_name);
PowerToysSettings::PowerToyValues values(m_moduleName);
for (auto const& setting : m_configBools)
{

View File

@ -25,7 +25,7 @@ interface __declspec(uuid("{BA4E77C4-6F44-4C5D-93D3-CBDE880495C2}")) IFancyZones
{
IFACEMETHOD_(void, SetCallback)(interface IFancyZonesCallback* callback) = 0;
IFACEMETHOD_(bool, GetConfig)(_Out_ PWSTR buffer, _Out_ int *buffer_size) = 0;
IFACEMETHOD_(void, SetConfig)(PCWSTR config) = 0;
IFACEMETHOD_(void, SetConfig)(PCWSTR serializedPowerToysSettingsJson) = 0;
IFACEMETHOD_(void, CallCustomAction)(PCWSTR action) = 0;
IFACEMETHOD_(Settings, GetSettings)() = 0;
};

View File

@ -129,7 +129,7 @@ void Zone::StampZone(HWND window, bool stamp) noexcept
}
}
winrt::com_ptr<IZone> MakeZone(RECT zoneRect) noexcept
winrt::com_ptr<IZone> MakeZone(const RECT& zoneRect) noexcept
{
return winrt::make_self<Zone>(zoneRect);
}

View File

@ -11,4 +11,4 @@ interface __declspec(uuid("{8228E934-B6EF-402A-9892-15A1441BF8B0}")) IZone : pub
IFACEMETHOD_(size_t, Id)() = 0;
};
winrt::com_ptr<IZone> MakeZone(RECT zoneRect) noexcept;
winrt::com_ptr<IZone> MakeZone(const RECT& zoneRect) noexcept;

View File

@ -1,12 +1,113 @@
#include "pch.h"
#include "util.h"
#include "lib/ZoneSet.h"
#include "lib/RegistryHelpers.h"
#include <common/dpi_aware.h>
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<ZoneSet, IZoneSet>
{
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<IZone> zone) noexcept;
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneFromPoint(POINT pt) noexcept;
IFACEMETHODIMP_(int) GetZoneIndexFromWindow(HWND window) noexcept;
IFACEMETHODIMP_(std::vector<winrt::com_ptr<IZone>>) 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<IZone>)
ZoneFromPoint(POINT pt) noexcept;
IFACEMETHODIMP_(int)
GetZoneIndexFromWindow(HWND window) noexcept;
IFACEMETHODIMP_(std::vector<winrt::com_ptr<IZone>>)
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<IZone> ZoneFromWindow(HWND window) noexcept;
std::vector<winrt::com_ptr<IZone>> m_zones;
@ -44,7 +162,8 @@ IFACEMETHODIMP ZoneSet::AddZone(winrt::com_ptr<IZone> zone) noexcept
return S_OK;
}
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneSet::ZoneFromPoint(POINT pt) noexcept
IFACEMETHODIMP_(winrt::com_ptr<IZone>)
ZoneSet::ZoneFromPoint(POINT pt) noexcept
{
winrt::com_ptr<IZone> smallestKnownZone = nullptr;
// To reduce redundant calculations, we will store the last known zones area.
@ -61,16 +180,16 @@ IFACEMETHODIMP_(winrt::com_ptr<IZone>) 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<smallestKnownZoneArea)
if (newZoneArea < smallestKnownZoneArea)
{
smallestKnownZone = zone;
newZoneArea = smallestKnownZoneArea;
smallestKnownZoneArea = newZoneArea;
}
}
}
@ -80,38 +199,8 @@ IFACEMETHODIMP_(winrt::com_ptr<IZone>) 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<DWORD>(zoneCount);
int i = 0;
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++)
{
winrt::com_ptr<IZone> zone = iter->as<IZone>();
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<BYTE*>(&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<int>(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<IZone> oldZone;
winrt::com_ptr<IZone> newZone;
if (m_zones.empty())
{
return;
}
winrt::com_ptr<IZone> oldZone = nullptr;
winrt::com_ptr<IZone> 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<int>(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<JSONHelpers::CanvasLayoutInfo>(zoneSet.info))
{
const auto& zoneSetInfo = std::get<JSONHelpers::CanvasLayoutInfo>(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<JSONHelpers::GridLayoutInfo>(zoneSet.info))
{
const auto& info = std::get<JSONHelpers::GridLayoutInfo>(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<IZone> ZoneSet::ZoneFromWindow(HWND window) noexcept
{
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++)

View File

@ -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<IZone> zone) = 0;
IFACEMETHOD_(winrt::com_ptr<IZone>, ZoneFromPoint)(POINT pt) = 0;
IFACEMETHOD_(int, GetZoneIndexFromWindow)(HWND window) = 0;
IFACEMETHOD_(std::vector<winrt::com_ptr<IZone>>, 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{};
};

File diff suppressed because it is too large Load Diff

View File

@ -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<IZoneWindow> MakeZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor,
PCWSTR deviceId, PCWSTR virtualDesktopId, bool flashZones) noexcept;
const std::wstring& uniqueId, bool flashZones) noexcept;

View File

@ -0,0 +1,27 @@
#include "pch.h"
#include "util.h"
#include <common/dpi_aware.h>
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<GetDpiForMonitorInternalFunc>(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;
}

View File

@ -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<int>(opacity * 2.55);
}
UINT GetDpiForMonitor(HMONITOR monitor) noexcept;

View File

@ -0,0 +1,475 @@
#include "pch.h"
#include <filesystem>
#include <lib/FancyZones.h>
#include <lib/Settings.h>
#include <common/common.h>
#include "util.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(FancyZonesUnitTests)
{
HINSTANCE m_hInst;
winrt::com_ptr<IFancyZonesSettings> 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<std::thread> threads;
std::atomic<int> 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<std::thread> threads;
std::atomic<int> 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<std::thread> threads;
std::atomic<int> counter = 0;
const int expectedCount = 20;
auto func = [&]() {
auto idHash = std::hash<std::thread::id>()(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<IFancyZonesSettings> m_settings = nullptr;
winrt::com_ptr<IZoneWindowHost> 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<IZoneWindowHost>();
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<IFancyZonesSettings> m_settings = nullptr;
winrt::com_ptr<IFancyZonesCallback> 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<IFancyZonesCallback>();
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));
}
}
};
}

View File

@ -0,0 +1,714 @@
#include "pch.h"
#include <filesystem>
#include <lib/Settings.h>
#include <lib/FancyZones.h>
#include <common/settings_helpers.h>
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<IFancyZonesSettings> m_settings = nullptr;
PCWSTR m_moduleName = L"FancyZonesTest";
struct FZCallback : public winrt::implements<FZCallback, IFancyZonesCallback>
{
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<IFancyZonesSettings> 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));
}
};
}

File diff suppressed because it is too large Load Diff

View File

@ -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());
}
}
;
}

View File

@ -96,12 +96,15 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="FancyZones.Spec.cpp" />
<ClCompile Include="FancyZonesSettings.Spec.cpp" />
<ClCompile Include="JsonHelpers.Tests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="RegistryHelpers.Spec.cpp" />
<ClCompile Include="Util.Spec.cpp" />
<ClCompile Include="Util.cpp" />
<ClCompile Include="Zone.Spec.cpp" />
<ClCompile Include="ZoneSet.Spec.cpp" />
<ClCompile Include="ZoneWindow.Spec.cpp" />

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
@ -24,15 +24,24 @@
<ClCompile Include="Zone.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="RegistryHelpers.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Util.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoneWindow.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="JsonHelpers.Tests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Util.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FancyZonesSettings.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FancyZones.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">

View File

@ -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<Mocks::HwndCreator*>(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<std::mutex> 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<std::mutex> 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();
}
}

View File

@ -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<HINSTANCE>(++s_nextInstance);
}
}
HWND WindowCreate(HINSTANCE hInst);
}

View File

@ -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<IZone> 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<IZone>& 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<IZone> 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<IZone> zone = MakeZone(zoneRect);
HWND newWindow = Mocks::Window();
void addMany(const winrt::com_ptr<IZone>& 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<IZone> 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<IZone> zone = MakeZone(zoneRect);
HWND newWindow = Mocks::Window();
zone->RemoveWindowFromZone(newWindow, false);
}
}
;
TEST_METHOD(TestCreateZoneZeroRect)
{
RECT zoneRect{ 0, 0, 0, 0 };
winrt::com_ptr<IZone> zone = MakeZone(zoneRect);
Assert::IsNotNull(&zone);
CustomAssert::AreEqual(zoneRect, zone->GetZoneRect());
}
TEST_METHOD(GetSetId)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
constexpr size_t id = 10;
zone->SetId(id);
Assert::AreEqual(zone->Id(), id);
}
TEST_METHOD(IsEmpty)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
Assert::IsTrue(zone->IsEmpty());
}
TEST_METHOD(IsNonEmptyStampTrue)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
addWindow(zone, true);
Assert::IsFalse(zone->IsEmpty());
}
TEST_METHOD(IsNonEmptyStampFalse)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
addWindow(zone, false);
Assert::IsFalse(zone->IsEmpty());
}
TEST_METHOD(IsNonEmptyManyWindows)
{
winrt::com_ptr<IZone> 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<IZone> 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<IZone> zone = MakeZone(m_zoneRect);
addMany(zone);
Assert::IsFalse(zone->IsEmpty());
}
TEST_METHOD(ContainsWindowEmpty)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
HWND newWindow = Mocks::WindowCreate(m_hInst);
Assert::IsFalse(zone->ContainsWindow(newWindow));
}
TEST_METHOD(ContainsWindowNot)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
addMany(zone);
HWND newWindow = Mocks::WindowCreate(m_hInst);
Assert::IsFalse(zone->ContainsWindow(newWindow));
}
TEST_METHOD(ContainsWindowStampTrue)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
HWND window = addWindow(zone, true);
Assert::IsTrue(zone->ContainsWindow(window));
}
TEST_METHOD(ContainsWindowStampFalse)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
HWND window = addWindow(zone, false);
Assert::IsTrue(zone->ContainsWindow(window));
}
TEST_METHOD(ContainsWindowManyWindows)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
HWND zoneWindow = Mocks::WindowCreate(m_hInst);
std::vector<HWND> 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<IZone> zone = MakeZone(m_zoneRect);
HWND window = Mocks::WindowCreate(m_hInst);
std::vector<HWND> 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<IZone> zone = MakeZone(m_zoneRect);
std::vector<HWND> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> 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<IZone> zone = MakeZone(m_zoneRect);
std::vector<HWND> 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<IZone> 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<IZone> 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<IZone> 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<IZone> zone = MakeZone(m_zoneRect);
HWND window = addWindow(zone, true);
HANDLE actual = GetProp(window, ZONE_STAMP);
Assert::IsNull(actual);
}
TEST_METHOD(StampFalse)
{
winrt::com_ptr<IZone> zone = MakeZone(m_zoneRect);
HWND window = addWindow(zone, false);
HANDLE actual = GetProp(window, ZONE_STAMP);
Assert::IsNull(actual);
}
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,14 @@
#include "pch.h"
#include <filesystem>
#include <common/common.h>
#include <lib/util.h>
#include <lib/ZoneSet.h>
#include <lib/ZoneWindow.h>
#include <lib/FancyZones.h>
#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<IZoneWindow> 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<IZoneWindow> zoneWindow = MakeZoneWindow(dynamic_cast<IZoneWindowHost*>(&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<IZoneWindow> zoneWindow = MakeZoneWindow(dynamic_cast<IZoneWindowHost*>(&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<IZoneWindow> 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<IZoneWindow> 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<IZoneWindow> 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<int>(ZoneSetLayoutType::Focus); type < static_cast<int>(ZoneSetLayoutType::Custom); type++)
{
const auto expectedZoneSet = ZoneSetData{ CreateGuidString(), static_cast<ZoneSetLayoutType>(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);
}
};
}