acdream/src/AcDream.App/Plugins/BufferedUiRegistry.cs

364 lines
12 KiB
C#

using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.Plugin.Abstractions;
namespace AcDream.App.Plugins;
/// <summary>
/// Buffers plugin <see cref="IUiRegistry.AddMarkupPanel"/> calls (which run in
/// Program.cs before the GL window opens) until GameWindow drains them into the
/// UiHost tree after construction.
/// </summary>
public sealed class BufferedUiRegistry : IScopedUiRegistry
{
public readonly record struct Pending(
PluginUiOwner Owner,
PluginPanelDescriptor Descriptor,
string MarkupPath,
object Binding)
{
internal long RegistrationId { get; init; }
internal string? MarkupContent { get; init; }
/// <summary>Stable, manifest-scoped retained-window persistence key.</summary>
public string WindowName =>
$"plugin:{Owner.Id}:{Descriptor.WindowId}";
}
private sealed class Registration(
PluginUiOwner owner,
PluginPanelDescriptor descriptor,
string markupPath,
object binding,
string? markupContent = null)
{
internal PluginUiOwner Owner { get; } = owner;
internal PluginPanelDescriptor Descriptor { get; } = descriptor;
internal string MarkupPath { get; } = markupPath;
internal object Binding { get; } = binding;
internal string? MarkupContent { get; } = markupContent;
internal bool Drained { get; set; }
internal UiRoot? Root { get; set; }
internal UiElement? Element { get; set; }
internal Action? WindowCleanup { get; set; }
}
private readonly object _gate = new();
private readonly Dictionary<long, Registration> _registrations = [];
private long _nextRegistrationId;
public void AddMarkupPanel(string markupPath, object binding)
=> _ = RegisterMarkupPanel(markupPath, binding);
public void AddPanel(
PluginPanelDescriptor descriptor,
string markupPath,
object binding)
=> _ = RegisterPanel(
new PluginUiOwner("unscoped", descriptor.Title),
descriptor,
markupPath,
binding);
public IDisposable RegisterPanel(
PluginPanelDescriptor descriptor,
string markupPath,
object binding) => RegisterPanel(
new PluginUiOwner("unscoped", descriptor.Title),
descriptor,
markupPath,
binding);
public IDisposable RegisterPanelContent(
PluginPanelDescriptor descriptor,
string markupContent,
object binding) => RegisterPanelContent(
new PluginUiOwner("unscoped", descriptor.Title),
descriptor,
markupContent,
binding);
public bool ViewExists(string viewName) =>
ViewExists(new PluginUiOwner("unscoped", "Plugin"), viewName);
public bool IsViewVisible(string viewName) =>
IsViewVisible(new PluginUiOwner("unscoped", "Plugin"), viewName);
public bool ControlExists(string viewName, string controlName) =>
ControlExists(
new PluginUiOwner("unscoped", "Plugin"), viewName, controlName);
public bool SetControlLabel(
string viewName,
string controlName,
string label) => SetControlLabel(
new PluginUiOwner("unscoped", "Plugin"), viewName, controlName, label);
public bool SetControlVisible(
string viewName,
string controlName,
bool visible) => SetControlVisible(
new PluginUiOwner("unscoped", "Plugin"), viewName, controlName, visible);
public IDisposable RegisterMarkupPanel(string markupPath, object binding)
=> RegisterPanel(
new PluginUiOwner("legacy", "Plugin"),
new PluginPanelDescriptor(
Path.GetFileNameWithoutExtension(markupPath),
Path.GetFileNameWithoutExtension(markupPath)),
markupPath,
binding);
public IDisposable RegisterPanel(
PluginUiOwner owner,
PluginPanelDescriptor descriptor,
string markupPath,
object binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(owner.Id);
ArgumentException.ThrowIfNullOrWhiteSpace(owner.DisplayName);
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.WindowId);
ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.Title);
ArgumentException.ThrowIfNullOrWhiteSpace(markupPath);
ArgumentNullException.ThrowIfNull(binding);
long id;
lock (_gate)
{
id = checked(++_nextRegistrationId);
_registrations.Add(
id,
new Registration(owner, descriptor, markupPath, binding));
}
return new RegistrationToken(this, id);
}
public IDisposable RegisterPanelContent(
PluginUiOwner owner,
PluginPanelDescriptor descriptor,
string markupContent,
object binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(owner.Id);
ArgumentException.ThrowIfNullOrWhiteSpace(owner.DisplayName);
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.WindowId);
ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.Title);
ArgumentException.ThrowIfNullOrWhiteSpace(markupContent);
ArgumentNullException.ThrowIfNull(binding);
long id;
lock (_gate)
{
id = checked(++_nextRegistrationId);
_registrations.Add(
id,
new Registration(
owner,
descriptor,
$"<inline:{descriptor.WindowId}>",
binding,
markupContent));
}
return new RegistrationToken(this, id);
}
/// <summary>Returns each not-yet-drained active registration once.</summary>
public IReadOnlyList<Pending> Drain()
{
lock (_gate)
{
var pending = new List<Pending>(_registrations.Count);
foreach ((long id, Registration registration) in _registrations)
{
if (registration.Drained)
continue;
registration.Drained = true;
pending.Add(new Pending(
registration.Owner,
registration.Descriptor,
registration.MarkupPath,
registration.Binding)
{
RegistrationId = id,
MarkupContent = registration.MarkupContent,
});
}
return pending;
}
}
internal void CompleteMount(Pending pending, UiRoot root, UiElement element)
{
bool stillRegistered;
lock (_gate)
{
stillRegistered = _registrations.TryGetValue(
pending.RegistrationId,
out Registration? registration);
if (stillRegistered)
{
registration!.Root = root;
registration.Element = element;
}
}
// A plugin can fail/disable while markup is being built. Never leave
// the just-built child mounted if its host-owned token was rolled back.
if (!stillRegistered)
root.RemoveChild(element);
}
/// <summary>
/// Publishes the window-manager half of a mounted registration. Disposal
/// may race between retained-tree mount and window registration, so a late
/// publication cleans itself up immediately when ownership is already gone.
/// </summary>
internal void CompleteWindowMount(Pending pending, Action cleanup)
{
ArgumentNullException.ThrowIfNull(cleanup);
bool stillRegistered;
lock (_gate)
{
stillRegistered = _registrations.TryGetValue(
pending.RegistrationId,
out Registration? registration);
if (stillRegistered)
registration!.WindowCleanup = cleanup;
}
if (!stillRegistered)
cleanup();
}
internal void FailMount(Pending pending) => Remove(pending.RegistrationId);
internal int RegistrationCount
{
get
{
lock (_gate)
return _registrations.Count;
}
}
public bool ViewExists(PluginUiOwner owner, string viewName)
{
lock (_gate)
return FindRegistrationLocked(owner, viewName) is not null;
}
public bool IsViewVisible(PluginUiOwner owner, string viewName)
{
UiElement? view;
lock (_gate)
view = FindRegistrationLocked(owner, viewName)?.Element;
return view?.Visible == true;
}
public bool ControlExists(
PluginUiOwner owner,
string viewName,
string controlName) =>
FindControl(owner, viewName, controlName) is not null;
public bool SetControlLabel(
PluginUiOwner owner,
string viewName,
string controlName,
string label)
{
UiElement? control = FindControl(owner, viewName, controlName);
switch (control)
{
case UiSimpleButton button:
button.TextSource = null;
button.Text = label;
return true;
case UiMarkupToggle toggle:
toggle.TextSource = null;
toggle.Text = label;
return true;
case UiLabel text:
text.TextSource = null;
text.Text = label;
return true;
default:
return false;
}
}
public bool SetControlVisible(
PluginUiOwner owner,
string viewName,
string controlName,
bool visible)
{
UiElement? control = FindControl(owner, viewName, controlName);
if (control is null)
return false;
control.VisibleSource = null;
control.Visible = visible;
return true;
}
private UiElement? FindControl(
PluginUiOwner owner,
string viewName,
string controlName)
{
UiElement? view;
lock (_gate)
view = FindRegistrationLocked(owner, viewName)?.Element;
return view is null ? null : FindByName(view, controlName);
}
private Registration? FindRegistrationLocked(
PluginUiOwner owner,
string viewName) => _registrations.Values.FirstOrDefault(registration =>
registration.Owner == owner
&& (registration.Descriptor.WindowId.Equals(
viewName, StringComparison.Ordinal)
|| registration.Descriptor.Title.Equals(
viewName, StringComparison.Ordinal)));
private static UiElement? FindByName(UiElement root, string name)
{
if (root.Name?.Equals(name, StringComparison.Ordinal) == true)
return root;
foreach (UiElement child in root.Children)
{
UiElement? found = FindByName(child, name);
if (found is not null)
return found;
}
return null;
}
private void Remove(long id)
{
UiRoot? root;
UiElement? element;
Action? windowCleanup;
lock (_gate)
{
if (!_registrations.Remove(id, out Registration? registration))
return;
root = registration.Root;
element = registration.Element;
windowCleanup = registration.WindowCleanup;
}
windowCleanup?.Invoke();
if (root is not null && element is not null)
root.RemoveChild(element);
}
private sealed class RegistrationToken(
BufferedUiRegistry owner,
long registrationId) : IDisposable
{
private BufferedUiRegistry? _owner = owner;
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Remove(registrationId);
}
}