[FancyZones] Editor multi monitor support (#6562)

Co-authored-by: Enrico Giordani <enrico.giordani@gmail.com>
Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com>
This commit is contained in:
Seraphima Zykova
2020-11-17 11:38:19 +03:00
committed by GitHub
parent 687fc2e169
commit b8e5ccfb7b
88 changed files with 4887 additions and 1503 deletions

View File

@@ -1,10 +1,9 @@
// 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.IO.Abstractions;
using System.Text.Json;
using System.Windows;
@@ -14,33 +13,21 @@ namespace FancyZonesEditor.Models
// Free form Layout Model, which specifies independent zone rects
public class CanvasLayoutModel : LayoutModel
{
// Localizable strings
private const string ErrorPersistingCanvasLayout = "Error persisting canvas layout";
// Non-localizable strings
private const string ModelTypeID = "canvas";
public CanvasLayoutModel(string uuid, string name, LayoutType type, IList<Int32Rect> zones, int workAreaWidth, int workAreaHeight)
public Rect CanvasRect { get; private set; }
public CanvasLayoutModel(string uuid, string name, LayoutType type, IList<Int32Rect> zones, int width, int height)
: base(uuid, name, type)
{
lastWorkAreaWidth = workAreaWidth;
lastWorkAreaHeight = workAreaHeight;
IsScaled = false;
if (ShouldScaleLayout())
{
ScaleLayout(zones);
}
else
{
Zones = zones;
}
Zones = zones;
CanvasRect = new Rect(new Size(width, height));
}
public CanvasLayoutModel(string name, LayoutType type)
: base(name, type)
{
IsScaled = false;
}
public CanvasLayoutModel(string name)
@@ -51,12 +38,6 @@ namespace FancyZonesEditor.Models
// Zones - the list of all zones in this layout, described as independent rectangles
public IList<Int32Rect> Zones { get; private set; } = new List<Int32Rect>();
private int lastWorkAreaWidth = (int)Settings.WorkArea.Width;
private int lastWorkAreaHeight = (int)Settings.WorkArea.Height;
public bool IsScaled { get; private set; }
// RemoveZoneAt
// Removes the specified index from the Zones list, and fires a property changed notification for the Zones property
public void RemoveZoneAt(int index)
@@ -102,34 +83,6 @@ namespace FancyZonesEditor.Models
}
}
private bool ShouldScaleLayout()
{
// Scale if:
// - at least one dimension changed
// - orientation remained the same
return (lastWorkAreaHeight != Settings.WorkArea.Height || lastWorkAreaWidth != Settings.WorkArea.Width) &&
((lastWorkAreaHeight > lastWorkAreaWidth && Settings.WorkArea.Height > Settings.WorkArea.Width) ||
(lastWorkAreaWidth > lastWorkAreaHeight && Settings.WorkArea.Width > Settings.WorkArea.Height));
}
private void ScaleLayout(IList<Int32Rect> zones)
{
foreach (Int32Rect zone in zones)
{
double widthFactor = (double)Settings.WorkArea.Width / lastWorkAreaWidth;
double heightFactor = (double)Settings.WorkArea.Height / lastWorkAreaHeight;
int scaledX = (int)(zone.X * widthFactor);
int scaledY = (int)(zone.Y * heightFactor);
int scaledWidth = (int)(zone.Width * widthFactor);
int scaledHeight = (int)(zone.Height * heightFactor);
Zones.Add(new Int32Rect(scaledX, scaledY, scaledWidth, scaledHeight));
}
lastWorkAreaHeight = (int)Settings.WorkArea.Height;
lastWorkAreaWidth = (int)Settings.WorkArea.Width;
IsScaled = true;
}
private struct Zone
{
public int X { get; set; }
@@ -165,13 +118,15 @@ namespace FancyZonesEditor.Models
// Implements the LayoutModel.PersistData abstract method
protected override void PersistData()
{
AddCustomLayout(this);
CanvasLayoutInfo layoutInfo = new CanvasLayoutInfo
{
RefWidth = lastWorkAreaWidth,
RefHeight = lastWorkAreaHeight,
RefWidth = (int)CanvasRect.Width,
RefHeight = (int)CanvasRect.Height,
Zones = new Zone[Zones.Count],
};
for (int i = 0; i < Zones.Count; ++i)
{
Zone zone = new Zone
@@ -187,26 +142,19 @@ namespace FancyZonesEditor.Models
CanvasLayoutJson jsonObj = new CanvasLayoutJson
{
Uuid = "{" + Guid.ToString().ToUpper() + "}",
Uuid = Uuid,
Name = Name,
Type = ModelTypeID,
Info = layoutInfo,
};
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
};
try
{
string jsonString = JsonSerializer.Serialize(jsonObj, options);
FileSystem.File.WriteAllText(Settings.AppliedZoneSetTmpFile, jsonString);
}
catch (Exception ex)
{
ShowExceptionMessageBox(ErrorPersistingCanvasLayout, ex);
}
string jsonString = JsonSerializer.Serialize(jsonObj, options);
AddCustomLayoutJson(JsonSerializer.Deserialize<JsonElement>(jsonString));
SerializeCreatedCustomZonesets();
}
}
}

View File

