using System; using System.Collections.Generic; using AcDream.UI.Abstractions.Panels.Settings; namespace AcDream.App.UI; /// /// Bridges typed retained-window lifecycle events to per-character, /// per-resolution settings. It deliberately ignores the temporary pre-login /// default character key so startup layout cannot overwrite a real /// character's state. /// public sealed class RetailWindowLayoutPersistence : IDisposable { private readonly RetailWindowManager _manager; private readonly SettingsStore _store; private readonly Func _characterKey; private readonly Func<(int Width, int Height)> _screenSize; private readonly HashSet _stateManagedVisibilityWindows; private readonly List _attached = new(); private bool _restoring; private bool _disposed; public RetailWindowLayoutPersistence( RetailWindowManager manager, SettingsStore store, Func characterKey, Func<(int Width, int Height)> screenSize, IEnumerable? stateManagedVisibilityWindows = null) { _manager = manager ?? throw new ArgumentNullException(nameof(manager)); _store = store ?? throw new ArgumentNullException(nameof(store)); _characterKey = characterKey ?? throw new ArgumentNullException(nameof(characterKey)); _screenSize = screenSize ?? throw new ArgumentNullException(nameof(screenSize)); _stateManagedVisibilityWindows = stateManagedVisibilityWindows is null ? new HashSet(StringComparer.Ordinal) : new HashSet(stateManagedVisibilityWindows, StringComparer.Ordinal); foreach (RetailWindowHandle handle in manager.Windows) Attach(handle); } /// Restore all registered windows after character and screen are known. public void RestoreAll() { ObjectDisposedException.ThrowIf(_disposed, this); string character = _characterKey(); if (!CanPersist(character)) return; var screen = ValidScreenSize(); string resolution = ResolutionKey(screen); _restoring = true; try { foreach (RetailWindowHandle handle in _attached) { UiWindowLayout fallback = Capture(handle); UiWindowLayout? saved = _store.LoadWindowLayout( character, resolution, handle.Name, fallback); if (saved is not { } layout) continue; Apply( handle, layout, screen, restoreVisibility: !_stateManagedVisibilityWindows.Contains(handle.Name)); // Lazily migrate legacy position-only entries into the complete schema. _store.SaveWindowLayout(character, resolution, handle.Name, Capture(handle)); } } finally { _restoring = false; } } /// Force-save every attached window to the current automatic profile. public void SaveAll() { ObjectDisposedException.ThrowIf(_disposed, this); string character = _characterKey(); if (!CanPersist(character)) return; var screen = ValidScreenSize(); string resolution = ResolutionKey(screen); foreach (RetailWindowHandle handle in _attached) _store.SaveWindowLayout(character, resolution, handle.Name, Capture(handle)); } /// Save every attached window to a portable named retail UI profile. public void SaveNamed(string profileName) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(profileName); foreach (RetailWindowHandle handle in _attached) _store.SaveNamedWindowLayout(profileName, handle.Name, Capture(handle)); } /// Restore every window found in a portable named retail UI profile. public void RestoreNamed(string profileName) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(profileName); var screen = ValidScreenSize(); _restoring = true; try { foreach (RetailWindowHandle handle in _attached) { UiWindowLayout? saved = _store.LoadNamedWindowLayout( profileName, handle.Name, Capture(handle)); if (saved is not { } layout) continue; Apply( handle, layout, screen, restoreVisibility: !_stateManagedVisibilityWindows.Contains(handle.Name)); } } finally { _restoring = false; } } private void Attach(RetailWindowHandle handle) { _attached.Add(handle); handle.Moved += OnChanged; handle.Resized += OnChanged; if (!_stateManagedVisibilityWindows.Contains(handle.Name)) { handle.Shown += OnChanged; handle.Hidden += OnChanged; } } private void OnChanged(RetailWindowHandle handle) { if (_restoring || _disposed) return; string character = _characterKey(); if (!CanPersist(character)) return; try { var screen = ValidScreenSize(); _store.SaveWindowLayout( character, ResolutionKey(screen), handle.Name, Capture(handle)); } catch (Exception ex) { Console.WriteLine($"settings: window layout save failed [{handle.Name}]: {ex.Message}"); } } private static UiWindowLayout Capture(RetailWindowHandle handle) { RetainedWindowState state = handle.StateController?.CaptureWindowState() ?? default; return new UiWindowLayout( handle.Left, state.PersistedTop ?? handle.Top, handle.Width, state.PersistedHeight ?? handle.Height, handle.IsVisible, state.Collapsed, state.Maximized); } private static void Apply( RetailWindowHandle handle, UiWindowLayout layout, (int Width, int Height) screen, bool restoreVisibility) { UiElement frame = handle.OuterFrame; float width = ClampDimension(layout.Width, frame.Width, frame.MinWidth, frame.MaxWidth, screen.Width); float height = ClampDimension(layout.Height, frame.Height, frame.MinHeight, frame.MaxHeight, screen.Height); handle.ResizeTo(width, height); float maxX = MathF.Max(0f, screen.Width - handle.Width); float maxY = MathF.Max(0f, screen.Height - handle.Height); float x = Math.Clamp(FiniteOr(layout.X, handle.Left), 0f, maxX); float y = Math.Clamp(FiniteOr(layout.Y, handle.Top), 0f, maxY); handle.MoveTo(x, y); handle.StateController?.RestoreWindowState(new RetainedWindowState( Collapsed: layout.Collapsed, Maximized: layout.Maximized)); if (restoreVisibility) { if (layout.Visible) handle.Show(); else handle.Hide(); } } private (int Width, int Height) ValidScreenSize() { var screen = _screenSize(); return (Math.Max(1, screen.Width), Math.Max(1, screen.Height)); } private static float ClampDimension( float saved, float current, float minimum, float maximum, int screenExtent) { float value = FiniteOr(saved, current); float upper = MathF.Max(minimum, MathF.Min(maximum, screenExtent)); return Math.Clamp(value, minimum, upper); } private static float FiniteOr(float value, float fallback) => float.IsFinite(value) ? value : fallback; private static string ResolutionKey((int Width, int Height) screen) => $"{screen.Width}x{screen.Height}"; private static bool CanPersist(string key) => !string.IsNullOrWhiteSpace(key) && !string.Equals(key, "default", StringComparison.OrdinalIgnoreCase); public void Dispose() { if (_disposed) return; _disposed = true; foreach (RetailWindowHandle handle in _attached) { handle.Moved -= OnChanged; handle.Resized -= OnChanged; if (!_stateManagedVisibilityWindows.Contains(handle.Name)) { handle.Shown -= OnChanged; handle.Hidden -= OnChanged; } } _attached.Clear(); } }