Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes. Co-Authored-By: Codex <codex@openai.com>
307 lines
10 KiB
C#
307 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Owns the named retained-window registry, raise policy, lifecycle delivery,
|
|
/// and controller teardown. <see cref="UiRoot"/> remains responsible for raw
|
|
/// input ownership and reports completed interactions to this manager.
|
|
/// </summary>
|
|
public sealed class RetailWindowManager : IDisposable
|
|
{
|
|
private readonly UiRoot _root;
|
|
private readonly Dictionary<string, RetailWindowHandle> _byName =
|
|
new(StringComparer.Ordinal);
|
|
private readonly Dictionary<UiElement, RetailWindowHandle> _byFrame = new();
|
|
private readonly Dictionary<RetailWindowHandle, UiElement> _defaultInputs = new();
|
|
private bool _disposed;
|
|
|
|
internal RetailWindowManager(UiRoot root)
|
|
{
|
|
_root = root;
|
|
_root.ElementVisibilityChanged += OnElementVisibilityChanged;
|
|
_root.KeyboardFocusChanged += OnKeyboardFocusChanged;
|
|
_root.PointerCaptureChanged += OnPointerCaptureChanged;
|
|
_root.WindowMoved += OnWindowMoved;
|
|
_root.WindowResized += OnWindowResized;
|
|
_root.UiLockChanged += OnUiLockChanged;
|
|
}
|
|
|
|
public bool IsLocked => _root.UiLocked;
|
|
public IReadOnlyCollection<RetailWindowHandle> Windows => _byName.Values;
|
|
public event Action<string, bool>? WindowVisibilityChanged;
|
|
|
|
public RetailWindowHandle Register(
|
|
string name,
|
|
UiElement outerFrame,
|
|
UiElement? contentRoot = null,
|
|
IRetainedPanelController? controller = null,
|
|
IRetainedWindowStateController? stateController = null)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(outerFrame);
|
|
|
|
if (!ReferenceEquals(outerFrame.Parent, _root))
|
|
throw new InvalidOperationException(
|
|
$"Retained window '{name}' must be mounted as a direct UiRoot child before registration.");
|
|
|
|
IRetainedWindowStateController? resolvedStateController =
|
|
stateController ?? outerFrame as IRetainedWindowStateController;
|
|
|
|
if (_byName.TryGetValue(name, out var sameName))
|
|
{
|
|
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
|
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
|
&& ReferenceEquals(sameName.Controller, controller)
|
|
&& ReferenceEquals(sameName.StateController, resolvedStateController))
|
|
return sameName;
|
|
|
|
Unregister(name);
|
|
}
|
|
|
|
if (_byFrame.TryGetValue(outerFrame, out var sameFrame))
|
|
Unregister(sameFrame.Name);
|
|
|
|
var handle = new RetailWindowHandle(
|
|
this,
|
|
name,
|
|
outerFrame,
|
|
contentRoot ?? outerFrame,
|
|
controller,
|
|
resolvedStateController);
|
|
_byName.Add(name, handle);
|
|
_byFrame.Add(outerFrame, handle);
|
|
handle.NotifyInitialState();
|
|
return handle;
|
|
}
|
|
|
|
public bool TryGet(string name, out RetailWindowHandle handle)
|
|
=> _byName.TryGetValue(name, out handle!);
|
|
|
|
public void AttachController(string name, IRetainedPanelController controller)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(controller);
|
|
if (!_byName.TryGetValue(name, out var handle))
|
|
throw new KeyNotFoundException($"No retained window named '{name}' is registered.");
|
|
handle.AttachController(controller);
|
|
}
|
|
|
|
internal bool TryGet(UiElement outerFrame, out RetailWindowHandle handle)
|
|
=> _byFrame.TryGetValue(outerFrame, out handle!);
|
|
|
|
public bool Show(string name)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
handle.OuterFrame.Visible = true;
|
|
BringToFront(handle.OuterFrame);
|
|
return true;
|
|
}
|
|
|
|
public bool Hide(string name)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
handle.OuterFrame.Visible = false;
|
|
return true;
|
|
}
|
|
|
|
public bool Toggle(string name)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
if (handle.OuterFrame.Visible)
|
|
{
|
|
Hide(name);
|
|
return false;
|
|
}
|
|
|
|
Show(name);
|
|
return true;
|
|
}
|
|
|
|
public bool Close(string name)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
Hide(name);
|
|
handle.NotifyClosed();
|
|
return true;
|
|
}
|
|
|
|
public bool IsVisible(string name)
|
|
=> _byName.TryGetValue(name, out var handle) && handle.OuterFrame.Visible;
|
|
|
|
public bool MoveTo(string name, float left, float top)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
var frame = handle.OuterFrame;
|
|
if (frame.ConstrainDragToParent && frame.Parent is { } parent)
|
|
{
|
|
left = Math.Clamp(left, 0f, Math.Max(0f, parent.Width - frame.Width));
|
|
top = Math.Clamp(top, 0f, Math.Max(0f, parent.Height - frame.Height));
|
|
}
|
|
if (frame.Left == left && frame.Top == top) return true;
|
|
frame.Left = left;
|
|
frame.Top = top;
|
|
_root.NotifyWindowMoved(frame);
|
|
return true;
|
|
}
|
|
|
|
public bool ResizeTo(string name, float width, float height)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
var frame = handle.OuterFrame;
|
|
if (!frame.ResizeX) width = frame.Width;
|
|
if (!frame.ResizeY) height = frame.Height;
|
|
width = Math.Clamp(width, frame.MinWidth, frame.MaxWidth);
|
|
height = Math.Clamp(height, frame.MinHeight, frame.MaxHeight);
|
|
if (frame.Width == width && frame.Height == height) return true;
|
|
frame.Width = width;
|
|
frame.Height = height;
|
|
_root.NotifyWindowResized(frame);
|
|
return true;
|
|
}
|
|
|
|
public bool SetOpacity(string name, float opacity)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
handle.OuterFrame.Opacity = Math.Clamp(opacity, 0f, 1f);
|
|
return true;
|
|
}
|
|
|
|
public void SetLocked(bool locked) => _root.UiLocked = locked;
|
|
|
|
public bool Unregister(string name)
|
|
{
|
|
if (!_byName.TryGetValue(name, out var handle)) return false;
|
|
|
|
if (handle.OuterFrame.Visible)
|
|
handle.OuterFrame.Visible = false;
|
|
else
|
|
_root.ClearSubtreeOwnership(handle.OuterFrame);
|
|
|
|
_byName.Remove(name);
|
|
_byFrame.Remove(handle.OuterFrame);
|
|
_defaultInputs.Remove(handle);
|
|
handle.NotifyClosed();
|
|
handle.DisposeController();
|
|
return true;
|
|
}
|
|
|
|
public void BringToFront(UiElement window)
|
|
{
|
|
int top = window.ZOrder;
|
|
foreach (var child in _root.Children)
|
|
if (!ReferenceEquals(child, window))
|
|
top = Math.Max(top, child.ZOrder + 1);
|
|
window.ZOrder = top;
|
|
}
|
|
|
|
internal void PrepareToHide(UiElement subtree)
|
|
{
|
|
if (!_byFrame.TryGetValue(subtree, out var handle)) return;
|
|
if (_root.DefaultTextInput is { } input && IsWithin(input, subtree))
|
|
_defaultInputs[handle] = input;
|
|
}
|
|
|
|
internal void OnSubtreeRemoving(UiElement subtree)
|
|
{
|
|
foreach (var handle in _byName.Values
|
|
.Where(handle => IsWithin(handle.OuterFrame, subtree))
|
|
.ToArray())
|
|
{
|
|
handle.NotifyVisibility(false);
|
|
_byName.Remove(handle.Name);
|
|
_byFrame.Remove(handle.OuterFrame);
|
|
_defaultInputs.Remove(handle);
|
|
handle.NotifyClosed();
|
|
handle.DisposeController();
|
|
}
|
|
}
|
|
|
|
private void OnElementVisibilityChanged(UiElement element, bool visible)
|
|
{
|
|
if (!_byFrame.TryGetValue(element, out var handle)) return;
|
|
|
|
if (visible
|
|
&& _root.DefaultTextInput is null
|
|
&& _defaultInputs.TryGetValue(handle, out var input)
|
|
&& IsWithin(input, handle.OuterFrame))
|
|
{
|
|
_root.DefaultTextInput = input;
|
|
}
|
|
|
|
handle.NotifyVisibility(visible);
|
|
WindowVisibilityChanged?.Invoke(handle.Name, visible);
|
|
}
|
|
|
|
private void OnKeyboardFocusChanged(UiElement? oldFocus, UiElement? newFocus)
|
|
{
|
|
foreach (var handle in _byName.Values.ToArray())
|
|
{
|
|
bool hadFocus = IsWithin(oldFocus, handle.OuterFrame);
|
|
bool hasFocus = IsWithin(newFocus, handle.OuterFrame);
|
|
if (hadFocus || hasFocus)
|
|
handle.NotifyDescendantFocusChanged(hasFocus ? newFocus : null);
|
|
}
|
|
}
|
|
|
|
private void OnPointerCaptureChanged(UiElement? oldCapture, UiElement? newCapture)
|
|
{
|
|
foreach (var handle in _byName.Values.ToArray())
|
|
{
|
|
bool hadCapture = IsWithin(oldCapture, handle.OuterFrame);
|
|
bool hasCapture = IsWithin(newCapture, handle.OuterFrame);
|
|
if (hadCapture || hasCapture)
|
|
handle.NotifyDescendantCaptureChanged(hasCapture ? newCapture : null);
|
|
}
|
|
}
|
|
|
|
private void OnWindowMoved(string name, UiElement window)
|
|
{
|
|
if (_byName.TryGetValue(name, out var handle)
|
|
&& ReferenceEquals(handle.OuterFrame, window))
|
|
handle.NotifyMoved();
|
|
}
|
|
|
|
private void OnWindowResized(string name, UiElement window)
|
|
{
|
|
if (_byName.TryGetValue(name, out var handle)
|
|
&& ReferenceEquals(handle.OuterFrame, window))
|
|
handle.NotifyResized();
|
|
}
|
|
|
|
private void OnUiLockChanged(bool locked)
|
|
{
|
|
foreach (var handle in _byName.Values.ToArray())
|
|
handle.NotifyLockChanged(locked);
|
|
}
|
|
|
|
private static bool IsWithin(UiElement? element, UiElement subtree)
|
|
{
|
|
while (element is not null)
|
|
{
|
|
if (ReferenceEquals(element, subtree)) return true;
|
|
element = element.Parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
foreach (string name in _byName.Keys.ToArray())
|
|
Unregister(name);
|
|
|
|
_root.ElementVisibilityChanged -= OnElementVisibilityChanged;
|
|
_root.KeyboardFocusChanged -= OnKeyboardFocusChanged;
|
|
_root.PointerCaptureChanged -= OnPointerCaptureChanged;
|
|
_root.WindowMoved -= OnWindowMoved;
|
|
_root.WindowResized -= OnWindowResized;
|
|
_root.UiLockChanged -= OnUiLockChanged;
|
|
}
|
|
}
|