@@ -0,0 +1,81 @@
// 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.Text;
using System.Windows;
namespace FancyZonesEditor.Utils
{
public class Device
{
public string Id { get; set; }
public Rect UnscaledBounds { get; private set; }
public Rect ScaledBounds { get; private set; }
public Rect WorkAreaRect { get; private set; }
public int Dpi { get; set; }
public bool Primary { get; private set; }
public Device(string id, int dpi, Rect bounds, Rect workArea, bool primary)
{
Id = id;
Dpi = dpi;
WorkAreaRect = workArea;
UnscaledBounds = bounds;
ScaledBounds = bounds;
Primary = primary;
}
public Device(Rect bounds, Rect workArea, bool primary)
{
WorkAreaRect = workArea;
UnscaledBounds = bounds;
ScaledBounds = bounds;
Primary = primary;
}
public void Scale(double scaleFactor)
{
WorkAreaRect = new Rect(Math.Round(WorkAreaRect.X * scaleFactor), Math.Round(WorkAreaRect.Y * scaleFactor), Math.Round(WorkAreaRect.Width * scaleFactor), Math.Round(WorkAreaRect.Height * scaleFactor));
ScaledBounds = new Rect(Math.Round(ScaledBounds.X * scaleFactor), Math.Round(ScaledBounds.Y * scaleFactor), Math.Round(ScaledBounds.Width * scaleFactor), Math.Round(ScaledBounds.Height * scaleFactor));
}
public double ScaleCoordinate(double coordinate)
{
float dpi = Dpi != 0 ? Dpi : 96f;
double scaleFactor = 96f / dpi;
return Math.Round(coordinate * scaleFactor);
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("ID: ");
sb.AppendLine(Id);
sb.Append("DPI: ");
sb.AppendLine(Dpi.ToString());
sb.Append("Is primary: ");
sb.AppendLine(Primary.ToString());
string workArea = string.Format("({0}, {1}, {2}, {3})", WorkAreaRect.X, WorkAreaRect.Y, WorkAreaRect.Width, WorkAreaRect.Height);
string bounds = string.Format("({0}, {1}, {2}, {3})", UnscaledBounds.X, UnscaledBounds.Y, UnscaledBounds.Width, UnscaledBounds.Height);
string scaledBounds = string.Format("({0}, {1}, {2}, {3})", ScaledBounds.X, ScaledBounds.Y, ScaledBounds.Width, ScaledBounds.Height);
sb.Append("Work area: ");
sb.AppendLine(workArea);
sb.Append("Unscaled bounds: ");
sb.AppendLine(bounds);
sb.Append("Scaled bounds: ");
sb.AppendLine(scaledBounds);
return sb.ToString();
}
}
}

View File

@@ -13,9 +13,6 @@ namespace FancyZonesEditor.Models
// Grid-styled Layout Model, which specifies rows, columns, percentage sizes, and row/column spans
public class GridLayoutModel : LayoutModel
{
// Localizable strings
private const string ErrorPersistingGridLayout = "Error persisting grid layout";
// Non-localizable strings
private const string ModelTypeID = "grid";
@@ -204,6 +201,8 @@ namespace FancyZonesEditor.Models
// Implements the LayoutModel.PersistData abstract method
protected override void PersistData()
{
AddCustomLayout(this);
GridLayoutInfo layoutInfo = new GridLayoutInfo
{
Rows = Rows,
@@ -212,6 +211,7 @@ namespace FancyZonesEditor.Models
ColumnsPercentage = ColumnPercents,
CellChildMap = new int[Rows][],
};
for (int row = 0; row < Rows; row++)
{
layoutInfo.CellChildMap[row] = new int[Columns];
@@ -223,7 +223,7 @@ namespace FancyZonesEditor.Models
GridLayoutJson jsonObj = new GridLayoutJson
{
Uuid = "{" + Guid.ToString().ToUpper() + "}",
Uuid = Uuid,
Name = Name,
Type = ModelTypeID,
Info = layoutInfo,
@@ -233,15 +233,9 @@ namespace FancyZonesEditor.Models
PropertyNamingPolicy = new DashCaseNamingPolicy(),
};
try
{
string jsonString = JsonSerializer.Serialize(jsonObj, options);
FileSystem.File.WriteAllText(Settings.AppliedZoneSetTmpFile, jsonString);
}
catch (Exception ex)
{
ShowExceptionMessageBox(ErrorPersistingGridLayout, ex);
}
string jsonString = JsonSerializer.Serialize(jsonObj, options);
AddCustomLayoutJson(JsonSerializer.Deserialize<JsonElement>(jsonString));
SerializeCreatedCustomZonesets();
}
}
}

View File

