diff --git a/src/TeamsISO.App/Assets/Fonts/Inter.ttf b/src/TeamsISO.App/Assets/Fonts/Inter.ttf new file mode 100644 index 0000000..1cb674b Binary files /dev/null and b/src/TeamsISO.App/Assets/Fonts/Inter.ttf differ diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index d589741..e8fd153 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -304,18 +304,43 @@ - - - - - - + + + + + + + + + + + + + + + + /// Restore the window's previous placement after the HWND is created (so + /// SetWindowPos / WindowState transitions actually take effect). Falls + /// silently back to the XAML-default startup location if no snapshot exists. + /// + private void OnSourceInitialized(object? sender, EventArgs e) + { + WindowStateStore.TryApply(this); + } + + /// Persist the placement on close so next launch lands in the same spot. + private void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e) + { + WindowStateStore.Save(this); + } + /// Custom min button — chrome'd window has no system caption buttons. private void OnMinimize(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; diff --git a/src/TeamsISO.App/Services/WindowStateStore.cs b/src/TeamsISO.App/Services/WindowStateStore.cs new file mode 100644 index 0000000..dec4ecd --- /dev/null +++ b/src/TeamsISO.App/Services/WindowStateStore.cs @@ -0,0 +1,116 @@ +using System.IO; +using System.Text.Json; +using System.Windows; + +namespace TeamsISO.App.Services; + +/// +/// Saves / restores the main window's size, position, and state across launches. +/// Stored as JSON at %LOCALAPPDATA%\TeamsISO\window.json. 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. +/// +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); + + /// Save the current window placement. + 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. + } + } + + /// + /// 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. + /// + public static bool TryApply(Window window) + { + try + { + if (!File.Exists(Path)) return false; + var json = File.ReadAllText(Path); + var snap = JsonSerializer.Deserialize(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; + } + } + + /// + /// Approximate "is at least one corner of the saved rect within the virtual + /// screen?" check. Uses SystemParameters.VirtualScreen* which spans every + /// monitor. + /// + 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; + } +} diff --git a/src/TeamsISO.App/TeamsISO.App.csproj b/src/TeamsISO.App/TeamsISO.App.csproj index 9b9eab7..62e68d9 100644 --- a/src/TeamsISO.App/TeamsISO.App.csproj +++ b/src/TeamsISO.App/TeamsISO.App.csproj @@ -20,6 +20,11 @@ + + diff --git a/src/TeamsISO.App/Themes/WildDragonTheme.xaml b/src/TeamsISO.App/Themes/WildDragonTheme.xaml index d29ef83..9282d82 100644 --- a/src/TeamsISO.App/Themes/WildDragonTheme.xaml +++ b/src/TeamsISO.App/Themes/WildDragonTheme.xaml @@ -69,7 +69,13 @@ - Inter, Segoe UI Variable Display, Segoe UI, sans-serif + + pack://application:,,,/Assets/Fonts/#Inter, Inter, Segoe UI Variable Display, Segoe UI, sans-serif JetBrains Mono, Cascadia Mono, Consolas, monospace