Add IconTexture/Wrap to INotification (#1738) (#1739)

* Add IconTexture/Wrap to INotification (#1738)

Notification record and IActiveNotification interface now supports
setting or updating the texture wraps being used, and SetIconTexture has
gotten more overloads to support leaveOpen mechanism that can commonly
be found with Stream wrappers.

ImGui widget is updated to support testing setting "leaveOpen" and
updating "IconTexture" property via setter, making it possible to check
whether IDTW.Dispose is being called under given conditions.

Some changes to doccomments are made.

* typo
This commit is contained in:
srkizer 2024-03-22 22:47:50 +09:00 committed by GitHub
parent 12d70f0749
commit 55bd845a63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 231 additions and 40 deletions

View file

@ -56,7 +56,7 @@ public interface IActiveNotification : INotification
/// from <see cref="INotification.Icon"/>.</param>
/// <remarks>
/// <para>The texture passed will be disposed when the notification is dismissed or a new different texture is set
/// via another call to this function. You do not have to dispose it yourself.</para>
/// via another call to this function or overwriting the property. You do not have to dispose it yourself.</para>
/// <para>If <see cref="DismissReason"/> is not <c>null</c>, then calling this function will simply dispose the
/// passed <paramref name="textureWrap"/> without actually updating the icon.</para>
/// </remarks>
@ -68,8 +68,8 @@ public interface IActiveNotification : INotification
/// revert back to the icon specified from <see cref="INotification.Icon"/>.</param>
/// <remarks>
/// <para>The texture resulted from the passed <see cref="Task{TResult}"/> will be disposed when the notification
/// is dismissed or a new different texture is set via another call to this function. You do not have to dispose the
/// resulted instance of <see cref="IDalamudTextureWrap"/> yourself.</para>
/// is dismissed or a new different texture is set via another call to this function over overwriting the property.
/// You do not have to dispose the resulted instance of <see cref="IDalamudTextureWrap"/> yourself.</para>
/// <para>If the task fails for any reason, the exception will be silently ignored and the icon specified from
/// <see cref="INotification.Icon"/> will be used instead.</para>
/// <para>If <see cref="DismissReason"/> is not <c>null</c>, then calling this function will simply dispose the
@ -77,6 +77,38 @@ public interface IActiveNotification : INotification
/// </remarks>
void SetIconTexture(Task<IDalamudTextureWrap?>? textureWrapTask);
/// <summary>Sets the icon from <see cref="IDalamudTextureWrap"/>, overriding the icon.</summary>
/// <param name="textureWrap">The new texture wrap to use, or null to clear and revert back to the icon specified
/// from <see cref="INotification.Icon"/>.</param>
/// <param name="leaveOpen">Whether to keep the passed <paramref name="textureWrap"/> not disposed.</param>
/// <remarks>
/// <para>If <paramref name="leaveOpen"/> is <c>false</c>, the texture passed will be disposed when the
/// notification is dismissed or a new different texture is set via another call to this function. You do not have
/// to dispose it yourself.</para>
/// <para>If <see cref="DismissReason"/> is not <c>null</c> and <paramref name="leaveOpen"/> is <c>false</c>, then
/// calling this function will simply dispose the passed <paramref name="textureWrap"/> without actually updating
/// the icon.</para>
/// </remarks>
void SetIconTexture(IDalamudTextureWrap? textureWrap, bool leaveOpen);
/// <summary>Sets the icon from <see cref="IDalamudTextureWrap"/>, overriding the icon, once the given task
/// completes.</summary>
/// <param name="textureWrapTask">The task that will result in a new texture wrap to use, or null to clear and
/// revert back to the icon specified from <see cref="INotification.Icon"/>.</param>
/// <param name="leaveOpen">Whether to keep the result from the passed <paramref name="textureWrapTask"/> not
/// disposed.</param>
/// <remarks>
/// <para>If <paramref name="leaveOpen"/> is <c>false</c>, the texture resulted from the passed
/// <see cref="Task{TResult}"/> will be disposed when the notification is dismissed or a new different texture is
/// set via another call to this function. You do not have to dispose the resulted instance of
/// <see cref="IDalamudTextureWrap"/> yourself.</para>
/// <para>If the task fails for any reason, the exception will be silently ignored and the icon specified from
/// <see cref="INotification.Icon"/> will be used instead.</para>
/// <para>If <see cref="DismissReason"/> is not <c>null</c>, then calling this function will simply dispose the
/// result of the passed <paramref name="textureWrapTask"/> without actually updating the icon.</para>
/// </remarks>
void SetIconTexture(Task<IDalamudTextureWrap?>? textureWrapTask, bool leaveOpen);
/// <summary>Generates a new value to use for <see cref="Id"/>.</summary>
/// <returns>The new value.</returns>
internal static long CreateNewId() => Interlocked.Increment(ref idCounter);

View file

@ -22,20 +22,43 @@ public interface INotification
/// <summary>Gets or sets the type of the notification.</summary>
NotificationType Type { get; set; }
/// <summary>Gets or sets the icon source.</summary>
/// <remarks>Use <see cref="IActiveNotification.SetIconTexture(IDalamudTextureWrap?)"/> or
/// <summary>Gets or sets the icon source, in case <see cref="IconTextureTask"/> is not set or the task has faulted.
/// </summary>
INotificationIcon? Icon { get; set; }
/// <summary>Gets or sets a texture wrap that will be used in place of <see cref="Icon"/> if set.</summary>
/// <remarks>
/// <para>A texture wrap set via this property will <b>NOT</b> be disposed when the notification is dismissed.
/// Use <see cref="IActiveNotification.SetIconTexture(IDalamudTextureWrap?)"/> or
/// <see cref="IActiveNotification.SetIconTexture(Task{IDalamudTextureWrap?}?)"/> to use a texture, after calling
/// <see cref="INotificationManager.AddNotification"/>. Call either of those functions with <c>null</c> to revert
/// the effective icon back to this property.</remarks>
INotificationIcon? Icon { get; set; }
/// the effective icon back to this property.</para>
/// <para>This property and <see cref="IconTextureTask"/> are bound together. If the task is not <c>null</c> but
/// <see cref="Task.IsCompletedSuccessfully"/> is <c>false</c> (because the task is still in progress or faulted,)
/// the property will return <c>null</c>. Setting this property will set <see cref="IconTextureTask"/> to a new
/// completed <see cref="Task{TResult}"/> with the new value as its result.</para>
/// </remarks>
public IDalamudTextureWrap? IconTexture { get; set; }
/// <summary>Gets or sets a task that results in a texture wrap that will be used in place of <see cref="Icon"/> if
/// available.</summary>
/// <remarks>
/// <para>A texture wrap set via this property will <b>NOT</b> be disposed when the notification is dismissed.
/// Use <see cref="IActiveNotification.SetIconTexture(IDalamudTextureWrap?)"/> or
/// <see cref="IActiveNotification.SetIconTexture(Task{IDalamudTextureWrap?}?)"/> to use a texture, after calling
/// <see cref="INotificationManager.AddNotification"/>. Call either of those functions with <c>null</c> to revert
/// the effective icon back to this property.</para>
/// <para>This property and <see cref="IconTexture"/> are bound together.</para>
/// </remarks>
Task<IDalamudTextureWrap?>? IconTextureTask { get; set; }
/// <summary>Gets or sets the hard expiry.</summary>
/// <remarks>
/// Setting this value will override <see cref="InitialDuration"/> and <see cref="ExtensionDurationSinceLastInterest"/>, in that
/// the notification will be dismissed when this expiry expires.<br />
/// Set to <see cref="DateTime.MaxValue"/> to make only <see cref="InitialDuration"/> take effect.<br />
/// If neither <see cref="HardExpiry"/> nor <see cref="InitialDuration"/> is not MaxValue, then the notification
/// will not expire after a set time. It must be explicitly dismissed by the user of via calling
/// If both <see cref="HardExpiry"/> and <see cref="InitialDuration"/> are MaxValue, then the notification
/// will not expire after a set time. It must be explicitly dismissed by the user or via calling
/// <see cref="IActiveNotification.DismissNow"/>.<br />
/// Updating this value will reset the dismiss timer.
/// </remarks>

View file

@ -404,7 +404,7 @@ internal sealed partial class ActiveNotification
var maxCoord = minCoord + size;
var iconColor = this.Type.ToColor();
if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap))
if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.IconTextureTask))
return;
if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true)

View file

@ -1,5 +1,4 @@
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
@ -24,15 +23,15 @@ internal sealed partial class ActiveNotification : IActiveNotification
private readonly Easing progressEasing;
private readonly Easing expandoEasing;
/// <summary>Whether to call <see cref="IDisposable.Dispose"/> on <see cref="DisposeInternal"/>.</summary>
private bool hasIconTextureOwnership;
/// <summary>Gets the time of starting to count the timer for the expiration.</summary>
private DateTime lastInterestTime;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
private DateTime extendedExpiry;
/// <summary>The icon texture to use if specified; otherwise, icon will be used from <see cref="Icon"/>.</summary>
private Task<IDalamudTextureWrap>? iconTextureWrap;
/// <summary>The plugin that initiated this notification.</summary>
private LocalPlugin? initiatorPlugin;
@ -119,6 +118,34 @@ internal sealed partial class ActiveNotification : IActiveNotification
set => this.underlyingNotification.Icon = value;
}
/// <inheritdoc/>
public IDalamudTextureWrap? IconTexture
{
get => this.underlyingNotification.IconTexture;
set => this.IconTextureTask = value is null ? null : Task.FromResult(value);
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap?>? IconTextureTask
{
get => this.underlyingNotification.IconTextureTask;
set
{
// Do nothing if the value did not change.
if (this.underlyingNotification.IconTextureTask == value)
return;
if (this.hasIconTextureOwnership)
{
_ = this.underlyingNotification.IconTextureTask?.ToContentDisposedTask(true);
this.underlyingNotification.IconTextureTask = null;
this.hasIconTextureOwnership = false;
}
this.underlyingNotification.IconTextureTask = value;
}
}
/// <inheritdoc/>
public DateTime HardExpiry
{
@ -239,26 +266,36 @@ internal sealed partial class ActiveNotification : IActiveNotification
}
/// <inheritdoc/>
public void SetIconTexture(IDalamudTextureWrap? textureWrap)
{
this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap));
}
public void SetIconTexture(IDalamudTextureWrap? textureWrap) =>
this.SetIconTexture(textureWrap, false);
/// <inheritdoc/>
public void SetIconTexture(Task<IDalamudTextureWrap?>? textureWrapTask)
public void SetIconTexture(IDalamudTextureWrap? textureWrap, bool leaveOpen) =>
this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap), leaveOpen);
/// <inheritdoc/>
public void SetIconTexture(Task<IDalamudTextureWrap?>? textureWrapTask) =>
this.SetIconTexture(textureWrapTask, false);
/// <inheritdoc/>
public void SetIconTexture(Task<IDalamudTextureWrap?>? textureWrapTask, bool leaveOpen)
{
// If we're requested to replace the texture with the same texture, do nothing.
if (this.underlyingNotification.IconTextureTask == textureWrapTask)
return;
if (this.DismissReason is not null)
{
textureWrapTask?.ToContentDisposedTask(true);
if (!leaveOpen)
textureWrapTask?.ToContentDisposedTask(true);
return;
}
// After replacing, if the old texture is not the old texture, then dispose the old texture.
if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrapTask) is { } wrapTaskToDispose &&
wrapTaskToDispose != textureWrapTask)
{
wrapTaskToDispose.ToContentDisposedTask(true);
}
if (this.hasIconTextureOwnership)
_ = this.underlyingNotification.IconTextureTask?.ToContentDisposedTask(true);
this.hasIconTextureOwnership = !leaveOpen;
this.underlyingNotification.IconTextureTask = textureWrapTask;
}
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
@ -280,6 +317,11 @@ internal sealed partial class ActiveNotification : IActiveNotification
if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType()))
this.Icon = null;
// Clear the texture if we don't have the ownership.
// The texture probably was owned by the plugin being unloaded in such case.
if (!this.hasIconTextureOwnership)
this.IconTextureTask = null;
this.isInitiatorUnloaded = true;
this.UserDismissable = true;
this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration;
@ -358,8 +400,13 @@ internal sealed partial class ActiveNotification : IActiveNotification
/// <summary>Clears the resources associated with this instance of <see cref="ActiveNotification"/>.</summary>
internal void DisposeInternal()
{
if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapTaskToDispose)
wrapTaskToDispose.ToContentDisposedTask(true);
if (this.hasIconTextureOwnership)
{
_ = this.underlyingNotification.IconTextureTask?.ToContentDisposedTask(true);
this.underlyingNotification.IconTextureTask = null;
this.hasIconTextureOwnership = false;
}
this.Dismiss = null;
this.Click = null;
this.DrawActions = null;