@@ -6,77 +6,15 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.CompilerServices;
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
{
protected static readonly IFileSystem FileSystem = new FileSystem();
// Localizable strings
private const string ErrorMessageBoxTitle = "FancyZones Editor Exception Handler";
private const string ErrorMessageBoxMessage = "Please report the bug to ";
private const string ErrorLayoutMalformedData = "Layout '{0}' has malformed data";
private const string ErrorSerializingDeletedLayouts = "Error serializing deleted layouts";
private const string ErrorLoadingCustomLayouts = "Error loading custom layouts";
private const string ErrorApplyingLayout = "Error applying layout";
// Non-localizable strings
private const string NameStr = "name";
private const string CustomZoneSetsJsonTag = "custom-zone-sets";
private const string TypeJsonTag = "type";
private const string UuidJsonTag = "uuid";
private const string InfoJsonTag = "info";
private const string GridJsonTag = "grid";
private const string RowsJsonTag = "rows";
private const string ColumnsJsonTag = "columns";
private const string RowsPercentageJsonTag = "rows-percentage";
private const string ColumnsPercentageJsonTag = "columns-percentage";
private const string CellChildMapJsonTag = "cell-child-map";
private const string ZonesJsonTag = "zones";
private const string CanvasJsonTag = "canvas";
private const string RefWidthJsonTag = "ref-width";
private const string RefHeightJsonTag = "ref-height";
private const string XJsonTag = "X";
private const string YJsonTag = "Y";
private const string WidthJsonTag = "width";
private const string HeightJsonTag = "height";
private const string FocusJsonTag = "focus";
private const string PriorityGridJsonTag = "priority-grid";
private const string CustomJsonTag = "custom";
private const string PowerToysIssuesURL = "https://aka.ms/powerToysReportBug";
public static void ShowExceptionMessageBox(string message, Exception exception = null)
{
string fullMessage = ErrorMessageBoxMessage + PowerToysIssuesURL + " \n" + message;
if (exception != null)
{
fullMessage += ": " + exception.Message;
}
MessageBox.Show(fullMessage, ErrorMessageBoxTitle);
}
protected LayoutModel()
{
_guid = Guid.NewGuid();
@@ -136,6 +74,14 @@ namespace FancyZonesEditor.Models
private Guid _guid;
public string Uuid
{
get
{
return "{" + Guid.ToString().ToUpper() + "}";
}
}
// 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
public bool IsSelected
@@ -157,6 +103,25 @@ namespace FancyZonesEditor.Models
private bool _isSelected;
public bool IsApplied
{
get
{
return _isApplied;
}
set
{
if (_isApplied != value)
{
_isApplied = value;
FirePropertyChanged();
}
}
}
private bool _isApplied;
// implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
@@ -177,186 +142,52 @@ namespace FancyZonesEditor.Models
}
}
private struct DeletedCustomZoneSetsWrapper
// Adds new custom Layout
public void AddCustomLayout(LayoutModel model)
{
public List<string> DeletedCustomZoneSets { get; set; }
bool updated = false;
for (int i = 0; i < _customModels.Count && !updated; i++)
{
if (_customModels[i].Uuid == model.Uuid)
{
_customModels[i] = model;
updated = true;
}
}
if (!updated)
{
_customModels.Add(model);
}
}
// Add custom layouts json data that would be serialized to a temp file
public void AddCustomLayoutJson(JsonElement json)
{
_createdCustomLayouts.Add(json);
}
public static void SerializeDeletedCustomZoneSets()
{
DeletedCustomZoneSetsWrapper deletedLayouts = new DeletedCustomZoneSetsWrapper
{
DeletedCustomZoneSets = _deletedCustomModels,
};
App.FancyZonesEditorIO.SerializeDeletedCustomZoneSets(_deletedCustomModels);
}
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
};
try
{
string jsonString = JsonSerializer.Serialize(deletedLayouts, options);
FileSystem.File.WriteAllText(Settings.DeletedCustomZoneSetsTmpFile, jsonString);
}
catch (Exception ex)
{
ShowExceptionMessageBox(ErrorSerializingDeletedLayouts, ex);
}
public static void SerializeCreatedCustomZonesets()
{
App.FancyZonesEditorIO.SerializeCreatedCustomZonesets(_createdCustomLayouts);
}
// Loads all the custom Layouts from tmp file passed by FancyZonesLib
public static ObservableCollection<LayoutModel> LoadCustomModels()
{
_customModels = new ObservableCollection<LayoutModel>();
try
{
Stream inputStream = FileSystem.File.Open(Settings.FancyZonesSettingsFile, FileMode.Open);
JsonDocument jsonObject = JsonDocument.Parse(inputStream, options: default);
JsonElement.ArrayEnumerator customZoneSetsEnumerator = jsonObject.RootElement.GetProperty(CustomZoneSetsJsonTag).EnumerateArray();
while (customZoneSetsEnumerator.MoveNext())
{
var current = customZoneSetsEnumerator.Current;
string name = current.GetProperty(NameStr).GetString();
string type = current.GetProperty(TypeJsonTag).GetString();
string uuid = current.GetProperty(UuidJsonTag).GetString();
var info = current.GetProperty(InfoJsonTag);
if (type.Equals(GridJsonTag))
{
bool error = false;
int rows = info.GetProperty(RowsJsonTag).GetInt32();
int columns = info.GetProperty(ColumnsJsonTag).GetInt32();
List<int> rowsPercentage = new List<int>(rows);
JsonElement.ArrayEnumerator rowsPercentageEnumerator = info.GetProperty(RowsPercentageJsonTag).EnumerateArray();
List<int> columnsPercentage = new List<int>(columns);
JsonElement.ArrayEnumerator columnsPercentageEnumerator = info.GetProperty(ColumnsPercentageJsonTag).EnumerateArray();
if (rows <= 0 || columns <= 0 || rowsPercentageEnumerator.Count() != rows || columnsPercentageEnumerator.Count() != columns)
{
error = true;
}
while (!error && rowsPercentageEnumerator.MoveNext())
{
int percentage = rowsPercentageEnumerator.Current.GetInt32();
if (percentage <= 0)
{
error = true;
break;
}
rowsPercentage.Add(percentage);
}
while (!error && columnsPercentageEnumerator.MoveNext())
{
int percentage = columnsPercentageEnumerator.Current.GetInt32();
if (percentage <= 0)
{
error = true;
break;
}
columnsPercentage.Add(percentage);
}
int i = 0;
JsonElement.ArrayEnumerator cellChildMapRows = info.GetProperty(CellChildMapJsonTag).EnumerateArray();
int[,] cellChildMap = new int[rows, columns];
if (cellChildMapRows.Count() != rows)
{
error = true;
}
while (!error && cellChildMapRows.MoveNext())
{
int j = 0;
JsonElement.ArrayEnumerator cellChildMapRowElems = cellChildMapRows.Current.EnumerateArray();
if (cellChildMapRowElems.Count() != columns)
{
error = true;
break;
}
while (cellChildMapRowElems.MoveNext())
{
cellChildMap[i, j++] = cellChildMapRowElems.Current.GetInt32();
}
i++;
}
if (error)
{
ShowExceptionMessageBox(string.Format(ErrorLayoutMalformedData, name));
_deletedCustomModels.Add(Guid.Parse(uuid).ToString().ToUpper());
continue;
}
_customModels.Add(new GridLayoutModel(uuid, name, LayoutType.Custom, rows, columns, rowsPercentage, columnsPercentage, cellChildMap));
}
else if (type.Equals(CanvasJsonTag))
{
int lastWorkAreaWidth = info.GetProperty(RefWidthJsonTag).GetInt32();
int lastWorkAreaHeight = info.GetProperty(RefHeightJsonTag).GetInt32();
JsonElement.ArrayEnumerator zonesEnumerator = info.GetProperty(ZonesJsonTag).EnumerateArray();
IList<Int32Rect> zones = new List<Int32Rect>();
bool error = false;
if (lastWorkAreaWidth <= 0 || lastWorkAreaHeight <= 0)
{
error = true;
}
while (!error && zonesEnumerator.MoveNext())
{
int x = zonesEnumerator.Current.GetProperty(XJsonTag).GetInt32();
int y = zonesEnumerator.Current.GetProperty(YJsonTag).GetInt32();
int width = zonesEnumerator.Current.GetProperty(WidthJsonTag).GetInt32();
int height = zonesEnumerator.Current.GetProperty(HeightJsonTag).GetInt32();
if (width <= 0 || height <= 0)
{
error = true;
break;
}
zones.Add(new Int32Rect(x, y, width, height));
}
if (error)
{
ShowExceptionMessageBox(string.Format(ErrorLayoutMalformedData, name));
_deletedCustomModels.Add(Guid.Parse(uuid).ToString().ToUpper());
continue;
}
_customModels.Add(new CanvasLayoutModel(uuid, name, LayoutType.Custom, zones, lastWorkAreaWidth, lastWorkAreaHeight));
}
}
inputStream.Close();
}
catch (Exception ex)
{
ShowExceptionMessageBox(ErrorLoadingCustomLayouts, ex);
return new ObservableCollection<LayoutModel>();
}
App.FancyZonesEditorIO.ParseLayouts(ref _customModels, ref _deletedCustomModels);
return _customModels;
}
private static ObservableCollection<LayoutModel> _customModels = null;
private static List<string> _deletedCustomModels = new List<string>();
private static List<JsonElement> _createdCustomLayouts = new List<JsonElement>();
// Callbacks that the base LayoutModel makes to derived types
protected abstract void PersistData();
@@ -369,83 +200,18 @@ namespace FancyZonesEditor.Models
Apply();
}
private struct ActiveZoneSetWrapper
{
public string Uuid { get; set; }
public string Type { get; set; }
}
private struct AppliedZoneSet
{
public string DeviceId { get; set; }
public ActiveZoneSetWrapper ActiveZoneset { get; set; }
public bool EditorShowSpacing { get; set; }
public int EditorSpacing { get; set; }
public int EditorZoneCount { get; set; }
public int EditorSensitivityRadius { get; set; }
}
public void Apply()
{
ActiveZoneSetWrapper activeZoneSet = new ActiveZoneSetWrapper
{
Uuid = "{" + Guid.ToString().ToUpper() + "}",
};
MainWindowSettingsModel settings = ((App)App.Current).MainWindowSettings;
settings.ResetAppliedModel();
IsApplied = true;
switch (Type)
{
case LayoutType.Focus:
activeZoneSet.Type = FocusJsonTag;
break;
case LayoutType.Rows:
activeZoneSet.Type = RowsJsonTag;
break;
case LayoutType.Columns:
activeZoneSet.Type = ColumnsJsonTag;
break;
case LayoutType.Grid:
activeZoneSet.Type = GridJsonTag;
break;
case LayoutType.PriorityGrid:
activeZoneSet.Type = PriorityGridJsonTag;
break;
case LayoutType.Custom:
activeZoneSet.Type = CustomJsonTag;
break;
}
// update settings
App.Overlay.CurrentLayoutSettings.ZonesetUuid = Uuid;
App.Overlay.CurrentLayoutSettings.Type = Type;
Settings settings = ((App)Application.Current).ZoneSettings;
AppliedZoneSet zoneSet = new AppliedZoneSet
{
DeviceId = Settings.UniqueKey,
ActiveZoneset = activeZoneSet,
EditorShowSpacing = settings.ShowSpacing,
EditorSpacing = settings.Spacing,
EditorZoneCount = settings.ZoneCount,
EditorSensitivityRadius = settings.SensitivityRadius,
};
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
};
try
{
string jsonString = JsonSerializer.Serialize(zoneSet, options);
FileSystem.File.WriteAllText(Settings.ActiveZoneSetTmpFile, jsonString);
}
catch (Exception ex)
{
ShowExceptionMessageBox(ErrorApplyingLayout, ex);
}
// update temp file
App.FancyZonesEditorIO.SerializeAppliedLayouts();
}
}
}

