From 55bd845a633cbbf4214b4fdf8d3712d0ce481298 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 22 Mar 2024 22:47:50 +0900 Subject: [PATCH] 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 --- .../ImGuiNotification/IActiveNotification.cs | 38 ++++++++- .../ImGuiNotification/INotification.cs | 35 ++++++-- .../Internal/ActiveNotification.ImGui.cs | 2 +- .../Internal/ActiveNotification.cs | 83 +++++++++++++++---- .../ImGuiNotification/Notification.cs | 13 +++ .../Windows/Data/Widgets/ImGuiWidget.cs | 79 +++++++++++++++--- .../Textures/DalamudTextureWrapExtensions.cs | 21 +++++ 7 files changed, 231 insertions(+), 40 deletions(-) create mode 100644 Dalamud/Interface/Textures/DalamudTextureWrapExtensions.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index e677471b4..8ed0d1e20 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -56,7 +56,7 @@ public interface IActiveNotification : INotification /// from . /// /// 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. + /// via another call to this function or overwriting the property. You do not have to dispose it yourself. /// If is not null, then calling this function will simply dispose the /// passed without actually updating the icon. /// @@ -68,8 +68,8 @@ public interface IActiveNotification : INotification /// revert back to the icon specified from . /// /// The texture resulted from the 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 the - /// resulted instance of yourself. + /// 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 yourself. /// If the task fails for any reason, the exception will be silently ignored and the icon specified from /// will be used instead. /// If is not null, then calling this function will simply dispose the @@ -77,6 +77,38 @@ public interface IActiveNotification : INotification /// void SetIconTexture(Task? textureWrapTask); + /// Sets the icon from , overriding the icon. + /// The new texture wrap to use, or null to clear and revert back to the icon specified + /// from . + /// Whether to keep the passed not disposed. + /// + /// If is false, 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. + /// If is not null and is false, then + /// calling this function will simply dispose the passed without actually updating + /// the icon. + /// + void SetIconTexture(IDalamudTextureWrap? textureWrap, bool leaveOpen); + + /// Sets the icon from , overriding the icon, once the given task + /// completes. + /// The task that will result in a new texture wrap to use, or null to clear and + /// revert back to the icon specified from . + /// Whether to keep the result from the passed not + /// disposed. + /// + /// If is false, the texture resulted from the 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 the resulted instance of + /// yourself. + /// If the task fails for any reason, the exception will be silently ignored and the icon specified from + /// will be used instead. + /// If is not null, then calling this function will simply dispose the + /// result of the passed without actually updating the icon. + /// + void SetIconTexture(Task? textureWrapTask, bool leaveOpen); + /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index f9a043c0b..af34e0a1b 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -22,20 +22,43 @@ public interface INotification /// Gets or sets the type of the notification. NotificationType Type { get; set; } - /// Gets or sets the icon source. - /// Use or + /// Gets or sets the icon source, in case is not set or the task has faulted. + /// + INotificationIcon? Icon { get; set; } + + /// Gets or sets a texture wrap that will be used in place of if set. + /// + /// A texture wrap set via this property will NOT be disposed when the notification is dismissed. + /// Use or /// to use a texture, after calling /// . Call either of those functions with null to revert - /// the effective icon back to this property. - INotificationIcon? Icon { get; set; } + /// the effective icon back to this property. + /// This property and are bound together. If the task is not null but + /// is false (because the task is still in progress or faulted,) + /// the property will return null. Setting this property will set to a new + /// completed with the new value as its result. + /// + public IDalamudTextureWrap? IconTexture { get; set; } + + /// Gets or sets a task that results in a texture wrap that will be used in place of if + /// available. + /// + /// A texture wrap set via this property will NOT be disposed when the notification is dismissed. + /// Use or + /// to use a texture, after calling + /// . Call either of those functions with null to revert + /// the effective icon back to this property. + /// This property and are bound together. + /// + Task? IconTextureTask { get; set; } /// Gets or sets the hard expiry. /// /// Setting this value will override and , in that /// the notification will be dismissed when this expiry expires.
/// Set to to make only take effect.
- /// If neither nor 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 and are MaxValue, then the notification + /// will not expire after a set time. It must be explicitly dismissed by the user or via calling /// .
/// Updating this value will reset the dismiss timer. ///
diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 6702f3e4b..fdccaab7b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -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) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 5ae7de5f7..f1084fd20 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -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; + /// Whether to call on . + private bool hasIconTextureOwnership; + /// Gets the time of starting to count the timer for the expiration. private DateTime lastInterestTime; /// Gets the extended expiration time from . private DateTime extendedExpiry; - /// The icon texture to use if specified; otherwise, icon will be used from . - private Task? iconTextureWrap; - /// The plugin that initiated this notification. private LocalPlugin? initiatorPlugin; @@ -119,6 +118,34 @@ internal sealed partial class ActiveNotification : IActiveNotification set => this.underlyingNotification.Icon = value; } + /// + public IDalamudTextureWrap? IconTexture + { + get => this.underlyingNotification.IconTexture; + set => this.IconTextureTask = value is null ? null : Task.FromResult(value); + } + + /// + public Task? 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; + } + } + /// public DateTime HardExpiry { @@ -239,26 +266,36 @@ internal sealed partial class ActiveNotification : IActiveNotification } /// - public void SetIconTexture(IDalamudTextureWrap? textureWrap) - { - this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap)); - } + public void SetIconTexture(IDalamudTextureWrap? textureWrap) => + this.SetIconTexture(textureWrap, false); /// - public void SetIconTexture(Task? textureWrapTask) + public void SetIconTexture(IDalamudTextureWrap? textureWrap, bool leaveOpen) => + this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap), leaveOpen); + + /// + public void SetIconTexture(Task? textureWrapTask) => + this.SetIconTexture(textureWrapTask, false); + + /// + public void SetIconTexture(Task? 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; } /// Removes non-Dalamud invocation targets from events. @@ -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 /// Clears the resources associated with this instance of . 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; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 5175985c7..0475628fd 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -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 /// public INotificationIcon? Icon { get; set; } + /// + public IDalamudTextureWrap? IconTexture + { + get => this.IconTextureTask?.IsCompletedSuccessfully is true ? this.IconTextureTask.Result : null; + set => this.IconTextureTask = value is null ? null : Task.FromResult(value); + } + + /// + public Task? IconTextureTask { get; set; } + /// public DateTime HardExpiry { get; set; } = DateTime.MaxValue; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 086b0c1ad..6817c82b3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -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; /// internal class ImGuiWidget : IDataWindowWidget { + private readonly HashSet notifications = new(); private NotificationTemplate notificationTemplate; /// @@ -39,8 +41,10 @@ internal class ImGuiWidget : IDataWindowWidget /// public void Draw() { + this.notifications.RemoveWhere(x => x.DismissReason.HasValue); + var interfaceManager = Service.Get(); - var notifications = Service.Get(); + var nm = Service.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.Get(); var tm = Service.Get(); switch (this.notificationTemplate.IconInt) { case 5: n.SetIconTexture( - dam.GetDalamudTextureWrap( - Enum.Parse( - NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); + DisposeLoggingTextureWrap.Wrap( + dam.GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))), + this.notificationTemplate.LeaveTexturesOpen); break; case 6: n.SetIconTexture( dam.GetDalamudTextureWrapAsync( - Enum.Parse( - NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt])) + .ContinueWith( + r => r.IsCompletedSuccessfully + ? Task.FromResult(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.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.Get().AddNotification( + "Texture disposed", + "ImGui Widget", + NotificationType.Info); + } + } } diff --git a/Dalamud/Interface/Textures/DalamudTextureWrapExtensions.cs b/Dalamud/Interface/Textures/DalamudTextureWrapExtensions.cs new file mode 100644 index 000000000..ceae9a476 --- /dev/null +++ b/Dalamud/Interface/Textures/DalamudTextureWrapExtensions.cs @@ -0,0 +1,21 @@ +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.Textures; + +/// Extension methods for . +public static class DalamudTextureWrapExtensions +{ + /// Checks if two instances of point to a same underlying resource. + /// + /// The resource 1. + /// The resource 2. + /// true if both instances point to a same underlying resource. + 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; + } +}