mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 18:01:09 -05:00
feat(desktop): shared window placement in the WinForms settings format
WindowPlacement reads and writes a window's position, size and maximized state under the names the WinForms app already stores (MainWindowLocation "x, y", MainWindowSize "width, height", MainWindowState), in screen pixels. A minimized window restores normal. FitTo drops a position that is on no current screen and a size too big for its screen, as the WinForms main window does when it restores.
This commit is contained in:
1 parent
b92d079e77
commit
53e5b78e3c
2 files changed
+234
No files matched your search
@@ -0,0 +1,129 @@
|
||||
namespace OpenNest.Desktop.Tests;
|
||||
|
||||
public class WindowPlacementTests
|
||||
{
|
||||
private static readonly PixelRect Primary = new(0, 0, 1920, 1040);
|
||||
private static readonly PixelRect Right = new(1920, 0, 2560, 1400);
|
||||
|
||||
[Fact]
|
||||
public void Read_TheWinFormsAppsMaximizedWindow()
|
||||
{
|
||||
// As DesktopPC's real user-settings.json holds it.
|
||||
var settings = new Dictionary<string, string>
|
||||
{
|
||||
["MainWindowLocation"] = "-8, -8",
|
||||
["MainWindowSize"] = "1936, 1048",
|
||||
["MainWindowState"] = "Maximized",
|
||||
};
|
||||
|
||||
var placement = WindowPlacement.Read(settings, WindowPlacement.MainWindow);
|
||||
|
||||
Assert.Equal(new WindowPlacement((-8, -8), (1936, 1048), true), placement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_NothingSaved_IsNull()
|
||||
{
|
||||
var empty = new Dictionary<string, string>();
|
||||
|
||||
Assert.Null(WindowPlacement.Read(empty, WindowPlacement.MainWindow));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Minimized")]
|
||||
[InlineData("Normal")]
|
||||
[InlineData("garbage")]
|
||||
public void Read_OnlyMaximizedRestoresMaximized(string state)
|
||||
{
|
||||
var settings = new Dictionary<string, string>
|
||||
{
|
||||
["MainWindowSize"] = "800, 600",
|
||||
["MainWindowState"] = state,
|
||||
};
|
||||
|
||||
Assert.False(WindowPlacement.Read(settings, WindowPlacement.MainWindow)!.Maximized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("abc")]
|
||||
[InlineData("800")]
|
||||
[InlineData("800, 600, 4")]
|
||||
[InlineData("0, 600")]
|
||||
[InlineData("800, -1")]
|
||||
public void Read_UnusableSizeIsDropped(string size)
|
||||
{
|
||||
var settings = new Dictionary<string, string>
|
||||
{
|
||||
["MainWindowLocation"] = "10, 20",
|
||||
["MainWindowSize"] = size,
|
||||
};
|
||||
|
||||
var placement = WindowPlacement.Read(settings, WindowPlacement.MainWindow);
|
||||
|
||||
Assert.Equal(new WindowPlacement((10, 20), null, false), placement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTo_UsesTheWinFormsNamesAndFormat()
|
||||
{
|
||||
var settings = new Dictionary<string, string> { ["Other"] = "kept" };
|
||||
|
||||
var placement = new WindowPlacement((100, 50), (1200, 800), false);
|
||||
|
||||
placement.WriteTo(settings, WindowPlacement.MainWindow);
|
||||
|
||||
Assert.Equal(new Dictionary<string, string>
|
||||
{
|
||||
["Other"] = "kept",
|
||||
["MainWindowLocation"] = "100, 50",
|
||||
["MainWindowSize"] = "1200, 800",
|
||||
["MainWindowState"] = "Normal",
|
||||
}, settings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteThenRead_RoundTrips()
|
||||
{
|
||||
var settings = new Dictionary<string, string>();
|
||||
var placement = new WindowPlacement((-1500, 30), (1400, 860), true);
|
||||
|
||||
placement.WriteTo(settings, "DrawingSetsWindow");
|
||||
|
||||
Assert.Equal(placement, WindowPlacement.Read(settings, "DrawingSetsWindow"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitTo_KeepsAPlacementOnASecondScreen()
|
||||
{
|
||||
var placement = new WindowPlacement((2000, 100), (2400, 1300), false);
|
||||
|
||||
Assert.Equal(placement, placement.FitTo([Primary, Right]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitTo_DropsAPositionOffEveryScreen()
|
||||
{
|
||||
// The second monitor was unplugged since the window closed there.
|
||||
var placement = new WindowPlacement((2000, 100), (1200, 800), false);
|
||||
|
||||
Assert.Equal(new WindowPlacement(null, (1200, 800), false), placement.FitTo([Primary]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitTo_DropsASizeTooBigForItsScreen()
|
||||
{
|
||||
var placement = new WindowPlacement((100, 100), (2400, 1300), false);
|
||||
|
||||
var fitted = placement.FitTo([Primary, Right]);
|
||||
|
||||
Assert.Equal(new WindowPlacement((100, 100), null, false), fitted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitTo_KeepsMaximized()
|
||||
{
|
||||
var placement = new WindowPlacement((-8, -8), (1936, 1048), true);
|
||||
|
||||
Assert.Equal(new WindowPlacement(null, null, true), placement.FitTo([Primary]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace OpenNest.Desktop;
|
||||
|
||||
/// <summary>A rectangle in screen pixels, standing in for each front end's own type.</summary>
|
||||
public readonly record struct PixelRect(int X, int Y, int Width, int Height)
|
||||
{
|
||||
public bool Contains(int x, int y) => x >= X && y >= Y && x < X + Width && y < Y + Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A window's remembered size, position and maximized state, stored in the shared
|
||||
/// user-settings file under the names the WinForms app already uses
|
||||
/// (<c><name>Location</c> "x, y", <c><name>Size</c> "width, height" and
|
||||
/// <c><name>State</c> "Normal" or "Maximized", all in screen pixels). A window is never
|
||||
/// restored minimized.
|
||||
/// </summary>
|
||||
public sealed record WindowPlacement(
|
||||
(int X, int Y)? Location,
|
||||
(int Width, int Height)? Size,
|
||||
bool Maximized)
|
||||
{
|
||||
/// <summary>
|
||||
/// The main window's names: MainWindowLocation, MainWindowSize and MainWindowState.
|
||||
/// </summary>
|
||||
public const string MainWindow = "MainWindow";
|
||||
|
||||
/// <summary>The placement saved under <paramref name="name"/>, or null if none.</summary>
|
||||
public static WindowPlacement Read(IReadOnlyDictionary<string, string> settings, string name)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
var location = settings.TryGetValue(name + "Location", out var l) ? ParsePair(l) : null;
|
||||
var size = settings.TryGetValue(name + "Size", out var s) ? ParsePair(s) : null;
|
||||
if (size is { } pair && (pair.A <= 0 || pair.B <= 0))
|
||||
size = null;
|
||||
var maximized = settings.TryGetValue(name + "State", out var state)
|
||||
&& string.Equals(state?.Trim(), "Maximized", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (location is null && size is null && !maximized)
|
||||
return null;
|
||||
|
||||
return new WindowPlacement(
|
||||
location is { } at ? (at.A, at.B) : null,
|
||||
size is { } wh ? (wh.A, wh.B) : null,
|
||||
maximized);
|
||||
}
|
||||
|
||||
/// <summary>Stores it under <paramref name="name"/> in the WinForms format.</summary>
|
||||
public void WriteTo(IDictionary<string, string> settings, string name)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
if (Location is { } at)
|
||||
settings[name + "Location"] = Format(at.X, at.Y);
|
||||
if (Size is { } size)
|
||||
settings[name + "Size"] = Format(size.Width, size.Height);
|
||||
settings[name + "State"] = Maximized ? "Maximized" : "Normal";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What still fits the current screens: the position is kept only when it lies on one of
|
||||
/// <paramref name="workingAreas"/>, and the size only when it fits that screen (the first
|
||||
/// screen when the position was dropped), as the WinForms app restores its main window.
|
||||
/// </summary>
|
||||
public WindowPlacement FitTo(IReadOnlyList<PixelRect> workingAreas)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workingAreas);
|
||||
if (workingAreas.Count == 0)
|
||||
return this with { Location = null, Size = null };
|
||||
|
||||
PixelRect? screen = null;
|
||||
if (Location is { } at)
|
||||
{
|
||||
foreach (var area in workingAreas)
|
||||
{
|
||||
if (area.Contains(at.X, at.Y))
|
||||
{
|
||||
screen = area;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var target = screen ?? workingAreas[0];
|
||||
var fits = Size is { } wh && wh.Width <= target.Width && wh.Height <= target.Height;
|
||||
var size = fits ? Size : null;
|
||||
return this with { Location = screen is null ? null : Location, Size = size };
|
||||
}
|
||||
|
||||
private static (int A, int B)? ParsePair(string text)
|
||||
{
|
||||
var parts = text?.Split(',');
|
||||
if (parts is not { Length: 2 })
|
||||
return null;
|
||||
|
||||
return TryParseInt(parts[0], out var a) && TryParseInt(parts[1], out var b) ? (a, b) : null;
|
||||
}
|
||||
|
||||
private static bool TryParseInt(string text, out int value) =>
|
||||
int.TryParse(text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
|
||||
|
||||
private static string Format(int a, int b) =>
|
||||
string.Create(CultureInfo.InvariantCulture, $"{a}, {b}");
|
||||
}
|
||||
Reference in new issue
Block a user