teamsiso/src/TeamsISO.App/Services/WindowStateStore.cs
Zac Gaetano 0c82ac71f0
Some checks failed
CI / build-and-test (push) Failing after 27s
feat: bundle Inter font, emergency stop button, window persistence + tests
Four polish items + a test pass.

1. Inter Variable (rsms/inter v3.19, OFL) is bundled at Assets/Fonts/Inter.ttf (~800 KB) and registered as a WPF Resource. WildDragonTheme.xaml's Wd.Font.Sans now points at pack://application:,,,/Assets/Fonts/#Inter so the typography matches wilddragon.net regardless of whether the user has Inter installed system-wide. Falls back to Segoe UI Variable Display if the resource is missing.

2. 'Stop all ISOs' button at the right of the participants header. Bound to a new MainViewModel.StopAllIsosCommand that snapshots the enabled list, awaits DisableIsoAsync sequentially, and silently swallows per-pipeline failures (best-effort emergency stop). CanExecute gates on whether any ISO is currently enabled.

3. WindowStateStore service persists the main window's Left/Top/Width/Height/State to %LOCALAPPDATA%\\TeamsISO\\window.json on close and restores it on SourceInitialized. Multi-monitor friendly: a saved position with no corner inside any virtual screen is rejected so a disconnected monitor doesn't strand the window off-screen.

4. Two new unit tests cover FrameProcessor's drops + duplicates accounting. 76/76 unit tests pass (was 74).
2026-05-08 13:59:14 -04:00

116 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Text.Json;
using System.Windows;
namespace TeamsISO.App.Services;
/// <summary>
/// Saves / restores the main window's size, position, and state across launches.
/// Stored as JSON at <c>%LOCALAPPDATA%\TeamsISO\window.json</c>. Multi-monitor
/// friendly: a saved position that no longer falls inside any working area is
/// rejected on restore so the window doesn't disappear off-screen when a monitor
/// has been disconnected.
/// </summary>
public static class WindowStateStore
{
private static readonly string Path =
System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TeamsISO",
"window.json");
public sealed record Snapshot(
double Left,
double Top,
double Width,
double Height,
WindowState State);
/// <summary>Save the current window placement.</summary>
public static void Save(Window window)
{
try
{
var snap = new Snapshot(
Left: window.Left,
Top: window.Top,
Width: window.ActualWidth,
Height: window.ActualHeight,
State: window.WindowState == WindowState.Minimized ? WindowState.Normal : window.WindowState);
var dir = System.IO.Path.GetDirectoryName(Path);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(Path, JsonSerializer.Serialize(snap, new JsonSerializerOptions { WriteIndented = true }));
}
catch
{
// Best-effort persistence; never crash on shutdown for a UI nicety.
}
}
/// <summary>
/// Apply a previously-saved placement. Clamps onto a visible work area so a
/// monitor change doesn't strand the window off-screen. Returns true if a
/// valid snapshot was applied; false if no file existed or the snapshot was
/// rejected for being entirely outside any visible work area.
/// </summary>
public static bool TryApply(Window window)
{
try
{
if (!File.Exists(Path)) return false;
var json = File.ReadAllText(Path);
var snap = JsonSerializer.Deserialize<Snapshot>(json);
if (snap is null) return false;
// Sanity-check sizes (don't restore a 0×0 or absurdly large window).
if (snap.Width < 320 || snap.Height < 240) return false;
if (snap.Width > 16000 || snap.Height > 12000) return false;
// Reject if entirely off-screen (any working area on any screen contains
// a corner). System.Windows.Forms gives us per-monitor work areas here;
// we deliberately stick with WPF's SystemParameters which only reports the
// primary, so we use a generous on-screen check rather than refusing
// multi-monitor positions.
if (!IsAnyCornerOnScreen(snap)) return false;
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Left = snap.Left;
window.Top = snap.Top;
window.Width = snap.Width;
window.Height = snap.Height;
window.WindowState = snap.State;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Approximate "is at least one corner of the saved rect within the virtual
/// screen?" check. Uses SystemParameters.VirtualScreen* which spans every
/// monitor.
/// </summary>
private static bool IsAnyCornerOnScreen(Snapshot snap)
{
var minX = SystemParameters.VirtualScreenLeft;
var minY = SystemParameters.VirtualScreenTop;
var maxX = minX + SystemParameters.VirtualScreenWidth;
var maxY = minY + SystemParameters.VirtualScreenHeight;
var corners = new[]
{
(snap.Left, snap.Top),
(snap.Left + snap.Width, snap.Top),
(snap.Left, snap.Top + snap.Height),
(snap.Left + snap.Width, snap.Top + snap.Height),
};
foreach (var (x, y) in corners)
{
if (x >= minX && x <= maxX && y >= minY && y <= maxY)
return true;
}
return false;
}
}