View File

@@ -0,0 +1,33 @@
// 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 FancyZonesEditor.Models;
namespace FancyZonesEditor
{
public class LayoutSettings
{
public static bool DefaultShowSpacing => true;
public static int DefaultSpacing => 16;
public static int DefaultZoneCount => 3;
public static int DefaultSensitivityRadius => 20;
public string DeviceId { get; set; } = string.Empty;
public string ZonesetUuid { get; set; } = string.Empty;
public LayoutType Type { get; set; } = LayoutType.PriorityGrid;
public bool ShowSpacing { get; set; } = DefaultShowSpacing;
public int Spacing { get; set; } = DefaultSpacing;
public int ZoneCount { get; set; } = DefaultZoneCount;
public int SensitivityRadius { get; set; } = DefaultSensitivityRadius;
}
}

View File

@@ -0,0 +1,17 @@
// 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.
namespace FancyZonesEditor.Models
{
public enum LayoutType
{
Blank = -1,
Focus,
Columns,
Rows,
Grid,
PriorityGrid,
Custom,
}
}

View File

@@ -1,4 +1,4 @@
// 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.
@@ -6,10 +6,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.IO.Abstractions;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Windows;
using FancyZonesEditor.Models;
@@ -18,30 +15,16 @@ namespace FancyZonesEditor
// Settings
// These are the configuration settings used by the rest of the editor
// 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
public class MainWindowSettingsModel : INotifyPropertyChanged
{
private enum CmdArgs
private enum DeviceIdParts
{
WorkAreaSize = 1,
PowerToysPID,
}
private enum WorkAreaCmdArgElements
{
X = 0,
Y,
Name = 0,
Width,
Height,
VirtualDesktopId,
}
private enum ParseDeviceMode
{
Prod,
Debug,
}
private static readonly IFileSystem _fileSystem = new FileSystem();
private static CanvasLayoutModel _blankCustomModel;
private readonly CanvasLayoutModel _focusModel;
private readonly GridLayoutModel _rowsModel;
@@ -63,33 +46,9 @@ namespace FancyZonesEditor
public static readonly string RegistryPath = "SOFTWARE\\SuperFancyZones";
public static readonly string FullRegistryPath = "HKEY_CURRENT_USER\\" + RegistryPath;
private const string ZonesSettingsFile = "\\Microsoft\\PowerToys\\FancyZones\\zones-settings.json";
private const string ActiveZoneSetsTmpFileName = "FancyZonesActiveZoneSets.json";
private const string AppliedZoneSetsTmpFileName = "FancyZonesAppliedZoneSets.json";
private const string DeletedCustomZoneSetsTmpFileName = "FancyZonesDeletedCustomZoneSets.json";
private const string LayoutTypeBlankStr = "blank";
private const string NullUuidStr = "null";
// DeviceInfo JSON tags
private const string DeviceIdJsonTag = "device-id";
private const string ActiveZoneSetJsonTag = "active-zoneset";
private const string UuidJsonTag = "uuid";
private const string TypeJsonTag = "type";
private const string EditorShowSpacingJsonTag = "editor-show-spacing";
private const string EditorSpacingJsonTag = "editor-spacing";
private const string EditorZoneCountJsonTag = "editor-zone-count";
private const string EditorSensitivityRadiusJsonTag = "editor-sensitivity-radius";
private const string FocusJsonTag = "focus";
private const string ColumnsJsonTag = "columns";
private const string RowsJsonTag = "rows";
private const string GridJsonTag = "grid";
private const string PriorityGridJsonTag = "priority-grid";
private const string CustomJsonTag = "custom";
private const string DebugMode = "Debug";
// hard coded data for all the "Priority Grid" configurations that are unique to "Grid"
private static readonly byte[][] _priorityData = new byte[][]
{
@@ -124,19 +83,8 @@ namespace FancyZonesEditor
}
}
public Settings()
public MainWindowSettingsModel()
{
string tmpDirPath = _fileSystem.Path.GetTempPath();
ActiveZoneSetTmpFile = tmpDirPath + ActiveZoneSetsTmpFileName;
AppliedZoneSetTmpFile = tmpDirPath + AppliedZoneSetsTmpFileName;
DeletedCustomZoneSetsTmpFile = tmpDirPath + DeletedCustomZoneSetsTmpFileName;
var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
FancyZonesSettingsFile = localAppDataDir + ZonesSettingsFile;
ParseCommandLineArgs();
// Initialize the five default layout models: Focus, Columns, Rows, Grid, and PriorityGrid
DefaultModels = new List<LayoutModel>(5);
_focusModel = new CanvasLayoutModel(Properties.Resources.Template_Layout_Focus, LayoutType.Focus);
@@ -164,7 +112,7 @@ namespace FancyZonesEditor
_blankCustomModel = new CanvasLayoutModel(Properties.Resources.Custom_Layout_Create_New, LayoutType.Blank);
UpdateLayoutModels();
UpdateTemplateLayoutModels();
}
// ZoneCount - number of zones selected in the picker window
@@ -172,82 +120,79 @@ namespace FancyZonesEditor
{
get
{
return _zoneCount;
return App.Overlay.CurrentLayoutSettings.ZoneCount;
}
set
{
if (_zoneCount != value)
if (App.Overlay.CurrentLayoutSettings.ZoneCount != value)
{
_zoneCount = value;
UpdateLayoutModels();
FirePropertyChanged();
App.Overlay.CurrentLayoutSettings.ZoneCount = value;
UpdateTemplateLayoutModels();
FirePropertyChanged(nameof(ZoneCount));
}
}
}
private int _zoneCount;
// Spacing - how much space in between zones of the grid do you want
public int Spacing
{
get
{
return _spacing;
return App.Overlay.CurrentLayoutSettings.Spacing;
}
set
{
if (_spacing != value)
value = Math.Max(0, value);
if (App.Overlay.CurrentLayoutSettings.Spacing != value)
{
_spacing = Math.Max(MaxNegativeSpacing, value);
FirePropertyChanged();
App.Overlay.CurrentLayoutSettings.Spacing = value;
UpdateTemplateLayoutModels();
FirePropertyChanged(nameof(Spacing));
}
}
}
private int _spacing;
// ShowSpacing - is the Spacing value used or ignored?
public bool ShowSpacing
{
get
{
return _showSpacing;
return App.Overlay.CurrentLayoutSettings.ShowSpacing;
}
set
{
if (_showSpacing != value)
if (App.Overlay.CurrentLayoutSettings.ShowSpacing != value)
{
_showSpacing = value;
FirePropertyChanged();
App.Overlay.CurrentLayoutSettings.ShowSpacing = value;
UpdateTemplateLayoutModels();
FirePropertyChanged(nameof(ShowSpacing));
}
}
}
private bool _showSpacing;
// SensitivityRadius - how much space inside the zone to highlight the adjacent zone too
public int SensitivityRadius
{
get
{
return _sensitivityRadius;
return App.Overlay.CurrentLayoutSettings.SensitivityRadius;
}
set
{
if (_sensitivityRadius != value)
value = Math.Max(0, value);
if (App.Overlay.CurrentLayoutSettings.SensitivityRadius != value)
{
_sensitivityRadius = Math.Max(0, value);
FirePropertyChanged();
App.Overlay.CurrentLayoutSettings.SensitivityRadius = value;
UpdateTemplateLayoutModels();
FirePropertyChanged(nameof(SensitivityRadius));
}
}
}
private int _sensitivityRadius;
// IsShiftKeyPressed - is the shift key currently being held down
public bool IsShiftKeyPressed
{
@@ -261,7 +206,7 @@ namespace FancyZonesEditor
if (_isShiftKeyPressed != value)
{
_isShiftKeyPressed = value;
FirePropertyChanged();
FirePropertyChanged(nameof(IsShiftKeyPressed));
}
}
}
@@ -281,41 +226,16 @@ namespace FancyZonesEditor
if (_isCtrlKeyPressed != value)
{
_isCtrlKeyPressed = value;
FirePropertyChanged();
FirePropertyChanged(nameof(IsCtrlKeyPressed));
}
}
}
private bool _isCtrlKeyPressed;
public static Rect WorkArea { get; private set; }
public static List<Rect> UsedWorkAreas { get; private set; }
public static string UniqueKey { get; private set; }
public static string ActiveZoneSetUUid { get; private set; }
public static LayoutType ActiveZoneSetLayoutType { get; private set; }
public static string ActiveZoneSetTmpFile { get; private set; }
public static string AppliedZoneSetTmpFile { get; private set; }
public static string DeletedCustomZoneSetsTmpFile { get; private set; }
public static string FancyZonesSettingsFile { get; private set; }
public static int PowerToysPID
{
get { return _powerToysPID; }
}
private static int _powerToysPID;
// UpdateLayoutModels
// Update the five default layouts based on the new ZoneCount
private void UpdateLayoutModels()
private void UpdateTemplateLayoutModels()
{
// Update the "Focus" Default Layout
_focusModel.Zones.Clear();
@@ -328,9 +248,13 @@ namespace FancyZonesEditor
// If changing focus layout zones size and/or increment,
// same change should be applied in ZoneSet.cpp (ZoneSet::CalculateFocusLayout)
Int32Rect focusZoneRect = new Int32Rect(100, 100, (int)(WorkArea.Width * 0.4), (int)(WorkArea.Height * 0.4));
int focusRectXIncrement = (ZoneCount <= 1) ? 0 : 50;
int focusRectYIncrement = (ZoneCount <= 1) ? 0 : 50;
var workingArea = App.Overlay.WorkArea;
int topLeftCoordinate = (int)App.Overlay.ScaleCoordinateWithCurrentMonitorDpi(100); // TODO: replace magic numbers
int width = (int)(workingArea.Width * 0.4);
int height = (int)(workingArea.Height * 0.4);
Int32Rect focusZoneRect = new Int32Rect(topLeftCoordinate, topLeftCoordinate, width, height);
int focusRectXIncrement = (ZoneCount <= 1) ? 0 : (int)App.Overlay.ScaleCoordinateWithCurrentMonitorDpi(50);
int focusRectYIncrement = (ZoneCount <= 1) ? 0 : (int)App.Overlay.ScaleCoordinateWithCurrentMonitorDpi(50);
for (int i = 0; i < ZoneCount; i++)
{
@@ -422,128 +346,6 @@ namespace FancyZonesEditor
}
}
private void ParseDeviceInfoData(ParseDeviceMode mode = ParseDeviceMode.Prod)
{
try
{
string layoutType = LayoutTypeBlankStr;
ActiveZoneSetUUid = NullUuidStr;
JsonElement jsonObject = default(JsonElement);
if (_fileSystem.File.Exists(Settings.ActiveZoneSetTmpFile))
{
Stream inputStream = _fileSystem.File.Open(Settings.ActiveZoneSetTmpFile, FileMode.Open);
jsonObject = JsonDocument.Parse(inputStream, options: default).RootElement;
inputStream.Close();
UniqueKey = jsonObject.GetProperty(DeviceIdJsonTag).GetString();
ActiveZoneSetUUid = jsonObject.GetProperty(ActiveZoneSetJsonTag).GetProperty(UuidJsonTag).GetString();
layoutType = jsonObject.GetProperty(ActiveZoneSetJsonTag).GetProperty(TypeJsonTag).GetString();
}
if (mode == ParseDeviceMode.Debug || ActiveZoneSetUUid == NullUuidStr || layoutType == LayoutTypeBlankStr)
{
// Default or there is no active layout on current device
ActiveZoneSetLayoutType = LayoutType.Focus;
_showSpacing = true;
_spacing = 16;
_zoneCount = 3;
_sensitivityRadius = 20;
}
else
{
switch (layoutType)
{
case FocusJsonTag:
ActiveZoneSetLayoutType = LayoutType.Focus;
break;
case ColumnsJsonTag:
ActiveZoneSetLayoutType = LayoutType.Columns;
break;
case RowsJsonTag:
ActiveZoneSetLayoutType = LayoutType.Rows;
break;
case GridJsonTag:
ActiveZoneSetLayoutType = LayoutType.Grid;
break;
case PriorityGridJsonTag:
ActiveZoneSetLayoutType = LayoutType.PriorityGrid;
break;
case CustomJsonTag:
ActiveZoneSetLayoutType = LayoutType.Custom;
break;
}
_showSpacing = jsonObject.GetProperty(EditorShowSpacingJsonTag).GetBoolean();
_spacing = jsonObject.GetProperty(EditorSpacingJsonTag).GetInt32();
_zoneCount = jsonObject.GetProperty(EditorZoneCountJsonTag).GetInt32();
_sensitivityRadius = jsonObject.GetProperty(EditorSensitivityRadiusJsonTag).GetInt32();
}
}
catch (Exception ex)
{
LayoutModel.ShowExceptionMessageBox(Properties.Resources.Error_Parsing_Device_Info, ex);
}
}
private void ParseCommandLineArgs()
{
WorkArea = SystemParameters.WorkArea;
UsedWorkAreas = new List<Rect> { WorkArea };
string[] args = Environment.GetCommandLineArgs();
if (args.Length == 2)
{
if (args[1].Equals(DebugMode))
{
ParseDeviceInfoData(ParseDeviceMode.Debug);
}
else
{
MessageBox.Show(Properties.Resources.Error_Invalid_Arguments, Properties.Resources.Error_Message_Box_Title);
((App)Application.Current).Shutdown();
}
}
else if (args.Length == 3)
{
UsedWorkAreas.Clear();
foreach (var singleMonitorString in args[(int)CmdArgs.WorkAreaSize].Split('/'))
{
var parsedLocation = singleMonitorString.Split('_');
if (parsedLocation.Length != 4)
{
MessageBox.Show(Properties.Resources.Error_Invalid_Arguments, Properties.Resources.Error_Message_Box_Title);
((App)Application.Current).Shutdown();
}
var x = int.Parse(parsedLocation[(int)WorkAreaCmdArgElements.X]);
var y = int.Parse(parsedLocation[(int)WorkAreaCmdArgElements.Y]);
var width = int.Parse(parsedLocation[(int)WorkAreaCmdArgElements.Width]);
var height = int.Parse(parsedLocation[(int)WorkAreaCmdArgElements.Height]);
Rect thisMonitor = new Rect(x, y, width, height);
if (UsedWorkAreas.Count == 0)
{
WorkArea = thisMonitor;
}
else
{
WorkArea = Rect.Union(WorkArea, thisMonitor);
}
UsedWorkAreas.Add(thisMonitor);
}
int.TryParse(args[(int)CmdArgs.PowerToysPID], out _powerToysPID);
ParseDeviceInfoData();
}
else
{
MessageBox.Show(Properties.Resources.Error_Invalid_Arguments, Properties.Resources.Error_Message_Box_Title);
((App)Application.Current).Shutdown();
}
}
public IList<LayoutModel> DefaultModels { get; }
public static ObservableCollection<LayoutModel> CustomModels
@@ -567,6 +369,120 @@ namespace FancyZonesEditor
return model.Type != LayoutType.Custom;
}
public LayoutModel UpdateSelectedLayoutModel()
{
UpdateTemplateLayoutModels();
ResetAppliedModel();
ResetSelectedModel();
LayoutModel foundModel = null;
LayoutSettings currentApplied = App.Overlay.CurrentLayoutSettings;
// set new layout
if (currentApplied.Type == LayoutType.Custom)
{
foreach (LayoutModel model in MainWindowSettingsModel.CustomModels)
{
if ("{" + model.Guid.ToString().ToUpper() + "}" == currentApplied.ZonesetUuid.ToUpper())
{
// found match
foundModel = model;
break;
}
}
}
else
{
foreach (LayoutModel model in DefaultModels)
{
if (model.Type == currentApplied.Type)
{
// found match
foundModel = model;
break;
}
}
}
if (foundModel == null)
{
foundModel = DefaultModels[0];
}
foundModel.IsSelected = true;
foundModel.IsApplied = true;
FirePropertyChanged(nameof(IsCustomLayoutActive));
return foundModel;
}
public void ResetSelectedModel()
{
foreach (LayoutModel model in CustomModels)
{
if (model.IsSelected)
{
model.IsSelected = false;
break;
}
}
foreach (LayoutModel model in DefaultModels)
{
if (model.IsSelected)
{
model.IsSelected = false;
break;
}
}
}
public void ResetAppliedModel()
{
foreach (LayoutModel model in CustomModels)
{
if (model.IsApplied)
{
model.IsApplied = false;
break;
}
}
foreach (LayoutModel model in DefaultModels)
{
if (model.IsApplied)
{
model.IsApplied = false;
break;
}
}
}
public void UpdateDesktopDependantProperties(LayoutSettings prevSettings)
{
UpdateTemplateLayoutModels();
if (prevSettings.ZoneCount != ZoneCount)
{
FirePropertyChanged(nameof(ZoneCount));
}
if (prevSettings.Spacing != Spacing)
{
FirePropertyChanged(nameof(Spacing));
}
if (prevSettings.ShowSpacing != ShowSpacing)
{
FirePropertyChanged(nameof(ShowSpacing));
}
if (prevSettings.SensitivityRadius != SensitivityRadius)
{
FirePropertyChanged(nameof(SensitivityRadius));
}
}
// implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