View file

@ -1,4 +1,7 @@
using System.Threading.Tasks;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications;
namespace Dalamud.Interface.ImGuiNotification;
@ -26,6 +29,16 @@ public sealed record Notification : INotification
/// <inheritdoc/>
public INotificationIcon? Icon { get; set; }
/// <inheritdoc/>
public IDalamudTextureWrap? IconTexture
{
get => this.IconTextureTask?.IsCompletedSuccessfully is true ? this.IconTextureTask.Result : null;
set => this.IconTextureTask = value is null ? null : Task.FromResult(value);
}
/// <inheritdoc/>
public Task<IDalamudTextureWrap?>? IconTextureTask { get; set; }
/// <inheritdoc/>
public DateTime HardExpiry { get; set; } = DateTime.MaxValue;

View file

@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Game.Text;
@ -18,6 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class ImGuiWidget : IDataWindowWidget
{
private readonly HashSet<IActiveNotification> notifications = new();
private NotificationTemplate notificationTemplate;
/// <inheritdoc/>
@ -39,8 +41,10 @@ internal class ImGuiWidget : IDataWindowWidget
/// <inheritdoc/>
public void Draw()
{
this.notifications.RemoveWhere(x => x.DismissReason.HasValue);
var interfaceManager = Service<InterfaceManager>.Get();
var notifications = Service<NotificationManager>.Get();
var nm = Service<NotificationManager>.Get();
ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size);
ImGui.Text("OverrideGameCursor: " + interfaceManager.OverrideGameCursor);
@ -139,6 +143,8 @@ internal class ImGuiWidget : IDataWindowWidget
"Action Bar (always on if not user dismissable for the example)",
ref this.notificationTemplate.ActionBar);
ImGui.Checkbox("Leave Textures Open", ref this.notificationTemplate.LeaveTexturesOpen);
if (ImGui.Button("Add notification"))
{
var text =
@ -152,7 +158,7 @@ internal class ImGuiWidget : IDataWindowWidget
if (this.notificationTemplate.ManualType)
type = (NotificationType)this.notificationTemplate.TypeInt;
var n = notifications.AddNotification(
var n = nm.AddNotification(
new()
{
Content = text,
@ -198,27 +204,40 @@ internal class ImGuiWidget : IDataWindowWidget
},
});
this.notifications.Add(n);
var dam = Service<DalamudAssetManager>.Get();
var tm = Service<TextureManager>.Get();
switch (this.notificationTemplate.IconInt)
{
case 5:
n.SetIconTexture(
dam.GetDalamudTextureWrap(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt])));
DisposeLoggingTextureWrap.Wrap(
dam.GetDalamudTextureWrap(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))),
this.notificationTemplate.LeaveTexturesOpen);
break;
case 6:
n.SetIconTexture(
dam.GetDalamudTextureWrapAsync(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt])));
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))
.ContinueWith(
r => r.IsCompletedSuccessfully
? Task.FromResult<IDalamudTextureWrap>(DisposeLoggingTextureWrap.Wrap(r.Result))
: r).Unwrap(),
this.notificationTemplate.LeaveTexturesOpen);
break;
case 7:
n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText));
n.SetIconTexture(
DisposeLoggingTextureWrap.Wrap(tm.GetTextureFromGame(this.notificationTemplate.IconText)),
this.notificationTemplate.LeaveTexturesOpen);
break;
case 8:
n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText)));
n.SetIconTexture(
DisposeLoggingTextureWrap.Wrap(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))),
this.notificationTemplate.LeaveTexturesOpen);
break;
}
@ -261,7 +280,7 @@ internal class ImGuiWidget : IDataWindowWidget
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"{nclick}");
ImGui.SameLine();
if (ImGui.Button("Update"))
{
@ -274,13 +293,23 @@ internal class ImGuiWidget : IDataWindowWidget
ImGui.SameLine();
if (ImGui.Button("Dismiss"))
an.Notification.DismissNow();
ImGui.SameLine();
ImGui.SetNextItemWidth(an.MaxCoord.X - ImGui.GetCursorPosX());
ImGui.InputText("##input", ref testString, 255);
};
}
}
ImGui.SameLine();
if (ImGui.Button("Replace images using setter"))
{
foreach (var n in this.notifications)
{
var i = (uint)Random.Shared.NextInt64(0, 200000);
n.IconTexture = DisposeLoggingTextureWrap.Wrap(Service<TextureManager>.Get().GetIcon(i));
}
}
}
private static void NewRandom(out string? title, out NotificationType type, out float progress)
@ -395,6 +424,7 @@ internal class ImGuiWidget : IDataWindowWidget
public bool Minimized;
public bool UserDismissable;
public bool ActionBar;
public bool LeaveTexturesOpen;
public int ProgressMode;
public void Reset()
@ -416,8 +446,33 @@ internal class ImGuiWidget : IDataWindowWidget
this.Minimized = true;
this.UserDismissable = true;
this.ActionBar = true;
this.LeaveTexturesOpen = true;
this.ProgressMode = 0;
this.RespectUiHidden = true;
}
}
private sealed class DisposeLoggingTextureWrap : IDalamudTextureWrap
{
private readonly IDalamudTextureWrap inner;
public DisposeLoggingTextureWrap(IDalamudTextureWrap inner) => this.inner = inner;
public nint ImGuiHandle => this.inner.ImGuiHandle;
public int Width => this.inner.Width;
public int Height => this.inner.Height;
public static DisposeLoggingTextureWrap? Wrap(IDalamudTextureWrap? inner) => inner is null ? null : new(inner);
public void Dispose()
{
this.inner.Dispose();
Service<NotificationManager>.Get().AddNotification(
"Texture disposed",
"ImGui Widget",
NotificationType.Info);
}
}
}

View file

@ -0,0 +1,21 @@
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.Textures;
/// <summary>Extension methods for <see cref="IDalamudTextureWrap"/>.</summary>
public static class DalamudTextureWrapExtensions
{
/// <summary>Checks if two instances of <see cref="IDalamudTextureWrap"/> point to a same underlying resource.
/// </summary>
/// <param name="a">The resource 1.</param>
/// <param name="b">The resource 2.</param>
/// <returns><c>true</c> if both instances point to a same underlying resource.</returns>
public static bool ResourceEquals(this IDalamudTextureWrap? a, IDalamudTextureWrap? b)
{
if (a is null != b is null)
return false;
if (a is null)
return false;
return a.ImGuiHandle == b.ImGuiHandle;
}
}