feat(ui): persist retained window layouts
This commit is contained in:
parent
a8e9503d2e
commit
921c388e2c
22 changed files with 705 additions and 141 deletions
176
src/AcDream.App/UI/RetailWindowLayoutPersistence.cs
Normal file
176
src/AcDream.App/UI/RetailWindowLayoutPersistence.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges typed retained-window lifecycle events to per-character,
|
||||
/// per-resolution settings. It deliberately ignores the temporary pre-login
|
||||
/// <c>default</c> character key so startup layout cannot overwrite a real
|
||||
/// character's state.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||
{
|
||||
private readonly RetailWindowManager _manager;
|
||||
private readonly SettingsStore _store;
|
||||
private readonly Func<string> _characterKey;
|
||||
private readonly Func<(int Width, int Height)> _screenSize;
|
||||
private readonly List<RetailWindowHandle> _attached = new();
|
||||
private bool _restoring;
|
||||
private bool _disposed;
|
||||
|
||||
public RetailWindowLayoutPersistence(
|
||||
RetailWindowManager manager,
|
||||
SettingsStore store,
|
||||
Func<string> characterKey,
|
||||
Func<(int Width, int Height)> screenSize)
|
||||
{
|
||||
_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));
|
||||
|
||||
foreach (RetailWindowHandle handle in manager.Windows)
|
||||
Attach(handle);
|
||||
}
|
||||
|
||||
/// <summary>Restore all registered windows after character and screen are known.</summary>
|
||||
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);
|
||||
// Lazily migrate legacy position-only entries into the complete schema.
|
||||
_store.SaveWindowLayout(character, resolution, handle.Name, Capture(handle));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_restoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Attach(RetailWindowHandle handle)
|
||||
{
|
||||
_attached.Add(handle);
|
||||
handle.Moved += OnChanged;
|
||||
handle.Resized += OnChanged;
|
||||
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)
|
||||
{
|
||||
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 (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;
|
||||
handle.Shown -= OnChanged;
|
||||
handle.Hidden -= OnChanged;
|
||||
}
|
||||
_attached.Clear();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue