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; + } +}