Dalamud/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs
srkizer 87b9edb448
Add IInternal/PublicDisposableService (#1696)
* Add IInternal/PublicDisposableService

Plugins are exposed interfaces that are not inherited from
`IDisposable`, but services implementing plugin interfaces often
implement `IDisposable`. Some plugins may try to call
`IDisposable.Dispose` on everything provided, and it also is possible to
use `using` clause too eagerly while working on Dalamud itself, such as
writing `using var smth = await Service<SomeService>.GetAsync();`. Such
behaviors often lead to a difficult-to-debug errors, and making those
services either not an `IDisposable` or making `IDisposable.Dispose` do
nothing if the object has been loaded would prevent such errors. As
`ServiceManager` must be the only class dealing with construction and
disposal of services, `IInternalDisposableService` has been added to
limit who can dispose the object. `IPublicDisposableService` also has
been added to classes that can be constructed and accessed directly by
plugins; for those, `Dispose` will be ignored if the instance is a
service instance, and only `DisposeService` will respond.

In addition, `DalamudPluginInterface` and `UiBuilder` also have been
changed so that their `IDisposable.Dispose` no longer respond, and
instead, internal functions have been added to only allow disposal from
Dalamud.

* Cleanup

* Postmerge fixes

* More explanation on RunOnFrameworkThread(ClearHooks)

* Mark ReliableFileStorage public ctor obsolete

---------

Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
2024-03-16 15:58:05 +00:00

239 lines
7.8 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Interface;
/// <summary>
/// Class responsible for managing elements in the title screen menu.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
internal class TitleScreenMenu : IServiceType, ITitleScreenMenu
{
/// <summary>
/// Gets the texture size needed for title screen menu logos.
/// </summary>
internal const uint TextureSize = 64;
private readonly List<TitleScreenMenuEntry> entries = new();
private TitleScreenMenuEntry[]? entriesView;
[ServiceManager.ServiceConstructor]
private TitleScreenMenu()
{
}
/// <summary>
/// Event to be called when the entry list has been changed.
/// </summary>
internal event Action? EntryListChange;
/// <inheritdoc/>
public IReadOnlyList<TitleScreenMenuEntry> Entries
{
get
{
lock (this.entries)
{
if (!this.entries.Any())
return Array.Empty<TitleScreenMenuEntry>();
return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray();
}
}
}
/// <inheritdoc/>
public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
TitleScreenMenuEntry entry;
lock (this.entries)
{
var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList();
var priority = entriesOfAssembly.Any()
? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1)
: 0;
entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered);
var i = this.entries.BinarySearch(entry);
if (i < 0)
i = ~i;
this.entries.Insert(i, entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
return entry;
}
/// <inheritdoc/>
public TitleScreenMenuEntry AddEntry(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
TitleScreenMenuEntry entry;
lock (this.entries)
{
entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered);
var i = this.entries.BinarySearch(entry);
if (i < 0)
i = ~i;
this.entries.Insert(i, entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
return entry;
}
/// <inheritdoc/>
public void RemoveEntry(TitleScreenMenuEntry entry)
{
lock (this.entries)
{
this.entries.Remove(entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
}
/// <summary>
/// Adds a new entry to the title screen menu.
/// </summary>
/// <param name="priority">Priority of the entry.</param>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
internal TitleScreenMenuEntry AddEntryCore(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
TitleScreenMenuEntry entry;
lock (this.entries)
{
entry = new(null, priority, text, texture, onTriggered)
{
IsInternal = true,
};
this.entries.Add(entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
return entry;
}
/// <summary>
/// Adds a new entry to the title screen menu.
/// </summary>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <param name="showConditionKeys">The keys that have to be held to display the menu.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
internal TitleScreenMenuEntry AddEntryCore(
string text,
IDalamudTextureWrap texture,
Action onTriggered,
params VirtualKey[] showConditionKeys)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
TitleScreenMenuEntry entry;
lock (this.entries)
{
var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList();
var priority = entriesOfAssembly.Any()
? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1)
: 0;
entry = new(null, priority, text, texture, onTriggered, showConditionKeys)
{
IsInternal = true,
};
this.entries.Add(entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
return entry;
}
}
/// <summary>
/// Plugin-scoped version of a TitleScreenMenu service.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<ITitleScreenMenu>]
#pragma warning restore SA1015
internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleScreenMenu
{
[ServiceManager.ServiceDependency]
private readonly TitleScreenMenu titleScreenMenuService = Service<TitleScreenMenu>.Get();
private readonly List<TitleScreenMenuEntry> pluginEntries = new();
/// <inheritdoc/>
public IReadOnlyList<TitleScreenMenuEntry>? Entries => this.titleScreenMenuService.Entries;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var entry in this.pluginEntries)
{
this.titleScreenMenuService.RemoveEntry(entry);
}
}
/// <inheritdoc/>
public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered)
{
var entry = this.titleScreenMenuService.AddEntry(text, texture, onTriggered);
this.pluginEntries.Add(entry);
return entry;
}
/// <inheritdoc/>
public TitleScreenMenuEntry AddEntry(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered)
{
var entry = this.titleScreenMenuService.AddEntry(priority, text, texture, onTriggered);
this.pluginEntries.Add(entry);
return entry;
}
/// <inheritdoc/>
public void RemoveEntry(TitleScreenMenuEntry entry)
{
this.pluginEntries.Remove(entry);
this.titleScreenMenuService.RemoveEntry(entry);
}
}