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
41 changed files with 8496 additions and 1905 deletions

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