View File

@@ -0,0 +1,58 @@
// 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.Reflection;
using System.Windows;
using System.Windows.Media;
using FancyZonesEditor.Utils;
namespace FancyZonesEditor.Models
{
public class Monitor
{
public LayoutOverlayWindow Window { get; private set; }
public LayoutSettings Settings { get; set; }
public Device Device { get; set; }
public Monitor(Rect bounds, Rect workArea, bool primary)
{
Window = new LayoutOverlayWindow();
Settings = new LayoutSettings();
Device = new Device(bounds, workArea, primary);
if (App.DebugMode)
{
long milliseconds = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
PropertyInfo[] properties = typeof(Brushes).GetProperties();
Window.Opacity = 0.5;
Window.Background = (Brush)properties[milliseconds % properties.Length].GetValue(null, null);
}
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
}
public Monitor(string id, int dpi, Rect bounds, Rect workArea, bool primary)
: this(bounds, workArea, primary)
{
Device = new Device(id, dpi, bounds, workArea, primary);
}
public void Scale(double scaleFactor)
{
Device.Scale(scaleFactor);
var workArea = Device.WorkAreaRect;
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
}
}
}

View File

@@ -0,0 +1,86 @@
// 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.ComponentModel;
using FancyZonesEditor.ViewModels;
namespace FancyZonesEditor.Utils
{
public class MonitorInfoModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MonitorInfoModel(int index, int height, int width, int dpi, bool selected = false)
{
Index = index;
ScreenBoundsHeight = height;
ScreenBoundsWidth = width;
DPI = dpi;
Selected = selected;
}
public int Index { get; set; }
public int ScreenBoundsHeight { get; set; }
public double DisplayHeight
{
get
{
return ScreenBoundsHeight * MonitorViewModel.DesktopPreviewMultiplier;
}
}
public int ScreenBoundsWidth { get; set; }
public double DisplayWidth
{
get
{
return ScreenBoundsWidth * MonitorViewModel.DesktopPreviewMultiplier;
}
}
public int DPI { get; set; }
public string Dimensions
{
get
{
if (App.DebugMode)
{
var rect = App.Overlay.Monitors[Index - 1].Device.WorkAreaRect;
return "Screen: (" + rect.X + ", " + rect.Y + "); (" + rect.Width + ", " + rect.Height + ")";
}
else
{
return ScreenBoundsWidth + " x " + ScreenBoundsHeight;
}
}
}
public bool Selected
{
get
{
return _selected;
}
set
{
if (_selected == value)
{
return;
}
_selected = value;
OnPropertyChanged(nameof(Selected));
}
}
private bool _selected;
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}