Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core. Co-Authored-By: Codex <codex@openai.com>
244 lines
8.5 KiB
C#
244 lines
8.5 KiB
C#
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 HashSet<string> _stateManagedVisibilityWindows;
|
|
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,
|
|
IEnumerable<string>? 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<string>(StringComparer.Ordinal)
|
|
: new HashSet<string>(stateManagedVisibilityWindows, StringComparer.Ordinal);
|
|
|
|
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,
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Force-save every attached window to the current automatic profile.</summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>Save every attached window to a portable named retail UI profile.</summary>
|
|
public void SaveNamed(string profileName)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
ArgumentNullException.ThrowIfNull(profileName);
|
|
foreach (RetailWindowHandle handle in _attached)
|
|
_store.SaveNamedWindowLayout(profileName, handle.Name, Capture(handle));
|
|
}
|
|
|
|
/// <summary>Restore every window found in a portable named retail UI profile.</summary>
|
|
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();
|
|
}
|
|
}
|