From 3ba395bd70c7cbe0d8630840e3e17d3e4a4bf9d3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:31:13 +0900 Subject: [PATCH 01/41] Implement INotificationManager --- .../ImGuiNotification/IActiveNotification.cs | 109 ++++ .../ImGuiNotification/INotification.cs | 75 +++ .../ImGuiNotification/Notification.cs | 35 ++ .../NotificationDismissReason.cs | 22 + .../NotificationDismissedDelegate.cs | 10 + .../Notifications/ActiveNotification.cs | 508 ++++++++++++++++++ .../Notifications/NotificationConstants.cs | 74 +++ .../Notifications/NotificationManager.cs | 387 ++++--------- .../Windows/Data/Widgets/ImGuiWidget.cs | 82 ++- Dalamud/Interface/UiBuilder.cs | 51 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- .../Plugin/Services/INotificationManager.cs | 16 + 12 files changed, 1064 insertions(+), 307 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/IActiveNotification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/INotification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Notification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs create mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs create mode 100644 Dalamud/Interface/Internal/Notifications/ActiveNotification.cs create mode 100644 Dalamud/Interface/Internal/Notifications/NotificationConstants.cs create mode 100644 Dalamud/Plugin/Services/INotificationManager.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs new file mode 100644 index 000000000..3e8aef196 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -0,0 +1,109 @@ +using System.Threading; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents an active notification. +/// +public interface IActiveNotification : INotification +{ + /// + /// The counter for field. + /// + private static long idCounter; + + /// + /// Invoked upon dismissing the notification. + /// + /// + /// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded. + /// + event NotificationDismissedDelegate Dismiss; + + /// + /// Invoked upon clicking on the notification. + /// + /// + /// This event is not applicable when is set to false. + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action Click; + + /// + /// Invoked when the mouse enters the notification window. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action MouseEnter; + + /// + /// Invoked when the mouse leaves the notification window. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action MouseLeave; + + /// + /// Invoked upon drawing the action bar of the notification. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action DrawActions; + + /// + /// Gets the ID of this notification. + /// + long Id { get; } + + /// + /// Gets a value indicating whether the mouse cursor is on the notification window. + /// + bool IsMouseHovered { get; } + + /// + /// Gets a value indicating whether the notification has been dismissed. + /// This includes when the hide animation is being played. + /// + bool IsDismissed { get; } + + /// + /// Clones this notification as a . + /// + /// A new instance of . + Notification CloneNotification(); + + /// + /// Dismisses this notification. + /// + void DismissNow(); + + /// + /// Updates the notification data. + /// + /// + /// Call to update the icon using the new . + /// + /// The new notification entry. + void Update(INotification newNotification); + + /// + /// Loads the icon again using . + /// + void UpdateIcon(); + + /// + /// 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 new file mode 100644 index 000000000..f5f66725c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a notification. +/// +public interface INotification +{ + /// + /// Gets the content body of the notification. + /// + string Content { get; } + + /// + /// Gets the title of the notification. + /// + string? Title { get; } + + /// + /// Gets the type of the notification. + /// + NotificationType Type { get; } + + /// + /// Gets the icon creator function for the notification.
+ /// Currently , , and types + /// are accepted. + ///
+ /// + /// The icon created by the task returned will be owned by Dalamud, + /// i.e. it will be d automatically as needed.
+ /// If null is supplied for this property or of the returned task + /// is false, then the corresponding icon with will be used.
+ /// Use if you have an instance of that you + /// can transfer ownership to Dalamud and is available for use right away. + ///
+ Func>? IconCreator { get; } + + /// + /// Gets the expiry. + /// + DateTime Expiry { get; } + + /// + /// Gets a value indicating whether this notification may be interacted. + /// + /// + /// Set this value to true if you want to respond to user inputs from + /// . + /// Note that the close buttons for notifications are always provided and interactible. + /// + bool Interactible { get; } + + /// + /// Gets a value indicating whether clicking on the notification window counts as dismissing the notification. + /// + /// + /// This property has no effect if is false. + /// + bool ClickIsDismiss { get; } + + /// + /// Gets the new duration for this notification if mouse cursor is on the notification window. + /// If set to or less, then this feature is turned off. + /// + /// + /// This property is applicable regardless of . + /// + TimeSpan HoverExtendDuration { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs new file mode 100644 index 000000000..fb2caa4f6 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.Internal.Notifications; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a blueprint for a notification. +/// +public sealed record Notification : INotification +{ + /// + public string Content { get; set; } = string.Empty; + + /// + public string? Title { get; set; } + + /// + public NotificationType Type { get; set; } = NotificationType.None; + + /// + public Func>? IconCreator { get; set; } + + /// + public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + + /// + public bool Interactible { get; set; } + + /// + public bool ClickIsDismiss { get; set; } = true; + + /// + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs new file mode 100644 index 000000000..6e2fa338e --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Specifies the reason of dismissal for a notification. +/// +public enum NotificationDismissReason +{ + /// + /// The notification is dismissed because the expiry specified from is met. + /// + Timeout = 1, + + /// + /// The notification is dismissed because the user clicked on the close button on a notification window. + /// + Manual = 2, + + /// + /// The notification is dismissed from calling . + /// + Programmatical = 3, +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs new file mode 100644 index 000000000..5e899c32c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs @@ -0,0 +1,10 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Delegate representing the dismissal of an active notification. +/// +/// The notification being dismissed. +/// The reason of dismissal. +public delegate void NotificationDismissedDelegate( + IActiveNotification notification, + NotificationDismissReason dismissReason); diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs new file mode 100644 index 000000000..182714157 --- /dev/null +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -0,0 +1,508 @@ +using System.Numerics; +using System.Runtime.Loader; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Animation; +using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Interface.Internal.Notifications; + +/// +/// Represents an active notification. +/// +internal sealed class ActiveNotification : IActiveNotification, IDisposable +{ + private readonly Easing showEasing; + private readonly Easing hideEasing; + + private Notification underlyingNotification; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying notification. + /// The initiator plugin. Use null if originated by Dalamud. + public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) + { + this.underlyingNotification = underlyingNotification with { }; + this.InitiatorPlugin = initiatorPlugin; + this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); + this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); + + this.showEasing.Start(); + } + + /// + public event NotificationDismissedDelegate? Dismiss; + + /// + public event Action? Click; + + /// + public event Action? DrawActions; + + /// + public event Action? MouseEnter; + + /// + public event Action? MouseLeave; + + /// + public long Id { get; } = IActiveNotification.CreateNewId(); + + /// + /// Gets the tick of creating this notification. + /// + public long CreatedAt { get; } = Environment.TickCount64; + + /// + public string Content => this.underlyingNotification.Content; + + /// + public string? Title => this.underlyingNotification.Title; + + /// + public NotificationType Type => this.underlyingNotification.Type; + + /// + public Func>? IconCreator => this.underlyingNotification.IconCreator; + + /// + public DateTime Expiry => this.underlyingNotification.Expiry; + + /// + public bool Interactible => this.underlyingNotification.Interactible; + + /// + public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss; + + /// + public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration; + + /// + public bool IsMouseHovered { get; private set; } + + /// + public bool IsDismissed => this.hideEasing.IsRunning; + + /// + /// Gets or sets the plugin that initiated this notification. + /// + public LocalPlugin? InitiatorPlugin { get; set; } + + /// + /// Gets or sets the icon of this notification. + /// + public Task? IconTask { get; set; } + + /// + /// Gets the default color of the notification. + /// + private Vector4 DefaultIconColor => this.Type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// + /// Gets the default icon of the notification. + /// + private string? DefaultIconString => this.Type switch + { + NotificationType.None => null, + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), + _ => null, + }; + + /// + /// Gets the default title of the notification. + /// + private string? DefaultTitle => this.Type switch + { + NotificationType.None => null, + NotificationType.Success => NotificationType.Success.ToString(), + NotificationType.Warning => NotificationType.Warning.ToString(), + NotificationType.Error => NotificationType.Error.ToString(), + NotificationType.Info => NotificationType.Info.ToString(), + _ => null, + }; + + /// + public void Dispose() + { + this.ClearIconTask(); + this.underlyingNotification.IconCreator = null; + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.InitiatorPlugin = null; + } + + /// + public Notification CloneNotification() => this.underlyingNotification with { }; + + /// + public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); + + /// + /// Dismisses this notification. Multiple calls will be ignored. + /// + /// The reason of dismissal. + public void DismissNow(NotificationDismissReason reason) + { + if (this.hideEasing.IsRunning) + return; + + this.hideEasing.Start(); + try + { + this.Dismiss?.Invoke(this, reason); + } + catch (Exception e) + { + Log.Error( + e, + $"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); + } + } + + /// + /// Updates animations. + /// + /// true if the notification is over. + public bool UpdateAnimations() + { + this.showEasing.Update(); + this.hideEasing.Update(); + return this.hideEasing.IsRunning && this.hideEasing.IsDone; + } + + /// + /// Draws this notification. + /// + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float maxWidth, float offsetY) + { + if (!this.IsDismissed + && DateTime.Now > this.Expiry + && (this.HoverExtendDuration <= TimeSpan.Zero || !this.IsMouseHovered)) + { + this.DismissNow(NotificationDismissReason.Timeout); + } + + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var notificationManager = Service.Get(); + var interfaceManager = Service.Get(); + var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; + unboundedWidth += NotificationConstants.ScaledIconSize; + unboundedWidth += Math.Max( + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + ImGui.CalcTextSize(this.Content).X); + + var width = Math.Min(maxWidth, unboundedWidth); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGui.Begin( + $"##NotifyWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + (this.Interactible ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs) | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoFocusOnAppearing); + + var basePos = ImGui.GetCursorPos(); + this.DrawIcon( + notificationManager, + basePos, + basePos + new Vector2(NotificationConstants.ScaledIconSize)); + basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; + width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); + this.DrawTitle(basePos, basePos + new Vector2(width, 0)); + basePos.Y = ImGui.GetCursorPosY(); + this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + this.Click?.InvokeSafely(this); + if (this.ClickIsDismiss) + this.DismissNow(NotificationDismissReason.Manual); + } + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + ImGui.End(); + + if (!this.IsDismissed) + this.DrawCloseButton(interfaceManager, windowPos); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(2); + ImGui.PopID(); + + if (windowPos.X <= ImGui.GetIO().MousePos.X + && windowPos.Y <= ImGui.GetIO().MousePos.Y + && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X + && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) + { + if (!this.IsMouseHovered) + { + this.IsMouseHovered = true; + this.MouseEnter.InvokeSafely(this); + } + } + else if (this.IsMouseHovered) + { + if (this.HoverExtendDuration > TimeSpan.Zero) + { + var newExpiry = DateTime.Now + this.HoverExtendDuration; + if (newExpiry > this.Expiry) + this.underlyingNotification.Expiry = newExpiry; + } + + this.IsMouseHovered = false; + this.MouseLeave.InvokeSafely(this); + } + + return windowSize.Y; + } + + /// + public void Update(INotification newNotification) + { + this.underlyingNotification.Content = newNotification.Content; + this.underlyingNotification.Title = newNotification.Title; + this.underlyingNotification.Type = newNotification.Type; + this.underlyingNotification.IconCreator = newNotification.IconCreator; + this.underlyingNotification.Expiry = newNotification.Expiry; + } + + /// + public void UpdateIcon() + { + this.ClearIconTask(); + this.IconTask = this.IconCreator?.Invoke(); + } + + /// + /// Removes non-Dalamud invocation targets from events. + /// + public void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); + this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); + + return; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && + AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext) + { + @delegate = (T)Delegate.Remove(@delegate, il); + } + } + + return @delegate; + } + } + + private void ClearIconTask() + { + _ = this.IconTask?.ContinueWith( + r => + { + if (r.IsCompletedSuccessfully && r.Result is IDisposable d) + d.Dispose(); + }); + this.IconTask = null; + } + + private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) + { + string? iconString; + IFontHandle? fontHandle; + switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) + { + case IDalamudTextureWrap wrap: + { + var size = wrap.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.Image(wrap.ImGuiHandle, size); + return; + } + + case SeIconChar icon: + iconString = string.Empty + (char)icon; + fontHandle = notificationManager.IconAxisFontHandle; + break; + case FontAwesomeIcon icon: + iconString = icon.ToIconString(); + fontHandle = notificationManager.IconFontAwesomeFontHandle; + break; + default: + iconString = this.DefaultIconString; + fontHandle = notificationManager.IconFontAwesomeFontHandle; + break; + } + + if (string.IsNullOrWhiteSpace(iconString)) + return; + + using (fontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } + } + + private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) + { + ImGui.PushTextWrapPos(maxCoord.X); + + if ((this.Title ?? this.DefaultTitle) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.SetCursorPos(minCoord); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + } + + private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 screenCoord) + { + using (interfaceManager.IconFontHandle?.Push()) + { + var str = FontAwesomeIcon.Times.ToIconString(); + var size = NotificationConstants.ScaledCloseButtonMinSize; + var textSize = ImGui.CalcTextSize(str); + size = Math.Max(size, Math.Max(textSize.X, textSize.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + // ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos(screenCoord, ImGuiCond.Always, new(1, 0)); + ImGui.SetNextWindowSizeConstraints(new(size), new(size)); + ImGui.Begin( + $"##CloseButtonWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoFocusOnAppearing); + + if (ImGui.Button(str, new(size))) + this.DismissNow(); + + ImGui.End(); + ImGui.PopStyleColor(2); + ImGui.PopStyleVar(4); + } + } + + private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(maxCoord.X); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } +} diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs new file mode 100644 index 000000000..44b1fa832 --- /dev/null +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -0,0 +1,74 @@ +using System.Numerics; + +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.Internal.Notifications; + +/// +/// Constants for drawing notification windows. +/// +internal static class NotificationConstants +{ + // ..............................[X] + // ..[i]..title title title title .. + // .. by this_plugin .. + // .. .. + // .. body body body body .. + // .. some more wrapped body .. + // .. .. + // .. action buttons .. + // ................................. + + /// The string to show in place of this_plugin if the notification is shown by Dalamud. + public const string DefaultInitiator = "Dalamud"; + + /// The size of the icon. + public const float IconSize = 32; + + /// The background opacity of a notification window. + public const float BackgroundOpacity = 0.82f; + + /// Duration of show animation. + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); + + /// Default duration of the notification. + public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); + + /// Duration of hide animation. + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Text color for the close button [X]. + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the title. + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + + /// Text color for the name of the initiator. + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the body. + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + + /// Gets the scaled padding of the window (dot(.) in the above diagram). + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + + /// Gets the distance from the right bottom border of the viewport + /// to the right bottom border of a notification window. + /// + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between two notification windows. + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between components. + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled size of the icon. + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + + /// Gets the scaled size of the close button. + public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); +} diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 67ad3ee8f..fd92c30df 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -1,12 +1,15 @@ -using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface.Colors; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; -using Dalamud.Utility; -using ImGuiNET; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; namespace Dalamud.Interface.Internal.Notifications; @@ -14,51 +17,66 @@ namespace Dalamud.Interface.Internal.Notifications; /// Class handling notifications/toasts in ImGui. /// Ported from https://github.com/patrickcjk/imgui-notify. /// +[InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal class NotificationManager : IServiceType +internal class NotificationManager : INotificationManager, IServiceType, IDisposable { - /// - /// Value indicating the bottom-left X padding. - /// - internal const float NotifyPaddingX = 20.0f; - - /// - /// Value indicating the bottom-left Y padding. - /// - internal const float NotifyPaddingY = 20.0f; - - /// - /// Value indicating the Y padding between each message. - /// - internal const float NotifyPaddingMessageY = 10.0f; - - /// - /// Value indicating the fade-in and out duration. - /// - internal const int NotifyFadeInOutTime = 500; - - /// - /// Value indicating the default time until the notification is dismissed. - /// - internal const int NotifyDefaultDismiss = 3000; - - /// - /// Value indicating the maximum opacity. - /// - internal const float NotifyOpacity = 0.82f; - - /// - /// Value indicating default window flags for the notifications. - /// - internal const ImGuiWindowFlags NotifyToastFlags = - ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoInputs | - ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing; - - private readonly List notifications = new(); + private readonly List notifications = new(); + private readonly ConcurrentBag pendingNotifications = new(); [ServiceManager.ServiceConstructor] - private NotificationManager() + private NotificationManager(FontAtlasFactory fontAtlasFactory) { + this.PrivateAtlas = fontAtlasFactory.CreateFontAtlas( + nameof(NotificationManager), + FontAtlasAutoRebuildMode.Async); + this.IconAxisFontHandle = + this.PrivateAtlas.NewGameFontHandle(new(GameFontFamily.Axis, NotificationConstants.IconSize)); + this.IconFontAwesomeFontHandle = + this.PrivateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont(new() { SizePx = NotificationConstants.IconSize }))); + } + + /// Gets the handle to AXIS fonts, sized for use as an icon. + public IFontHandle IconAxisFontHandle { get; } + + /// Gets the handle to FontAwesome fonts, sized for use as an icon. + public IFontHandle IconFontAwesomeFontHandle { get; } + + private IFontAtlas PrivateAtlas { get; } + + /// + public void Dispose() + { + this.PrivateAtlas.Dispose(); + foreach (var n in this.pendingNotifications) + n.Dispose(); + foreach (var n in this.notifications) + n.Dispose(); + this.pendingNotifications.Clear(); + this.notifications.Clear(); + } + + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = new ActiveNotification(notification, null); + this.pendingNotifications.Add(an); + return an; + } + + /// + /// Adds a notification originating from a plugin. + /// + /// The notification. + /// The source plugin. + /// The new notification. + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) + { + var an = new ActiveNotification(notification, plugin); + this.pendingNotifications.Add(an); + return an; } /// @@ -67,252 +85,77 @@ internal class NotificationManager : IServiceType /// The content of the notification. /// The title of the notification. /// The type of the notification. - /// The time the notification should be displayed for. - public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) - { - this.notifications.Add(new Notification - { - Content = content, - Title = title, - NotificationType = type, - DurationMs = msDelay, - }); - } + public void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None) => + this.AddNotification( + new() + { + Content = content, + Title = title, + Type = type, + }); /// /// Draw all currently queued notifications. /// public void Draw() { - var viewportSize = ImGuiHelpers.MainViewport.Size; + var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; - for (var i = 0; i < this.notifications.Count; i++) - { - var tn = this.notifications.ElementAt(i); + while (this.pendingNotifications.TryTake(out var newNotification)) + this.notifications.Add(newNotification); - if (tn.GetPhase() == Notification.Phase.Expired) - { - this.notifications.RemoveAt(i); - continue; - } + var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - var opacity = tn.GetFadePercent(); + this.notifications.RemoveAll(x => x.UpdateAnimations()); + foreach (var tn in this.notifications) + height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; + } +} - var iconColor = tn.Color; - iconColor.W = opacity; +/// +/// Plugin-scoped version of a service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class NotificationManagerPluginScoped : INotificationManager, IServiceType, IDisposable +{ + private readonly LocalPlugin localPlugin; + private readonly ConcurrentDictionary notifications = new(); - var windowName = $"##NOTIFY{i}"; + [ServiceManager.ServiceDependency] + private readonly NotificationManager notificationManagerService = Service.Get(); - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowBgAlpha(opacity); - ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - ImGui.Begin(windowName, NotifyToastFlags); + [ServiceManager.ServiceConstructor] + private NotificationManagerPluginScoped(LocalPlugin localPlugin) => + this.localPlugin = localPlugin; - ImGui.PushTextWrapPos(viewportSize.X / 3.0f); - - var wasTitleRendered = false; - - if (!tn.Icon.IsNullOrEmpty()) - { - wasTitleRendered = true; - ImGui.PushFont(InterfaceManager.IconFont); - ImGui.TextColored(iconColor, tn.Icon); - ImGui.PopFont(); - } - - var textColor = ImGuiColors.DalamudWhite; - textColor.W = opacity; - - ImGui.PushStyleColor(ImGuiCol.Text, textColor); - - if (!tn.Title.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.Title); - wasTitleRendered = true; - } - else if (!tn.DefaultTitle.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.DefaultTitle); - wasTitleRendered = true; - } - - if (wasTitleRendered && !tn.Content.IsNullOrEmpty()) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5.0f); - } - - if (!tn.Content.IsNullOrEmpty()) - { - if (wasTitleRendered) - { - ImGui.Separator(); - } - - ImGui.TextUnformatted(tn.Content); - } - - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - - height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; - - ImGui.End(); - } + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + return an; } - /// - /// Container class for notifications. - /// - internal class Notification + /// + public void Dispose() { - /// - /// Possible notification phases. - /// - internal enum Phase + while (!this.notifications.IsEmpty) { - /// - /// Phase indicating fade-in. - /// - FadeIn, - - /// - /// Phase indicating waiting until fade-out. - /// - Wait, - - /// - /// Phase indicating fade-out. - /// - FadeOut, - - /// - /// Phase indicating that the notification has expired. - /// - Expired, - } - - /// - /// Gets the type of the notification. - /// - internal NotificationType NotificationType { get; init; } - - /// - /// Gets the title of the notification. - /// - internal string? Title { get; init; } - - /// - /// Gets the content of the notification. - /// - internal string Content { get; init; } - - /// - /// Gets the duration of the notification in milliseconds. - /// - internal uint DurationMs { get; init; } - - /// - /// Gets the creation time of the notification. - /// - internal DateTime CreationTime { get; init; } = DateTime.Now; - - /// - /// Gets the default color of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal Vector4 Color => this.NotificationType switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the icon of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? Icon => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the default title of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? DefaultTitle => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the elapsed time since creating the notification. - /// - internal TimeSpan ElapsedTime => DateTime.Now - this.CreationTime; - - /// - /// Gets the phase of the notification. - /// - /// The phase of the notification. - internal Phase GetPhase() - { - var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) - return Phase.Expired; - else if (elapsed > NotifyFadeInOutTime + this.DurationMs) - return Phase.FadeOut; - else if (elapsed > NotifyFadeInOutTime) - return Phase.Wait; - else - return Phase.FadeIn; - } - - /// - /// Gets the opacity of the notification. - /// - /// The opacity, in a range from 0 to 1. - internal float GetFadePercent() - { - var phase = this.GetPhase(); - var elapsed = this.ElapsedTime.TotalMilliseconds; - - if (phase == Phase.FadeIn) + foreach (var n in this.notifications.Keys) { - return (float)elapsed / NotifyFadeInOutTime * NotifyOpacity; + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); } - else if (phase == Phase.FadeOut) - { - return (1.0f - (((float)elapsed - NotifyFadeInOutTime - this.DurationMs) / - NotifyFadeInOutTime)) * NotifyOpacity; - } - - return 1.0f * NotifyOpacity; } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 2c7ceb95b..ebf3157fa 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -44,32 +44,66 @@ internal class ImGuiWidget : IDataWindowWidget if (ImGui.Button("Add random notification")) { - var rand = new Random(); - - var title = rand.Next(0, 5) switch - { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; - - var type = rand.Next(0, 4) switch - { - 0 => NotificationType.Error, - 1 => NotificationType.Warning, - 2 => NotificationType.Info, - 3 => NotificationType.Success, - 4 => NotificationType.None, - _ => NotificationType.None, - }; - const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; - notifications.AddNotification(text, title, type); + NewRandom(out var title, out var type); + var n = notifications.AddNotification( + new() + { + Content = text, + Title = title, + Type = type, + Interactible = true, + ClickIsDismiss = false, + }); + + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => + { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type); + an.Update(an.CloneNotification() with { Title = title, Type = type }); + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); + ImGui.SameLine(); + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; } } + + private static void NewRandom(out string? title, out NotificationType type) + { + var rand = new Random(); + + title = rand.Next(0, 5) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + type = rand.Next(0, 4) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; + } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index d260868a0..6da6ebc4a 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -9,12 +10,14 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -29,11 +32,13 @@ namespace Dalamud.Interface; /// public sealed class UiBuilder : IDisposable { + private readonly LocalPlugin localPlugin; private readonly Stopwatch stopwatch; private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly Framework framework = Service.Get(); + private readonly ConcurrentDictionary notifications = new(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -52,8 +57,10 @@ public sealed class UiBuilder : IDisposable /// You do not have to call this manually. /// /// The plugin namespace. - internal UiBuilder(string namespaceName) + /// The relevant local plugin. + internal UiBuilder(string namespaceName, LocalPlugin localPlugin) { + this.localPlugin = localPlugin; try { this.stopwatch = new Stopwatch(); @@ -556,22 +563,46 @@ public sealed class UiBuilder : IDisposable /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - public void AddNotification( - string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) + [Obsolete($"Use {nameof(INotificationManager)}.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public async void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None, + uint msDelay = 3000) { - Service - .GetAsync() - .ContinueWith(task => + var nm = await Service.GetAsync(); + var an = nm.AddNotification( + new() { - if (task.IsCompletedSuccessfully) - task.Result.AddNotification(content, title, type, msDelay); - }); + Content = content, + Title = title, + Type = type, + Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), + }, + this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); } /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.scopedFinalizer.Dispose(); + + // Taken from NotificationManagerPluginScoped. + // TODO: remove on API 10. + while (!this.notifications.IsEmpty) + { + foreach (var n in this.notifications.Keys) + { + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); + } + } + } /// /// Open the registered configuration UI, if it exists. diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa49..5e103ecbe 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(plugin.Name); + this.UiBuilder = new UiBuilder(plugin.Name, plugin); this.configs = Service.Get().PluginConfigs; this.Reason = reason; diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs new file mode 100644 index 000000000..1d31ddd35 --- /dev/null +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -0,0 +1,16 @@ +using Dalamud.Interface.ImGuiNotification; + +namespace Dalamud.Plugin.Services; + +/// +/// Manager for notifications provided by Dalamud using ImGui. +/// +public interface INotificationManager +{ + /// + /// Adds a notification. + /// + /// The new notification. + /// The added notification. + IActiveNotification AddNotification(Notification notification); +} From 034389711301c34c5af0901fedf8d79f9ed4bdec Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:31:32 +0900 Subject: [PATCH 02/41] Better error message for FontHandle --- .../ManagedFontAtlas/Internals/FontHandle.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 47254a5c9..ba890f7c2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -24,7 +24,6 @@ internal abstract class FontHandle : IFontHandle private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); private static long nextNonMainThreadFontAccessWarningCheck; - private readonly InterfaceManager interfaceManager; private readonly List pushedFonts = new(8); private IFontHandleManager? manager; @@ -36,7 +35,6 @@ internal abstract class FontHandle : IFontHandle /// An instance of . protected FontHandle(IFontHandleManager manager) { - this.interfaceManager = Service.Get(); this.manager = manager; } @@ -58,7 +56,11 @@ internal abstract class FontHandle : IFontHandle /// Gets the associated . /// /// When the object has already been disposed. - protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name); + protected IFontHandleManager Manager => + this.manager + ?? throw new ObjectDisposedException( + this.GetType().Name, + "Did you write `using (fontHandle)` instead of `using (fontHandle.Push())`?"); /// public void Dispose() @@ -122,7 +124,7 @@ internal abstract class FontHandle : IFontHandle } } - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); return locked.ImFont; } @@ -196,7 +198,7 @@ internal abstract class FontHandle : IFontHandle ThreadSafety.AssertMainThread(); // Warn if the client is not properly managing the pushed font stack. - var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls; + var cumulativePresentCalls = Service.Get().CumulativePresentCalls; if (this.lastCumulativePresentCalls != cumulativePresentCalls) { this.lastCumulativePresentCalls = cumulativePresentCalls; @@ -213,7 +215,7 @@ internal abstract class FontHandle : IFontHandle if (this.TryLock(out _) is { } locked) { font = locked.ImFont; - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); } var rented = SimplePushedFont.Rent(this.pushedFonts, font); From 2935d18c37b3cda53f6c9d11383bd8882555d219 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:45:06 +0900 Subject: [PATCH 03/41] Show plugin icons as fallback icon --- .../Notifications/ActiveNotification.cs | 71 +++++++++++++------ .../Windows/Data/Widgets/ImGuiWidget.cs | 5 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 182714157..e444e63ef 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -7,9 +7,11 @@ using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -383,23 +385,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) { - string? iconString; - IFontHandle? fontHandle; + string? iconString = null; + IFontHandle? fontHandle = null; + IDalamudTextureWrap? iconTexture = null; switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) { case IDalamudTextureWrap wrap: - { - var size = wrap.Size; - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.Image(wrap.ImGuiHandle, size); - return; - } - + iconTexture = wrap; + break; case SeIconChar icon: iconString = string.Empty + (char)icon; fontHandle = notificationManager.IconAxisFontHandle; @@ -415,16 +408,52 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } if (string.IsNullOrWhiteSpace(iconString)) - return; - - using (fontHandle.Push()) { - var size = ImGui.CalcTextSize(iconString); + var dam = Service.Get(); + if (this.InitiatorPlugin is null) + { + iconTexture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + } + else + { + if (!Service.Get().TryGetIcon( + this.InitiatorPlugin, + this.InitiatorPlugin.Manifest, + this.InitiatorPlugin.IsThirdParty, + out iconTexture) || iconTexture is null) + { + iconTexture = this.InitiatorPlugin switch + { + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; + } + } + } + + if (iconTexture is not null) + { + var size = iconTexture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; var pos = ((minCoord + maxCoord) - size) / 2; ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); + ImGui.Image(iconTexture.ImGuiHandle, size); + } + else if (fontHandle is not null) + { + using (fontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index ebf3157fa..2eee81ee2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -55,6 +55,7 @@ internal class ImGuiWidget : IDataWindowWidget Type = type, Interactible = true, ClickIsDismiss = false, + Expiry = DateTime.MaxValue, }); var nclick = 0; @@ -85,7 +86,7 @@ internal class ImGuiWidget : IDataWindowWidget { var rand = new Random(); - title = rand.Next(0, 5) switch + title = rand.Next(0, 7) switch { 0 => "This is a toast", 1 => "Truly, a toast", @@ -96,7 +97,7 @@ internal class ImGuiWidget : IDataWindowWidget _ => null, }; - type = rand.Next(0, 4) switch + type = rand.Next(0, 5) switch { 0 => NotificationType.Error, 1 => NotificationType.Warning, From 8d15dfc031ef4cc907bee7e4105c164b32fcddc1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:57:22 +0900 Subject: [PATCH 04/41] Fix vertical offset when title is empty --- Dalamud/Interface/Internal/Notifications/ActiveNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index e444e63ef..7feb989c3 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -461,10 +461,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { ImGui.PushTextWrapPos(maxCoord.X); + ImGui.SetCursorPos(minCoord); if ((this.Title ?? this.DefaultTitle) is { } title) { ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.SetCursorPos(minCoord); ImGui.TextUnformatted(title); ImGui.PopStyleColor(); } From 97066b7442cd5a2d6e1e0beb8493166794512e2d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:59:38 +0900 Subject: [PATCH 05/41] Fix layout --- .../Interface/Internal/Notifications/ActiveNotification.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 7feb989c3..178cdb041 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -227,7 +227,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; unboundedWidth += Math.Max( - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + Math.Max( + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + ImGui.CalcTextSize(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator).X), ImGui.CalcTextSize(this.Content).X); var width = Math.Min(maxWidth, unboundedWidth); From 54decfe7d3f09282eaab3c40783e46ee8ca7f7a0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 06:15:56 +0900 Subject: [PATCH 06/41] Add expiry progressbar --- .../Notifications/ActiveNotification.cs | 51 ++++++++++++++++--- .../Notifications/NotificationConstants.cs | 5 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 178cdb041..90c99ab11 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -64,9 +64,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public long Id { get; } = IActiveNotification.CreateNewId(); /// - /// Gets the tick of creating this notification. + /// Gets the time of creating this notification. /// - public long CreatedAt { get; } = Environment.TickCount64; + public DateTime CreatedAt { get; } = DateTime.Now; + + /// + /// Gets the time of starting to count the timer for the expiration. + /// + public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; /// public string Content => this.underlyingNotification.Content; @@ -249,6 +254,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushID(this.Id.GetHashCode()); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); unsafe { ImGui.PushStyleColor( @@ -289,13 +295,36 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); + float expiryRatio; + if (this.IsDismissed) + { + expiryRatio = 0f; + } + else if (this.Expiry == DateTime.MaxValue || (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)) + { + expiryRatio = 1f; + } + else + { + expiryRatio = (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + } + + expiryRatio = Math.Clamp(expiryRatio, 0f, 1f); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2(0, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * expiryRatio }, + ImGui.GetColorU32(this.DefaultIconColor)); + ImGui.PopClipRect(); + ImGui.End(); if (!this.IsDismissed) this.DrawCloseButton(interfaceManager, windowPos); ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + ImGui.PopStyleVar(3); ImGui.PopID(); if (windowPos.X <= ImGui.GetIO().MousePos.X @@ -315,7 +344,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { var newExpiry = DateTime.Now + this.HoverExtendDuration; if (newExpiry > this.Expiry) + { this.underlyingNotification.Expiry = newExpiry; + this.ExpiryRelativeToTime = DateTime.Now; + } } this.IsMouseHovered = false; @@ -332,7 +364,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.underlyingNotification.Title = newNotification.Title; this.underlyingNotification.Type = newNotification.Type; this.underlyingNotification.IconCreator = newNotification.IconCreator; - this.underlyingNotification.Expiry = newNotification.Expiry; + if (this.underlyingNotification.Expiry != newNotification.Expiry) + { + this.underlyingNotification.Expiry = newNotification.Expiry; + this.ExpiryRelativeToTime = DateTime.Now; + } + + this.underlyingNotification.Interactible = newNotification.Interactible; + this.underlyingNotification.ClickIsDismiss = newNotification.ClickIsDismiss; + this.underlyingNotification.HoverExtendDuration = newNotification.HoverExtendDuration; } /// @@ -491,7 +531,6 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); ImGui.PushStyleColor(ImGuiCol.Button, 0); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); @@ -511,7 +550,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.End(); ImGui.PopStyleColor(2); - ImGui.PopStyleVar(4); + ImGui.PopStyleVar(3); } } diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs index 44b1fa832..bf71cd87e 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -70,5 +70,8 @@ internal static class NotificationConstants public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the close button. - public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); + public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); + + /// Gets the height of the expiry progress bar. + public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); } From 199722d29a4d85cb8020cac62fa6fbb2280c9329 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 06:22:54 +0900 Subject: [PATCH 07/41] Set IconTask on ActiveNotification ctor --- Dalamud/Interface/Internal/Notifications/ActiveNotification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 90c99ab11..5c343288e 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -43,6 +43,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.showEasing.Start(); + this.UpdateIcon(); } /// From 04c6be567136f475a8fc854b2be97c16760a1d7b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 23:42:01 +0900 Subject: [PATCH 08/41] Add progressbar --- .../ImGuiNotification/IActiveNotification.cs | 98 ++-- .../ImGuiNotification/INotification.cs | 53 +- .../ImGuiNotification/Notification.cs | 4 +- .../Notifications/ActiveNotification.cs | 552 ++++++++++++------ .../Notifications/NotificationConstants.cs | 61 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 216 ++++++- 6 files changed, 698 insertions(+), 286 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 3e8aef196..d1aa1d95b 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,28 +1,24 @@ using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents an active notification. -/// +/// Represents an active notification. public interface IActiveNotification : INotification { - /// - /// The counter for field. - /// + /// The counter for field. private static long idCounter; - /// - /// Invoked upon dismissing the notification. - /// - /// - /// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded. - /// + /// Invoked upon dismissing the notification. + /// The event callback will not be called, + /// if a user interacts with the notification after the plugin is unloaded. event NotificationDismissedDelegate Dismiss; - /// - /// Invoked upon clicking on the notification. - /// + /// Invoked upon clicking on the notification. /// /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. @@ -30,9 +26,7 @@ public interface IActiveNotification : INotification /// event Action Click; - /// - /// Invoked when the mouse enters the notification window. - /// + /// Invoked when the mouse enters the notification window. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -40,9 +34,7 @@ public interface IActiveNotification : INotification /// event Action MouseEnter; - /// - /// Invoked when the mouse leaves the notification window. - /// + /// Invoked when the mouse leaves the notification window. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -50,9 +42,7 @@ public interface IActiveNotification : INotification /// event Action MouseLeave; - /// - /// Invoked upon drawing the action bar of the notification. - /// + /// Invoked upon drawing the action bar of the notification. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -60,50 +50,60 @@ public interface IActiveNotification : INotification /// event Action DrawActions; - /// - /// Gets the ID of this notification. - /// + /// + new string Content { get; set; } + + /// + new string? Title { get; set; } + + /// + new NotificationType Type { get; set; } + + /// + new Func>? IconCreator { get; set; } + + /// + new DateTime Expiry { get; set; } + + /// + new bool Interactible { get; set; } + + /// + new TimeSpan HoverExtendDuration { get; set; } + + /// + new float Progress { get; set; } + + /// Gets the ID of this notification. long Id { get; } - /// - /// Gets a value indicating whether the mouse cursor is on the notification window. - /// + /// Gets a value indicating whether the mouse cursor is on the notification window. bool IsMouseHovered { get; } - /// - /// Gets a value indicating whether the notification has been dismissed. - /// This includes when the hide animation is being played. - /// + /// Gets a value indicating whether the notification has been dismissed. + /// This includes when the hide animation is being played. bool IsDismissed { get; } - /// - /// Clones this notification as a . - /// + /// Clones this notification as a . /// A new instance of . Notification CloneNotification(); - /// - /// Dismisses this notification. - /// + /// Dismisses this notification. void DismissNow(); - /// - /// Updates the notification data. - /// + /// Updates the notification data. /// /// Call to update the icon using the new . + /// If is true, then this function is a no-op. /// /// The new notification entry. void Update(INotification newNotification); - /// - /// Loads the icon again using . - /// + /// Loads the icon again using . + /// If is true, then this function is a no-op. void UpdateIcon(); - /// - /// Generates a new value to use for . - /// + /// 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 f5f66725c..cbd8ad633 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -6,31 +6,21 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a notification. -/// +/// Represents a notification. public interface INotification { - /// - /// Gets the content body of the notification. - /// + /// Gets the content body of the notification. string Content { get; } - /// - /// Gets the title of the notification. - /// + /// Gets the title of the notification. string? Title { get; } - /// - /// Gets the type of the notification. - /// + /// Gets the type of the notification. NotificationType Type { get; } - /// - /// Gets the icon creator function for the notification.
+ /// Gets the icon creator function for the notification.
/// Currently , , and types - /// are accepted. - ///
+ /// are accepted.
/// /// The icon created by the task returned will be owned by Dalamud, /// i.e. it will be d automatically as needed.
@@ -41,35 +31,30 @@ public interface INotification ///
Func>? IconCreator { get; } - /// - /// Gets the expiry. - /// + /// Gets the expiry. + /// Set to to make the notification not have an expiry time + /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } - /// - /// Gets a value indicating whether this notification may be interacted. - /// + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from /// . /// Note that the close buttons for notifications are always provided and interactible. + /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, + /// unless is set. /// bool Interactible { get; } - - /// - /// Gets a value indicating whether clicking on the notification window counts as dismissing the notification. - /// - /// - /// This property has no effect if is false. - /// - bool ClickIsDismiss { get; } - /// - /// Gets the new duration for this notification if mouse cursor is on the notification window. - /// If set to or less, then this feature is turned off. - /// + /// Gets the new duration for this notification if mouse cursor is on the notification window. /// + /// If set to or less, then this feature is turned off. /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } + + /// Gets the progress for the progress bar of the notification. + /// The progress should either be in the range between 0 and 1 or be a negative value. + /// Specifying a negative value will show an indeterminate progress bar. + float Progress { get; } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index fb2caa4f6..ccfb250c3 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -28,8 +28,8 @@ public sealed record Notification : INotification public bool Interactible { get; set; } /// - public bool ClickIsDismiss { get; set; } = true; + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public float Progress { get; set; } = 1f; } diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 5c343288e..c1fecdd3b 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -20,19 +20,25 @@ using Serilog; namespace Dalamud.Interface.Internal.Notifications; -/// -/// Represents an active notification. -/// +/// Represents an active notification. internal sealed class ActiveNotification : IActiveNotification, IDisposable { + private readonly Notification underlyingNotification; + private readonly Easing showEasing; private readonly Easing hideEasing; + private readonly Easing progressEasing; - private Notification underlyingNotification; + /// The progress before for the progress bar animation with . + private float progressBefore; - /// - /// Initializes a new instance of the class. - /// + /// Used for calculating correct dismissal progressbar animation (left edge). + private float prevProgressL; + + /// Used for calculating correct dismissal progressbar animation (right edge). + private float prevProgressR; + + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) @@ -41,8 +47,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration); this.showEasing.Start(); + this.progressEasing.Start(); this.UpdateIcon(); } @@ -64,39 +72,111 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public long Id { get; } = IActiveNotification.CreateNewId(); - /// - /// Gets the time of creating this notification. - /// + /// Gets the time of creating this notification. public DateTime CreatedAt { get; } = DateTime.Now; - /// - /// Gets the time of starting to count the timer for the expiration. - /// + /// Gets the time of starting to count the timer for the expiration. public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; - /// - public string Content => this.underlyingNotification.Content; + /// + public string Content + { + get => this.underlyingNotification.Content; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Content = value; + } + } - /// - public string? Title => this.underlyingNotification.Title; + /// + public string? Title + { + get => this.underlyingNotification.Title; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Title = value; + } + } - /// - public NotificationType Type => this.underlyingNotification.Type; + /// + public NotificationType Type + { + get => this.underlyingNotification.Type; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Type = value; + } + } - /// - public Func>? IconCreator => this.underlyingNotification.IconCreator; + /// + public Func>? IconCreator + { + get => this.underlyingNotification.IconCreator; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.IconCreator = value; + } + } - /// - public DateTime Expiry => this.underlyingNotification.Expiry; + /// + public DateTime Expiry + { + get => this.underlyingNotification.Expiry; + set + { + if (this.underlyingNotification.Expiry == value || this.IsDismissed) + return; + this.underlyingNotification.Expiry = value; + this.ExpiryRelativeToTime = DateTime.Now; + } + } - /// - public bool Interactible => this.underlyingNotification.Interactible; + /// + public bool Interactible + { + get => this.underlyingNotification.Interactible; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Interactible = value; + } + } - /// - public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss; + /// + public TimeSpan HoverExtendDuration + { + get => this.underlyingNotification.HoverExtendDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.HoverExtendDuration = value; + } + } - /// - public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration; + /// + public float Progress + { + get => this.underlyingNotification.Progress; + set + { + if (this.IsDismissed) + return; + + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = value; + this.progressEasing.Restart(); + } + } /// public bool IsMouseHovered { get; private set; } @@ -104,19 +184,32 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public bool IsDismissed => this.hideEasing.IsRunning; - /// - /// Gets or sets the plugin that initiated this notification. - /// + /// Gets a value indicating whether has been unloaded. + public bool IsInitiatorUnloaded { get; private set; } + + /// Gets or sets the plugin that initiated this notification. public LocalPlugin? InitiatorPlugin { get; set; } - /// - /// Gets or sets the icon of this notification. - /// + /// Gets or sets the icon of this notification. public Task? IconTask { get; set; } - /// - /// Gets the default color of the notification. - /// + /// Gets the eased progress. + private float ProgressEased + { + get + { + if (this.Progress < 0) + return 0f; + + if (Math.Abs(this.Progress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + return this.Progress; + + var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); + return this.progressBefore + (state * (this.Progress - this.progressBefore)); + } + } + + /// Gets the default color of the notification. private Vector4 DefaultIconColor => this.Type switch { NotificationType.None => ImGuiColors.DalamudWhite, @@ -127,9 +220,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => ImGuiColors.DalamudWhite, }; - /// - /// Gets the default icon of the notification. - /// + /// Gets the default icon of the notification. private string? DefaultIconString => this.Type switch { NotificationType.None => null, @@ -140,9 +231,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => null, }; - /// - /// Gets the default title of the notification. - /// + /// Gets the default title of the notification. private string? DefaultTitle => this.Type switch { NotificationType.None => null, @@ -153,6 +242,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => null, }; + /// Gets the string for the initiator field. + private string InitiatorString => + this.InitiatorPlugin is not { } initiatorPlugin + ? NotificationConstants.DefaultInitiator + : this.IsInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) + : initiatorPlugin.Name; + /// public void Dispose() { @@ -170,9 +267,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); - /// - /// Dismisses this notification. Multiple calls will be ignored. - /// + /// Dismisses this notification. Multiple calls will be ignored. /// The reason of dismissal. public void DismissNow(NotificationDismissReason reason) { @@ -192,20 +287,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - /// Updates animations. - /// + /// Updates animations. /// true if the notification is over. public bool UpdateAnimations() { this.showEasing.Update(); this.hideEasing.Update(); + this.progressEasing.Update(); return this.hideEasing.IsRunning && this.hideEasing.IsDone; } - /// - /// Draws this notification. - /// + /// Draws this notification. /// The maximum width of the notification window. /// The offset from the bottom. /// The height of the notification. @@ -230,13 +322,29 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var notificationManager = Service.Get(); var interfaceManager = Service.Get(); - var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; + var unboundedWidth = ImGui.CalcTextSize(this.Content).X; + float closeButtonHorizontalSpaceReservation; + using (interfaceManager.IconFontHandle?.Push()) + { + closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X; + closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding; + } + + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.InitiatorString).X); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation); + + unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; - unboundedWidth += Math.Max( - Math.Max( - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, - ImGui.CalcTextSize(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator).X), - ImGui.CalcTextSize(this.Content).X); var width = Math.Min(maxWidth, unboundedWidth); @@ -244,16 +352,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var viewportPos = viewport.WorkPos; var viewportSize = viewport.WorkSize; - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); ImGui.PushID(this.Id.GetHashCode()); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); unsafe @@ -267,67 +366,88 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable NotificationConstants.BackgroundOpacity)); } + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); + ImGui.PushStyleVar( + ImGuiStyleVar.WindowPadding, + new Vector2(NotificationConstants.ScaledWindowPadding, 0)); ImGui.Begin( - $"##NotifyWindow{this.Id}", + $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactible ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs) | + (this.Interactible + ? ImGuiWindowFlags.None + : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.NoFocusOnAppearing); - - var basePos = ImGui.GetCursorPos(); - this.DrawIcon( - notificationManager, - basePos, - basePos + new Vector2(NotificationConstants.ScaledIconSize)); - basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; - width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); - this.DrawTitle(basePos, basePos + new Vector2(width, 0)); - basePos.Y = ImGui.GetCursorPosY(); - this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); - if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - this.Click?.InvokeSafely(this); - if (this.ClickIsDismiss) - this.DismissNow(NotificationDismissReason.Manual); - } + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + this.DrawNotificationMainWindowContent(notificationManager, width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); - - float expiryRatio; - if (this.IsDismissed) - { - expiryRatio = 0f; - } - else if (this.Expiry == DateTime.MaxValue || (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)) - { - expiryRatio = 1f; - } - else - { - expiryRatio = (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); - } - - expiryRatio = Math.Clamp(expiryRatio, 0f, 1f); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos + new Vector2(0, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * expiryRatio }, - ImGui.GetColorU32(this.DefaultIconColor)); - ImGui.PopClipRect(); + var hovered = ImGui.IsWindowHovered(); ImGui.End(); + ImGui.PopStyleVar(); - if (!this.IsDismissed) - this.DrawCloseButton(interfaceManager, windowPos); + offsetY += windowSize.Y; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.Begin( + $"##NotifyActionWindow{this.Id}", + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + this.DrawNotificationActionWindowContent(interfaceManager, width); + windowSize.Y += actionWindowHeight; + windowPos.Y -= actionWindowHeight; + hovered |= ImGui.IsWindowHovered(); + + ImGui.End(); + ImGui.PopStyleVar(); ImGui.PopStyleColor(); - ImGui.PopStyleVar(3); + ImGui.PopStyleVar(2); ImGui.PopID(); + if (hovered) + { + if (this.Click is null) + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } + if (windowPos.X <= ImGui.GetIO().MousePos.X && windowPos.Y <= ImGui.GetIO().MousePos.Y && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X @@ -361,31 +481,28 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void Update(INotification newNotification) { - this.underlyingNotification.Content = newNotification.Content; - this.underlyingNotification.Title = newNotification.Title; - this.underlyingNotification.Type = newNotification.Type; - this.underlyingNotification.IconCreator = newNotification.IconCreator; - if (this.underlyingNotification.Expiry != newNotification.Expiry) - { - this.underlyingNotification.Expiry = newNotification.Expiry; - this.ExpiryRelativeToTime = DateTime.Now; - } - - this.underlyingNotification.Interactible = newNotification.Interactible; - this.underlyingNotification.ClickIsDismiss = newNotification.ClickIsDismiss; - this.underlyingNotification.HoverExtendDuration = newNotification.HoverExtendDuration; + if (this.IsDismissed) + return; + this.Content = newNotification.Content; + this.Title = newNotification.Title; + this.Type = newNotification.Type; + this.IconCreator = newNotification.IconCreator; + this.Expiry = newNotification.Expiry; + this.Interactible = newNotification.Interactible; + this.HoverExtendDuration = newNotification.HoverExtendDuration; + this.Progress = newNotification.Progress; } /// public void UpdateIcon() { + if (this.IsDismissed) + return; this.ClearIconTask(); this.IconTask = this.IconCreator?.Invoke(); } - /// - /// Removes non-Dalamud invocation targets from events. - /// + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); @@ -395,6 +512,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); + this.underlyingNotification.Interactible = false; + this.IsInitiatorUnloaded = true; + + var now = DateTime.Now; + var newMaxExpiry = now + NotificationConstants.DefaultDisplayDuration; + if (this.underlyingNotification.Expiry > newMaxExpiry) + { + this.underlyingNotification.Expiry = newMaxExpiry; + this.ExpiryRelativeToTime = now; + } + return; T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate @@ -426,6 +554,84 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.IconTask = null; } + private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) + { + var basePos = ImGui.GetCursorPos(); + this.DrawIcon( + notificationManager, + basePos, + basePos + new Vector2(NotificationConstants.ScaledIconSize)); + basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; + width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); + this.DrawTitle(basePos, basePos + new Vector2(width, 0)); + basePos.Y = ImGui.GetCursorPosY(); + this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + + // Intention was to have left, right, and bottom have the window padding and top have the component gap, + // but as ImGui only allows horz/vert padding, we add the extra bottom padding. + // Top padding is zero, as the action window will add the padding. + ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); + + float progressL, progressR; + if (this.IsDismissed) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + progressL = midpoint - (length * v); + progressR = midpoint + (length * v); + } + else if (this.Expiry == DateTime.MaxValue) + { + if (this.Progress >= 0) + { + progressL = 0f; + progressR = this.ProgressEased; + } + else + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + progressR = Math.Min(elapsed, 2f / 3) / (2f / 3); + progressL = MathF.Pow(progressL, 3); + progressR = 1f - MathF.Pow(1f - progressR, 3); + } + + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + progressL = 0f; + progressR = 1f; + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + else + { + progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + progressR = 1f; + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + + progressR = Math.Clamp(progressR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * progressL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * progressR }, + ImGui.GetColorU32(this.DefaultIconColor)); + ImGui.PopClipRect(); + } + private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) { string? iconString = null; @@ -486,8 +692,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.SetCursorPos(pos); ImGui.Image(iconTexture.ImGuiHandle, size); } - else if (fontHandle is not null) + else { + // Just making it extremely sure + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (fontHandle is null || iconString is null) + // ReSharper disable once HeuristicUnreachableCode + return; + using (fontHandle.Push()) { var size = ImGui.CalcTextSize(iconString); @@ -514,47 +726,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator); + ImGui.TextUnformatted(this.InitiatorString); ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); } - private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 screenCoord) - { - using (interfaceManager.IconFontHandle?.Push()) - { - var str = FontAwesomeIcon.Times.ToIconString(); - var size = NotificationConstants.ScaledCloseButtonMinSize; - var textSize = ImGui.CalcTextSize(str); - size = Math.Max(size, Math.Max(textSize.X, textSize.Y)); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - // ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos(screenCoord, ImGuiCond.Always, new(1, 0)); - ImGui.SetNextWindowSizeConstraints(new(size), new(size)); - ImGui.Begin( - $"##CloseButtonWindow{this.Id}", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.NoFocusOnAppearing); - - if (ImGui.Button(str, new(size))) - this.DismissNow(); - - ImGui.End(); - ImGui.PopStyleColor(2); - ImGui.PopStyleVar(3); - } - } - private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) { ImGui.SetCursorPos(minCoord); @@ -576,4 +754,44 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } } + + private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width) + { + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + this.IsMouseHovered + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + + this.DrawCloseButton( + interfaceManager, + new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding), + NotificationConstants.ScaledWindowPadding); + } + + private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) + { + using (interfaceManager.IconFontHandle?.Push()) + { + var str = FontAwesomeIcon.Times.ToIconString(); + var textSize = ImGui.CalcTextSize(str); + var size = Math.Max(textSize.X, textSize.Y); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!this.IsMouseHovered) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); + if (ImGui.Button(str, new(size + (pad * 2)))) + this.DismissNow(); + + ImGui.PopStyleColor(2); + if (!this.IsMouseHovered) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + } + } } diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs index bf71cd87e..3592c2a00 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -9,7 +9,9 @@ namespace Dalamud.Interface.Internal.Notifications; /// internal static class NotificationConstants { - // ..............................[X] + // .............................[..] + // ..when.......................[XX] + // .. .. // ..[i]..title title title title .. // .. by this_plugin .. // .. .. @@ -28,6 +30,9 @@ internal static class NotificationConstants /// The background opacity of a notification window. public const float BackgroundOpacity = 0.82f; + /// The duration of indeterminate progress bar loop in milliseconds. + public const float IndeterminateProgressbarLoopDuration = 2000f; + /// Duration of show animation. public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); @@ -40,6 +45,12 @@ internal static class NotificationConstants /// Duration of hide animation. public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Duration of hide animation. + public static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + + /// Text color for the when. + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + /// Text color for the close button [X]. public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -52,6 +63,21 @@ internal static class NotificationConstants /// Text color for the body. public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + /// Gets the relative time format strings. + private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = + { + (TimeSpan.FromDays(7), null), + (TimeSpan.FromDays(2), "{0:%d} days ago"), + (TimeSpan.FromDays(1), "yesterday"), + (TimeSpan.FromHours(2), "{0:%h} hours ago"), + (TimeSpan.FromHours(1), "an hour ago"), + (TimeSpan.FromMinutes(2), "{0:%m} minutes ago"), + (TimeSpan.FromMinutes(1), "a minute ago"), + (TimeSpan.FromSeconds(2), "{0:%s} seconds ago"), + (TimeSpan.FromSeconds(1), "a second ago"), + (TimeSpan.MinValue, "just now"), + }; + /// Gets the scaled padding of the window (dot(.) in the above diagram). public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -69,9 +95,36 @@ internal static class NotificationConstants /// Gets the scaled size of the icon. public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); - /// Gets the scaled size of the close button. - public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); - /// Gets the height of the expiry progress bar. public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + + /// Gets the string format of the initiator name field, if the initiator is unloaded. + public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + + /// + /// Formats an instance of as a relative time. + /// + /// When. + /// The formatted string. + public static string FormatRelativeDateTime(this DateTime when) + { + var ts = DateTime.Now - when; + foreach (var (minSpan, formatString) in RelativeFormatStrings) + { + if (ts < minSpan) + continue; + if (formatString is null) + break; + return string.Format(formatString, ts); + } + + return when.FormatAbsoluteDateTime(); + } + + /// + /// Formats an instance of as an absolute time. + /// + /// When. + /// The formatted string. + public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 2eee81ee2..060498ba7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,5 +1,8 @@ -using Dalamud.Interface.Internal.Notifications; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -9,11 +12,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ImGuiWidget : IDataWindowWidget { + private NotificationTemplate notificationTemplate; + /// public string[]? CommandShortcuts { get; init; } = { "imgui" }; - + /// - public string DisplayName { get; init; } = "ImGui"; + public string DisplayName { get; init; } = "ImGui"; /// public bool Ready { get; set; } @@ -22,6 +27,7 @@ internal class ImGuiWidget : IDataWindowWidget public void Load() { this.Ready = true; + this.notificationTemplate.Reset(); } /// @@ -38,51 +44,134 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Separator(); - ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); + ImGui.TextUnformatted( + $"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); ImGui.Separator(); - if (ImGui.Button("Add random notification")) - { - const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + ImGui.Checkbox("##manualContent", ref this.notificationTemplate.ManualContent); + ImGui.SameLine(); + ImGui.InputText("Content##content", ref this.notificationTemplate.Content, 255); + + ImGui.Checkbox("##manualTitle", ref this.notificationTemplate.ManualTitle); + ImGui.SameLine(); + ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); + ImGui.SameLine(); + ImGui.Combo( + "Type##type", + ref this.notificationTemplate.TypeInt, + NotificationTemplate.TypeTitles, + NotificationTemplate.TypeTitles.Length); + + ImGui.Combo( + "Duration", + ref this.notificationTemplate.DurationInt, + NotificationTemplate.DurationTitles, + NotificationTemplate.DurationTitles.Length); + + ImGui.Combo( + "Progress", + ref this.notificationTemplate.ProgressMode, + NotificationTemplate.ProgressModeTitles, + NotificationTemplate.ProgressModeTitles.Length); + + ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); + + ImGui.Checkbox("Action Bar", ref this.notificationTemplate.ActionBar); + + if (ImGui.Button("Add notification")) + { + var text = + "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + NewRandom(out var title, out var type, out var progress); + if (this.notificationTemplate.ManualTitle) + title = this.notificationTemplate.Title; + if (this.notificationTemplate.ManualContent) + text = this.notificationTemplate.Content; + if (this.notificationTemplate.ManualType) + type = (NotificationType)this.notificationTemplate.TypeInt; + + var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt]; - NewRandom(out var title, out var type); var n = notifications.AddNotification( new() { Content = text, Title = title, Type = type, - Interactible = true, - ClickIsDismiss = false, - Expiry = DateTime.MaxValue, + Interactible = this.notificationTemplate.Interactible, + Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, + Progress = this.notificationTemplate.ProgressMode switch + { + 0 => 1f, + 1 => progress, + 2 => 0f, + 3 => 0f, + 4 => -1f, + _ => 0.5f, + }, }); - - var nclick = 0; - n.Click += _ => nclick++; - n.DrawActions += an => + switch (this.notificationTemplate.ProgressMode) { - if (ImGui.Button("Update in place")) - { - NewRandom(out title, out type); - an.Update(an.CloneNotification() with { Title = title, Type = type }); - } + case 2: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.IsDismissed; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } + }); + break; + case 3: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.IsDismissed; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } - if (an.IsMouseHovered) + n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + }); + break; + } + + if (this.notificationTemplate.ActionBar) + { + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type, out progress); + an.Title = title; + an.Type = type; + an.Progress = progress; + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); - ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); - }; + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; + } } } - private static void NewRandom(out string? title, out NotificationType type) + private static void NewRandom(out string? title, out NotificationType type, out float progress) { var rand = new Random(); @@ -106,5 +195,72 @@ internal class ImGuiWidget : IDataWindowWidget 4 => NotificationType.None, _ => NotificationType.None, }; + + if (rand.Next() % 2 == 0) + progress = -1; + else + progress = rand.NextSingle(); + } + + private struct NotificationTemplate + { + public static readonly string[] ProgressModeTitles = + { + "Default", + "Random", + "Increasing", + "Increasing & Auto Dismiss", + "Indeterminate", + }; + + public static readonly string[] TypeTitles = + { + nameof(NotificationType.None), + nameof(NotificationType.Success), + nameof(NotificationType.Warning), + nameof(NotificationType.Error), + nameof(NotificationType.Info), + }; + + public static readonly string[] DurationTitles = + { + "Infinite", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + + public static readonly TimeSpan[] Durations = + { + TimeSpan.MaxValue, + TimeSpan.FromSeconds(1), + NotificationConstants.DefaultDisplayDuration, + TimeSpan.FromSeconds(10), + }; + + public bool ManualContent; + public string Content; + public bool ManualTitle; + public string Title; + public bool ManualType; + public int TypeInt; + public int DurationInt; + public bool Interactible; + public bool ActionBar; + public int ProgressMode; + + public void Reset() + { + this.ManualContent = false; + this.Content = string.Empty; + this.ManualTitle = false; + this.Title = string.Empty; + this.ManualType = false; + this.TypeInt = (int)NotificationType.None; + this.DurationInt = 2; + this.Interactible = true; + this.ActionBar = true; + this.ProgressMode = 0; + } } } From 7aba15ef5b5c68bba3fd0eb96ee4145fdd256b17 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 23:56:49 +0900 Subject: [PATCH 09/41] Normalize namespaces --- Dalamud.CorePlugin/PluginImpl.cs | 7 +- Dalamud.CorePlugin/PluginWindow.cs | 74 ++++++++++++++++++- Dalamud/Game/ChatHandlers.cs | 1 + .../ImGuiNotification/IActiveNotification.cs | 2 - .../ImGuiNotification/INotification.cs | 4 +- .../Internal}/ActiveNotification.cs | 1 + .../Internal}/NotificationConstants.cs | 4 +- .../Internal}/NotificationManager.cs | 4 +- .../ImGuiNotification/Notification.cs | 1 + .../NotificationDismissReason.cs | 20 ++--- .../NotificationDismissedDelegate.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 1 + .../Notifications/NotificationType.cs | 29 +++----- .../Windows/Data/Widgets/DataShareWidget.cs | 1 + .../Windows/Data/Widgets/ImGuiWidget.cs | 1 + .../PluginInstaller/PluginInstallerWindow.cs | 1 + .../PluginInstaller/ProfileManagerWidget.cs | 1 + .../Internal/Windows/PluginStatWindow.cs | 1 + Dalamud/Interface/UiBuilder.cs | 1 + .../Plugin/Internal/Types/LocalDevPlugin.cs | 1 + Dalamud/Utility/Api10ToDoAttribute.cs | 12 ++- 21 files changed, 124 insertions(+), 47 deletions(-) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/ActiveNotification.cs (99%) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/NotificationConstants.cs (98%) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/NotificationManager.cs (98%) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index cb9b4368a..afeaad426 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -56,15 +56,16 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, INotificationManager notificationManager) { + this.NotificationManager = notificationManager; try { // this.InitLoc(); this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow()); + this.windowSystem.AddWindow(new PluginWindow(this)); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -84,6 +85,8 @@ namespace Dalamud.CorePlugin } } + public INotificationManager NotificationManager { get; } + /// /// Gets the plugin interface. /// diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs index 27be82f41..33b8505c4 100644 --- a/Dalamud.CorePlugin/PluginWindow.cs +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -1,7 +1,9 @@ using System; using System.Numerics; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; + using ImGuiNET; namespace Dalamud.CorePlugin @@ -14,15 +16,19 @@ namespace Dalamud.CorePlugin /// /// Initializes a new instance of the class. /// - public PluginWindow() + /// + public PluginWindow(PluginImpl pluginImpl) : base("CorePlugin") { + this.PluginImpl = pluginImpl; this.IsOpen = true; this.Size = new Vector2(810, 520); this.SizeCondition = ImGuiCond.FirstUseEver; } + public PluginImpl PluginImpl { get; } + /// public void Dispose() { @@ -36,6 +42,72 @@ namespace Dalamud.CorePlugin /// public override void Draw() { + if (ImGui.Button("Legacy")) + this.PluginImpl.Interface.UiBuilder.AddNotification("asdf"); + if (ImGui.Button("Test")) + { + const string text = + "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + NewRandom(out var title, out var type); + var n = this.PluginImpl.NotificationManager.AddNotification( + new() + { + Content = text, + Title = title, + Type = type, + Interactible = true, + Expiry = DateTime.MaxValue, + }); + + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => + { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type); + an.Update(an.CloneNotification() with { Title = title, Type = type }); + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); + ImGui.SameLine(); + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; + } + } + + private static void NewRandom(out string? title, out NotificationType type) + { + var rand = new Random(); + + title = rand.Next(0, 7) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + type = rand.Next(0, 5) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; } } } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 836fb5ec8..5dd6ed3ba 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index d1aa1d95b..2e0c62783 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,8 +1,6 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Game.Text; -using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index cbd8ad633..a5d56d783 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -35,7 +35,7 @@ public interface INotification /// Set to to make the notification not have an expiry time /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } - + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from @@ -52,7 +52,7 @@ public interface INotification /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } - + /// Gets the progress for the progress bar of the notification. /// The progress should either be in the range between 0 and 1 or be a negative value. /// Specifying a negative value will show an indeterminate progress bar. diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs similarity index 99% rename from Dalamud/Interface/Internal/Notifications/ActiveNotification.cs rename to Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c1fecdd3b..963b74b6c 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs similarity index 98% rename from Dalamud/Interface/Internal/Notifications/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 3592c2a00..a16fb904d 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -2,7 +2,7 @@ using System.Numerics; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// /// Constants for drawing notification windows. @@ -94,7 +94,7 @@ internal static class NotificationConstants /// Gets the scaled size of the icon. public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); - + /// Gets the height of the expiry progress bar. public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs similarity index 98% rename from Dalamud/Interface/Internal/Notifications/NotificationManager.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index fd92c30df..b67605541 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; @@ -11,7 +11,7 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// /// Class handling notifications/toasts in ImGui. diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index ccfb250c3..bab6f6f23 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs index 6e2fa338e..47e52b142 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -1,22 +1,16 @@ namespace Dalamud.Interface.ImGuiNotification; -/// -/// Specifies the reason of dismissal for a notification. -/// +/// Specifies the reason of dismissal for a notification. public enum NotificationDismissReason { - /// - /// The notification is dismissed because the expiry specified from is met. - /// + /// The notification is dismissed because the expiry specified from is + /// met. Timeout = 1, - - /// - /// The notification is dismissed because the user clicked on the close button on a notification window. + + /// The notification is dismissed because the user clicked on the close button on a notification window. /// Manual = 2, - - /// - /// The notification is dismissed from calling . - /// + + /// The notification is dismissed from calling . Programmatical = 3, } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs index 5e899c32c..09d6fd818 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs @@ -1,8 +1,6 @@ namespace Dalamud.Interface.ImGuiNotification; -/// -/// Delegate representing the dismissal of an active notification. -/// +/// Delegate representing the dismissal of an active notification. /// The notification being dismissed. /// The reason of dismissal. public delegate void NotificationDismissedDelegate( diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3db799be0..c811e9287 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -14,6 +14,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; diff --git a/Dalamud/Interface/Internal/Notifications/NotificationType.cs b/Dalamud/Interface/Internal/Notifications/NotificationType.cs index 1885ec809..5fffbe9af 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationType.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationType.cs @@ -1,32 +1,23 @@ -namespace Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; -/// -/// Possible notification types. -/// +namespace Dalamud.Interface.Internal.Notifications; + +/// Possible notification types. +[Api10ToDo(Api10ToDoAttribute.MoveNamespace, nameof(ImGuiNotification.Internal))] public enum NotificationType { - /// - /// No special type. - /// + /// No special type. None, - /// - /// Type indicating success. - /// + /// Type indicating success. Success, - /// - /// Type indicating a warning. - /// + /// Type indicating a warning. Warning, - /// - /// Type indicating an error. - /// + /// Type indicating an error. Error, - /// - /// Type indicating generic information. - /// + /// Type indicating generic information. Info, } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 92f340a7b..346255dfe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Reflection; using System.Text; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 060498ba7..67a65f74f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 95c227662..210290f17 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,6 +15,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index eafea9d16..857002771 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -7,6 +7,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index a1d93bb8c..bfa30cafd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -7,6 +7,7 @@ using System.Reflection; using Dalamud.Game; using Dalamud.Hooking.Internal; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 6da6ebc4a..64ff0cc45 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 580d5c161..1f9f503e0 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types.Manifest; diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs index f397f8f0c..a13aaead5 100644 --- a/Dalamud/Utility/Api10ToDoAttribute.cs +++ b/Dalamud/Utility/Api10ToDoAttribute.cs @@ -11,9 +11,19 @@ internal sealed class Api10ToDoAttribute : Attribute /// public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; + /// + /// Marks that this should be moved to an another namespace. + /// + public const string MoveNamespace = "Move to another namespace."; + /// /// Initializes a new instance of the class. /// /// The explanation. - public Api10ToDoAttribute(string what) => _ = what; + /// The explanation 2. + public Api10ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } } From 3a6aa13c3b23a4533333263af6a6e4716d5bf47e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:21:45 +0900 Subject: [PATCH 10/41] Add IconSource --- .../ImGuiNotification/IActiveNotification.cs | 11 +- .../ImGuiNotification/INotification.cs | 26 ++-- .../INotificationIconSource.cs | 20 +++ .../INotificationMaterializedIcon.cs | 18 +++ .../IconSource/FilePathIconSource.cs | 50 +++++++ .../IconSource/FontAwesomeIconIconSource.cs | 62 +++++++++ .../IconSource/GamePathIconSource.cs | 51 ++++++++ .../IconSource/SeIconCharIconSource.cs | 55 ++++++++ .../IconSource/TextureWrapTaskIconSource.cs | 59 +++++++++ .../Internal/ActiveNotification.cs | 122 ++++-------------- .../Internal/NotificationUtilities.cs | 63 +++++++++ .../ImGuiNotification/Notification.cs | 4 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 84 +++++++++++- 13 files changed, 506 insertions(+), 119 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 2e0c62783..fecccf092 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,5 +1,4 @@ using System.Threading; -using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; @@ -57,8 +56,10 @@ public interface IActiveNotification : INotification /// new NotificationType Type { get; set; } - /// - new Func>? IconCreator { get; set; } + /// Gets or sets the icon source. + /// Setting a new value to this property does not change the icon. Use to do so. + /// + new INotificationIconSource? IconSource { get; set; } /// new DateTime Expiry { get; set; } @@ -91,13 +92,13 @@ public interface IActiveNotification : INotification /// Updates the notification data. /// - /// Call to update the icon using the new . + /// Call to update the icon using the new . /// If is true, then this function is a no-op. /// /// The new notification entry. void Update(INotification newNotification); - /// Loads the icon again using . + /// Loads the icon again using . /// If is true, then this function is a no-op. void UpdateIcon(); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index a5d56d783..c4a7b46ac 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,7 +1,4 @@ -using System.Threading.Tasks; - -using Dalamud.Game.Text; -using Dalamud.Interface.Internal; +using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -18,18 +15,17 @@ public interface INotification /// Gets the type of the notification. NotificationType Type { get; } - /// Gets the icon creator function for the notification.
- /// Currently , , and types - /// are accepted.
- /// - /// The icon created by the task returned will be owned by Dalamud, - /// i.e. it will be d automatically as needed.
- /// If null is supplied for this property or of the returned task - /// is false, then the corresponding icon with will be used.
- /// Use if you have an instance of that you - /// can transfer ownership to Dalamud and is available for use right away. + /// Gets the icon source. + /// The following icon sources are currently available.
+ ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
  • + ///
///
- Func>? IconCreator { get; } + INotificationIconSource? IconSource { get; } /// Gets the expiry. /// Set to to make the notification not have an expiry time diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs new file mode 100644 index 000000000..8a73e2a64 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins should NOT implement this interface. +public interface INotificationIconSource : ICloneable, IDisposable +{ + /// The internal interface. + internal interface IInternal : INotificationIconSource + { + /// Materializes the icon resource. + /// The materialized resource. + INotificationMaterializedIcon Materialize(); + } + + /// + new INotificationIconSource Clone(); + + /// + object ICloneable.Clone() => this.Clone(); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs new file mode 100644 index 000000000..9be498af1 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a materialized icon. +/// +internal interface INotificationMaterializedIcon : IDisposable +{ + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// The initiator plugin. + void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin); +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs new file mode 100644 index 000000000..b1886154a --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct FilePathIconSource : INotificationIconSource.IInternal +{ + /// The path to a .tex file inside the game resources. + public readonly string FilePath; + + /// Initializes a new instance of the struct. + /// The path to a .tex file inside the game resources. + public FilePathIconSource(string filePath) => this.FilePath = filePath; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.FilePath); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly FileInfo fileInfo; + + public MaterializedIcon(string filePath) => this.fileInfo = new(filePath); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + Service.Get().GetTextureFromFile(this.fileInfo), + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs new file mode 100644 index 000000000..8e28940ba --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs @@ -0,0 +1,62 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of as the icon of a notification. +public readonly struct FontAwesomeIconIconSource : INotificationIconSource.IInternal +{ + /// The icon character. + public readonly FontAwesomeIcon Char; + + /// Initializes a new instance of the struct. + /// The character. + public FontAwesomeIconIconSource(FontAwesomeIcon c) => this.Char = c; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + + /// Draws the icon. + /// The icon string. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + internal static void DrawIconStatic(string iconString, Vector2 minCoord, Vector2 maxCoord, Vector4 color) + { + using (Service.Get().IconFontAwesomeFontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } + } + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string iconString; + + public MaterializedIcon(FontAwesomeIcon c) => this.iconString = c.ToIconString(); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + DrawIconStatic(this.iconString, minCoord, maxCoord, color); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs new file mode 100644 index 000000000..9b669e62a --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs @@ -0,0 +1,51 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct GamePathIconSource : INotificationIconSource.IInternal +{ + /// The path to a .tex file inside the game resources. + public readonly string GamePath; + + /// Initializes a new instance of the struct. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathIconSource(string gamePath) => this.GamePath = gamePath; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.GamePath); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string gamePath; + + public MaterializedIcon(string gamePath) => this.gamePath = gamePath; + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + Service.Get().GetTextureFromGame(this.gamePath), + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs new file mode 100644 index 000000000..d34b776bc --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs @@ -0,0 +1,55 @@ +using System.Numerics; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of as the icon of a notification. +public readonly struct SeIconCharIconSource : INotificationIconSource.IInternal +{ + /// The icon character. + public readonly SeIconChar Char; + + /// Initializes a new instance of the struct. + /// The character. + public SeIconCharIconSource(SeIconChar c) => this.Char = c; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string iconString; + + public MaterializedIcon(SeIconChar c) => this.iconString = c.ToIconString(); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) + { + using (Service.Get().IconAxisFontHandle.Push()) + { + var size = ImGui.CalcTextSize(this.iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(this.iconString); + ImGui.PopStyleColor(); + } + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs new file mode 100644 index 000000000..28fdc4d96 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs @@ -0,0 +1,59 @@ +using System.Numerics; +using System.Threading.Tasks; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of future as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInternal +{ + /// The function that returns a task resulting in a new instance of . + /// + /// Dalamud will take ownership of the result. Do not call . + public readonly Func?>? TextureWrapTaskFunc; + + /// Gets the default materialized icon, for the purpose of displaying the plugin icon. + internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); + + /// Initializes a new instance of the struct. + /// The function. + public TextureWrapTaskIconSource(Func?>? taskFunc) => + this.TextureWrapTaskFunc = taskFunc; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.TextureWrapTaskFunc); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private Task? task; + + public MaterializedIcon(Func?>? taskFunc) => this.task = taskFunc?.Invoke(); + + public void Dispose() + { + this.task?.ToContentDisposedTask(true); + this.task = null; + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + this.task?.IsCompletedSuccessfully is true ? this.task.Result : null, + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 963b74b6c..64b812197 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,25 +1,21 @@ using System.Numerics; using System.Runtime.Loader; -using System.Threading.Tasks; -using Dalamud.Game.Text; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.Internal.Windows; -using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; -using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; using Serilog; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. internal sealed class ActiveNotification : IActiveNotification, IDisposable @@ -115,15 +111,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public Func>? IconCreator + /// + public INotificationIconSource? IconSource { - get => this.underlyingNotification.IconCreator; + get => this.underlyingNotification.IconSource; set { if (this.IsDismissed) return; - this.underlyingNotification.IconCreator = value; + this.underlyingNotification.IconSource = value; } } @@ -192,7 +188,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public LocalPlugin? InitiatorPlugin { get; set; } /// Gets or sets the icon of this notification. - public Task? IconTask { get; set; } + public INotificationMaterializedIcon? MaterializedIcon { get; set; } /// Gets the eased progress. private float ProgressEased @@ -255,7 +251,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public void Dispose() { this.ClearIconTask(); - this.underlyingNotification.IconCreator = null; + this.underlyingNotification.IconSource = null; this.Dismiss = null; this.Click = null; this.DrawActions = null; @@ -487,7 +483,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Content = newNotification.Content; this.Title = newNotification.Title; this.Type = newNotification.Type; - this.IconCreator = newNotification.IconCreator; + this.IconSource = newNotification.IconSource; this.Expiry = newNotification.Expiry; this.Interactible = newNotification.Interactible; this.HoverExtendDuration = newNotification.HoverExtendDuration; @@ -500,7 +496,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable if (this.IsDismissed) return; this.ClearIconTask(); - this.IconTask = this.IconCreator?.Invoke(); + this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } /// Removes non-Dalamud invocation targets from events. @@ -546,20 +542,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void ClearIconTask() { - _ = this.IconTask?.ContinueWith( - r => - { - if (r.IsCompletedSuccessfully && r.Result is IDisposable d) - d.Dispose(); - }); - this.IconTask = null; + this.MaterializedIcon?.Dispose(); + this.MaterializedIcon = null; } private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) { var basePos = ImGui.GetCursorPos(); this.DrawIcon( - notificationManager, basePos, basePos + new Vector2(NotificationConstants.ScaledIconSize)); basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; @@ -633,84 +623,26 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PopClipRect(); } - private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) + private void DrawIcon(Vector2 minCoord, Vector2 maxCoord) { - string? iconString = null; - IFontHandle? fontHandle = null; - IDalamudTextureWrap? iconTexture = null; - switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) + if (this.MaterializedIcon is not null) { - case IDalamudTextureWrap wrap: - iconTexture = wrap; - break; - case SeIconChar icon: - iconString = string.Empty + (char)icon; - fontHandle = notificationManager.IconAxisFontHandle; - break; - case FontAwesomeIcon icon: - iconString = icon.ToIconString(); - fontHandle = notificationManager.IconFontAwesomeFontHandle; - break; - default: - iconString = this.DefaultIconString; - fontHandle = notificationManager.IconFontAwesomeFontHandle; - break; + this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); + return; } - if (string.IsNullOrWhiteSpace(iconString)) + var defaultIconString = this.DefaultIconString; + if (!string.IsNullOrWhiteSpace(defaultIconString)) { - var dam = Service.Get(); - if (this.InitiatorPlugin is null) - { - iconTexture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); - } - else - { - if (!Service.Get().TryGetIcon( - this.InitiatorPlugin, - this.InitiatorPlugin.Manifest, - this.InitiatorPlugin.IsThirdParty, - out iconTexture) || iconTexture is null) - { - iconTexture = this.InitiatorPlugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; - } - } + FontAwesomeIconIconSource.DrawIconStatic(defaultIconString, minCoord, maxCoord, this.DefaultIconColor); + return; } - if (iconTexture is not null) - { - var size = iconTexture.Size; - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.Image(iconTexture.ImGuiHandle, size); - } - else - { - // Just making it extremely sure - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (fontHandle is null || iconString is null) - // ReSharper disable once HeuristicUnreachableCode - return; - - using (fontHandle.Push()) - { - var size = ImGui.CalcTextSize(iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); - } - } + TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( + minCoord, + maxCoord, + this.DefaultIconColor, + this.InitiatorPlugin); } private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs new file mode 100644 index 000000000..f442ef553 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -0,0 +1,63 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Utilities for implementing stuff under . +/// +internal static class NotificationUtilities +{ + /// + /// Draws the given texture, or the icon of the plugin if texture is null. + /// + /// The texture. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The initiator plugin. + public static void DrawTexture( + IDalamudTextureWrap? texture, + Vector2 minCoord, + Vector2 maxCoord, + LocalPlugin? initiatorPlugin) + { + if (texture is null) + { + var dam = Service.Get(); + if (initiatorPlugin is null) + { + texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + } + else + { + if (!Service.Get().TryGetIcon( + initiatorPlugin, + initiatorPlugin.Manifest, + initiatorPlugin.IsThirdParty, + out texture) || texture is null) + { + texture = initiatorPlugin switch + { + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; + } + } + } + + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(texture.ImGuiHandle, size); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index bab6f6f23..8f5ec2423 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; @@ -20,7 +18,7 @@ public sealed record Notification : INotification public NotificationType Type { get; set; } = NotificationType.None; /// - public Func>? IconCreator { get; set; } + public INotificationIconSource? IconSource { get; set; } /// public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 67a65f74f..71cba3297 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,8 +1,13 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; +using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -66,6 +71,41 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.TypeTitles, NotificationTemplate.TypeTitles.Length); + ImGui.Combo( + "Icon Source##iconSourceCombo", + ref this.notificationTemplate.IconSourceInt, + NotificationTemplate.IconSourceTitles, + NotificationTemplate.IconSourceTitles.Length); + switch (this.notificationTemplate.IconSourceInt) + { + case 1: + case 2: + ImGui.InputText( + "Icon Text##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + case 3: + ImGui.Combo( + "Icon Source##iconSourceAssetCombo", + ref this.notificationTemplate.IconSourceAssetInt, + NotificationTemplate.AssetSources, + NotificationTemplate.AssetSources.Length); + break; + case 4: + ImGui.InputText( + "Game Path##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + case 5: + ImGui.InputText( + "File Path##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + } + ImGui.Combo( "Duration", ref this.notificationTemplate.DurationInt, @@ -114,6 +154,26 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, + IconSource = this.notificationTemplate.IconSourceInt switch + { + 1 => new SeIconCharIconSource( + (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 + ? 0 + : this.notificationTemplate.IconSourceText[0])), + 2 => new FontAwesomeIconIconSource( + (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 + ? 0 + : this.notificationTemplate.IconSourceText[0])), + 3 => new TextureWrapTaskIconSource( + () => + Service.Get().GetDalamudTextureWrapAsync( + Enum.Parse( + NotificationTemplate.AssetSources[ + this.notificationTemplate.IconSourceAssetInt]))), + 4 => new GamePathIconSource(this.notificationTemplate.IconSourceText), + 5 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + _ => null, + }, }); switch (this.notificationTemplate.ProgressMode) { @@ -205,6 +265,22 @@ internal class ImGuiWidget : IDataWindowWidget private struct NotificationTemplate { + public static readonly string[] IconSourceTitles = + { + "None (use Type)", + "SeIconChar", + "FontAwesomeIcon", + "TextureWrapTask from DalamudAssets", + "GamePath", + "FilePath", + }; + + public static readonly string[] AssetSources = + Enum.GetValues() + .Where(x => x.GetAttribute()?.Purpose is DalamudAssetPurpose.TextureFromPng) + .Select(Enum.GetName) + .ToArray(); + public static readonly string[] ProgressModeTitles = { "Default", @@ -243,6 +319,9 @@ internal class ImGuiWidget : IDataWindowWidget public string Content; public bool ManualTitle; public string Title; + public int IconSourceInt; + public string IconSourceText; + public int IconSourceAssetInt; public bool ManualType; public int TypeInt; public int DurationInt; @@ -256,6 +335,9 @@ internal class ImGuiWidget : IDataWindowWidget this.Content = string.Empty; this.ManualTitle = false; this.Title = string.Empty; + this.IconSourceInt = 0; + this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; + this.IconSourceAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; From 06bbc558a8fcd36837437bf10f5c7b1d846f5c7c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:23:01 +0900 Subject: [PATCH 11/41] Revert CorePlugin commits --- Dalamud.CorePlugin/PluginImpl.cs | 7 +-- Dalamud.CorePlugin/PluginWindow.cs | 74 +----------------------------- 2 files changed, 3 insertions(+), 78 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index afeaad426..cb9b4368a 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -56,16 +56,15 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, INotificationManager notificationManager) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) { - this.NotificationManager = notificationManager; try { // this.InitLoc(); this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow(this)); + this.windowSystem.AddWindow(new PluginWindow()); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -85,8 +84,6 @@ namespace Dalamud.CorePlugin } } - public INotificationManager NotificationManager { get; } - /// /// Gets the plugin interface. /// diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs index 33b8505c4..27be82f41 100644 --- a/Dalamud.CorePlugin/PluginWindow.cs +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -1,9 +1,7 @@ using System; using System.Numerics; -using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; - using ImGuiNET; namespace Dalamud.CorePlugin @@ -16,19 +14,15 @@ namespace Dalamud.CorePlugin /// /// Initializes a new instance of the class. /// - /// - public PluginWindow(PluginImpl pluginImpl) + public PluginWindow() : base("CorePlugin") { - this.PluginImpl = pluginImpl; this.IsOpen = true; this.Size = new Vector2(810, 520); this.SizeCondition = ImGuiCond.FirstUseEver; } - public PluginImpl PluginImpl { get; } - /// public void Dispose() { @@ -42,72 +36,6 @@ namespace Dalamud.CorePlugin /// public override void Draw() { - if (ImGui.Button("Legacy")) - this.PluginImpl.Interface.UiBuilder.AddNotification("asdf"); - if (ImGui.Button("Test")) - { - const string text = - "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; - - NewRandom(out var title, out var type); - var n = this.PluginImpl.NotificationManager.AddNotification( - new() - { - Content = text, - Title = title, - Type = type, - Interactible = true, - Expiry = DateTime.MaxValue, - }); - - var nclick = 0; - n.Click += _ => nclick++; - n.DrawActions += an => - { - if (ImGui.Button("Update in place")) - { - NewRandom(out title, out type); - an.Update(an.CloneNotification() with { Title = title, Type = type }); - } - - if (an.IsMouseHovered) - { - ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); - ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); - }; - } - } - - private static void NewRandom(out string? title, out NotificationType type) - { - var rand = new Random(); - - title = rand.Next(0, 7) switch - { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; - - type = rand.Next(0, 5) switch - { - 0 => NotificationType.Error, - 1 => NotificationType.Warning, - 2 => NotificationType.Info, - 3 => NotificationType.Success, - 4 => NotificationType.None, - _ => NotificationType.None, - }; } } } From 1685e15113a82eb5c2d130d16032f7fede48fd31 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:36:03 +0900 Subject: [PATCH 12/41] Not anymore --- .../Internal/NotificationManager.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index b67605541..e5a27550c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -13,10 +13,7 @@ using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Class handling notifications/toasts in ImGui. -/// Ported from https://github.com/patrickcjk/imgui-notify. -/// +/// Class handling notifications/toasts in ImGui. [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] internal class NotificationManager : INotificationManager, IServiceType, IDisposable @@ -66,9 +63,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos return an; } - /// - /// Adds a notification originating from a plugin. - /// + /// Adds a notification originating from a plugin. /// The notification. /// The source plugin. /// The new notification. @@ -79,9 +74,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos return an; } - /// - /// Add a notification to the notification queue. - /// + /// Add a notification to the notification queue. /// The content of the notification. /// The title of the notification. /// The type of the notification. @@ -97,9 +90,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Type = type, }); - /// - /// Draw all currently queued notifications. - /// + /// Draw all currently queued notifications. public void Draw() { var viewportSize = ImGuiHelpers.MainViewport.WorkSize; @@ -116,9 +107,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos } } -/// -/// Plugin-scoped version of a service. -/// +/// Plugin-scoped version of a service. [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.ScopedService] From c12bdaabb31d9070a4c2597884d4b680a64bfef1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:42:46 +0900 Subject: [PATCH 13/41] Format --- .../INotificationMaterializedIcon.cs | 4 +--- .../Internal/NotificationConstants.cs | 12 +++--------- .../Internal/NotificationUtilities.cs | 8 ++------ Dalamud/Interface/ImGuiNotification/Notification.cs | 4 +--- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs index 9be498af1..0657a94a4 100644 --- a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs @@ -4,9 +4,7 @@ using Dalamud.Plugin.Internal.Types; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a materialized icon. -/// +/// Represents a materialized icon. internal interface INotificationMaterializedIcon : IDisposable { /// Draws the icon. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index a16fb904d..1da979430 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -4,9 +4,7 @@ using Dalamud.Interface.Utility; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Constants for drawing notification windows. -/// +/// Constants for drawing notification windows. internal static class NotificationConstants { // .............................[..] @@ -101,9 +99,7 @@ internal static class NotificationConstants /// Gets the string format of the initiator name field, if the initiator is unloaded. public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; - /// - /// Formats an instance of as a relative time. - /// + /// Formats an instance of as a relative time. /// When. /// The formatted string. public static string FormatRelativeDateTime(this DateTime when) @@ -121,9 +117,7 @@ internal static class NotificationConstants return when.FormatAbsoluteDateTime(); } - /// - /// Formats an instance of as an absolute time. - /// + /// Formats an instance of as an absolute time. /// When. /// The formatted string. public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs index f442ef553..3bf8add07 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -9,14 +9,10 @@ using ImGuiNET; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Utilities for implementing stuff under . -/// +/// Utilities for implementing stuff under . internal static class NotificationUtilities { - /// - /// Draws the given texture, or the icon of the plugin if texture is null. - /// + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 8f5ec2423..be2b9237d 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -3,9 +3,7 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a blueprint for a notification. -/// +/// Represents a blueprint for a notification. public sealed record Notification : INotification { /// From 1ca2d2000ba47d411f532387dc0a22b72351216f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:56:06 +0900 Subject: [PATCH 14/41] Add UserDismissable --- .../ImGuiNotification/IActiveNotification.cs | 3 +++ .../ImGuiNotification/INotification.cs | 6 ++++- .../Internal/ActiveNotification.cs | 24 +++++++++++++++---- .../ImGuiNotification/Notification.cs | 3 +++ .../Windows/Data/Widgets/ImGuiWidget.cs | 11 +++++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index fecccf092..cbe5d9e25 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -67,6 +67,9 @@ public interface IActiveNotification : INotification /// new bool Interactible { get; set; } + /// + new bool UserDismissable { get; set; } + /// new TimeSpan HoverExtendDuration { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index c4a7b46ac..92b28fb15 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -38,9 +38,13 @@ public interface INotification /// . /// Note that the close buttons for notifications are always provided and interactible. /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, - /// unless is set. + /// unless is set or is unset. /// bool Interactible { get; } + + /// Gets a value indicating whether the user can dismiss the notification by themselves. + /// Consider adding a cancel button to . + bool UserDismissable { get; } /// Gets the new duration for this notification if mouse cursor is on the notification window. /// diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 64b812197..246c6cce5 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -148,6 +148,18 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } + /// + public bool UserDismissable + { + get => this.underlyingNotification.UserDismissable; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.UserDismissable = value; + } + } + /// public TimeSpan HoverExtendDuration { @@ -317,7 +329,6 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable if (opacity <= 0) return 0; - var notificationManager = Service.Get(); var interfaceManager = Service.Get(); var unboundedWidth = ImGui.CalcTextSize(this.Content).X; float closeButtonHorizontalSpaceReservation; @@ -386,7 +397,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); - this.DrawNotificationMainWindowContent(notificationManager, width); + this.DrawNotificationMainWindowContent(width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var hovered = ImGui.IsWindowHovered(); @@ -433,7 +444,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { if (this.Click is null) { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) this.DismissNow(NotificationDismissReason.Manual); } else @@ -546,7 +557,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MaterializedIcon = null; } - private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) + private void DrawNotificationMainWindowContent(float width) { var basePos = ImGui.GetCursorPos(); this.DrawIcon( @@ -706,6 +717,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) { + if (!this.UserDismissable) + return; + using (interfaceManager.IconFontHandle?.Push()) { var str = FontAwesomeIcon.Times.ToIconString(); @@ -719,7 +733,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); if (ImGui.Button(str, new(size + (pad * 2)))) - this.DismissNow(); + this.DismissNow(NotificationDismissReason.Manual); ImGui.PopStyleColor(2); if (!this.IsMouseHovered) diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index be2b9237d..e082aaaed 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -24,6 +24,9 @@ public sealed record Notification : INotification /// public bool Interactible { get; set; } + /// + public bool UserDismissable { get; set; } + /// public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 71cba3297..6239c9749 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -120,7 +120,11 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); - ImGui.Checkbox("Action Bar", ref this.notificationTemplate.ActionBar); + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); + + ImGui.Checkbox( + "Action Bar (always on if not user dismissable for the example)", + ref this.notificationTemplate.ActionBar); if (ImGui.Button("Add notification")) { @@ -144,6 +148,7 @@ internal class ImGuiWidget : IDataWindowWidget Title = title, Type = type, Interactible = this.notificationTemplate.Interactible, + UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, Progress = this.notificationTemplate.ProgressMode switch { @@ -203,7 +208,7 @@ internal class ImGuiWidget : IDataWindowWidget break; } - if (this.notificationTemplate.ActionBar) + if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) { var nclick = 0; n.Click += _ => nclick++; @@ -326,6 +331,7 @@ internal class ImGuiWidget : IDataWindowWidget public int TypeInt; public int DurationInt; public bool Interactible; + public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -342,6 +348,7 @@ internal class ImGuiWidget : IDataWindowWidget this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; this.Interactible = true; + this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; } From df9212ac5854ad804d17c204fe95b38bd4789f2b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:28:33 +0900 Subject: [PATCH 15/41] Expose DefaultDisplayDuration --- .../{Internal => }/NotificationConstants.cs | 70 +++++++++---------- .../Windows/Data/Widgets/ImGuiWidget.cs | 1 + 2 files changed, 36 insertions(+), 35 deletions(-) rename Dalamud/Interface/ImGuiNotification/{Internal => }/NotificationConstants.cs (66%) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs similarity index 66% rename from Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 1da979430..62d288836 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -2,10 +2,10 @@ using System.Numerics; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.ImGuiNotification.Internal; +namespace Dalamud.Interface.ImGuiNotification; /// Constants for drawing notification windows. -internal static class NotificationConstants +public static class NotificationConstants { // .............................[..] // ..when.......................[XX] @@ -19,47 +19,47 @@ internal static class NotificationConstants // .. action buttons .. // ................................. - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - public const string DefaultInitiator = "Dalamud"; - - /// The size of the icon. - public const float IconSize = 32; - - /// The background opacity of a notification window. - public const float BackgroundOpacity = 0.82f; - - /// The duration of indeterminate progress bar loop in milliseconds. - public const float IndeterminateProgressbarLoopDuration = 2000f; - - /// Duration of show animation. - public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); - /// Default duration of the notification. public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); - /// Default duration of the notification. + /// Default duration of the notification, after the mouse cursor leaves the notification window. public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); - /// Duration of hide animation. - public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + /// The string to show in place of this_plugin if the notification is shown by Dalamud. + internal const string DefaultInitiator = "Dalamud"; + + /// The size of the icon. + internal const float IconSize = 32; + + /// The background opacity of a notification window. + internal const float BackgroundOpacity = 0.82f; + + /// The duration of indeterminate progress bar loop in milliseconds. + internal const float IndeterminateProgressbarLoopDuration = 2000f; + + /// Duration of show animation. + internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. - public static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Duration of hide animation. + internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); /// Text color for the when. - public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the close button [X]. - public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the title. - public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); /// Text color for the name of the initiator. - public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the body. - public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = @@ -77,32 +77,32 @@ internal static class NotificationConstants }; /// Gets the scaled padding of the window (dot(.) in the above diagram). - public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); /// Gets the distance from the right bottom border of the viewport /// to the right bottom border of a notification window. /// - public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between two notification windows. - public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between components. - public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the icon. - public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. - public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - public static string FormatRelativeDateTime(this DateTime when) + internal static string FormatRelativeDateTime(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStrings) @@ -120,5 +120,5 @@ internal static class NotificationConstants /// Formats an instance of as an absolute time. /// When. /// The formatted string. - public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 6239c9749..65418cdbe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; From eaf447164abec712e493fd02b842589eebe2e3c6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:41:45 +0900 Subject: [PATCH 16/41] Ensure that crossthread progress update do not result in animation jerkiness --- .../Internal/ActiveNotification.cs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 246c6cce5..c81bba7ff 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -35,6 +35,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// Used for calculating correct dismissal progressbar animation (right edge). private float prevProgressR; + /// New progress value to be updated on next call to . + private float? newProgress; + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. @@ -175,15 +178,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public float Progress { - get => this.underlyingNotification.Progress; + get => this.newProgress ?? this.underlyingNotification.Progress; set { if (this.IsDismissed) return; - this.progressBefore = this.ProgressEased; - this.underlyingNotification.Progress = value; - this.progressEasing.Restart(); + this.newProgress = value; } } @@ -207,14 +208,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { get { - if (this.Progress < 0) + var underlyingProgress = this.underlyingNotification.Progress; + if (underlyingProgress < 0) return 0f; - if (Math.Abs(this.Progress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) - return this.Progress; + if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + return underlyingProgress; var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); - return this.progressBefore + (state * (this.Progress - this.progressBefore)); + return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); } } @@ -271,7 +273,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } /// - public Notification CloneNotification() => this.underlyingNotification with { }; + public Notification CloneNotification() + { + var newValue = this.underlyingNotification with { }; + if (this.newProgress is { } p) + newValue.Progress = p; + return newValue; + } /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -303,6 +311,16 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.showEasing.Update(); this.hideEasing.Update(); this.progressEasing.Update(); + + if (this.newProgress is { } p) + { + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = p; + this.progressEasing.Restart(); + this.progressEasing.Update(); + this.newProgress = null; + } + return this.hideEasing.IsRunning && this.hideEasing.IsDone; } @@ -498,7 +516,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Expiry = newNotification.Expiry; this.Interactible = newNotification.Interactible; this.HoverExtendDuration = newNotification.HoverExtendDuration; - this.Progress = newNotification.Progress; + this.newProgress = newNotification.Progress; } /// From cf54a0281243b7e126280918bae411f0ee93ab65 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:50:30 +0900 Subject: [PATCH 17/41] fixes --- .../ImGuiNotification/IActiveNotification.cs | 12 ++++----- .../ImGuiNotification/INotification.cs | 8 +++--- .../Internal/ActiveNotification.cs | 26 +++++++++---------- .../ImGuiNotification/Notification.cs | 5 ++-- .../Windows/Data/Widgets/ImGuiWidget.cs | 8 +++--- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index cbe5d9e25..0a8f656b9 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -17,7 +17,7 @@ public interface IActiveNotification : INotification /// Invoked upon clicking on the notification. /// - /// This event is not applicable when is set to false. + /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -25,7 +25,7 @@ public interface IActiveNotification : INotification /// Invoked when the mouse enters the notification window. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -33,7 +33,7 @@ public interface IActiveNotification : INotification /// Invoked when the mouse leaves the notification window. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -41,7 +41,7 @@ public interface IActiveNotification : INotification /// Invoked upon drawing the action bar of the notification. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -64,8 +64,8 @@ public interface IActiveNotification : INotification /// new DateTime Expiry { get; set; } - /// - new bool Interactible { get; set; } + /// + new bool Interactable { get; set; } /// new bool UserDismissable { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 92b28fb15..6b47b69f4 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -36,12 +36,12 @@ public interface INotification /// /// Set this value to true if you want to respond to user inputs from /// . - /// Note that the close buttons for notifications are always provided and interactible. + /// Note that the close buttons for notifications are always provided and interactable. /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, /// unless is set or is unset. /// - bool Interactible { get; } - + bool Interactable { get; } + /// Gets a value indicating whether the user can dismiss the notification by themselves. /// Consider adding a cancel button to . bool UserDismissable { get; } @@ -49,7 +49,7 @@ public interface INotification /// Gets the new duration for this notification if mouse cursor is on the notification window. /// /// If set to or less, then this feature is turned off. - /// This property is applicable regardless of . + /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c81bba7ff..2e82af6a3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -139,15 +139,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public bool Interactible + /// + public bool Interactable { - get => this.underlyingNotification.Interactible; + get => this.underlyingNotification.Interactable; set { if (this.IsDismissed) return; - this.underlyingNotification.Interactible = value; + this.underlyingNotification.Interactable = value; } } @@ -407,7 +407,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactible + (this.Interactable ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | @@ -514,7 +514,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Type = newNotification.Type; this.IconSource = newNotification.IconSource; this.Expiry = newNotification.Expiry; - this.Interactible = newNotification.Interactible; + this.Interactable = newNotification.Interactable; this.HoverExtendDuration = newNotification.HoverExtendDuration; this.newProgress = newNotification.Progress; } @@ -538,16 +538,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - this.underlyingNotification.Interactible = false; + this.Interactable = true; this.IsInitiatorUnloaded = true; + this.UserDismissable = true; + this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; - var now = DateTime.Now; - var newMaxExpiry = now + NotificationConstants.DefaultDisplayDuration; - if (this.underlyingNotification.Expiry > newMaxExpiry) - { - this.underlyingNotification.Expiry = newMaxExpiry; - this.ExpiryRelativeToTime = now; - } + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + if (this.Expiry > newMaxExpiry) + this.Expiry = newMaxExpiry; return; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index e082aaaed..97279d6c1 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -22,10 +21,10 @@ public sealed record Notification : INotification public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; /// - public bool Interactible { get; set; } + public bool Interactable { get; set; } = true; /// - public bool UserDismissable { get; set; } + public bool UserDismissable { get; set; } = true; /// public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 65418cdbe..74dc8939c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -119,7 +119,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); - ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); + ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); @@ -148,7 +148,7 @@ internal class ImGuiWidget : IDataWindowWidget Content = text, Title = title, Type = type, - Interactible = this.notificationTemplate.Interactible, + Interactable = this.notificationTemplate.Interactable, UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, Progress = this.notificationTemplate.ProgressMode switch @@ -331,7 +331,7 @@ internal class ImGuiWidget : IDataWindowWidget public bool ManualType; public int TypeInt; public int DurationInt; - public bool Interactible; + public bool Interactable; public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -348,7 +348,7 @@ internal class ImGuiWidget : IDataWindowWidget this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; - this.Interactible = true; + this.Interactable = true; this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; From 9644dd9922b608a2b180df700158924f6e086431 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 03:31:15 +0900 Subject: [PATCH 18/41] Ensure that TextureWrapTaskIconSource.Materialize do not throw --- .../IconSource/TextureWrapTaskIconSource.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs index 28fdc4d96..2a5473760 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; +using Serilog; + namespace Dalamud.Interface.ImGuiNotification.IconSource; /// Represents the use of future as the icon of a notification. @@ -41,7 +43,18 @@ public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInte { private Task? task; - public MaterializedIcon(Func?>? taskFunc) => this.task = taskFunc?.Invoke(); + public MaterializedIcon(Func?>? taskFunc) + { + try + { + this.task = taskFunc?.Invoke(); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture."); + this.task = null; + } + } public void Dispose() { From 42b6f8fd4b5e8a7031922ccd3df56daf996e1783 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 04:16:36 +0900 Subject: [PATCH 19/41] fix disposes and add TextureWrapIconSource --- .../ImGuiNotification/IActiveNotification.cs | 25 ++----- .../ImGuiNotification/INotification.cs | 9 ++- .../IconSource/TextureWrapIconSource.cs | 63 ++++++++++++++++ .../Internal/ActiveNotification.cs | 72 ++++++++----------- .../Internal/NotificationManager.cs | 26 +++++-- .../Internal/NotificationUtilities.cs | 23 +++++- .../ImGuiNotification/Notification.cs | 7 ++ .../Windows/Data/Widgets/ImGuiWidget.cs | 34 ++++++--- Dalamud/Interface/UiBuilder.cs | 1 + .../Plugin/Services/INotificationManager.cs | 8 ++- 10 files changed, 187 insertions(+), 81 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 0a8f656b9..3ae1a76ce 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -56,11 +56,6 @@ public interface IActiveNotification : INotification /// new NotificationType Type { get; set; } - /// Gets or sets the icon source. - /// Setting a new value to this property does not change the icon. Use to do so. - /// - new INotificationIconSource? IconSource { get; set; } - /// new DateTime Expiry { get; set; } @@ -86,25 +81,19 @@ public interface IActiveNotification : INotification /// This includes when the hide animation is being played. bool IsDismissed { get; } - /// Clones this notification as a . - /// A new instance of . - Notification CloneNotification(); - /// Dismisses this notification. void DismissNow(); - /// Updates the notification data. - /// - /// Call to update the icon using the new . - /// If is true, then this function is a no-op. - /// - /// The new notification entry. - void Update(INotification newNotification); - - /// Loads the icon again using . + /// Loads the icon again using the same . /// If is true, then this function is a no-op. void UpdateIcon(); + /// Disposes the previous icon source, take ownership of the new icon source, + /// and calls . + /// Thew new icon source. + /// If is true, then this function is a no-op. + void UpdateIconSource(INotificationIconSource? newIconSource); + /// 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 6b47b69f4..e80ff96c0 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -4,7 +4,7 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. -public interface INotification +public interface INotification : IDisposable { /// Gets the content body of the notification. string Content { get; } @@ -16,10 +16,15 @@ public interface INotification NotificationType Type { get; } /// Gets the icon source. - /// The following icon sources are currently available.
+ /// + /// The assigned value will be disposed upon the call on this instance of + /// .
+ ///
+ /// The following icon sources are currently available.
///
    ///
  • ///
  • + ///
  • ///
  • ///
  • ///
  • diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs new file mode 100644 index 000000000..b3d4392cf --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs @@ -0,0 +1,63 @@ +using System.Numerics; +using System.Threading; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of future as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public sealed class TextureWrapIconSource : INotificationIconSource.IInternal +{ + private IDalamudTextureWrap? wrap; + + /// Initializes a new instance of the class. + /// The texture wrap to handle over the ownership. + /// + /// If true, this class will own the passed , and you must not call + /// on the passed wrap. + /// If false, this class will create a new reference of the passed wrap, and you should call + /// on the passed wrap. + /// In both cases, this class must be disposed after use. + public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) => + this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource(); + + /// Gets the underlying texture wrap. + public IDalamudTextureWrap? Wrap => this.wrap; + + /// + public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false); + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.wrap, null) is { } w) + w.Dispose(); + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private IDalamudTextureWrap? wrap; + + public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap; + + public void Dispose() + { + if (Interlocked.Exchange(ref this.wrap, null) is { } w) + w.Dispose(); + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + this.wrap, + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 2e82af6a3..3f14ec50b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -43,7 +43,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) { - this.underlyingNotification = underlyingNotification with { }; + this.underlyingNotification = underlyingNotification with + { + IconSource = underlyingNotification.IconSource?.Clone(), + }; this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); @@ -51,7 +54,16 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.showEasing.Start(); this.progressEasing.Start(); - this.UpdateIcon(); + try + { + this.UpdateIcon(); + } + catch (Exception e) + { + // Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the + // error accordingly. + Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored."); + } } /// @@ -114,17 +126,8 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public INotificationIconSource? IconSource - { - get => this.underlyingNotification.IconSource; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.IconSource = value; - } - } + /// + public INotificationIconSource? IconSource => this.underlyingNotification.IconSource; /// public DateTime Expiry @@ -264,23 +267,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void Dispose() { - this.ClearIconTask(); - this.underlyingNotification.IconSource = null; + this.ClearMaterializedIcon(); + this.underlyingNotification.Dispose(); this.Dismiss = null; this.Click = null; this.DrawActions = null; this.InitiatorPlugin = null; } - /// - public Notification CloneNotification() - { - var newValue = this.underlyingNotification with { }; - if (this.newProgress is { } p) - newValue.Progress = p; - return newValue; - } - /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -504,30 +498,26 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable return windowSize.Y; } - /// - public void Update(INotification newNotification) - { - if (this.IsDismissed) - return; - this.Content = newNotification.Content; - this.Title = newNotification.Title; - this.Type = newNotification.Type; - this.IconSource = newNotification.IconSource; - this.Expiry = newNotification.Expiry; - this.Interactable = newNotification.Interactable; - this.HoverExtendDuration = newNotification.HoverExtendDuration; - this.newProgress = newNotification.Progress; - } - /// public void UpdateIcon() { if (this.IsDismissed) return; - this.ClearIconTask(); + this.ClearMaterializedIcon(); this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } + /// + public void UpdateIconSource(INotificationIconSource? newIconSource) + { + if (this.IsDismissed || this.underlyingNotification.IconSource == newIconSource) + return; + + this.underlyingNotification.IconSource?.Dispose(); + this.underlyingNotification.IconSource = newIconSource; + this.UpdateIcon(); + } + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -567,7 +557,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - private void ClearIconTask() + private void ClearMaterializedIcon() { this.MaterializedIcon?.Dispose(); this.MaterializedIcon = null; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index e5a27550c..fdea6146a 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -56,8 +56,9 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos } /// - public IActiveNotification AddNotification(Notification notification) + public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) { + using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; @@ -65,10 +66,13 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Adds a notification originating from a plugin. /// The notification. + /// Dispose when this function returns. /// The source plugin. - /// The new notification. - public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) + /// The added notification. + /// will be honored even on exceptions. + public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin) { + using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; @@ -88,7 +92,8 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Content = content, Title = title, Type = type, - }); + }, + true); /// Draw all currently queued notifications. public void Draw() @@ -101,7 +106,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - this.notifications.RemoveAll(x => x.UpdateAnimations()); + this.notifications.RemoveAll(static x => + { + if (!x.UpdateAnimations()) + return false; + + x.Dispose(); + return true; + }); foreach (var tn in this.notifications) height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; } @@ -127,9 +139,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT this.localPlugin = localPlugin; /// - public IActiveNotification AddNotification(Notification notification) + public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) { - var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); + var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); return an; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs index 3bf8add07..3e24f628c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -23,7 +23,22 @@ internal static class NotificationUtilities Vector2 maxCoord, LocalPlugin? initiatorPlugin) { - if (texture is null) + var handle = nint.Zero; + var size = Vector2.Zero; + if (texture is not null) + { + try + { + handle = texture.ImGuiHandle; + size = texture.Size; + } + catch + { + // must have been disposed or something; ignore the texture + } + } + + if (handle == nint.Zero) { var dam = Service.Get(); if (initiatorPlugin is null) @@ -46,14 +61,16 @@ internal static class NotificationUtilities }; } } + + handle = texture.ImGuiHandle; + size = texture.Size; } - var size = texture.Size; if (size.X > maxCoord.X - minCoord.X) size *= (maxCoord.X - minCoord.X) / size.X; if (size.Y > maxCoord.Y - minCoord.Y) size *= (maxCoord.Y - minCoord.Y) / size.Y; ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); - ImGui.Image(texture.ImGuiHandle, size); + ImGui.Image(handle, size); } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 97279d6c1..3b452bd2d 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -31,4 +31,11 @@ public sealed record Notification : INotification /// public float Progress { get; set; } = 1f; + + /// + public void Dispose() + { + this.IconSource?.Dispose(); + this.IconSource = null; + } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 74dc8939c..1093ca4b8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -79,27 +79,26 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.IconSourceTitles.Length); switch (this.notificationTemplate.IconSourceInt) { - case 1: - case 2: + case 1 or 2: ImGui.InputText( "Icon Text##iconSourceText", ref this.notificationTemplate.IconSourceText, 255); break; - case 3: + case 3 or 4: ImGui.Combo( "Icon Source##iconSourceAssetCombo", ref this.notificationTemplate.IconSourceAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; - case 4: + case 5 or 7: ImGui.InputText( "Game Path##iconSourceText", ref this.notificationTemplate.IconSourceText, 255); break; - case 5: + case 6 or 8: ImGui.InputText( "File Path##iconSourceText", ref this.notificationTemplate.IconSourceText, @@ -170,17 +169,31 @@ internal class ImGuiWidget : IDataWindowWidget (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => new TextureWrapTaskIconSource( + 3 => new TextureWrapIconSource( + Service.Get().GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[ + this.notificationTemplate.IconSourceAssetInt])), + false), + 4 => new TextureWrapTaskIconSource( () => Service.Get().GetDalamudTextureWrapAsync( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt]))), - 4 => new GamePathIconSource(this.notificationTemplate.IconSourceText), - 5 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + 5 => new GamePathIconSource(this.notificationTemplate.IconSourceText), + 6 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + 7 => new TextureWrapIconSource( + Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), + false), + 8 => new TextureWrapIconSource( + Service.Get().GetTextureFromFile( + new(this.notificationTemplate.IconSourceText)), + false), _ => null, }, - }); + }, + true); switch (this.notificationTemplate.ProgressMode) { case 2: @@ -276,9 +289,12 @@ internal class ImGuiWidget : IDataWindowWidget "None (use Type)", "SeIconChar", "FontAwesomeIcon", + "TextureWrap from DalamudAssets", "TextureWrapTask from DalamudAssets", "GamePath", "FilePath", + "TextureWrap from GamePath", + "TextureWrap from FilePath", }; public static readonly string[] AssetSources = diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 64ff0cc45..1237c9c1f 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -581,6 +581,7 @@ public sealed class UiBuilder : IDisposable Type = type, Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), }, + true, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 1d31ddd35..441cc31f7 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -11,6 +11,12 @@ public interface INotificationManager /// Adds a notification. /// /// The new notification. + /// + /// Dispose when this function returns, even if the function throws an exception. + /// Set to false to reuse for multiple calls to this function, in which case, + /// you should call on the value supplied to at a + /// later time. + /// /// The added notification. - IActiveNotification AddNotification(Notification notification); + IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); } From f4349461375d3186144f10974fa7bf0885209c3b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 11:54:24 +0900 Subject: [PATCH 20/41] Turn impls of IconSource internal --- .../ImGuiNotification/INotification.cs | 2 +- .../INotificationIconSource.cs | 68 +++++++++++++++++++ .../Internal/ActiveNotification.cs | 4 +- .../IconSource/FilePathIconSource.cs | 17 +++-- .../IconSource/FontAwesomeIconIconSource.cs | 19 +++--- .../IconSource/GamePathIconSource.cs | 17 +++-- .../IconSource/SeIconCharIconSource.cs | 19 +++--- .../IconSource/TextureWrapIconSource.cs | 7 +- .../IconSource/TextureWrapTaskIconSource.cs | 21 +++--- .../{Internal => }/NotificationUtilities.cs | 29 +++++++- .../Windows/Data/Widgets/ImGuiWidget.cs | 18 ++--- 11 files changed, 153 insertions(+), 68 deletions(-) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/FilePathIconSource.cs (74%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/FontAwesomeIconIconSource.cs (73%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/GamePathIconSource.cs (76%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/SeIconCharIconSource.cs (70%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/TextureWrapIconSource.cs (89%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/TextureWrapTaskIconSource.cs (80%) rename Dalamud/Interface/ImGuiNotification/{Internal => }/NotificationUtilities.cs (65%) diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index e80ff96c0..d8ac95c22 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs index 8a73e2a64..1fee67098 100644 --- a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs @@ -1,3 +1,10 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; +using Dalamud.Interface.Internal; + namespace Dalamud.Interface.ImGuiNotification; /// Icon source for . @@ -12,6 +19,67 @@ public interface INotificationIconSource : ICloneable, IDisposable INotificationMaterializedIcon Materialize(); } + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The texture wrap. + /// + /// If true, this class will own the passed , and you must not call + /// on the passed wrap. + /// If false, this class will create a new reference of the passed wrap, and you should call + /// on the passed wrap. + /// In both cases, the returned object must be disposed after use. + /// A new instance of that should be disposed after use. + /// If any errors are thrown or is null, the default icon will be displayed + /// instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) => + new TextureWrapIconSource(wrap, takeOwnership); + + /// Gets a new instance of that will source the icon from an + /// returning a resulting in an + /// . + /// The function that returns a task that results a texture wrap. + /// A new instance of that should be disposed after use. + /// If any errors are thrown or is null, the default icon will be + /// displayed instead.
    + /// Use if you will have a wrap available without waiting.
    + /// should not contain a reference to a resource; if it does, the resource will be + /// released when all instances of derived from the returned object are freed + /// by the garbage collector, which will result in non-deterministic resource releases.
    + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(Func?>? wrapTaskFunc) => + new TextureWrapTaskIconSource(wrapTaskFunc); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath); + /// new INotificationIconSource Clone(); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 3f14ec50b..53262c08e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -4,7 +4,7 @@ using System.Runtime.Loader; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; @@ -18,7 +18,7 @@ using Serilog; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. -internal sealed class ActiveNotification : IActiveNotification, IDisposable +internal sealed class ActiveNotification : IActiveNotification { private readonly Notification underlyingNotification; diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs similarity index 74% rename from Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs index b1886154a..a741931a5 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs @@ -1,33 +1,32 @@ using System.IO; using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of a texture from a file as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct FilePathIconSource : INotificationIconSource.IInternal +internal class FilePathIconSource : INotificationIconSource.IInternal { - /// The path to a .tex file inside the game resources. - public readonly string FilePath; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. public FilePathIconSource(string filePath) => this.FilePath = filePath; + /// Gets the path to a .tex file inside the game resources. + public string FilePath { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.FilePath); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs similarity index 73% rename from Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs index 8e28940ba..86a6f835c 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs @@ -1,32 +1,31 @@ using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Plugin.Internal.Types; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. -public readonly struct FontAwesomeIconIconSource : INotificationIconSource.IInternal +internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal { - /// The icon character. - public readonly FontAwesomeIcon Char; + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar; - /// Initializes a new instance of the struct. - /// The character. - public FontAwesomeIconIconSource(FontAwesomeIcon c) => this.Char = c; + /// Gets the icon character. + public FontAwesomeIcon IconChar { get; } /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); /// Draws the icon. /// The icon string. diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs similarity index 76% rename from Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs index 9b669e62a..974e60ee7 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs @@ -1,34 +1,33 @@ using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of a game-shipped texture as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct GamePathIconSource : INotificationIconSource.IInternal +internal class GamePathIconSource : INotificationIconSource.IInternal { - /// The path to a .tex file inside the game resources. - public readonly string GamePath; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. /// Use to get the game path from icon IDs. public GamePathIconSource(string gamePath) => this.GamePath = gamePath; + /// Gets the path to a .tex file inside the game resources. + public string GamePath { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.GamePath); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs similarity index 70% rename from Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs index d34b776bc..83fd0bef6 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs @@ -1,33 +1,32 @@ using System.Numerics; using Dalamud.Game.Text; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Plugin.Internal.Types; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. -public readonly struct SeIconCharIconSource : INotificationIconSource.IInternal +internal class SeIconCharIconSource : INotificationIconSource.IInternal { - /// The icon character. - public readonly SeIconChar Char; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The character. - public SeIconCharIconSource(SeIconChar c) => this.Char = c; + public SeIconCharIconSource(SeIconChar c) => this.IconChar = c; + + /// Gets the icon character. + public SeIconChar IconChar { get; } /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); private sealed class MaterializedIcon : INotificationMaterializedIcon { diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs similarity index 89% rename from Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs index b3d4392cf..a10b09bce 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs @@ -1,15 +1,14 @@ using System.Numerics; using System.Threading; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of future as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public sealed class TextureWrapIconSource : INotificationIconSource.IInternal +internal class TextureWrapIconSource : INotificationIconSource.IInternal { private IDalamudTextureWrap? wrap; @@ -38,7 +37,7 @@ public sealed class TextureWrapIconSource : INotificationIconSource.IInternal } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs similarity index 80% rename from Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs index 2a5473760..4039b6955 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs @@ -1,42 +1,41 @@ using System.Numerics; using System.Threading.Tasks; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of future as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInternal +internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal { - /// The function that returns a task resulting in a new instance of . - /// - /// Dalamud will take ownership of the result. Do not call . - public readonly Func?>? TextureWrapTaskFunc; - /// Gets the default materialized icon, for the purpose of displaying the plugin icon. internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The function. public TextureWrapTaskIconSource(Func?>? taskFunc) => this.TextureWrapTaskFunc = taskFunc; + /// Gets the function that returns a task resulting in a new instance of . + /// + /// Dalamud will take ownership of the result. Do not call . + public Func?>? TextureWrapTaskFunc { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.TextureWrapTaskFunc); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs similarity index 65% rename from Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs rename to Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 3e24f628c..9b3602b68 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -1,5 +1,8 @@ +using System.IO; using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Game.Text; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; using Dalamud.Plugin.Internal.Types; @@ -7,17 +10,37 @@ using Dalamud.Storage.Assets; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.Internal; +namespace Dalamud.Interface.ImGuiNotification; /// Utilities for implementing stuff under . -internal static class NotificationUtilities +public static class NotificationUtilities { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => + INotificationIconSource.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => + INotificationIconSource.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => + INotificationIconSource.From(wrap, takeOwnership); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => + INotificationIconSource.FromFile(fileInfo.FullName); + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. /// The initiator plugin. - public static void DrawTexture( + internal static void DrawTexture( IDalamudTextureWrap? texture, Vector2 minCoord, Vector2 maxCoord, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 1093ca4b8..3b518af84 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; @@ -161,32 +161,32 @@ internal class ImGuiWidget : IDataWindowWidget }, IconSource = this.notificationTemplate.IconSourceInt switch { - 1 => new SeIconCharIconSource( + 1 => INotificationIconSource.From( (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 2 => new FontAwesomeIconIconSource( + 2 => INotificationIconSource.From( (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => new TextureWrapIconSource( + 3 => INotificationIconSource.From( Service.Get().GetDalamudTextureWrap( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt])), false), - 4 => new TextureWrapTaskIconSource( + 4 => INotificationIconSource.From( () => Service.Get().GetDalamudTextureWrapAsync( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt]))), - 5 => new GamePathIconSource(this.notificationTemplate.IconSourceText), - 6 => new FilePathIconSource(this.notificationTemplate.IconSourceText), - 7 => new TextureWrapIconSource( + 5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText), + 6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText), + 7 => INotificationIconSource.From( Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), false), - 8 => new TextureWrapIconSource( + 8 => INotificationIconSource.From( Service.Get().GetTextureFromFile( new(this.notificationTemplate.IconSourceText)), false), From e96089f8b20963b2934837aece761879bf77b43b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 13:04:17 +0900 Subject: [PATCH 21/41] Separate progress and expiry animations --- .../ImGuiNotification/IActiveNotification.cs | 3 + .../ImGuiNotification/INotification.cs | 9 +- .../Internal/ActiveNotification.cs | 119 +++++++++++++----- .../ImGuiNotification/Notification.cs | 3 + .../NotificationConstants.cs | 22 +++- .../Windows/Data/Widgets/ImGuiWidget.cs | 5 + 6 files changed, 122 insertions(+), 39 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 3ae1a76ce..e6355cd90 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -59,6 +59,9 @@ public interface IActiveNotification : INotification /// new DateTime Expiry { get; set; } + /// + new bool ShowIndeterminateIfNoExpiry { get; set; } + /// new bool Interactable { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index d8ac95c22..9d6167a95 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -37,6 +37,10 @@ public interface INotification : IDisposable /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } + /// Gets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; } + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from @@ -58,8 +62,7 @@ public interface INotification : IDisposable /// TimeSpan HoverExtendDuration { get; } - /// Gets the progress for the progress bar of the notification. - /// The progress should either be in the range between 0 and 1 or be a negative value. - /// Specifying a negative value will show an indeterminate progress bar. + /// Gets the progress for the background progress bar of the notification. + /// The progress should be in the range between 0 and 1. float Progress { get; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 53262c08e..a71c35c49 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -50,7 +50,7 @@ internal sealed class ActiveNotification : IActiveNotification this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); - this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.showEasing.Start(); this.progressEasing.Start(); @@ -142,6 +142,18 @@ internal sealed class ActiveNotification : IActiveNotification } } + /// + public bool ShowIndeterminateIfNoExpiry + { + get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; + } + } + /// public bool Interactable { @@ -212,9 +224,6 @@ internal sealed class ActiveNotification : IActiveNotification get { var underlyingProgress = this.underlyingNotification.Progress; - if (underlyingProgress < 0) - return 0f; - if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) return underlyingProgress; @@ -409,6 +418,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationMainWindowContent(width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); @@ -440,6 +450,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationActionWindowContent(interfaceManager, width); windowSize.Y += actionWindowHeight; windowPos.Y -= actionWindowHeight; @@ -517,7 +528,7 @@ internal sealed class ActiveNotification : IActiveNotification this.underlyingNotification.IconSource = newIconSource; this.UpdateIcon(); } - + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -563,6 +574,49 @@ internal sealed class ActiveNotification : IActiveNotification this.MaterializedIcon = null; } + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + private void DrawNotificationMainWindowContent(float width) { var basePos = ImGui.GetCursorPos(); @@ -580,62 +634,61 @@ internal sealed class ActiveNotification : IActiveNotification // Top padding is zero, as the action window will add the padding. ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); - float progressL, progressR; + float barL, barR; if (this.IsDismissed) { var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; var length = (this.prevProgressR - this.prevProgressL) / 2f; - progressL = midpoint - (length * v); - progressR = midpoint + (length * v); + barL = midpoint - (length * v); + barR = midpoint + (length * v); } else if (this.Expiry == DateTime.MaxValue) { - if (this.Progress >= 0) - { - progressL = 0f; - progressR = this.ProgressEased; - } - else + if (this.ShowIndeterminateIfNoExpiry) { var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % NotificationConstants.IndeterminateProgressbarLoopDuration) / NotificationConstants.IndeterminateProgressbarLoopDuration); - progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - progressR = Math.Min(elapsed, 2f / 3) / (2f / 3); - progressL = MathF.Pow(progressL, 3); - progressR = 1f - MathF.Pow(1f - progressR, 3); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; } - - this.prevProgressL = progressL; - this.prevProgressR = progressR; } else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) { - progressL = 0f; - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } else { - progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } - progressR = Math.Clamp(progressR, 0f, 1f); + barR = Math.Clamp(barR, 0f, 1f); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); ImGui.PushClipRect(windowPos, windowPos + windowSize, false); ImGui.GetWindowDrawList().AddRectFilled( windowPos + new Vector2( - windowSize.X * progressL, + windowSize.X * barL, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * progressR }, + windowPos + windowSize with { X = windowSize.X * barR }, ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 3b452bd2d..9c89dc305 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -20,6 +20,9 @@ public sealed record Notification : INotification /// public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + /// + public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + /// public bool Interactable { get; set; } = true; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 62d288836..800531f39 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -37,14 +37,24 @@ public static class NotificationConstants /// The duration of indeterminate progress bar loop in milliseconds. internal const float IndeterminateProgressbarLoopDuration = 2000f; + /// The duration of the progress wave animation in milliseconds. + internal const float ProgressWaveLoopDuration = 2000f; + + /// The time ratio of a progress wave loop where the animation is idle. + internal const float ProgressWaveIdleTimeRatio = 0.5f; + + /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. + /// + internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + /// Duration of show animation. internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); - /// Duration of hide animation. - internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of progress change animation. + internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -61,6 +71,12 @@ public static class NotificationConstants /// Text color for the body. internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = { @@ -94,7 +110,7 @@ public static class NotificationConstants internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 3b518af84..ae3f16576 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -120,6 +120,8 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); ImGui.Checkbox( @@ -147,6 +149,7 @@ internal class ImGuiWidget : IDataWindowWidget Content = text, Title = title, Type = type, + ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, Interactable = this.notificationTemplate.Interactable, UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, @@ -347,6 +350,7 @@ internal class ImGuiWidget : IDataWindowWidget public bool ManualType; public int TypeInt; public int DurationInt; + public bool ShowIndeterminateIfNoExpiry; public bool Interactable; public bool UserDismissable; public bool ActionBar; @@ -364,6 +368,7 @@ internal class ImGuiWidget : IDataWindowWidget this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; + this.ShowIndeterminateIfNoExpiry = true; this.Interactable = true; this.UserDismissable = true; this.ActionBar = true; From 0040f611253add1cd7aabaffd26f12fbb95bb056 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 19:51:00 +0900 Subject: [PATCH 22/41] Make notifications minimizable, remove interactable --- .../ImGuiNotification/IActiveNotification.cs | 48 +- .../ImGuiNotification/INotification.cs | 101 +-- .../Internal/ActiveNotification.cs | 685 +++++++++++------- .../IconSource/FontAwesomeIconIconSource.cs | 31 +- .../IconSource/SeIconCharIconSource.cs | 25 +- .../Internal/NotificationManager.cs | 15 +- .../ImGuiNotification/Notification.cs | 64 +- .../NotificationConstants.cs | 31 + .../NotificationDismissReason.cs | 2 +- .../NotificationUtilities.cs | 42 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 66 +- Dalamud/Interface/UiBuilder.cs | 2 +- 12 files changed, 688 insertions(+), 424 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index e6355cd90..504c6d6d5 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,7 +1,5 @@ using System.Threading; -using Dalamud.Interface.Internal.Notifications; - namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -17,7 +15,6 @@ public interface IActiveNotification : INotification /// Invoked upon clicking on the notification. /// - /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -25,7 +22,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse enters the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -33,7 +29,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse leaves the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -41,42 +36,18 @@ public interface IActiveNotification : INotification /// Invoked upon drawing the action bar of the notification. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// event Action DrawActions; - /// - new string Content { get; set; } - - /// - new string? Title { get; set; } - - /// - new NotificationType Type { get; set; } - - /// - new DateTime Expiry { get; set; } - - /// - new bool ShowIndeterminateIfNoExpiry { get; set; } - - /// - new bool Interactable { get; set; } - - /// - new bool UserDismissable { get; set; } - - /// - new TimeSpan HoverExtendDuration { get; set; } - - /// - new float Progress { get; set; } - /// Gets the ID of this notification. long Id { get; } + /// Gets the effective expiry time. + /// Contains if the notification does not expire. + DateTime EffectiveExpiry { get; } + /// Gets a value indicating whether the mouse cursor is on the notification window. bool IsMouseHovered { get; } @@ -87,16 +58,15 @@ public interface IActiveNotification : INotification /// Dismisses this notification. void DismissNow(); + /// Extends this notifiation. + /// The extension time. + /// This does not override . + void ExtendBy(TimeSpan extension); + /// Loads the icon again using the same . /// If is true, then this function is a no-op. void UpdateIcon(); - /// Disposes the previous icon source, take ownership of the new icon source, - /// and calls . - /// Thew new icon source. - /// If is true, then this function is a no-op. - void UpdateIconSource(INotificationIconSource? newIconSource); - /// 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 9d6167a95..8f5a30e79 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -6,63 +5,69 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. public interface INotification : IDisposable { - /// Gets the content body of the notification. - string Content { get; } + /// Gets or sets the content body of the notification. + string Content { get; set; } - /// Gets the title of the notification. - string? Title { get; } + /// Gets or sets the title of the notification. + string? Title { get; set; } - /// Gets the type of the notification. - NotificationType Type { get; } + /// Gets or sets the text to display when the notification is minimized. + string? MinimizedText { get; set; } - /// Gets the icon source. + /// Gets or sets the type of the notification. + NotificationType Type { get; set; } + + /// Gets or sets the icon source. /// - /// The assigned value will be disposed upon the call on this instance of - /// .
    - ///
    - /// The following icon sources are currently available.
    - ///
      - ///
    • - ///
    • - ///
    • - ///
    • - ///
    • - ///
    • - ///
    + /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership + /// of the new value is transferred to this . Even if the assignment throws an + /// exception, the ownership is transferred, causing the value to be disposed. Assignment should not throw an + /// exception though, so wrapping the assignment in try...catch block is not required. + /// The assigned value will be disposed upon the call on this instance of + /// , unless the same value is assigned, in which case it will do nothing. + /// If this is an , then updating this property + /// will change the icon being displayed (calls ), unless + /// is true. ///
    - INotificationIconSource? IconSource { get; } + INotificationIconSource? IconSource { get; set; } - /// Gets the expiry. - /// Set to to make the notification not have an expiry time - /// (sticky, indeterminate, permanent, or persistent). - DateTime Expiry { get; } - - /// Gets a value indicating whether to show an indeterminate expiration animation if - /// is set to . - bool ShowIndeterminateIfNoExpiry { get; } - - /// Gets a value indicating whether this notification may be interacted. + /// Gets or sets the hard expiry. /// - /// Set this value to true if you want to respond to user inputs from - /// . - /// Note that the close buttons for notifications are always provided and interactable. - /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, - /// unless is set or is unset. + /// 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 + /// .
    + /// Updating this value will reset the dismiss timer. ///
    - bool Interactable { get; } + DateTime HardExpiry { get; set; } - /// Gets a value indicating whether the user can dismiss the notification by themselves. + /// Gets or sets the initial duration. + /// Set to to make only take effect. + /// Updating this value will reset the dismiss timer. + TimeSpan InitialDuration { get; set; } + + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window. + /// + /// If set to or less, then this feature is turned off, and hovering the mouse on the + /// notification will not make the notification stay.
    + /// Updating this value will reset the dismiss timer. + ///
    + TimeSpan HoverExtendDuration { get; set; } + + /// Gets or sets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; set; } + + /// Gets or sets a value indicating whether the notification has been minimized. + bool Minimized { get; set; } + + /// Gets or sets a value indicating whether the user can dismiss the notification by themselves. /// Consider adding a cancel button to . - bool UserDismissable { get; } + bool UserDismissable { get; set; } - /// Gets the new duration for this notification if mouse cursor is on the notification window. - /// - /// If set to or less, then this feature is turned off. - /// This property is applicable regardless of . - /// - TimeSpan HoverExtendDuration { get; } - - /// Gets the progress for the background progress bar of the notification. + /// Gets or sets the progress for the background progress bar of the notification. /// The progress should be in the range between 0 and 1. - float Progress { get; } + float Progress { get; set; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a71c35c49..a89ebeb0b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -25,6 +25,7 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing showEasing; private readonly Easing hideEasing; private readonly Easing progressEasing; + private readonly Easing expandoEasing; /// The progress before for the progress bar animation with . private float progressBefore; @@ -38,6 +39,9 @@ internal sealed class ActiveNotification : IActiveNotification /// New progress value to be updated on next call to . private float? newProgress; + /// New minimized value to be updated on next call to . + private bool? newMinimized; + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. @@ -51,6 +55,7 @@ internal sealed class ActiveNotification : IActiveNotification this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); + this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); this.showEasing.Start(); this.progressEasing.Start(); @@ -88,9 +93,12 @@ internal sealed class ActiveNotification : IActiveNotification public DateTime CreatedAt { get; } = DateTime.Now; /// Gets the time of starting to count the timer for the expiration. - public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; + public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; - /// + /// Gets the extended expiration time from . + public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + + /// public string Content { get => this.underlyingNotification.Content; @@ -102,7 +110,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// public string? Title { get => this.underlyingNotification.Title; @@ -114,7 +122,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// + public string? MinimizedText + { + get => this.underlyingNotification.MinimizedText; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.MinimizedText = value; + } + } + + /// public NotificationType Type { get => this.underlyingNotification.Type; @@ -127,22 +147,98 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource => this.underlyingNotification.IconSource; - - /// - public DateTime Expiry + public INotificationIconSource? IconSource { - get => this.underlyingNotification.Expiry; + get => this.underlyingNotification.IconSource; set { - if (this.underlyingNotification.Expiry == value || this.IsDismissed) + if (this.IsDismissed) + { + value?.Dispose(); return; - this.underlyingNotification.Expiry = value; - this.ExpiryRelativeToTime = DateTime.Now; + } + + this.underlyingNotification.IconSource = value; + this.UpdateIcon(); } } - /// + /// + public DateTime HardExpiry + { + get => this.underlyingNotification.HardExpiry; + set + { + if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) + return; + this.underlyingNotification.HardExpiry = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan InitialDuration + { + get => this.underlyingNotification.InitialDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.InitialDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan HoverExtendDuration + { + get => this.underlyingNotification.HoverExtendDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.HoverExtendDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public DateTime EffectiveExpiry + { + get + { + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + DateTime expiry; + var hoverExtendDuration = this.HoverExtendDuration; + if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + hoverExtendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.HoverRelativeToTime + hoverExtendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.ExtendedExpiry) + expiry = this.ExtendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + expiry = he; + return expiry; + } + } + + /// public bool ShowIndeterminateIfNoExpiry { get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; @@ -154,19 +250,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool Interactable + /// + public bool Minimized { - get => this.underlyingNotification.Interactable; + get => this.newMinimized ?? this.underlyingNotification.Minimized; set { if (this.IsDismissed) return; - this.underlyingNotification.Interactable = value; + this.newMinimized = value; } } - /// + /// public bool UserDismissable { get => this.underlyingNotification.UserDismissable; @@ -178,19 +274,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public TimeSpan HoverExtendDuration - { - get => this.underlyingNotification.HoverExtendDuration; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.HoverExtendDuration = value; - } - } - - /// + /// public float Progress { get => this.newProgress ?? this.underlyingNotification.Progress; @@ -198,7 +282,6 @@ internal sealed class ActiveNotification : IActiveNotification { if (this.IsDismissed) return; - this.newProgress = value; } } @@ -244,13 +327,13 @@ internal sealed class ActiveNotification : IActiveNotification }; /// Gets the default icon of the notification. - private string? DefaultIconString => this.Type switch + private char? DefaultIconChar => this.Type switch { NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), _ => null, }; @@ -273,6 +356,9 @@ internal sealed class ActiveNotification : IActiveNotification ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) : initiatorPlugin.Name; + /// Gets the effective text to display when minimized. + private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); + /// public void Dispose() { @@ -314,16 +400,38 @@ internal sealed class ActiveNotification : IActiveNotification this.showEasing.Update(); this.hideEasing.Update(); this.progressEasing.Update(); - - if (this.newProgress is { } p) + if (this.expandoEasing.IsRunning) { - this.progressBefore = this.ProgressEased; - this.underlyingNotification.Progress = p; - this.progressEasing.Restart(); - this.progressEasing.Update(); + this.expandoEasing.Update(); + if (this.expandoEasing.IsDone) + this.expandoEasing.Stop(); + } + + if (this.newProgress is { } newProgressValue) + { + if (Math.Abs(this.underlyingNotification.Progress - newProgressValue) > float.Epsilon) + { + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = newProgressValue; + this.progressEasing.Restart(); + this.progressEasing.Update(); + } + this.newProgress = null; } + if (this.newMinimized is { } newMinimizedValue) + { + if (this.underlyingNotification.Minimized != newMinimizedValue) + { + this.underlyingNotification.Minimized = newMinimizedValue; + this.expandoEasing.Restart(); + this.expandoEasing.Update(); + } + + this.newMinimized = null; + } + return this.hideEasing.IsRunning && this.hideEasing.IsDone; } @@ -333,12 +441,9 @@ internal sealed class ActiveNotification : IActiveNotification /// The height of the notification. public float Draw(float maxWidth, float offsetY) { - if (!this.IsDismissed - && DateTime.Now > this.Expiry - && (this.HoverExtendDuration <= TimeSpan.Zero || !this.IsMouseHovered)) - { + var effectiveExpiry = this.EffectiveExpiry; + if (!this.IsDismissed && DateTime.Now > effectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); - } var opacity = Math.Clamp( @@ -375,6 +480,12 @@ internal sealed class ActiveNotification : IActiveNotification unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + var width = Math.Min(maxWidth, unboundedWidth); var viewport = ImGuiHelpers.MainViewport; @@ -384,6 +495,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PushID(this.Id.GetHashCode()); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); unsafe { ImGui.PushStyleColor( @@ -402,83 +514,49 @@ internal sealed class ActiveNotification : IActiveNotification new Vector2(0, offsetY), ImGuiCond.Always, Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); - ImGui.PushStyleVar( - ImGuiStyleVar.WindowPadding, - new Vector2(NotificationConstants.ScaledWindowPadding, 0)); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); ImGui.Begin( $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactable - ? ImGuiWindowFlags.None - : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationMainWindowContent(width); + this.DrawTopBar(interfaceManager, width, actionWindowHeight); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentArea(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentArea(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + this.DrawExpiryBar(effectiveExpiry); + var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var hovered = ImGui.IsWindowHovered(); - ImGui.End(); - ImGui.PopStyleVar(); - - offsetY += windowSize.Y; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight)); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.Begin( - $"##NotifyActionWindow{this.Id}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - - this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationActionWindowContent(interfaceManager, width); - windowSize.Y += actionWindowHeight; - windowPos.Y -= actionWindowHeight; - hovered |= ImGui.IsWindowHovered(); - - ImGui.End(); - ImGui.PopStyleVar(); ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + ImGui.PopStyleVar(3); ImGui.PopID(); - if (hovered) - { - if (this.Click is null) - { - if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); - } - else - { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) - || ImGui.IsMouseClicked(ImGuiMouseButton.Right) - || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); - } - } - if (windowPos.X <= ImGui.GetIO().MousePos.X && windowPos.Y <= ImGui.GetIO().MousePos.Y && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X @@ -489,19 +567,28 @@ internal sealed class ActiveNotification : IActiveNotification this.IsMouseHovered = true; this.MouseEnter.InvokeSafely(this); } + + if (this.HoverExtendDuration > TimeSpan.Zero) + this.HoverRelativeToTime = DateTime.Now; + + if (hovered) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } } else if (this.IsMouseHovered) { - if (this.HoverExtendDuration > TimeSpan.Zero) - { - var newExpiry = DateTime.Now + this.HoverExtendDuration; - if (newExpiry > this.Expiry) - { - this.underlyingNotification.Expiry = newExpiry; - this.ExpiryRelativeToTime = DateTime.Now; - } - } - this.IsMouseHovered = false; this.MouseLeave.InvokeSafely(this); } @@ -509,6 +596,14 @@ internal sealed class ActiveNotification : IActiveNotification return windowSize.Y; } + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.ExtendedExpiry < newExpiry) + this.ExtendedExpiry = newExpiry; + } + /// public void UpdateIcon() { @@ -518,17 +613,6 @@ internal sealed class ActiveNotification : IActiveNotification this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } - /// - public void UpdateIconSource(INotificationIconSource? newIconSource) - { - if (this.IsDismissed || this.underlyingNotification.IconSource == newIconSource) - return; - - this.underlyingNotification.IconSource?.Dispose(); - this.underlyingNotification.IconSource = newIconSource; - this.UpdateIcon(); - } - /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -539,14 +623,13 @@ internal sealed class ActiveNotification : IActiveNotification this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - this.Interactable = true; this.IsInitiatorUnloaded = true; this.UserDismissable = true; this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.Expiry > newMaxExpiry) - this.Expiry = newMaxExpiry; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; return; @@ -617,23 +700,209 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PopClipRect(); } - private void DrawNotificationMainWindowContent(float width) + private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) { - var basePos = ImGui.GetCursorPos(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (interfaceManager.IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (this.IsMouseHovered) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + this.IsMouseHovered + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!this.IsMouseHovered) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!this.IsMouseHovered) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentArea(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + this.DrawIcon( - basePos, - basePos + new Vector2(NotificationConstants.ScaledIconSize)); - basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; - width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); - this.DrawTitle(basePos, basePos + new Vector2(width, 0)); - basePos.Y = ImGui.GetCursorPosY(); - this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); - // Intention was to have left, right, and bottom have the window padding and top have the component gap, - // but as ImGui only allows horz/vert padding, we add the extra bottom padding. - // Top padding is zero, as the action window will add the padding. - ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + this.DrawContentBody(textColumnOffset, textColumnWidth); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + if (this.MaterializedIcon is not null) + { + this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); + return; + } + + var defaultIconChar = this.DefaultIconChar; + if (defaultIconChar is not null) + { + NotificationUtilities.DrawIconString( + Service.Get().IconFontAwesomeFontHandle, + defaultIconChar.Value, + minCoord, + maxCoord, + this.DefaultIconColor); + return; + } + + TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( + minCoord, + maxCoord, + this.DefaultIconColor, + this.InitiatorPlugin); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.DefaultTitle) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } + + private void DrawExpiryBar(DateTime effectiveExpiry) + { float barL, barR; if (this.IsDismissed) { @@ -643,7 +912,14 @@ internal sealed class ActiveNotification : IActiveNotification barL = midpoint - (length * v); barR = midpoint + (length * v); } - else if (this.Expiry == DateTime.MaxValue) + else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) { if (this.ShowIndeterminateIfNoExpiry) { @@ -663,17 +939,10 @@ internal sealed class ActiveNotification : IActiveNotification this.prevProgressR = barR = 1f; } } - else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } else { - barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; @@ -692,112 +961,4 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } - - private void DrawIcon(Vector2 minCoord, Vector2 maxCoord) - { - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconString = this.DefaultIconString; - if (!string.IsNullOrWhiteSpace(defaultIconString)) - { - FontAwesomeIconIconSource.DrawIconStatic(defaultIconString, minCoord, maxCoord, this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.PushTextWrapPos(maxCoord.X); - - ImGui.SetCursorPos(minCoord); - if ((this.Title ?? this.DefaultTitle) is { } title) - { - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.TextUnformatted(title); - ImGui.PopStyleColor(); - } - - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); - ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorString); - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - } - - private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(maxCoord.X); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width) - { - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsMouseHovered - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - - this.DrawCloseButton( - interfaceManager, - new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding), - NotificationConstants.ScaledWindowPadding); - } - - private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) - { - if (!this.UserDismissable) - return; - - using (interfaceManager.IconFontHandle?.Push()) - { - var str = FontAwesomeIcon.Times.ToIconString(); - var textSize = ImGui.CalcTextSize(str); - var size = Math.Max(textSize.X, textSize.Y); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - if (!this.IsMouseHovered) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); - if (ImGui.Button(str, new(size + (pad * 2)))) - this.DismissNow(NotificationDismissReason.Manual); - - ImGui.PopStyleColor(2); - if (!this.IsMouseHovered) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - } - } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs index 86a6f835c..cfe790851 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs @@ -2,8 +2,6 @@ using System.Numerics; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -27,35 +25,22 @@ internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal /// public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - /// Draws the icon. - /// The icon string. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - internal static void DrawIconStatic(string iconString, Vector2 minCoord, Vector2 maxCoord, Vector4 color) - { - using (Service.Get().IconFontAwesomeFontHandle.Push()) - { - var size = ImGui.CalcTextSize(iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); - } - } - private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(FontAwesomeIcon c) => this.iconString = c.ToIconString(); + public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); public void Dispose() { } public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - DrawIconStatic(this.iconString, minCoord, maxCoord, color); + NotificationUtilities.DrawIconString( + Service.Get().IconFontAwesomeFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs index 83fd0bef6..19fe8e948 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs @@ -3,8 +3,6 @@ using System.Numerics; using Dalamud.Game.Text; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -30,25 +28,20 @@ internal class SeIconCharIconSource : INotificationIconSource.IInternal private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(SeIconChar c) => this.iconString = c.ToIconString(); + public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); public void Dispose() { } - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) - { - using (Service.Get().IconAxisFontHandle.Push()) - { - var size = ImGui.CalcTextSize(this.iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(this.iconString); - ImGui.PopStyleColor(); - } - } + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawIconString( + Service.Get().IconAxisFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index fdea6146a..b457539a3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -106,14 +106,15 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - this.notifications.RemoveAll(static x => - { - if (!x.UpdateAnimations()) - return false; + this.notifications.RemoveAll( + static x => + { + if (!x.UpdateAnimations()) + return false; - x.Dispose(); - return true; - }); + x.Dispose(); + return true; + }); foreach (var tn in this.notifications) height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 9c89dc305..dd1d87c42 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,3 +1,5 @@ +using System.Threading; + using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -5,40 +7,88 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { + private INotificationIconSource? iconSource; + + /// Initializes a new instance of the class. + public Notification() + { + } + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(INotification notification) => this.CopyValuesFrom(notification); + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// public string Content { get; set; } = string.Empty; /// public string? Title { get; set; } + /// + public string? MinimizedText { get; set; } + /// public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource { get; set; } + public INotificationIconSource? IconSource + { + get => this.iconSource; + set + { + var prevSource = Interlocked.Exchange(ref this.iconSource, value); + if (prevSource != value) + prevSource?.Dispose(); + } + } /// - public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + public DateTime HardExpiry { get; set; } = DateTime.MaxValue; + + /// + public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + + /// + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; /// - public bool Interactable { get; set; } = true; + public bool Minimized { get; set; } = true; /// public bool UserDismissable { get; set; } = true; - /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; - /// public float Progress { get; set; } = 1f; /// public void Dispose() { - this.IconSource?.Dispose(); + // Assign to the property; it will take care of disposing this.IconSource = null; } + + /// Copy values from the given instance of . + /// The instance of to copy from. + private void CopyValuesFrom(INotification copyFrom) + { + this.Content = copyFrom.Content; + this.Title = copyFrom.Title; + this.MinimizedText = copyFrom.MinimizedText; + this.Type = copyFrom.Type; + this.IconSource = copyFrom.IconSource?.Clone(); + this.HardExpiry = copyFrom.HardExpiry; + this.InitialDuration = copyFrom.InitialDuration; + this.HoverExtendDuration = copyFrom.HoverExtendDuration; + this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; + this.Minimized = copyFrom.Minimized; + this.UserDismissable = copyFrom.UserDismissable; + this.Progress = copyFrom.Progress; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 800531f39..08ef8aebd 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using Dalamud.Interface.Utility; @@ -56,6 +57,9 @@ public static class NotificationConstants /// Duration of progress change animation. internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of expando animation. + internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -92,6 +96,16 @@ public static class NotificationConstants (TimeSpan.MinValue, "just now"), }; + /// Gets the relative time format strings. + private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort = + { + (TimeSpan.FromDays(1), "{0:%d}d"), + (TimeSpan.FromHours(1), "{0:%h}h"), + (TimeSpan.FromMinutes(1), "{0:%m}m"), + (TimeSpan.FromSeconds(1), "{0:%s}s"), + (TimeSpan.MinValue, "now"), + }; + /// Gets the scaled padding of the window (dot(.) in the above diagram). internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -137,4 +151,21 @@ public static class NotificationConstants /// When. /// The formatted string. internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + + /// Formats an instance of as a relative time. + /// When. + /// The formatted string. + internal static string FormatRelativeDateTimeShort(this DateTime when) + { + var ts = DateTime.Now - when; + foreach (var (minSpan, formatString) in RelativeFormatStringsShort) + { + if (ts < minSpan) + continue; + return string.Format(formatString, ts); + } + + Debug.Assert(false, "must not reach here"); + return "???"; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs index 47e52b142..2c9d6d2a4 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.ImGuiNotification; /// Specifies the reason of dismissal for a notification. public enum NotificationDismissReason { - /// The notification is dismissed because the expiry specified from is + /// The notification is dismissed because the expiry specified from is /// met. Timeout = 1, diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 9b3602b68..016e9b793 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using Dalamud.Game.Text; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; @@ -19,22 +21,56 @@ public static class NotificationUtilities [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => INotificationIconSource.From(wrap, takeOwnership); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => INotificationIconSource.FromFile(fileInfo.FullName); + /// Draws an icon string. + /// The font handle to use. + /// The icon character. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + internal static unsafe void DrawIconString( + IFontHandle fontHandleLarge, + char c, + Vector2 minCoord, + Vector2 maxCoord, + Vector4 color) + { + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); + using (fontHandleLarge.Push()) + { + var font = ImGui.GetFont(); + ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var size = glyph.XY1 - glyph.XY0; + var smallerSizeDim = Math.Min(size.X, size.Y); + var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; + size *= scale; + var pos = ((minCoord + maxCoord) - size) / 2; + pos += ImGui.GetWindowPos(); + ImGui.GetWindowDrawList().AddImage( + font.ContainerAtlas.Textures[glyph.TextureIndex].TexID, + pos, + pos + size, + glyph.UV0, + glyph.UV1, + ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); + } + } + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index ae3f16576..4d3807417 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; @@ -64,6 +63,10 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.SameLine(); ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + ImGui.Checkbox("##manualMinimizedText", ref this.notificationTemplate.ManualMinimizedText); + ImGui.SameLine(); + ImGui.InputText("MinimizedText##minimizedText", ref this.notificationTemplate.MinimizedText, 255); + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); ImGui.SameLine(); ImGui.Combo( @@ -107,10 +110,16 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.Combo( - "Duration", - ref this.notificationTemplate.DurationInt, - NotificationTemplate.DurationTitles, - NotificationTemplate.DurationTitles.Length); + "Initial Duration", + ref this.notificationTemplate.InitialDurationInt, + NotificationTemplate.InitialDurationTitles, + NotificationTemplate.InitialDurationTitles.Length); + + ImGui.Combo( + "Hover Extend Duration", + ref this.notificationTemplate.HoverExtendDurationInt, + NotificationTemplate.HoverExtendDurationTitles, + NotificationTemplate.HoverExtendDurationTitles.Length); ImGui.Combo( "Progress", @@ -118,7 +127,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); - ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); @@ -141,18 +150,26 @@ internal class ImGuiWidget : IDataWindowWidget if (this.notificationTemplate.ManualType) type = (NotificationType)this.notificationTemplate.TypeInt; - var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt]; - var n = notifications.AddNotification( new() { Content = text, Title = title, + MinimizedText = this.notificationTemplate.ManualMinimizedText + ? this.notificationTemplate.MinimizedText + : null, Type = type, ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, - Interactable = this.notificationTemplate.Interactable, + Minimized = this.notificationTemplate.Minimized, UserDismissable = this.notificationTemplate.UserDismissable, - Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, + InitialDuration = + this.notificationTemplate.InitialDurationInt == 0 + ? TimeSpan.MaxValue + : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], + HoverExtendDuration = + this.notificationTemplate.HoverExtendDurationInt == 0 + ? TimeSpan.Zero + : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], Progress = this.notificationTemplate.ProgressMode switch { 0 => 1f, @@ -220,7 +237,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDisplayDuration); + n.InitialDuration = NotificationConstants.DefaultDisplayDuration; }); break; } @@ -324,7 +342,7 @@ internal class ImGuiWidget : IDataWindowWidget nameof(NotificationType.Info), }; - public static readonly string[] DurationTitles = + public static readonly string[] InitialDurationTitles = { "Infinite", "1 seconds", @@ -332,9 +350,17 @@ internal class ImGuiWidget : IDataWindowWidget "10 seconds", }; + public static readonly string[] HoverExtendDurationTitles = + { + "Disable", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + public static readonly TimeSpan[] Durations = { - TimeSpan.MaxValue, + TimeSpan.Zero, TimeSpan.FromSeconds(1), NotificationConstants.DefaultDisplayDuration, TimeSpan.FromSeconds(10), @@ -344,14 +370,17 @@ internal class ImGuiWidget : IDataWindowWidget public string Content; public bool ManualTitle; public string Title; + public bool ManualMinimizedText; + public string MinimizedText; public int IconSourceInt; public string IconSourceText; public int IconSourceAssetInt; public bool ManualType; public int TypeInt; - public int DurationInt; + public int InitialDurationInt; + public int HoverExtendDurationInt; public bool ShowIndeterminateIfNoExpiry; - public bool Interactable; + public bool Minimized; public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -362,14 +391,17 @@ internal class ImGuiWidget : IDataWindowWidget this.Content = string.Empty; this.ManualTitle = false; this.Title = string.Empty; + this.ManualMinimizedText = false; + this.MinimizedText = string.Empty; this.IconSourceInt = 0; this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; this.IconSourceAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; - this.DurationInt = 2; + this.InitialDurationInt = 2; + this.HoverExtendDurationInt = 2; this.ShowIndeterminateIfNoExpiry = true; - this.Interactable = true; + this.Minimized = true; this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1237c9c1f..417d77e7d 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -579,7 +579,7 @@ public sealed class UiBuilder : IDisposable Content = content, Title = title, Type = type, - Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), + InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, true, this.localPlugin); From e44180d4a2b029efe731be248eaeba1991826b47 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 20:04:33 +0900 Subject: [PATCH 23/41] honor notification window focus --- .../ImGuiNotification/IActiveNotification.cs | 5 +- .../ImGuiNotification/INotification.cs | 9 ++- .../Internal/ActiveNotification.cs | 73 +++++++++++++------ .../ImGuiNotification/Notification.cs | 4 +- .../NotificationConstants.cs | 6 ++ .../Windows/Data/Widgets/ImGuiWidget.cs | 11 ++- 6 files changed, 74 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 504c6d6d5..dd4101c92 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -49,7 +49,10 @@ public interface IActiveNotification : INotification DateTime EffectiveExpiry { get; } /// Gets a value indicating whether the mouse cursor is on the notification window. - bool IsMouseHovered { get; } + bool IsHovered { get; } + + /// Gets a value indicating whether the notification window is focused. + bool IsFocused { get; } /// Gets a value indicating whether the notification has been dismissed. /// This includes when the hide animation is being played. diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 8f5a30e79..349d66f72 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -33,7 +33,7 @@ public interface INotification : IDisposable /// Gets or sets the hard expiry. /// - /// Setting this value will override and , in that + /// 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 @@ -48,13 +48,14 @@ public interface INotification : IDisposable /// Updating this value will reset the dismiss timer. TimeSpan InitialDuration { get; set; } - /// Gets or sets the new duration for this notification once the mouse cursor leaves the window. + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the + /// window is no longer focused. /// /// If set to or less, then this feature is turned off, and hovering the mouse on the - /// notification will not make the notification stay.
    + /// notification or focusing on it will not make the notification stay.
    /// Updating this value will reset the dismiss timer. ///
    - TimeSpan HoverExtendDuration { get; set; } + TimeSpan DurationSinceLastInterest { get; set; } /// Gets or sets a value indicating whether to show an indeterminate expiration animation if /// is set to . diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a89ebeb0b..8591695a6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -93,7 +93,7 @@ internal sealed class ActiveNotification : IActiveNotification public DateTime CreatedAt { get; } = DateTime.Now; /// Gets the time of starting to count the timer for the expiration. - public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; + public DateTime LastInterestTime { get; private set; } = DateTime.Now; /// Gets the extended expiration time from . public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; @@ -172,7 +172,7 @@ internal sealed class ActiveNotification : IActiveNotification if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) return; this.underlyingNotification.HardExpiry = value; - this.HoverRelativeToTime = DateTime.Now; + this.LastInterestTime = DateTime.Now; } } @@ -185,20 +185,20 @@ internal sealed class ActiveNotification : IActiveNotification if (this.IsDismissed) return; this.underlyingNotification.InitialDuration = value; - this.HoverRelativeToTime = DateTime.Now; + this.LastInterestTime = DateTime.Now; } } /// - public TimeSpan HoverExtendDuration + public TimeSpan DurationSinceLastInterest { - get => this.underlyingNotification.HoverExtendDuration; + get => this.underlyingNotification.DurationSinceLastInterest; set { if (this.IsDismissed) return; - this.underlyingNotification.HoverExtendDuration = value; - this.HoverRelativeToTime = DateTime.Now; + this.underlyingNotification.DurationSinceLastInterest = value; + this.LastInterestTime = DateTime.Now; } } @@ -214,8 +214,8 @@ internal sealed class ActiveNotification : IActiveNotification : this.CreatedAt + initialDuration; DateTime expiry; - var hoverExtendDuration = this.HoverExtendDuration; - if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + var hoverExtendDuration = this.DurationSinceLastInterest; + if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) { expiry = DateTime.MaxValue; } @@ -224,7 +224,7 @@ internal sealed class ActiveNotification : IActiveNotification var expiryExtend = hoverExtendDuration == TimeSpan.MaxValue ? DateTime.MaxValue - : this.HoverRelativeToTime + hoverExtendDuration; + : this.LastInterestTime + hoverExtendDuration; expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; if (expiry < this.ExtendedExpiry) @@ -287,7 +287,10 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public bool IsMouseHovered { get; private set; } + public bool IsHovered { get; private set; } + + /// + public bool IsFocused { get; private set; } /// public bool IsDismissed => this.hideEasing.IsRunning; @@ -529,8 +532,12 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.IsFocused = ImGui.IsWindowFocused(); + if (this.IsFocused) + this.LastInterestTime = DateTime.Now; this.DrawWindowBackgroundProgressBar(); + this.DrawFocusIndicator(); this.DrawTopBar(interfaceManager, width, actionWindowHeight); if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) { @@ -562,14 +569,14 @@ internal sealed class ActiveNotification : IActiveNotification && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) { - if (!this.IsMouseHovered) + if (!this.IsHovered) { - this.IsMouseHovered = true; + this.IsHovered = true; this.MouseEnter.InvokeSafely(this); } - if (this.HoverExtendDuration > TimeSpan.Zero) - this.HoverRelativeToTime = DateTime.Now; + if (this.DurationSinceLastInterest > TimeSpan.Zero) + this.LastInterestTime = DateTime.Now; if (hovered) { @@ -587,9 +594,9 @@ internal sealed class ActiveNotification : IActiveNotification } } } - else if (this.IsMouseHovered) + else if (this.IsHovered) { - this.IsMouseHovered = false; + this.IsHovered = false; this.MouseLeave.InvokeSafely(this); } @@ -625,7 +632,7 @@ internal sealed class ActiveNotification : IActiveNotification this.IsInitiatorUnloaded = true; this.UserDismissable = true; - this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; + this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; if (this.EffectiveExpiry > newMaxExpiry) @@ -700,6 +707,23 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PopClipRect(); } + private void DrawFocusIndicator() + { + if (!this.IsFocused) + return; + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) { var windowPos = ImGui.GetWindowPos(); @@ -744,7 +768,7 @@ internal sealed class ActiveNotification : IActiveNotification relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; } - if (this.IsMouseHovered) + if (this.IsHovered || this.IsFocused) ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); else ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); @@ -755,7 +779,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.TextUnformatted( - this.IsMouseHovered + this.IsHovered || this.IsFocused ? this.CreatedAt.FormatAbsoluteDateTime() : this.CreatedAt.FormatRelativeDateTime()); ImGui.PopStyleColor(); @@ -799,7 +823,8 @@ internal sealed class ActiveNotification : IActiveNotification private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) { ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - if (!this.IsMouseHovered) + var alphaPush = !this.IsHovered && !this.IsFocused; + if (alphaPush) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); ImGui.PushStyleColor(ImGuiCol.Button, 0); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); @@ -808,7 +833,7 @@ internal sealed class ActiveNotification : IActiveNotification var r = ImGui.Button(icon.ToIconString(), new(size)); ImGui.PopStyleColor(2); - if (!this.IsMouseHovered) + if (alphaPush) ImGui.PopStyleVar(); ImGui.PopStyleVar(); return r; @@ -912,7 +937,7 @@ internal sealed class ActiveNotification : IActiveNotification barL = midpoint - (length * v); barR = midpoint + (length * v); } - else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) { barL = 0f; barR = 1f; @@ -942,7 +967,7 @@ internal sealed class ActiveNotification : IActiveNotification else { barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); + (effectiveExpiry - this.LastInterestTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index dd1d87c42..33a3ad974 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -53,7 +53,7 @@ public sealed record Notification : INotification public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; @@ -85,7 +85,7 @@ public sealed record Notification : INotification this.IconSource = copyFrom.IconSource?.Clone(); this.HardExpiry = copyFrom.HardExpiry; this.InitialDuration = copyFrom.InitialDuration; - this.HoverExtendDuration = copyFrom.HoverExtendDuration; + this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest; this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; this.Minimized = copyFrom.Minimized; this.UserDismissable = copyFrom.UserDismissable; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 08ef8aebd..d02ff47f5 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -60,6 +60,9 @@ public static class NotificationConstants /// Duration of expando animation. internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Text color for the rectangular border when the notification is focused. + internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -126,6 +129,9 @@ public static class NotificationConstants /// Gets the height of the expiry progress bar. internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the thickness of the focus indicator rectangle. + internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the string format of the initiator name field, if the initiator is unloaded. internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 4d3807417..dcd193496 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget this.notificationTemplate.InitialDurationInt == 0 ? TimeSpan.MaxValue : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], - HoverExtendDuration = + DurationSinceLastInterest = this.notificationTemplate.HoverExtendDurationInt == 0 ? TimeSpan.Zero : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], @@ -246,10 +246,12 @@ internal class ImGuiWidget : IDataWindowWidget if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) { var nclick = 0; + var testString = "input"; + n.Click += _ => nclick++; n.DrawActions += an => { - if (ImGui.Button("Update in place")) + if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); an.Title = title; @@ -257,7 +259,10 @@ internal class ImGuiWidget : IDataWindowWidget an.Progress = progress; } - if (an.IsMouseHovered) + ImGui.SameLine(); + ImGui.InputText("##input", ref testString, 255); + + if (an.IsHovered) { ImGui.SameLine(); if (ImGui.Button("Dismiss")) From a7d53807961e1df5939a536011d7b4d7cd4b2ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 23:20:08 +0900 Subject: [PATCH 24/41] Cleanup --- .../ImGuiNotification/IActiveNotification.cs | 38 +- .../ImGuiNotification/INotification.cs | 25 +- .../ImGuiNotification/INotificationIcon.cs | 54 ++ .../INotificationIconSource.cs | 88 -- .../INotificationMaterializedIcon.cs | 16 - .../Internal/ActiveNotification.ImGui.cs | 494 +++++++++++ .../Internal/ActiveNotification.cs | 818 +++--------------- .../Internal/IconSource/FilePathIconSource.cs | 49 -- .../IconSource/FontAwesomeIconIconSource.cs | 46 - .../Internal/IconSource/GamePathIconSource.cs | 50 -- .../IconSource/SeIconCharIconSource.cs | 47 - .../IconSource/TextureWrapIconSource.cs | 62 -- .../IconSource/TextureWrapTaskIconSource.cs | 71 -- .../{ => Internal}/NotificationConstants.cs | 122 ++- .../FilePathNotificationIcon.cs | 34 + .../FontAwesomeIconNotificationIcon.cs | 31 + .../GamePathNotificationIcon.cs | 34 + .../SeIconCharNotificationIcon.cs | 33 + .../Internal/NotificationManager.cs | 39 +- .../ImGuiNotification/Notification.cs | 61 +- .../NotificationUtilities.cs | 156 ++-- .../Windows/Data/Widgets/ImGuiWidget.cs | 83 +- Dalamud/Interface/UiBuilder.cs | 1 - .../Plugin/Services/INotificationManager.cs | 16 +- 24 files changed, 1056 insertions(+), 1412 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIcon.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs rename Dalamud/Interface/ImGuiNotification/{ => Internal}/NotificationConstants.cs (51%) create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index dd4101c92..340c052cd 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,5 +1,7 @@ using System.Threading; +using Dalamud.Interface.Internal; + namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -20,20 +22,6 @@ public interface IActiveNotification : INotification ///
    event Action Click; - /// Invoked when the mouse enters the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseEnter; - - /// Invoked when the mouse leaves the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseLeave; - /// Invoked upon drawing the action bar of the notification. /// /// Note that this function may be called even after has been invoked. @@ -44,16 +32,13 @@ public interface IActiveNotification : INotification /// Gets the ID of this notification. long Id { get; } + /// Gets the time of creating this notification. + DateTime CreatedAt { get; } + /// Gets the effective expiry time. /// Contains if the notification does not expire. DateTime EffectiveExpiry { get; } - /// Gets a value indicating whether the mouse cursor is on the notification window. - bool IsHovered { get; } - - /// Gets a value indicating whether the notification window is focused. - bool IsFocused { get; } - /// Gets a value indicating whether the notification has been dismissed. /// This includes when the hide animation is being played. bool IsDismissed { get; } @@ -66,9 +51,16 @@ public interface IActiveNotification : INotification /// This does not override . void ExtendBy(TimeSpan extension); - /// Loads the icon again using the same . - /// If is true, then this function is a no-op. - void UpdateIcon(); + /// 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 . + /// + /// 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 true, then calling this function will simply dispose the passed + /// without actually updating the icon. + /// + void SetIconTexture(IDalamudTextureWrap? textureWrap); /// Generates a new value to use for . /// The new value. diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 349d66f72..e6861726f 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,9 +1,10 @@ using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. -public interface INotification : IDisposable +public interface INotification { /// Gets or sets the content body of the notification. string Content { get; set; } @@ -18,22 +19,13 @@ public interface INotification : IDisposable NotificationType Type { get; set; } /// Gets or sets the icon source. - /// - /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership - /// of the new value is transferred to this . Even if the assignment throws an - /// exception, the ownership is transferred, causing the value to be disposed. Assignment should not throw an - /// exception though, so wrapping the assignment in try...catch block is not required. - /// The assigned value will be disposed upon the call on this instance of - /// , unless the same value is assigned, in which case it will do nothing. - /// If this is an , then updating this property - /// will change the icon being displayed (calls ), unless - /// is true. - /// - INotificationIconSource? IconSource { get; set; } + /// Use to use a texture, after calling + /// . + INotificationIcon? Icon { get; set; } /// Gets or sets the hard expiry. /// - /// Setting this value will override and , in that + /// 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 @@ -45,7 +37,8 @@ public interface INotification : IDisposable /// Gets or sets the initial duration. /// Set to to make only take effect. - /// Updating this value will reset the dismiss timer. + /// Updating this value will reset the dismiss timer, but the remaining duration will still be calculated + /// based on . TimeSpan InitialDuration { get; set; } /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the @@ -55,7 +48,7 @@ public interface INotification : IDisposable /// notification or focusing on it will not make the notification stay.
    /// Updating this value will reset the dismiss timer. /// - TimeSpan DurationSinceLastInterest { get; set; } + TimeSpan ExtensionDurationSinceLastInterest { get; set; } /// Gets or sets a value indicating whether to show an indeterminate expiration animation if /// is set to . diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs new file mode 100644 index 000000000..94c746b4f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins implementing this interface are left to their own on managing the resources contained by the +/// instance of their implementation of . In other words, they should not expect to have +/// called if their implementation is an . Dalamud will not +/// call on any instance of . On plugin unloads, the +/// icon may be reverted back to the default, if the instance of is not provided by +/// Dalamud. +public interface INotificationIcon +{ + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath); + + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// true if anything has been drawn. + bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs deleted file mode 100644 index 1fee67098..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -using Dalamud.Game.Text; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; -using Dalamud.Interface.Internal; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Icon source for . -/// Plugins should NOT implement this interface. -public interface INotificationIconSource : ICloneable, IDisposable -{ - /// The internal interface. - internal interface IInternal : INotificationIconSource - { - /// Materializes the icon resource. - /// The materialized resource. - INotificationMaterializedIcon Materialize(); - } - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The texture wrap. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, the returned object must be disposed after use. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be displayed - /// instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) => - new TextureWrapIconSource(wrap, takeOwnership); - - /// Gets a new instance of that will source the icon from an - /// returning a resulting in an - /// . - /// The function that returns a task that results a texture wrap. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be - /// displayed instead.
    - /// Use if you will have a wrap available without waiting.
    - /// should not contain a reference to a resource; if it does, the resource will be - /// released when all instances of derived from the returned object are freed - /// by the garbage collector, which will result in non-deterministic resource releases.
    - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(Func?>? wrapTaskFunc) => - new TextureWrapTaskIconSource(wrapTaskFunc); - - /// Gets a new instance of that will source the icon from a texture - /// file shipped as a part of the game resources. - /// The path to a texture file in the game virtual file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath); - - /// Gets a new instance of that will source the icon from an image - /// file from the file system. - /// The path to an image file in the file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath); - - /// - new INotificationIconSource Clone(); - - /// - object ICloneable.Clone() => this.Clone(); -} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs deleted file mode 100644 index 0657a94a4..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Represents a materialized icon. -internal interface INotificationMaterializedIcon : IDisposable -{ - /// Draws the icon. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - /// The initiator plugin. - void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin); -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs new file mode 100644 index 000000000..99b924923 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -0,0 +1,494 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification +{ + /// Draws this notification. + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float width, float offsetY) + { + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); + ImGui.Begin( + $"##NotifyMainWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput; + var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var warrantsExtension = + this.ExtensionDurationSinceLastInterest > TimeSpan.Zero + && (isHovered || isTakingKeyboardInput); + + this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + + if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry) + this.DismissNow(NotificationDismissReason.Timeout); + + if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) + this.lastInterestTime = DateTime.Now; + + this.DrawWindowBackgroundProgressBar(); + this.DrawTopBar(width, actionWindowHeight, isHovered); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentArea(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentArea(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + if (isTakingKeyboardInput) + this.DrawKeyboardInputIndicator(); + this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); + + if (ImGui.IsWindowHovered()) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } + + var windowSize = ImGui.GetWindowSize(); + ImGui.End(); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(3); + ImGui.PopID(); + + return windowSize.Y; + } + + /// Calculates the effective expiry, taking ImGui window state into account. + /// Notification will not dismiss while this paramter is true. + /// The calculated effective expiry. + /// Expected to be called BETWEEN and . + private DateTime CalculateEffectiveExpiry(ref bool warrantsExtension) + { + DateTime expiry; + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + var extendDuration = this.ExtensionDurationSinceLastInterest; + if (warrantsExtension) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + extendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.lastInterestTime + extendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.extendedExpiry) + expiry = this.extendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + { + expiry = he; + warrantsExtension = false; + } + + return expiry; + } + + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + + private void DrawKeyboardInputIndicator() + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + + private void DrawTopBar(float width, float height, bool drawActionButtons) + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (Service.Get().IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (drawActionButtons) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size, bool drawActionButtons) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!drawActionButtons) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!drawActionButtons) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentArea(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + + this.DrawIcon( + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); + + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + + this.DrawContentBody(textColumnOffset, textColumnWidth); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + var iconColor = this.Type.ToColor(); + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap)) + return; + + if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true) + return; + + if (NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.Type.ToChar(), + Service.Get().IconFontAwesomeFontHandle, + iconColor)) + return; + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.initiatorPlugin)) + return; + + NotificationUtilities.DrawIconFromDalamudLogo(minCoord, maxCoord); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.Type.ToTitle()) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } + + private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) + { + float barL, barR; + if (this.IsDismissed) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + barL = midpoint - (length * v); + barR = midpoint + (length * v); + } + else if (warrantsExtension) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) + { + if (this.ShowIndeterminateIfNoExpiry) + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; + } + } + else + { + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.lastInterestTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + + barR = Math.Clamp(barR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * barL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * barR }, + ImGui.GetColorU32(this.Type.ToColor())); + ImGui.PopClipRect(); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 8591695a6..357752f6e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,24 +1,21 @@ using System.Numerics; using System.Runtime.Loader; +using System.Threading; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; -using ImGuiNET; - using Serilog; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. -internal sealed class ActiveNotification : IActiveNotification +internal sealed partial class ActiveNotification : IActiveNotification { private readonly Notification underlyingNotification; @@ -27,6 +24,21 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing progressEasing; private readonly Easing expandoEasing; + /// 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 IDalamudTextureWrap? iconTextureWrap; + + /// The plugin that initiated this notification. + private LocalPlugin? initiatorPlugin; + + /// Whether has been unloaded. + private bool isInitiatorUnloaded; + /// The progress before for the progress bar animation with . private float progressBefore; @@ -36,10 +48,10 @@ internal sealed class ActiveNotification : IActiveNotification /// Used for calculating correct dismissal progressbar animation (right edge). private float prevProgressR; - /// New progress value to be updated on next call to . + /// New progress value to be updated on next call to . private float? newProgress; - /// New minimized value to be updated on next call to . + /// New minimized value to be updated on next call to . private bool? newMinimized; /// Initializes a new instance of the class. @@ -47,28 +59,16 @@ internal sealed class ActiveNotification : IActiveNotification /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) { - this.underlyingNotification = underlyingNotification with - { - IconSource = underlyingNotification.IconSource?.Clone(), - }; - this.InitiatorPlugin = initiatorPlugin; + this.underlyingNotification = underlyingNotification with { }; + this.initiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); + this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now; this.showEasing.Start(); this.progressEasing.Start(); - try - { - this.UpdateIcon(); - } - catch (Exception e) - { - // Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the - // error accordingly. - Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored."); - } } /// @@ -80,23 +80,11 @@ internal sealed class ActiveNotification : IActiveNotification /// public event Action? DrawActions; - /// - public event Action? MouseEnter; - - /// - public event Action? MouseLeave; - /// public long Id { get; } = IActiveNotification.CreateNewId(); - /// Gets the time of creating this notification. - public DateTime CreatedAt { get; } = DateTime.Now; - - /// Gets the time of starting to count the timer for the expiration. - public DateTime LastInterestTime { get; private set; } = DateTime.Now; - - /// Gets the extended expiration time from . - public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + /// + public DateTime CreatedAt { get; } /// public string Content @@ -147,19 +135,14 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource + public INotificationIcon? Icon { - get => this.underlyingNotification.IconSource; + get => this.underlyingNotification.Icon; set { if (this.IsDismissed) - { - value?.Dispose(); return; - } - - this.underlyingNotification.IconSource = value; - this.UpdateIcon(); + this.underlyingNotification.Icon = value; } } @@ -172,7 +155,7 @@ internal sealed class ActiveNotification : IActiveNotification if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) return; this.underlyingNotification.HardExpiry = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } @@ -185,58 +168,25 @@ internal sealed class ActiveNotification : IActiveNotification if (this.IsDismissed) return; this.underlyingNotification.InitialDuration = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } /// - public TimeSpan DurationSinceLastInterest + public TimeSpan ExtensionDurationSinceLastInterest { - get => this.underlyingNotification.DurationSinceLastInterest; + get => this.underlyingNotification.ExtensionDurationSinceLastInterest; set { if (this.IsDismissed) return; - this.underlyingNotification.DurationSinceLastInterest = value; - this.LastInterestTime = DateTime.Now; + this.underlyingNotification.ExtensionDurationSinceLastInterest = value; + this.lastInterestTime = DateTime.Now; } } /// - public DateTime EffectiveExpiry - { - get - { - var initialDuration = this.InitialDuration; - var expiryInitial = - initialDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.CreatedAt + initialDuration; - - DateTime expiry; - var hoverExtendDuration = this.DurationSinceLastInterest; - if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - expiry = DateTime.MaxValue; - } - else - { - var expiryExtend = - hoverExtendDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.LastInterestTime + hoverExtendDuration; - - expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; - if (expiry < this.ExtendedExpiry) - expiry = this.ExtendedExpiry; - } - - var he = this.HardExpiry; - if (he < expiry) - expiry = he; - return expiry; - } - } + public DateTime EffectiveExpiry { get; private set; } /// public bool ShowIndeterminateIfNoExpiry @@ -286,24 +236,9 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool IsHovered { get; private set; } - - /// - public bool IsFocused { get; private set; } - /// public bool IsDismissed => this.hideEasing.IsRunning; - /// Gets a value indicating whether has been unloaded. - public bool IsInitiatorUnloaded { get; private set; } - - /// Gets or sets the plugin that initiated this notification. - public LocalPlugin? InitiatorPlugin { get; set; } - - /// Gets or sets the icon of this notification. - public INotificationMaterializedIcon? MaterializedIcon { get; set; } - /// Gets the eased progress. private float ProgressEased { @@ -318,61 +253,17 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// Gets the default color of the notification. - private Vector4 DefaultIconColor => this.Type switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => ImGuiColors.DalamudWhite, - }; - - /// Gets the default icon of the notification. - private char? DefaultIconChar => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), - _ => null, - }; - - /// Gets the default title of the notification. - private string? DefaultTitle => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => null, - }; - /// Gets the string for the initiator field. private string InitiatorString => - this.InitiatorPlugin is not { } initiatorPlugin + this.initiatorPlugin is not { } plugin ? NotificationConstants.DefaultInitiator - : this.IsInitiatorUnloaded - ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) - : initiatorPlugin.Name; + : this.isInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name) + : plugin.Name; /// Gets the effective text to display when minimized. private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); - /// - public void Dispose() - { - this.ClearMaterializedIcon(); - this.underlyingNotification.Dispose(); - this.Dismiss = null; - this.Click = null; - this.DrawActions = null; - this.InitiatorPlugin = null; - } - /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -392,13 +283,78 @@ internal sealed class ActiveNotification : IActiveNotification { Log.Error( e, - $"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); + $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); } } - /// Updates animations. - /// true if the notification is over. - public bool UpdateAnimations() + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.extendedExpiry < newExpiry) + this.extendedExpiry = newExpiry; + } + + /// + public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + if (this.IsDismissed) + { + textureWrap?.Dispose(); + return; + } + + // After replacing, if the old texture is not the old texture, then dispose the old texture. + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrap) is { } wrapToDispose && + wrapToDispose != textureWrap) + { + wrapToDispose.Dispose(); + } + } + + /// Removes non-Dalamud invocation targets from events. + internal void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + + if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType())) + this.Icon = null; + + this.isInitiatorUnloaded = true; + this.UserDismissable = true; + this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration; + + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDuration; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; + + return; + + bool IsOwnedByDalamud(Type t) => AssemblyLoadContext.GetLoadContext(t.Assembly) == dalamudContext; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && !IsOwnedByDalamud(target.GetType())) + @delegate = (T)Delegate.Remove(@delegate, il); + } + + return @delegate; + } + } + + /// Updates the state of this notification, and release the relevant resource if this notification is no + /// longer in use. + /// true if the notification is over and relevant resources are released. + /// Intended to be called from the main thread only. + internal bool UpdateOrDisposeInternal() { this.showEasing.Update(); this.hideEasing.Update(); @@ -435,555 +391,21 @@ internal sealed class ActiveNotification : IActiveNotification this.newMinimized = null; } - return this.hideEasing.IsRunning && this.hideEasing.IsDone; + if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone) + return false; + + this.DisposeInternal(); + return true; } - /// Draws this notification. - /// The maximum width of the notification window. - /// The offset from the bottom. - /// The height of the notification. - public float Draw(float maxWidth, float offsetY) + /// Clears the resources associated with this instance of . + internal void DisposeInternal() { - var effectiveExpiry = this.EffectiveExpiry; - if (!this.IsDismissed && DateTime.Now > effectiveExpiry) - this.DismissNow(NotificationDismissReason.Timeout); - - var opacity = - Math.Clamp( - (float)(this.hideEasing.IsRunning - ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) - : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), - 0f, - 1f); - if (opacity <= 0) - return 0; - - var interfaceManager = Service.Get(); - var unboundedWidth = ImGui.CalcTextSize(this.Content).X; - float closeButtonHorizontalSpaceReservation; - using (interfaceManager.IconFontHandle?.Push()) - { - closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X; - closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding; - } - - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.InitiatorString).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation); - - unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; - unboundedWidth += NotificationConstants.ScaledIconSize; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - - var width = Math.Min(maxWidth, unboundedWidth); - - var viewport = ImGuiHelpers.MainViewport; - var viewportPos = viewport.WorkPos; - var viewportSize = viewport.WorkSize; - - ImGui.PushID(this.Id.GetHashCode()); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); - unsafe - { - ImGui.PushStyleColor( - ImGuiCol.WindowBg, - *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( - 1f, - 1f, - 1f, - NotificationConstants.BackgroundOpacity)); - } - - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints( - new(width, actionWindowHeight), - new( - width, - !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning - ? float.MaxValue - : actionWindowHeight)); - ImGui.Begin( - $"##NotifyMainWindow{this.Id}", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - this.IsFocused = ImGui.IsWindowFocused(); - if (this.IsFocused) - this.LastInterestTime = DateTime.Now; - - this.DrawWindowBackgroundProgressBar(); - this.DrawFocusIndicator(); - this.DrawTopBar(interfaceManager, width, actionWindowHeight); - if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) - { - this.DrawContentArea(width, actionWindowHeight); - } - else if (this.expandoEasing.IsRunning) - { - if (this.underlyingNotification.Minimized) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); - else - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); - this.DrawContentArea(width, actionWindowHeight); - ImGui.PopStyleVar(); - } - - this.DrawExpiryBar(effectiveExpiry); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var hovered = ImGui.IsWindowHovered(); - ImGui.End(); - - ImGui.PopStyleColor(); - ImGui.PopStyleVar(3); - ImGui.PopID(); - - if (windowPos.X <= ImGui.GetIO().MousePos.X - && windowPos.Y <= ImGui.GetIO().MousePos.Y - && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X - && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) - { - if (!this.IsHovered) - { - this.IsHovered = true; - this.MouseEnter.InvokeSafely(this); - } - - if (this.DurationSinceLastInterest > TimeSpan.Zero) - this.LastInterestTime = DateTime.Now; - - if (hovered) - { - if (this.Click is null) - { - if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); - } - else - { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) - || ImGui.IsMouseClicked(ImGuiMouseButton.Right) - || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); - } - } - } - else if (this.IsHovered) - { - this.IsHovered = false; - this.MouseLeave.InvokeSafely(this); - } - - return windowSize.Y; - } - - /// - public void ExtendBy(TimeSpan extension) - { - var newExpiry = DateTime.Now + extension; - if (this.ExtendedExpiry < newExpiry) - this.ExtendedExpiry = newExpiry; - } - - /// - public void UpdateIcon() - { - if (this.IsDismissed) - return; - this.ClearMaterializedIcon(); - this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); - } - - /// Removes non-Dalamud invocation targets from events. - public void RemoveNonDalamudInvocations() - { - var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); - this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); - this.Click = RemoveNonDalamudInvocationsCore(this.Click); - this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); - this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); - this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - - this.IsInitiatorUnloaded = true; - this.UserDismissable = true; - this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration; - - var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.EffectiveExpiry > newMaxExpiry) - this.HardExpiry = newMaxExpiry; - - return; - - T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate - { - if (@delegate is null) - return null; - - foreach (var il in @delegate.GetInvocationList()) - { - if (il.Target is { } target && - AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext) - { - @delegate = (T)Delegate.Remove(@delegate, il); - } - } - - return @delegate; - } - } - - private void ClearMaterializedIcon() - { - this.MaterializedIcon?.Dispose(); - this.MaterializedIcon = null; - } - - private void DrawWindowBackgroundProgressBar() - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.ProgressWaveLoopDuration) / - NotificationConstants.ProgressWaveLoopDuration); - elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; - - var colorElapsed = - elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / - NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; - - elapsed = Math.Clamp(elapsed, 0f, 1f); - colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); - colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); - - var progress = Math.Clamp(this.ProgressEased, 0f, 1f); - if (progress >= 1f) - elapsed = colorElapsed = 0f; - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var rb = windowPos + windowSize; - var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; - var rp = windowPos + windowSize with { X = windowSize.X * progress }; - - ImGui.PushClipRect(windowPos, rb, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos, - midp, - ImGui.GetColorU32( - Vector4.Lerp( - NotificationConstants.BackgroundProgressColorMin, - NotificationConstants.BackgroundProgressColorMax, - colorElapsed))); - ImGui.GetWindowDrawList().AddRectFilled( - midp with { Y = 0 }, - rp, - ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); - ImGui.PopClipRect(); - } - - private void DrawFocusIndicator() - { - if (!this.IsFocused) - return; - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRect( - windowPos, - windowPos + windowSize, - ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), - 0f, - ImDrawFlags.None, - NotificationConstants.FocusIndicatorThickness); - ImGui.PopClipRect(); - } - - private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) - { - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - - var rtOffset = new Vector2(width, 0); - using (interfaceManager.IconFontHandle?.Push()) - { - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - if (this.UserDismissable) - { - if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height)) - this.DismissNow(NotificationDismissReason.Manual); - rtOffset.X -= height; - } - - if (this.underlyingNotification.Minimized) - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height)) - this.Minimized = false; - } - else - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height)) - this.Minimized = true; - } - - rtOffset.X -= height; - ImGui.PopClipRect(); - } - - float relativeOpacity; - if (this.expandoEasing.IsRunning) - { - relativeOpacity = - this.underlyingNotification.Minimized - ? 1f - (float)this.expandoEasing.Value - : (float)this.expandoEasing.Value; - } - else - { - relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; - } - - if (this.IsHovered || this.IsFocused) - ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); - else - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - - if (relativeOpacity > 0) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsHovered || this.IsFocused - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - ImGui.PopStyleVar(); - } - - if (relativeOpacity < 1) - { - rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); - - var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); - this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); - - ltOffset.X = height; - - var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); - var agoSize = ImGui.CalcTextSize(agoText); - rtOffset.X -= agoSize.X; - ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted(agoText); - ImGui.PopStyleColor(); - - rtOffset.X -= NotificationConstants.ScaledWindowPadding; - - ImGui.PushClipRect( - windowPos + ltOffset with { Y = 0 }, - windowPos + rtOffset with { Y = height }, - true); - ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.TextUnformatted(this.EffectiveMinimizedText); - ImGui.PopClipRect(); - - ImGui.PopStyleVar(); - } - - ImGui.PopClipRect(); - } - - private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) - { - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - var alphaPush = !this.IsHovered && !this.IsFocused; - if (alphaPush) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0)); - var r = ImGui.Button(icon.ToIconString(), new(size)); - - ImGui.PopStyleColor(2); - if (alphaPush) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - return r; - } - - private void DrawContentArea(float width, float actionWindowHeight) - { - var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; - var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; - var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); - - this.DrawIcon( - new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), - new(NotificationConstants.ScaledIconSize)); - - textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); - textColumnOffset.Y += NotificationConstants.ScaledComponentGap; - - this.DrawContentBody(textColumnOffset, textColumnWidth); - } - - private void DrawIcon(Vector2 minCoord, Vector2 size) - { - var maxCoord = minCoord + size; - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconChar = this.DefaultIconChar; - if (defaultIconChar is not null) - { - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - defaultIconChar.Value, - minCoord, - maxCoord, - this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private float DrawTitle(Vector2 minCoord, float width) - { - ImGui.PushTextWrapPos(minCoord.X + width); - - ImGui.SetCursorPos(minCoord); - if ((this.Title ?? this.DefaultTitle) is { } title) - { - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.TextUnformatted(title); - ImGui.PopStyleColor(); - } - - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); - ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorString); - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - return ImGui.GetCursorPosY() - minCoord.Y; - } - - private void DrawContentBody(Vector2 minCoord, float width) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(minCoord.X + width); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawExpiryBar(DateTime effectiveExpiry) - { - float barL, barR; - if (this.IsDismissed) - { - var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; - var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; - var length = (this.prevProgressR - this.prevProgressL) / 2f; - barL = midpoint - (length * v); - barR = midpoint + (length * v); - } - else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else if (effectiveExpiry == DateTime.MaxValue) - { - if (this.ShowIndeterminateIfNoExpiry) - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.IndeterminateProgressbarLoopDuration) / - NotificationConstants.IndeterminateProgressbarLoopDuration); - barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - barR = Math.Min(elapsed, 2f / 3) / (2f / 3); - barL = MathF.Pow(barL, 3); - barR = 1f - MathF.Pow(1f - barR, 3); - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else - { - this.prevProgressL = barL = 0f; - this.prevProgressR = barR = 1f; - } - } - else - { - barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.LastInterestTime).TotalMilliseconds); - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - - barR = Math.Clamp(barR, 0f, 1f); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos + new Vector2( - windowSize.X * barL, - windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * barR }, - ImGui.GetColorU32(this.DefaultIconColor)); - ImGui.PopClipRect(); + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose) + wrapToDispose.Dispose(); + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.initiatorPlugin = null; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs deleted file mode 100644 index a741931a5..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.IO; -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a texture from a file as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class FilePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - public FilePathIconSource(string filePath) => this.FilePath = filePath; - - /// Gets the path to a .tex file inside the game resources. - public string FilePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.FilePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly FileInfo fileInfo; - - public MaterializedIcon(string filePath) => this.fileInfo = new(filePath); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromFile(this.fileInfo), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs deleted file mode 100644 index cfe790851..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar; - - /// Gets the icon character. - public FontAwesomeIcon IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs deleted file mode 100644 index 974e60ee7..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a game-shipped texture as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class GamePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - /// Use to get the game path from icon IDs. - public GamePathIconSource(string gamePath) => this.GamePath = gamePath; - - /// Gets the path to a .tex file inside the game resources. - public string GamePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.GamePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly string gamePath; - - public MaterializedIcon(string gamePath) => this.gamePath = gamePath; - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromGame(this.gamePath), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs deleted file mode 100644 index 19fe8e948..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.Text; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class SeIconCharIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public SeIconCharIconSource(SeIconChar c) => this.IconChar = c; - - /// Gets the icon character. - public SeIconChar IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconAxisFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs deleted file mode 100644 index a10b09bce..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Numerics; -using System.Threading; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapIconSource : INotificationIconSource.IInternal -{ - private IDalamudTextureWrap? wrap; - - /// Initializes a new instance of the class. - /// The texture wrap to handle over the ownership. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, this class must be disposed after use. - public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) => - this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource(); - - /// Gets the underlying texture wrap. - public IDalamudTextureWrap? Wrap => this.wrap; - - /// - public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false); - - /// - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private IDalamudTextureWrap? wrap; - - public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap; - - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.wrap, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs deleted file mode 100644 index 4039b6955..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Numerics; -using System.Threading.Tasks; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Utility; - -using Serilog; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal -{ - /// Gets the default materialized icon, for the purpose of displaying the plugin icon. - internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); - - /// Initializes a new instance of the class. - /// The function. - public TextureWrapTaskIconSource(Func?>? taskFunc) => - this.TextureWrapTaskFunc = taskFunc; - - /// Gets the function that returns a task resulting in a new instance of . - /// - /// Dalamud will take ownership of the result. Do not call . - public Func?>? TextureWrapTaskFunc { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.TextureWrapTaskFunc); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private Task? task; - - public MaterializedIcon(Func?>? taskFunc) - { - try - { - this.task = taskFunc?.Invoke(); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture."); - this.task = null; - } - } - - public void Dispose() - { - this.task?.ToContentDisposedTask(true); - this.task = null; - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.task?.IsCompletedSuccessfully is true ? this.task.Result : null, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs similarity index 51% rename from Dalamud/Interface/ImGuiNotification/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index d02ff47f5..f88eac53a 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,12 +1,14 @@ using System.Diagnostics; using System.Numerics; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.ImGuiNotification; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// Constants for drawing notification windows. -public static class NotificationConstants +internal static class NotificationConstants { // .............................[..] // ..when.......................[XX] @@ -20,69 +22,74 @@ public static class NotificationConstants // .. action buttons .. // ................................. - /// Default duration of the notification. - public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); - - /// Default duration of the notification, after the mouse cursor leaves the notification window. - public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - internal const string DefaultInitiator = "Dalamud"; + public const string DefaultInitiator = "Dalamud"; + + /// The string to measure size of, to decide the width of notification windows. + public const string NotificationWidthMeasurementString = + "The width of this text will decide the width\n" + + "of the notification window."; + + /// The ratio of maximum notification window width w.r.t. main viewport width. + public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3; /// The size of the icon. - internal const float IconSize = 32; + public const float IconSize = 32; /// The background opacity of a notification window. - internal const float BackgroundOpacity = 0.82f; + public const float BackgroundOpacity = 0.82f; /// The duration of indeterminate progress bar loop in milliseconds. - internal const float IndeterminateProgressbarLoopDuration = 2000f; + public const float IndeterminateProgressbarLoopDuration = 2000f; /// The duration of the progress wave animation in milliseconds. - internal const float ProgressWaveLoopDuration = 2000f; + public const float ProgressWaveLoopDuration = 2000f; /// The time ratio of a progress wave loop where the animation is idle. - internal const float ProgressWaveIdleTimeRatio = 0.5f; + public const float ProgressWaveIdleTimeRatio = 0.5f; /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. /// - internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); /// Duration of show animation. - internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. - internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of progress change animation. - internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Duration of expando animation. - internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); /// Text color for the rectangular border when the notification is focused. - internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + public static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); /// Text color for the when. - internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the close button [X]. - internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the title. - internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); /// Text color for the name of the initiator. - internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the body. - internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = @@ -110,35 +117,35 @@ public static class NotificationConstants }; /// Gets the scaled padding of the window (dot(.) in the above diagram). - internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); /// Gets the distance from the right bottom border of the viewport /// to the right bottom border of a notification window. /// - internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between two notification windows. - internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between components. - internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the icon. - internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the thickness of the focus indicator rectangle. - internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. - internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTime(this DateTime when) + public static string FormatRelativeDateTime(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStrings) @@ -156,12 +163,12 @@ public static class NotificationConstants /// Formats an instance of as an absolute time. /// When. /// The formatted string. - internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTimeShort(this DateTime when) + public static string FormatRelativeDateTimeShort(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStringsShort) @@ -174,4 +181,43 @@ public static class NotificationConstants Debug.Assert(false, "must not reach here"); return "???"; } + + /// Gets the color corresponding to the notification type. + /// The notification type. + /// The corresponding color. + public static Vector4 ToColor(this NotificationType type) => type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// Gets the char value corresponding to the notification type. + /// The notification type. + /// The corresponding char, or null. + public static char ToChar(this NotificationType type) => type switch + { + NotificationType.None => '\0', + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), + _ => '\0', + }; + + /// Gets the localized title string corresponding to the notification type. + /// The notification type. + /// The corresponding title. + public static string? ToTitle(this NotificationType type) => type switch + { + NotificationType.None => null, + NotificationType.Success => NotificationType.Success.ToString(), + NotificationType.Warning => NotificationType.Warning.ToString(), + NotificationType.Error => NotificationType.Error.ToString(), + NotificationType.Info => NotificationType.Info.ToString(), + _ => null, + }; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs new file mode 100644 index 000000000..3aa712160 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class FilePathNotificationIcon : INotificationIcon +{ + private readonly FileInfo fileInfo; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath); + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromFile(this.fileInfo)); + + /// + public override bool Equals(object? obj) => + obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName); + + /// + public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs new file mode 100644 index 000000000..0acfdee4c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs @@ -0,0 +1,31 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class FontAwesomeIconNotificationIcon : INotificationIcon +{ + private readonly char iconChar; + + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.iconChar, + Service.Get().IconFontAwesomeFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs new file mode 100644 index 000000000..c1db8820c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class GamePathNotificationIcon : INotificationIcon +{ + private readonly string gamePath; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromGame(this.gamePath)); + + /// + public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath); + + /// + public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs new file mode 100644 index 000000000..3bbd8dd81 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs @@ -0,0 +1,33 @@ +using System.Numerics; + +using Dalamud.Game.Text; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class SeIconCharNotificationIcon : INotificationIcon +{ + private readonly SeIconChar iconChar; + + /// Initializes a new instance of the class. + /// The character. + public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + (char)this.iconChar, + Service.Get().IconAxisFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index b457539a3..5ee9fed3e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -11,6 +11,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using ImGuiNET; + namespace Dalamud.Interface.ImGuiNotification.Internal; /// Class handling notifications/toasts in ImGui. @@ -41,6 +43,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Gets the handle to FontAwesome fonts, sized for use as an icon. public IFontHandle IconFontAwesomeFontHandle { get; } + /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } /// @@ -48,17 +51,16 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos { this.PrivateAtlas.Dispose(); foreach (var n in this.pendingNotifications) - n.Dispose(); + n.DisposeInternal(); foreach (var n in this.notifications) - n.Dispose(); + n.DisposeInternal(); this.pendingNotifications.Clear(); this.notifications.Clear(); } /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; @@ -66,13 +68,10 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Adds a notification originating from a plugin. /// The notification. - /// Dispose when this function returns. /// The source plugin. /// The added notification. - /// will be honored even on exceptions. - public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin) + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; @@ -92,8 +91,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Content = content, Title = title, Type = type, - }, - true); + }); /// Draw all currently queued notifications. public void Draw() @@ -104,19 +102,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); - var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); - this.notifications.RemoveAll( - static x => - { - if (!x.UpdateAnimations()) - return false; - - x.Dispose(); - return true; - }); + this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); foreach (var tn in this.notifications) - height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; + height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; } } @@ -140,9 +133,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT this.localPlugin = localPlugin; /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); return an; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 33a3ad974..612533cb8 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,4 @@ -using System.Threading; - +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -7,20 +6,10 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { - private INotificationIconSource? iconSource; - - /// Initializes a new instance of the class. - public Notification() - { - } - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(INotification notification) => this.CopyValuesFrom(notification); - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// + /// Gets the default value for and . + /// + public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration; /// public string Content { get; set; } = string.Empty; @@ -35,25 +24,16 @@ public sealed record Notification : INotification public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource - { - get => this.iconSource; - set - { - var prevSource = Interlocked.Exchange(ref this.iconSource, value); - if (prevSource != value) - prevSource?.Dispose(); - } - } + public INotificationIcon? Icon { get; set; } /// public DateTime HardExpiry { get; set; } = DateTime.MaxValue; /// - public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + public TimeSpan InitialDuration { get; set; } = DefaultDuration; /// - public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; @@ -66,29 +46,4 @@ public sealed record Notification : INotification /// public float Progress { get; set; } = 1f; - - /// - public void Dispose() - { - // Assign to the property; it will take care of disposing - this.IconSource = null; - } - - /// Copy values from the given instance of . - /// The instance of to copy from. - private void CopyValuesFrom(INotification copyFrom) - { - this.Content = copyFrom.Content; - this.Title = copyFrom.Title; - this.MinimizedText = copyFrom.MinimizedText; - this.Type = copyFrom.Type; - this.IconSource = copyFrom.IconSource?.Clone(); - this.HardExpiry = copyFrom.HardExpiry; - this.InitialDuration = copyFrom.InitialDuration; - this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest; - this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; - this.Minimized = copyFrom.Minimized; - this.UserDismissable = copyFrom.UserDismissable; - this.Progress = copyFrom.Progress; - } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 016e9b793..e82b95b75 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -17,44 +17,47 @@ namespace Dalamud.Interface.ImGuiNotification; /// Utilities for implementing stuff under . public static class NotificationUtilities { - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this SeIconChar iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => - INotificationIconSource.From(wrap, takeOwnership); + public static INotificationIcon ToIconSource(this FileInfo fileInfo) => + INotificationIcon.FromFile(fileInfo.FullName); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => - INotificationIconSource.FromFile(fileInfo.FullName); - - /// Draws an icon string. - /// The font handle to use. - /// The icon character. + /// Draws an icon from an and a . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. + /// The icon character. + /// The font handle to use. /// The foreground color. - internal static unsafe void DrawIconString( - IFontHandle fontHandleLarge, - char c, + /// true if anything has been drawn. + internal static unsafe bool DrawIconFrom( Vector2 minCoord, Vector2 maxCoord, + char c, + IFontHandle fontHandle, Vector4 color) { + if (c is '\0' or char.MaxValue) + return false; + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); - using (fontHandleLarge.Push()) + using (fontHandle.Push()) { var font = ImGui.GetFont(); - ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var glyphPtr = (ImGuiHelpers.ImFontGlyphReal*)font.FindGlyphNoFallback(c).NativePtr; + if (glyphPtr is null) + return false; + + ref readonly var glyph = ref *glyphPtr; var size = glyph.XY1 - glyph.XY0; var smallerSizeDim = Math.Min(size.X, size.Y); var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; @@ -69,67 +72,72 @@ public static class NotificationUtilities glyph.UV1, ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); } + + return true; } - /// Draws the given texture, or the icon of the plugin if texture is null. - /// The texture. + /// Draws an icon from an instance of . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. - /// The initiator plugin. - internal static void DrawTexture( - IDalamudTextureWrap? texture, - Vector2 minCoord, - Vector2 maxCoord, - LocalPlugin? initiatorPlugin) + /// The texture. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, IDalamudTextureWrap? texture) { - var handle = nint.Zero; - var size = Vector2.Zero; - if (texture is not null) + if (texture is null) + return false; + try { - try + var handle = texture.ImGuiHandle; + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(handle, size); + return true; + } + catch + { + return false; + } + } + + /// Draws an icon from an instance of . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The plugin. Dalamud icon will be drawn if null is given. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin) + { + var dam = Service.Get(); + if (plugin is null) + return false; + + if (!Service.Get().TryGetIcon( + plugin, + plugin.Manifest, + plugin.IsThirdParty, + out var texture) || texture is null) + { + texture = plugin switch { - handle = texture.ImGuiHandle; - size = texture.Size; - } - catch - { - // must have been disposed or something; ignore the texture - } + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; } - if (handle == nint.Zero) - { - var dam = Service.Get(); - if (initiatorPlugin is null) - { - texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); - } - else - { - if (!Service.Get().TryGetIcon( - initiatorPlugin, - initiatorPlugin.Manifest, - initiatorPlugin.IsThirdParty, - out texture) || texture is null) - { - texture = initiatorPlugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; - } - } + return DrawIconFrom(minCoord, maxCoord, texture); + } - handle = texture.ImGuiHandle; - size = texture.Size; - } - - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); - ImGui.Image(handle, size); + /// Draws the Dalamud logo as an icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord) + { + var dam = Service.Get(); + var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + DrawIconFrom(minCoord, maxCoord, texture); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index dcd193496..6c94a2273 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -116,7 +116,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.InitialDurationTitles.Length); ImGui.Combo( - "Hover Extend Duration", + "Extension Duration", ref this.notificationTemplate.HoverExtendDurationInt, NotificationTemplate.HoverExtendDurationTitles, NotificationTemplate.HoverExtendDurationTitles.Length); @@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget this.notificationTemplate.InitialDurationInt == 0 ? TimeSpan.MaxValue : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], - DurationSinceLastInterest = + ExtensionDurationSinceLastInterest = this.notificationTemplate.HoverExtendDurationInt == 0 ? TimeSpan.Zero : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], @@ -179,41 +179,40 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, - IconSource = this.notificationTemplate.IconSourceInt switch + Icon = this.notificationTemplate.IconSourceInt switch { - 1 => INotificationIconSource.From( + 1 => INotificationIcon.From( (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 2 => INotificationIconSource.From( + 2 => INotificationIcon.From( (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => INotificationIconSource.From( - Service.Get().GetDalamudTextureWrap( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt])), - false), - 4 => INotificationIconSource.From( - () => - Service.Get().GetDalamudTextureWrapAsync( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt]))), - 5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText), - 6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText), - 7 => INotificationIconSource.From( - Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), - false), - 8 => INotificationIconSource.From( - Service.Get().GetTextureFromFile( - new(this.notificationTemplate.IconSourceText)), - false), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText), _ => null, }, - }, - true); + }); + + var dam = Service.Get(); + var tm = Service.Get(); + switch (this.notificationTemplate.IconSourceInt) + { + case 5: + n.SetIconTexture( + dam.GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt]))); + break; + case 6: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText)); + break; + case 7: + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText))); + break; + } + switch (this.notificationTemplate.ProgressMode) { case 2: @@ -237,8 +236,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.ExtendBy(NotificationConstants.DefaultDisplayDuration); - n.InitialDuration = NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDuration); + n.InitialDuration = NotificationConstants.DefaultDuration; }); break; } @@ -251,6 +250,10 @@ internal class ImGuiWidget : IDataWindowWidget n.Click += _ => nclick++; n.DrawActions += an => { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"{nclick}"); + + ImGui.SameLine(); if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); @@ -260,18 +263,11 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.SameLine(); - ImGui.InputText("##input", ref testString, 255); - - if (an.IsHovered) - { - ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + ImGui.InputText("##input", ref testString, 255); }; } } @@ -315,10 +311,9 @@ internal class ImGuiWidget : IDataWindowWidget "None (use Type)", "SeIconChar", "FontAwesomeIcon", - "TextureWrap from DalamudAssets", - "TextureWrapTask from DalamudAssets", "GamePath", "FilePath", + "TextureWrap from DalamudAssets", "TextureWrap from GamePath", "TextureWrap from FilePath", }; @@ -367,7 +362,7 @@ internal class ImGuiWidget : IDataWindowWidget { TimeSpan.Zero, TimeSpan.FromSeconds(1), - NotificationConstants.DefaultDisplayDuration, + NotificationConstants.DefaultDuration, TimeSpan.FromSeconds(10), }; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 417d77e7d..3a90d52c1 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -581,7 +581,6 @@ public sealed class UiBuilder : IDisposable Type = type, InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, - true, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 441cc31f7..7d9ccd0b0 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -2,21 +2,11 @@ using Dalamud.Interface.ImGuiNotification; namespace Dalamud.Plugin.Services; -/// -/// Manager for notifications provided by Dalamud using ImGui. -/// +/// Manager for notifications provided by Dalamud using ImGui. public interface INotificationManager { - /// - /// Adds a notification. - /// + /// Adds a notification. /// The new notification. - /// - /// Dispose when this function returns, even if the function throws an exception. - /// Set to false to reuse for multiple calls to this function, in which case, - /// you should call on the value supplied to at a - /// later time. - /// /// The added notification. - IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); + IActiveNotification AddNotification(Notification notification); } From 92302ffd89596e8b940685f4d574d0e9afd21852 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 23:54:57 +0900 Subject: [PATCH 25/41] More cleanup --- .../EventArgs/INotificationClickArgs.cs | 9 ++ .../EventArgs/INotificationDismissArgs.cs | 12 ++ .../EventArgs/INotificationDrawArgs.cs | 19 +++ .../ImGuiNotification/IActiveNotification.cs | 33 +++--- .../ImGuiNotification/INotification.cs | 1 + .../Internal/ActiveNotification.EventArgs.cs | 87 ++++++++++++++ .../Internal/ActiveNotification.ImGui.cs | 19 +-- .../Internal/ActiveNotification.cs | 108 ++++-------------- .../GamePathNotificationIcon.cs | 2 +- .../Internal/NotificationManager.cs | 2 +- .../NotificationDismissedDelegate.cs | 8 -- .../Windows/Data/Widgets/ImGuiWidget.cs | 13 ++- Dalamud/Interface/UiBuilder.cs | 2 +- 13 files changed, 183 insertions(+), 132 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs new file mode 100644 index 000000000..b85a96004 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs @@ -0,0 +1,9 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationClickArgs +{ + /// Gets the notification being clicked. + IActiveNotification Notification { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs new file mode 100644 index 000000000..7f664efa1 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs @@ -0,0 +1,12 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDismissArgs +{ + /// Gets the notification being dismissed. + IActiveNotification Notification { get; } + + /// Gets the dismiss reason. + NotificationDismissReason Reason { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs new file mode 100644 index 000000000..221f769e0 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDrawArgs +{ + /// Gets the notification being drawn. + IActiveNotification Notification { get; } + + /// Gets the top left coordinates of the area being drawn. + Vector2 MinCoord { get; } + + /// Gets the bottom right coordinates of the area being drawn. + /// Note that can be , in which case there is no + /// vertical limits to the drawing region. + Vector2 MaxCoord { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 340c052cd..c3ea2b9de 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,49 +1,48 @@ using System.Threading; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.Internal; namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. +/// Not to be implemented by plugins. public interface IActiveNotification : INotification { /// The counter for field. private static long idCounter; /// Invoked upon dismissing the notification. - /// The event callback will not be called, - /// if a user interacts with the notification after the plugin is unloaded. - event NotificationDismissedDelegate Dismiss; + /// The event callback will not be called, if it gets dismissed after plugin unload. + event Action Dismiss; /// Invoked upon clicking on the notification. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action Click; + /// Note that this function may be called even after has been invoked. + event Action Click; /// Invoked upon drawing the action bar of the notification. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action DrawActions; + /// Note that this function may be called even after has been invoked. + event Action DrawActions; /// Gets the ID of this notification. + /// This value does not change. long Id { get; } /// Gets the time of creating this notification. + /// This value does not change. DateTime CreatedAt { get; } /// Gets the effective expiry time. /// Contains if the notification does not expire. + /// This value will change depending on property changes and user interactions. DateTime EffectiveExpiry { get; } - /// Gets a value indicating whether the notification has been dismissed. + /// Gets the reason how this notification got dismissed. null if not dismissed. /// This includes when the hide animation is being played. - bool IsDismissed { get; } + NotificationDismissReason? DismissReason { get; } /// Dismisses this notification. + /// If the notification has already been dismissed, this function does nothing. void DismissNow(); /// Extends this notifiation. @@ -57,8 +56,8 @@ public interface IActiveNotification : INotification /// /// 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 true, then calling this function will simply dispose the passed - /// without actually updating the icon. + /// If is not null, then calling this function will simply dispose the + /// passed without actually updating the icon. /// void SetIconTexture(IDalamudTextureWrap? textureWrap); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index e6861726f..2bc8e751c 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. +/// Not to be implemented by plugins. public interface INotification { /// Gets or sets the content body of the notification. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs new file mode 100644 index 000000000..428d9103f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs @@ -0,0 +1,87 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.EventArgs; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDismissArgs +{ + /// + public event Action? Dismiss; + + /// + IActiveNotification INotificationDismissArgs.Notification => this; + + /// + NotificationDismissReason INotificationDismissArgs.Reason => + this.DismissReason + ?? throw new InvalidOperationException("DismissReason must be set before using INotificationDismissArgs"); + + private void InvokeDismiss() + { + try + { + this.Dismiss?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Dismiss)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationClickArgs +{ + /// + public event Action? Click; + + /// + IActiveNotification INotificationClickArgs.Notification => this; + + private void InvokeClick() + { + try + { + this.Click?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Click)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDrawArgs +{ + private Vector2 drawActionArgMinCoord; + private Vector2 drawActionArgMaxCoord; + + /// + public event Action? DrawActions; + + /// + IActiveNotification INotificationDrawArgs.Notification => this; + + /// + Vector2 INotificationDrawArgs.MinCoord => this.drawActionArgMinCoord; + + /// + Vector2 INotificationDrawArgs.MaxCoord => this.drawActionArgMaxCoord; + + private void InvokeDrawActions(Vector2 minCoord, Vector2 maxCoord) + { + this.drawActionArgMinCoord = minCoord; + this.drawActionArgMaxCoord = maxCoord; + try + { + this.DrawActions?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.DrawActions)} error; event registration cancelled"); + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 99b924923..60e8e28e6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -2,7 +2,6 @@ using System.Numerics; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; -using Dalamud.Utility; using ImGuiNET; @@ -83,7 +82,7 @@ internal sealed partial class ActiveNotification this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); - if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry) + if (DateTime.Now > this.EffectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) @@ -121,7 +120,7 @@ internal sealed partial class ActiveNotification if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) || ImGui.IsMouseClicked(ImGuiMouseButton.Right) || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); + this.InvokeClick(); } } @@ -419,22 +418,16 @@ internal sealed partial class ActiveNotification ImGui.PopTextWrapPos(); if (this.DrawActions is not null) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } + this.InvokeDrawActions( + minCoord with { Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap }, + new(minCoord.X + width, float.MaxValue)); } } private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) { float barL, barR; - if (this.IsDismissed) + if (this.DismissReason is not null) { var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 357752f6e..475ae7e68 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,16 +1,15 @@ -using System.Numerics; using System.Runtime.Loader; using System.Threading; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.Colors; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; +using Serilog.Events; namespace Dalamud.Interface.ImGuiNotification.Internal; @@ -71,15 +70,6 @@ internal sealed partial class ActiveNotification : IActiveNotification this.progressEasing.Start(); } - /// - public event NotificationDismissedDelegate? Dismiss; - - /// - public event Action? Click; - - /// - public event Action? DrawActions; - /// public long Id { get; } = IActiveNotification.CreateNewId(); @@ -90,60 +80,35 @@ internal sealed partial class ActiveNotification : IActiveNotification public string Content { get => this.underlyingNotification.Content; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Content = value; - } + set => this.underlyingNotification.Content = value; } /// public string? Title { get => this.underlyingNotification.Title; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Title = value; - } + set => this.underlyingNotification.Title = value; } /// public string? MinimizedText { get => this.underlyingNotification.MinimizedText; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.MinimizedText = value; - } + set => this.underlyingNotification.MinimizedText = value; } /// public NotificationType Type { get => this.underlyingNotification.Type; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Type = value; - } + set => this.underlyingNotification.Type = value; } /// public INotificationIcon? Icon { get => this.underlyingNotification.Icon; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Icon = value; - } + set => this.underlyingNotification.Icon = value; } /// @@ -152,7 +117,7 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.HardExpiry; set { - if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) + if (this.underlyingNotification.HardExpiry == value) return; this.underlyingNotification.HardExpiry = value; this.lastInterestTime = DateTime.Now; @@ -165,8 +130,6 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.InitialDuration; set { - if (this.IsDismissed) - return; this.underlyingNotification.InitialDuration = value; this.lastInterestTime = DateTime.Now; } @@ -178,8 +141,6 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.ExtensionDurationSinceLastInterest; set { - if (this.IsDismissed) - return; this.underlyingNotification.ExtensionDurationSinceLastInterest = value; this.lastInterestTime = DateTime.Now; } @@ -188,57 +149,37 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public DateTime EffectiveExpiry { get; private set; } + /// + public NotificationDismissReason? DismissReason { get; private set; } + /// public bool ShowIndeterminateIfNoExpiry { get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; - } + set => this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; } /// public bool Minimized { get => this.newMinimized ?? this.underlyingNotification.Minimized; - set - { - if (this.IsDismissed) - return; - this.newMinimized = value; - } + set => this.newMinimized = value; } /// public bool UserDismissable { get => this.underlyingNotification.UserDismissable; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.UserDismissable = value; - } + set => this.underlyingNotification.UserDismissable = value; } /// public float Progress { get => this.newProgress ?? this.underlyingNotification.Progress; - set - { - if (this.IsDismissed) - return; - this.newProgress = value; - } + set => this.newProgress = value; } - /// - public bool IsDismissed => this.hideEasing.IsRunning; - /// Gets the eased progress. private float ProgressEased { @@ -271,20 +212,12 @@ internal sealed partial class ActiveNotification : IActiveNotification /// The reason of dismissal. public void DismissNow(NotificationDismissReason reason) { - if (this.hideEasing.IsRunning) + if (this.DismissReason is not null) return; + this.DismissReason = reason; this.hideEasing.Start(); - try - { - this.Dismiss?.Invoke(this, reason); - } - catch (Exception e) - { - Log.Error( - e, - $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); - } + this.InvokeDismiss(); } /// @@ -298,7 +231,7 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public void SetIconTexture(IDalamudTextureWrap? textureWrap) { - if (this.IsDismissed) + if (this.DismissReason is not null) { textureWrap?.Dispose(); return; @@ -408,4 +341,9 @@ internal sealed partial class ActiveNotification : IActiveNotification this.DrawActions = null; this.initiatorPlugin = null; } + + private void LogEventInvokeError(Exception exception, string message) => + Log.Error( + exception, + $"[{nameof(ActiveNotification)}:{this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}] {message}"); } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs index c1db8820c..e0699e1b6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -15,7 +15,7 @@ internal class GamePathNotificationIcon : INotificationIcon /// The path to a .tex file inside the game resources. /// Use to get the game path from icon IDs. public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; - + /// public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => NotificationUtilities.DrawIconFrom( diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 5ee9fed3e..973e93c72 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -137,7 +137,7 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT { var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); - an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); return an; } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs deleted file mode 100644 index 09d6fd818..000000000 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dalamud.Interface.ImGuiNotification; - -/// Delegate representing the dismissal of an active notification. -/// The notification being dismissed. -/// The reason of dismissal. -public delegate void NotificationDismissedDelegate( - IActiveNotification notification, - NotificationDismissReason dismissReason); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 6c94a2273..d51f18216 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -219,7 +219,7 @@ internal class ImGuiWidget : IDataWindowWidget Task.Run( async () => { - for (var i = 0; i <= 10 && !n.IsDismissed; i++) + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) { await Task.Delay(500); n.Progress = i / 10f; @@ -230,7 +230,7 @@ internal class ImGuiWidget : IDataWindowWidget Task.Run( async () => { - for (var i = 0; i <= 10 && !n.IsDismissed; i++) + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) { await Task.Delay(500); n.Progress = i / 10f; @@ -257,16 +257,17 @@ internal class ImGuiWidget : IDataWindowWidget if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); - an.Title = title; - an.Type = type; - an.Progress = progress; + an.Notification.Title = title; + an.Notification.Type = type; + an.Notification.Progress = progress; } ImGui.SameLine(); if (ImGui.Button("Dismiss")) - an.DismissNow(); + an.Notification.DismissNow(); ImGui.SameLine(); + ImGui.SetNextItemWidth(an.MaxCoord.X - ImGui.GetCursorPosX()); ImGui.InputText("##input", ref testString, 255); }; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3a90d52c1..2053d9354 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -583,7 +583,7 @@ public sealed class UiBuilder : IDisposable }, this.localPlugin); _ = this.notifications.TryAdd(an, 0); - an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); } /// From edb13c18e38e5edcd386184f7c73081be56f86a4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 00:00:10 +0900 Subject: [PATCH 26/41] more cleanup --- .../Internal/ActiveNotification.ImGui.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 60e8e28e6..ac10cc060 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -360,6 +360,10 @@ internal sealed partial class ActiveNotification textColumnOffset.Y += NotificationConstants.ScaledComponentGap; this.DrawContentBody(textColumnOffset, textColumnWidth); + textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; + + ImGui.SetCursorPos(textColumnOffset); + this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); } private void DrawIcon(Vector2 minCoord, Vector2 size) @@ -416,12 +420,6 @@ internal sealed partial class ActiveNotification ImGui.TextUnformatted(this.Content); ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - this.InvokeDrawActions( - minCoord with { Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap }, - new(minCoord.X + width, float.MaxValue)); - } } private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) From 18c1084fe3e0e2196fd8ae2b8bf98a80e92d88e5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 00:57:54 +0900 Subject: [PATCH 27/41] Make DateTime/TimeSpan localizable --- .../Internal/ActiveNotification.ImGui.cs | 7 +- .../Internal/NotificationConstants.cs | 66 --------- Dalamud/Localization.cs | 22 ++- Dalamud/Utility/DateTimeSpanExtensions.cs | 129 ++++++++++++++++++ 4 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 Dalamud/Utility/DateTimeSpanExtensions.cs diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index ac10cc060..5d496963d 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -288,8 +289,8 @@ internal sealed partial class ActiveNotification ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.TextUnformatted( ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); + ? this.CreatedAt.LocAbsolute() + : this.CreatedAt.LocRelativePastLong()); ImGui.PopStyleColor(); ImGui.PopStyleVar(); } @@ -304,7 +305,7 @@ internal sealed partial class ActiveNotification ltOffset.X = height; - var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoText = this.CreatedAt.LocRelativePastShort(); var agoSize = ImGui.CalcTextSize(agoText); rtOffset.X -= agoSize.X; ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index f88eac53a..50536baa3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Numerics; using Dalamud.Interface.Colors; @@ -91,31 +90,6 @@ internal static class NotificationConstants /// Color for the background progress bar (determinate progress only). public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); - /// Gets the relative time format strings. - private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = - { - (TimeSpan.FromDays(7), null), - (TimeSpan.FromDays(2), "{0:%d} days ago"), - (TimeSpan.FromDays(1), "yesterday"), - (TimeSpan.FromHours(2), "{0:%h} hours ago"), - (TimeSpan.FromHours(1), "an hour ago"), - (TimeSpan.FromMinutes(2), "{0:%m} minutes ago"), - (TimeSpan.FromMinutes(1), "a minute ago"), - (TimeSpan.FromSeconds(2), "{0:%s} seconds ago"), - (TimeSpan.FromSeconds(1), "a second ago"), - (TimeSpan.MinValue, "just now"), - }; - - /// Gets the relative time format strings. - private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort = - { - (TimeSpan.FromDays(1), "{0:%d}d"), - (TimeSpan.FromHours(1), "{0:%h}h"), - (TimeSpan.FromMinutes(1), "{0:%m}m"), - (TimeSpan.FromSeconds(1), "{0:%s}s"), - (TimeSpan.MinValue, "now"), - }; - /// Gets the scaled padding of the window (dot(.) in the above diagram). public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -142,46 +116,6 @@ internal static class NotificationConstants /// Gets the string format of the initiator name field, if the initiator is unloaded. public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; - /// Formats an instance of as a relative time. - /// When. - /// The formatted string. - public static string FormatRelativeDateTime(this DateTime when) - { - var ts = DateTime.Now - when; - foreach (var (minSpan, formatString) in RelativeFormatStrings) - { - if (ts < minSpan) - continue; - if (formatString is null) - break; - return string.Format(formatString, ts); - } - - return when.FormatAbsoluteDateTime(); - } - - /// Formats an instance of as an absolute time. - /// When. - /// The formatted string. - public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; - - /// Formats an instance of as a relative time. - /// When. - /// The formatted string. - public static string FormatRelativeDateTimeShort(this DateTime when) - { - var ts = DateTime.Now - when; - foreach (var (minSpan, formatString) in RelativeFormatStringsShort) - { - if (ts < minSpan) - continue; - return string.Format(formatString, ts); - } - - Debug.Assert(false, "must not reach here"); - return "???"; - } - /// Gets the color corresponding to the notification type. /// The notification type. /// The corresponding color. diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index b180f113a..39312ac52 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -36,6 +36,7 @@ public class Localization : IServiceType /// Use embedded loc resource files. public Localization(string locResourceDirectory, string locResourcePrefix = "", bool useEmbedded = false) { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.locResourceDirectory = locResourceDirectory; this.locResourcePrefix = locResourcePrefix; this.useEmbedded = useEmbedded; @@ -61,7 +62,24 @@ public class Localization : IServiceType /// /// Event that occurs when the language is changed. /// - public event LocalizationChangedDelegate LocalizationChanged; + public event LocalizationChangedDelegate? LocalizationChanged; + + /// + /// Gets an instance of that corresponds to the language configured from Dalamud Settings. + /// + public CultureInfo DalamudLanguageCultureInfo { get; private set; } + + /// + /// Gets an instance of that corresponds to . + /// + /// The language code which should be in . + /// The corresponding instance of . + public static CultureInfo GetCultureInfoFromLangCode(string langCode) => + CultureInfo.GetCultureInfo(langCode switch + { + "tw" => "zh-tw", + _ => langCode, + }); /// /// Search the set-up localization data for the provided assembly for the given string key and return it. @@ -108,6 +126,7 @@ public class Localization : IServiceType /// public void SetupWithFallbacks() { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.LocalizationChanged?.Invoke(FallbackLangCode); Loc.SetupWithFallbacks(this.assembly); } @@ -124,6 +143,7 @@ public class Localization : IServiceType return; } + this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode); this.LocalizationChanged?.Invoke(langCode); try diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs new file mode 100644 index 000000000..8f6a2a7ec --- /dev/null +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +using CheapLoc; + +using Dalamud.Logging.Internal; + +namespace Dalamud.Utility; + +/// +/// Utility functions for and . +/// +internal static class DateTimeSpanExtensions +{ + private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions)); + + private static ParsedRelativeFormatStrings? relativeFormatStringLong; + + private static ParsedRelativeFormatStrings? relativeFormatStringShort; + + /// Formats an instance of as a localized absolute time. + /// When. + /// The formatted string. + /// The string will be formatted according to Square Enix Account region settings, if Dalamud default + /// language is English. + public static unsafe string LocAbsolute(this DateTime when) + { + var culture = Service.GetNullable()?.DalamudLanguageCultureInfo ?? CultureInfo.InvariantCulture; + if (!Equals(culture, CultureInfo.InvariantCulture)) + return when.ToString("G", culture); + + var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); + var region = 0; + if (framework is not null) + region = framework->Region; + switch (region) + { + case 0: // jp + default: + return when.ToString("yyyy-MM-dd HH:mm:ss"); + case 1: // na + return when.ToString("MM/dd/yyyy HH:mm:ss"); + case 2: // eu + return when.ToString("dd-mm-yyyy HH:mm:ss"); + } + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastLong(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsLong", + "172800,{0:%d} days ago\n86400,yesterday\n7200,{0:%h} hours ago\n3600,an hour ago\n120,{0:%m} minutes ago\n60,a minute ago\n2,{0:%s} seconds ago\n1,a second ago\n-Infinity,just now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringLong?.FormatStringLoc != loc) + relativeFormatStringLong ??= new(loc); + + return relativeFormatStringLong.Format(DateTime.Now - when); + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastShort(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsShort", + "86400,{0:%d}d\n3600,{0:%h}h\n60,{0:%m}m\n1,{0:%s}s\n-Infinity,now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringShort?.FormatStringLoc != loc) + relativeFormatStringShort = new(loc); + + return relativeFormatStringShort.Format(DateTime.Now - when); + } + + private sealed class ParsedRelativeFormatStrings + { + private readonly List<(float MinSeconds, string FormatString)> formatStrings = new(); + + public ParsedRelativeFormatStrings(string value) + { + this.FormatStringLoc = value; + foreach (var line in value.Split("\n")) + { + var sep = line.IndexOf(','); + if (sep < 0) + { + Log.Error("A line without comma has been found: {line}", line); + continue; + } + + if (!float.TryParse( + line.AsSpan(0, sep), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out var seconds)) + { + Log.Error("Could not parse the duration: {line}", line); + continue; + } + + this.formatStrings.Add((seconds, line[(sep + 1)..])); + } + + this.formatStrings.Sort((a, b) => b.MinSeconds.CompareTo(a.MinSeconds)); + } + + public string FormatStringLoc { get; } + + /// Formats an instance of as a localized string. + /// The duration. + /// The formatted string. + public string Format(TimeSpan ts) + { + foreach (var (minSeconds, formatString) in this.formatStrings) + { + if (ts.TotalSeconds >= minSeconds) + return string.Format(formatString, ts); + } + + return this.formatStrings[^1].FormatString.Format(ts); + } + } +} From 62af691419622bf85d8c853c81b149cc3e34b419 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:05:47 +0900 Subject: [PATCH 28/41] More notification localizations --- .../Internal/NotificationConstants.cs | 20 +++++++++++-------- Dalamud/Localization.cs | 2 +- Dalamud/Utility/DateTimeSpanExtensions.cs | 16 ++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 50536baa3..de212160c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,5 +1,7 @@ using System.Numerics; +using CheapLoc; + using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; @@ -21,10 +23,8 @@ internal static class NotificationConstants // .. action buttons .. // ................................. - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - public const string DefaultInitiator = "Dalamud"; - /// The string to measure size of, to decide the width of notification windows. + /// Probably not worth localizing. public const string NotificationWidthMeasurementString = "The width of this text will decide the width\n" + "of the notification window."; @@ -113,8 +113,12 @@ internal static class NotificationConstants /// Gets the thickness of the focus indicator rectangle. public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the string to show in place of this_plugin if the notification is shown by Dalamud. + public static string DefaultInitiator => Loc.Localize("NotificationConstants.DefaultInitiator", "Dalamud"); + /// Gets the string format of the initiator name field, if the initiator is unloaded. - public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + public static string UnloadedInitiatorNameFormat => + Loc.Localize("NotificationConstants.UnloadedInitiatorNameFormat", "{0} (unloaded)"); /// Gets the color corresponding to the notification type. /// The notification type. @@ -148,10 +152,10 @@ internal static class NotificationConstants public static string? ToTitle(this NotificationType type) => type switch { NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), + NotificationType.Success => Loc.Localize("NotificationConstants.Title.Success", "Success"), + NotificationType.Warning => Loc.Localize("NotificationConstants.Title.Warning", "Warning"), + NotificationType.Error => Loc.Localize("NotificationConstants.Title.Error", "Error"), + NotificationType.Info => Loc.Localize("NotificationConstants.Title.Info", "Info"), _ => null, }; } diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 39312ac52..a9b0cf93d 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -70,7 +70,7 @@ public class Localization : IServiceType public CultureInfo DalamudLanguageCultureInfo { get; private set; } /// - /// Gets an instance of that corresponds to . + /// Gets an instance of that corresponds to . /// /// The language code which should be in . /// The corresponding instance of . diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs index 8f6a2a7ec..8422a4a26 100644 --- a/Dalamud/Utility/DateTimeSpanExtensions.cs +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -11,7 +11,7 @@ namespace Dalamud.Utility; /// /// Utility functions for and . /// -internal static class DateTimeSpanExtensions +public static class DateTimeSpanExtensions { private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions)); @@ -34,16 +34,12 @@ internal static class DateTimeSpanExtensions var region = 0; if (framework is not null) region = framework->Region; - switch (region) + return region switch { - case 0: // jp - default: - return when.ToString("yyyy-MM-dd HH:mm:ss"); - case 1: // na - return when.ToString("MM/dd/yyyy HH:mm:ss"); - case 2: // eu - return when.ToString("dd-mm-yyyy HH:mm:ss"); - } + 1 => when.ToString("MM/dd/yyyy HH:mm:ss"), // na + 2 => when.ToString("dd-mm-yyyy HH:mm:ss"), // eu + _ => when.ToString("yyyy-MM-dd HH:mm:ss"), // jp(0), cn(3), kr(4), and other possible errorneous cases + }; } /// Formats an instance of as a localized relative time. From a4a990cf3d296995fe9126ce27d8a9fb561dbaac Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:06:48 +0900 Subject: [PATCH 29/41] Reformat code --- .../ImGuiNotification/Internal/ActiveNotification.ImGui.cs | 2 +- .../Interface/ImGuiNotification/Internal/ActiveNotification.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 5d496963d..9363d97d9 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -362,7 +362,7 @@ internal sealed partial class ActiveNotification this.DrawContentBody(textColumnOffset, textColumnWidth); textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; - + ImGui.SetCursorPos(textColumnOffset); this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 475ae7e68..a9950745d 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -9,7 +9,6 @@ using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; -using Serilog.Events; namespace Dalamud.Interface.ImGuiNotification.Internal; From a1e2473774742db15154e8bff850a029806f075e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:09:07 +0900 Subject: [PATCH 30/41] Normalize names --- .../NotificationUtilities.cs | 6 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 62 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index e82b95b75..0ec2561fd 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -19,17 +19,17 @@ public static class NotificationUtilities { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this SeIconChar iconChar) => + public static INotificationIcon ToNotificationIcon(this SeIconChar iconChar) => INotificationIcon.From(iconChar); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) => + public static INotificationIcon ToNotificationIcon(this FontAwesomeIcon iconChar) => INotificationIcon.From(iconChar); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this FileInfo fileInfo) => + public static INotificationIcon ToNotificationIcon(this FileInfo fileInfo) => INotificationIcon.FromFile(fileInfo.FullName); /// Draws an icon from an and a . diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index d51f18216..47c5993cd 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -76,35 +76,35 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.TypeTitles.Length); ImGui.Combo( - "Icon Source##iconSourceCombo", - ref this.notificationTemplate.IconSourceInt, - NotificationTemplate.IconSourceTitles, - NotificationTemplate.IconSourceTitles.Length); - switch (this.notificationTemplate.IconSourceInt) + "Icon##iconCombo", + ref this.notificationTemplate.IconInt, + NotificationTemplate.IconTitles, + NotificationTemplate.IconTitles.Length); + switch (this.notificationTemplate.IconInt) { case 1 or 2: ImGui.InputText( - "Icon Text##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "Icon Text##iconText", + ref this.notificationTemplate.IconText, 255); break; case 3 or 4: ImGui.Combo( - "Icon Source##iconSourceAssetCombo", - ref this.notificationTemplate.IconSourceAssetInt, + "Asset##iconAssetCombo", + ref this.notificationTemplate.IconAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; case 5 or 7: ImGui.InputText( - "Game Path##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "Game Path##iconText", + ref this.notificationTemplate.IconText, 255); break; case 6 or 8: ImGui.InputText( - "File Path##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "File Path##iconText", + ref this.notificationTemplate.IconText, 255); break; } @@ -179,37 +179,37 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, - Icon = this.notificationTemplate.IconSourceInt switch + Icon = this.notificationTemplate.IconInt switch { 1 => INotificationIcon.From( - (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 + (SeIconChar)(this.notificationTemplate.IconText.Length == 0 ? 0 - : this.notificationTemplate.IconSourceText[0])), + : this.notificationTemplate.IconText[0])), 2 => INotificationIcon.From( - (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 + (FontAwesomeIcon)(this.notificationTemplate.IconText.Length == 0 ? 0 - : this.notificationTemplate.IconSourceText[0])), - 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText), - 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText), + : this.notificationTemplate.IconText[0])), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconText), _ => null, }, }); var dam = Service.Get(); var tm = Service.Get(); - switch (this.notificationTemplate.IconSourceInt) + switch (this.notificationTemplate.IconInt) { case 5: n.SetIconTexture( dam.GetDalamudTextureWrap( Enum.Parse( - NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt]))); + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 6: - n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText)); + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); break; case 7: - n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText))); + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))); break; } @@ -307,7 +307,7 @@ internal class ImGuiWidget : IDataWindowWidget private struct NotificationTemplate { - public static readonly string[] IconSourceTitles = + public static readonly string[] IconTitles = { "None (use Type)", "SeIconChar", @@ -373,9 +373,9 @@ internal class ImGuiWidget : IDataWindowWidget public string Title; public bool ManualMinimizedText; public string MinimizedText; - public int IconSourceInt; - public string IconSourceText; - public int IconSourceAssetInt; + public int IconInt; + public string IconText; + public int IconAssetInt; public bool ManualType; public int TypeInt; public int InitialDurationInt; @@ -394,9 +394,9 @@ internal class ImGuiWidget : IDataWindowWidget this.Title = string.Empty; this.ManualMinimizedText = false; this.MinimizedText = string.Empty; - this.IconSourceInt = 0; - this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; - this.IconSourceAssetInt = 0; + this.IconInt = 0; + this.IconText = "ui/icon/000000/000004_hr1.tex"; + this.IconAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.InitialDurationInt = 2; From 2a2fded520ee895dd33f8653491acd1d730f009a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:16:43 +0900 Subject: [PATCH 31/41] Fix user actions offset --- .../Internal/ActiveNotification.ImGui.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 9363d97d9..5f7d1a0fd 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -93,7 +93,7 @@ internal sealed partial class ActiveNotification this.DrawTopBar(width, actionWindowHeight, isHovered); if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) { - this.DrawContentArea(width, actionWindowHeight); + this.DrawContentAndActions(width, actionWindowHeight); } else if (this.expandoEasing.IsRunning) { @@ -101,7 +101,7 @@ internal sealed partial class ActiveNotification ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); else ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); - this.DrawContentArea(width, actionWindowHeight); + this.DrawContentAndActions(width, actionWindowHeight); ImGui.PopStyleVar(); } @@ -347,7 +347,7 @@ internal sealed partial class ActiveNotification return r; } - private void DrawContentArea(float width, float actionWindowHeight) + private void DrawContentAndActions(float width, float actionWindowHeight) { var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; @@ -361,10 +361,17 @@ internal sealed partial class ActiveNotification textColumnOffset.Y += NotificationConstants.ScaledComponentGap; this.DrawContentBody(textColumnOffset, textColumnWidth); - textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; - ImGui.SetCursorPos(textColumnOffset); - this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); + if (this.DrawActions is null) + return; + + var userActionOffset = new Vector2( + NotificationConstants.ScaledWindowPadding, + ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + ImGui.SetCursorPos(userActionOffset); + this.InvokeDrawActions( + userActionOffset, + new(width - NotificationConstants.ScaledWindowPadding, float.MaxValue)); } private void DrawIcon(Vector2 minCoord, Vector2 size) From 6b875bbcb58f288ea0c614f223591106a36d2498 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 17:27:19 +0900 Subject: [PATCH 32/41] Support SetIconTexture(Task?) --- .../ImGuiNotification/IActiveNotification.cs | 18 +++++++++++++++- .../ImGuiNotification/INotification.cs | 9 ++++++-- .../Internal/ActiveNotification.cs | 21 ++++++++++++------- .../NotificationUtilities.cs | 11 ++++++++++ .../Windows/Data/Widgets/ImGuiWidget.cs | 15 +++++++++---- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index c3ea2b9de..e677471b4 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,4 +1,5 @@ using System.Threading; +using System.Threading.Tasks; using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.Internal; @@ -50,7 +51,7 @@ public interface IActiveNotification : INotification /// This does not override . void ExtendBy(TimeSpan extension); - /// Sets the icon from , overriding the icon . + /// 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 . /// @@ -61,6 +62,21 @@ public interface IActiveNotification : INotification /// void SetIconTexture(IDalamudTextureWrap? textureWrap); + /// 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 . + /// + /// 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); + /// 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 2bc8e751c..207722c56 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,3 +1,6 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Services; @@ -20,8 +23,10 @@ public interface INotification NotificationType Type { get; set; } /// Gets or sets the icon source. - /// Use to use a texture, after calling - /// . + /// 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; } /// Gets or sets the hard expiry. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a9950745d..c54a9c6fa 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,5 +1,6 @@ using System.Runtime.Loader; using System.Threading; +using System.Threading.Tasks; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; @@ -29,7 +30,7 @@ internal sealed partial class ActiveNotification : IActiveNotification private DateTime extendedExpiry; /// The icon texture to use if specified; otherwise, icon will be used from . - private IDalamudTextureWrap? iconTextureWrap; + private Task? iconTextureWrap; /// The plugin that initiated this notification. private LocalPlugin? initiatorPlugin; @@ -229,18 +230,24 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap)); + } + + /// + public void SetIconTexture(Task? textureWrapTask) { if (this.DismissReason is not null) { - textureWrap?.Dispose(); + 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, textureWrap) is { } wrapToDispose && - wrapToDispose != textureWrap) + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrapTask) is { } wrapTaskToDispose && + wrapTaskToDispose != textureWrapTask) { - wrapToDispose.Dispose(); + wrapTaskToDispose.ToContentDisposedTask(true); } } @@ -333,8 +340,8 @@ 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 { } wrapToDispose) - wrapToDispose.Dispose(); + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapTaskToDispose) + wrapTaskToDispose.ToContentDisposedTask(true); this.Dismiss = null; this.Click = null; this.DrawActions = null; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 0ec2561fd..0ed552b42 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -1,6 +1,7 @@ using System.IO; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.Internal; @@ -103,6 +104,16 @@ public static class NotificationUtilities } } + /// Draws an icon from an instance of that results in an + /// . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The task that results in a texture. + /// true if anything has been drawn. + /// Exceptions from the task will be treated as if no texture is provided. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, Task? textureTask) => + textureTask?.IsCompletedSuccessfully is true && DrawIconFrom(minCoord, maxCoord, textureTask.Result); + /// Draws an icon from an instance of . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 47c5993cd..95119bb48 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -88,20 +88,20 @@ internal class ImGuiWidget : IDataWindowWidget ref this.notificationTemplate.IconText, 255); break; - case 3 or 4: + case 5 or 6: ImGui.Combo( "Asset##iconAssetCombo", ref this.notificationTemplate.IconAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; - case 5 or 7: + case 3 or 7: ImGui.InputText( "Game Path##iconText", ref this.notificationTemplate.IconText, 255); break; - case 6 or 8: + case 4 or 8: ImGui.InputText( "File Path##iconText", ref this.notificationTemplate.IconText, @@ -206,9 +206,15 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 6: - n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); + n.SetIconTexture( + dam.GetDalamudTextureWrapAsync( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 7: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); + break; + case 8: n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))); break; } @@ -315,6 +321,7 @@ internal class ImGuiWidget : IDataWindowWidget "GamePath", "FilePath", "TextureWrap from DalamudAssets", + "TextureWrap from DalamudAssets(Async)", "TextureWrap from GamePath", "TextureWrap from FilePath", }; From 16022ea46affad6604b1a71c3f799bd50560a8ae Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 00:46:23 +0900 Subject: [PATCH 33/41] Always show focus indicator if focused --- .../Internal/ActiveNotification.ImGui.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 5f7d1a0fd..d4a08ff69 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -75,14 +75,21 @@ internal sealed partial class ActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); - var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput; + var isFocused = ImGui.IsWindowFocused(); var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var isTakingKeyboardInput = isFocused && ImGui.GetIO().WantTextInput; var warrantsExtension = this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && (isHovered || isTakingKeyboardInput); this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + if (!isTakingKeyboardInput && !isHovered && isFocused) + { + ImGui.SetWindowFocus(null); + isFocused = false; + } + if (DateTime.Now > this.EffectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); @@ -105,8 +112,8 @@ internal sealed partial class ActiveNotification ImGui.PopStyleVar(); } - if (isTakingKeyboardInput) - this.DrawKeyboardInputIndicator(); + if (isFocused) + this.DrawFocusIndicator(); this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); if (ImGui.IsWindowHovered()) @@ -218,7 +225,7 @@ internal sealed partial class ActiveNotification ImGui.PopClipRect(); } - private void DrawKeyboardInputIndicator() + private void DrawFocusIndicator() { var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); From 76ca202f38cd1962511e5a20739e5bcb206924d1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 12:54:12 +0900 Subject: [PATCH 34/41] Comments on RemoveNonDalamudInvocations --- .../ImGuiNotification/Internal/ActiveNotification.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c54a9c6fa..019d9e281 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -252,6 +252,14 @@ internal sealed partial class ActiveNotification : IActiveNotification } /// Removes non-Dalamud invocation targets from events. + /// + /// This is done to prevent references of plugins being unloaded from outliving the plugin itself. + /// Anything that can contain plugin-provided types and functions count, which effectively means that events and + /// interface/object-typed fields need to be scrubbed. + /// As a notification can be marked as non-user-dismissable, in which case after removing event handlers there will + /// be no way to remove the notification, we force the notification to become user-dismissable, and reset the expiry + /// to the default duration on unload. + /// internal void RemoveNonDalamudInvocations() { var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); From ecfbcfe1944bf87460ad975c1f6b6d542de6633f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 12:55:28 +0900 Subject: [PATCH 35/41] Draw DefaultIcon instead of installed/3pp icon if plugin is gone --- .../Interface/ImGuiNotification/NotificationUtilities.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 0ed552b42..631263f95 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -131,12 +131,7 @@ public static class NotificationUtilities plugin.IsThirdParty, out var texture) || texture is null) { - texture = plugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; + texture = dam.GetDalamudTextureWrap(DalamudAsset.DefaultIcon); } return DrawIconFrom(minCoord, maxCoord, texture); From 9724e511e95e98c52f1a7981e6a1e2a1989371ba Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 13:05:46 +0900 Subject: [PATCH 36/41] Add INotification.RespectUiHidden --- Dalamud/Interface/ImGuiNotification/INotification.cs | 3 +++ .../ImGuiNotification/Internal/ActiveNotification.cs | 7 +++++++ .../ImGuiNotification/Internal/NotificationManager.cs | 9 +++++++++ Dalamud/Interface/ImGuiNotification/Notification.cs | 3 +++ Dalamud/Interface/Internal/InterfaceManager.cs | 2 +- .../Internal/Windows/Data/Widgets/ImGuiWidget.cs | 5 +++++ lib/FFXIVClientStructs | 2 +- 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 207722c56..f9a043c0b 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -60,6 +60,9 @@ public interface INotification /// is set to . bool ShowIndeterminateIfNoExpiry { get; set; } + /// Gets or sets a value indicating whether to respect the current UI visibility state. + bool RespectUiHidden { get; set; } + /// Gets or sets a value indicating whether the notification has been minimized. bool Minimized { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 019d9e281..3bc7c3837 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -90,6 +90,13 @@ internal sealed partial class ActiveNotification : IActiveNotification set => this.underlyingNotification.Title = value; } + /// + public bool RespectUiHidden + { + get => this.underlyingNotification.RespectUiHidden; + set => this.underlyingNotification.RespectUiHidden = value; + } + /// public string? MinimizedText { diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 973e93c72..272407615 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -20,6 +21,9 @@ namespace Dalamud.Interface.ImGuiNotification.Internal; [ServiceManager.EarlyLoadedService] internal class NotificationManager : INotificationManager, IServiceType, IDisposable { + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + private readonly List notifications = new(); private readonly ConcurrentBag pendingNotifications = new(); @@ -98,6 +102,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos { var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; + var uiHidden = this.gameGui.GameUiHidden; while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); @@ -109,7 +114,11 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); foreach (var tn in this.notifications) + { + if (uiHidden && tn.RespectUiHidden) + continue; height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; + } } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 612533cb8..5175985c7 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -38,6 +38,9 @@ public sealed record Notification : INotification /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + /// + public bool RespectUiHidden { get; set; } = true; + /// public bool Minimized { get; set; } = true; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index c811e9287..67e444cbe 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -923,7 +923,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { this.Draw?.Invoke(); - Service.Get().Draw(); + Service.GetNullable()?.Draw(); } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 95119bb48..086b0c1ad 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -127,6 +127,8 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); + ImGui.Checkbox("Respect UI Hidden", ref this.notificationTemplate.RespectUiHidden); + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); @@ -160,6 +162,7 @@ internal class ImGuiWidget : IDataWindowWidget : null, Type = type, ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, + RespectUiHidden = this.notificationTemplate.RespectUiHidden, Minimized = this.notificationTemplate.Minimized, UserDismissable = this.notificationTemplate.UserDismissable, InitialDuration = @@ -388,6 +391,7 @@ internal class ImGuiWidget : IDataWindowWidget public int InitialDurationInt; public int HoverExtendDurationInt; public bool ShowIndeterminateIfNoExpiry; + public bool RespectUiHidden; public bool Minimized; public bool UserDismissable; public bool ActionBar; @@ -413,6 +417,7 @@ internal class ImGuiWidget : IDataWindowWidget this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; + this.RespectUiHidden = true; } } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 722a2c512..ac2ced26f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02 +Subproject commit ac2ced26fc98153c65f5b8f0eaf0f464258ff683 From 27e96e12ead4bcc054e3eab944cb143fe838850a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 13:08:59 +0900 Subject: [PATCH 37/41] Postmerge --- .../Interface/Internal/Windows/ConsoleWindow.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1957ab720..ff5113275 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -12,6 +12,8 @@ using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -76,6 +78,8 @@ internal class ConsoleWindow : Window, IDisposable private int historyPos; private int copyStart = -1; + private IActiveNotification? prevCopyNotification; + /// Initializes a new instance of the class. /// An instance of . public ConsoleWindow(DalamudConfiguration configuration) @@ -441,10 +445,14 @@ internal class ConsoleWindow : Window, IDisposable return; ImGui.SetClipboardText(sb.ToString()); - Service.Get().AddNotification( - $"{n:n0} line(s) copied.", - this.WindowName, - NotificationType.Success); + this.prevCopyNotification?.DismissNow(); + this.prevCopyNotification = Service.Get().AddNotification( + new() + { + Title = this.WindowName, + Content = $"{n:n0} line(s) copied.", + Type = NotificationType.Success, + }); } private void DrawOptionsToolbar() From 3f4a91b726fd929bdb3cbf9062d9250331681313 Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Sat, 16 Mar 2024 16:42:24 +0100 Subject: [PATCH 38/41] Handle static declares in AddonLifecycleWidget (#1720) * Handle static declares old way: new way: * get full name instead of class name --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 5b2855298..26af2a8b2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -90,7 +90,7 @@ public class AddonLifecycleWidget : IDataWindowWidget ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Target}::{listener.FunctionDelegate.Method.Name}"); + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType.FullName}::{listener.FunctionDelegate.Method.Name}"); } ImGui.EndTable(); From 0656a524b181a43af7b5616676597f945fd64761 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Sat, 16 Mar 2024 16:45:19 +0100 Subject: [PATCH 39/41] Add missing space in cross-world PF links (#1717) --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 47c38b227..91dceb5d1 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -381,7 +381,7 @@ public class SeString { new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), // -> - new TextPayload($"Looking for Party ({recruiterName})"), + new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)), }; payloads.InsertRange(1, TextArrowPayloads); From 87b9edb4486c66486fa4db805e33b0e18693aa0d Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Mar 2024 00:58:05 +0900 Subject: [PATCH 40/41] 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.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> --- Dalamud.CorePlugin/PluginImpl.cs | 2 - .../Internal/DalamudConfiguration.cs | 4 +- Dalamud/Dalamud.cs | 22 ------ Dalamud/Data/DataManager.cs | 4 +- Dalamud/EntryPoint.cs | 2 + .../Game/Addon/Events/AddonEventManager.cs | 8 +-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 8 +-- Dalamud/Game/ClientState/ClientState.cs | 8 +-- .../Game/ClientState/Conditions/Condition.cs | 65 +++++++---------- .../Game/ClientState/GamePad/GamepadState.cs | 4 +- Dalamud/Game/Command/CommandManager.cs | 8 +-- Dalamud/Game/Config/GameConfig.cs | 8 +-- Dalamud/Game/DutyState/DutyState.cs | 8 +-- Dalamud/Game/Framework.cs | 8 +-- Dalamud/Game/Gui/ChatGui.cs | 8 +-- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 8 +-- Dalamud/Game/Gui/Dtr/DtrBar.cs | 8 +-- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 8 +-- Dalamud/Game/Gui/GameGui.cs | 8 +-- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 8 +-- Dalamud/Game/Gui/Toast/ToastGui.cs | 8 +-- Dalamud/Game/Internal/AntiDebug.cs | 56 ++++----------- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 69 +++++++------------ Dalamud/Game/Inventory/GameInventory.cs | 8 +-- Dalamud/Game/Network/GameNetwork.cs | 8 +-- .../Game/Network/Internal/NetworkHandlers.cs | 4 +- .../Game/Network/Internal/WinSockHandlers.cs | 4 +- Dalamud/Game/SigScanner.cs | 21 ++++-- Dalamud/Game/TargetSigScanner.cs | 12 +++- .../GameInteropProviderPluginScoped.cs | 4 +- Dalamud/Hooking/Internal/HookManager.cs | 4 +- .../Hooking/WndProcHook/WndProcHookManager.cs | 4 +- Dalamud/IServiceType.cs | 17 +++++ Dalamud/Interface/DragDrop/DragDropManager.cs | 9 ++- .../SingleFontChooserDialog.cs | 2 +- Dalamud/Interface/Internal/DalamudIme.cs | 4 +- .../Interface/Internal/DalamudInterface.cs | 4 +- .../ImGuiClipboardFunctionProvider.cs | 4 +- .../Internal/ImGuiDrawListFixProvider.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 64 +++++++++++------ Dalamud/Interface/Internal/TextureManager.cs | 4 +- .../Windows/Data/GameInventoryTestWidget.cs | 6 +- .../Internal/Windows/PluginImageCache.cs | 4 +- .../FontAtlasFactory.Implementation.cs | 22 ++++-- .../Internals/FontAtlasFactory.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 9 ++- .../TitleScreenMenu/TitleScreenMenu.cs | 4 +- Dalamud/Interface/UiBuilder.cs | 5 ++ Dalamud/IoC/Internal/ServiceScope.cs | 13 +++- Dalamud/Logging/Internal/TaskTracker.cs | 4 +- Dalamud/Logging/ScopedPluginLogService.cs | 8 +-- Dalamud/Networking/Http/HappyHttpClient.cs | 4 +- Dalamud/Plugin/DalamudPluginInterface.cs | 24 ++++--- Dalamud/Plugin/Internal/PluginManager.cs | 15 +++- .../Profiles/ProfileCommandHandler.cs | 4 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 6 +- Dalamud/ServiceManager.cs | 42 ++++++++++- Dalamud/Service{T}.cs | 43 +++++++----- Dalamud/Storage/Assets/DalamudAssetManager.cs | 12 +++- Dalamud/Storage/ReliableFileStorage.cs | 24 ++++++- Dalamud/Utility/DisposeSafety.cs | 11 ++- Dalamud/Utility/Util.cs | 37 ---------- 62 files changed, 441 insertions(+), 381 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index cb9b4368a..7c9adc6a8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -97,8 +97,6 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw -= this.OnDraw; this.windowSystem.RemoveAllWindows(); - - this.Interface.ExplicitDispose(); } /// diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 85a9507c9..70ed5dfde 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -26,7 +26,7 @@ namespace Dalamud.Configuration.Internal; #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 -internal sealed class DalamudConfiguration : IServiceType, IDisposable +internal sealed class DalamudConfiguration : IInternalDisposableService { private static readonly JsonSerializerSettings SerializerSettings = new() { @@ -502,7 +502,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // Make sure that we save, if a save is queued while we are shutting down this.Update(); diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8c858ce7c..f9d2aff3c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -187,27 +186,6 @@ internal sealed class Dalamud : IServiceType this.unloadSignal.WaitOne(); } - /// - /// Dispose subsystems related to plugin handling. - /// - public void DisposePlugins() - { - // this must be done before unloading interface manager, in order to do rebuild - // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game - // will not receive any windows messages - Service.GetNullable()?.Dispose(); - - // this must be done before unloading plugins, or it can cause a race condition - // due to rendering happening on another thread, where a plugin might receive - // a render call after it has been disposed, which can crash if it attempts to - // use any resources that it freed in its own Dispose method - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - } - /// /// Replace the current exception handler with the default one. /// diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index b08c6ffe7..da93f57c4 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Data; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DataManager : IDisposable, IServiceType, IDataManager +internal sealed class DataManager : IInternalDisposableService, IDataManager { private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; @@ -158,7 +158,7 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager #endregion /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.luminaCancellationTokenSource.Cancel(); } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d0f9e8845..1ad3ad8a9 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -138,7 +138,9 @@ public sealed class EntryPoint SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level +#pragma warning disable CS0618 // Type or member is obsolete var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!); +#pragma warning restore CS0618 // Type or member is obsolete var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs); // Set the appropriate logging level from the configuration diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 8ee09bed8..a9b9ef5fa 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Events; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType +internal unsafe class AddonEventManager : IInternalDisposableService { /// /// PluginName for Dalamud Internal use. @@ -62,7 +62,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onUpdateCursor.Dispose(); @@ -204,7 +204,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager { [ServiceManager.ServiceDependency] private readonly AddonEventManager eventManagerService = Service.Get(); @@ -225,7 +225,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 37f12ce3a..eefb3b5e9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType +internal unsafe class AddonLifecycle : IInternalDisposableService { private static readonly ModuleLog Log = new("AddonLifecycle"); @@ -89,7 +89,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType internal List EventListeners { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onAddonSetupHook.Dispose(); this.onAddonSetup2Hook.Dispose(); @@ -383,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle { [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); @@ -391,7 +391,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private readonly List eventListeners = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var listener in this.eventListeners) { diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index d387c2e2d..bd4259f5a 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.ClientState; ///
    [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class ClientState : IDisposable, IServiceType, IClientState +internal sealed class ClientState : IInternalDisposableService, IClientState { private static readonly ModuleLog Log = new("ClientState"); @@ -115,7 +115,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setupTerritoryTypeHook.Dispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent; @@ -196,7 +196,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState +internal class ClientStatePluginScoped : IInternalDisposableService, IClientState { [ServiceManager.ServiceDependency] private readonly ClientState clientStateService = Service.Get(); @@ -257,7 +257,7 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState public bool IsGPosing => this.clientStateService.IsGPosing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; this.clientStateService.Login -= this.LoginForward; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index a298b1502..dc8b28494 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.ClientState.Conditions; ///
    [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class Condition : IServiceType, ICondition +internal sealed partial class Condition : IInternalDisposableService, ICondition { /// /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. @@ -22,6 +22,8 @@ internal sealed partial class Condition : IServiceType, ICondition private readonly bool[] cache = new bool[MaxConditionEntries]; + private bool isDisposed; + [ServiceManager.ServiceConstructor] private Condition(ClientState clientState) { @@ -35,6 +37,9 @@ internal sealed partial class Condition : IServiceType, ICondition this.framework.Update += this.FrameworkUpdate; } + /// Finalizes an instance of the class. + ~Condition() => this.Dispose(false); + /// public event ICondition.ConditionChangeDelegate? ConditionChange; @@ -60,6 +65,9 @@ internal sealed partial class Condition : IServiceType, ICondition public bool this[ConditionFlag flag] => this[(int)flag]; + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + /// public bool Any() { @@ -89,6 +97,19 @@ internal sealed partial class Condition : IServiceType, ICondition return false; } + private void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + this.framework.Update -= this.FrameworkUpdate; + } + + this.isDisposed = true; + } + private void FrameworkUpdate(IFramework unused) { for (var i = 0; i < MaxConditionEntries; i++) @@ -112,44 +133,6 @@ internal sealed partial class Condition : IServiceType, ICondition } } -/// -/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. -/// -internal sealed partial class Condition : IDisposable -{ - private bool isDisposed; - - /// - /// Finalizes an instance of the class. - /// - ~Condition() - { - this.Dispose(false); - } - - /// - /// Disposes this instance, alongside its hooks. - /// - void IDisposable.Dispose() - { - GC.SuppressFinalize(this); - this.Dispose(true); - } - - private void Dispose(bool disposing) - { - if (this.isDisposed) - return; - - if (disposing) - { - this.framework.Update -= this.FrameworkUpdate; - } - - this.isDisposed = true; - } -} - /// /// Plugin-scoped version of a Condition service. /// @@ -159,7 +142,7 @@ internal sealed partial class Condition : IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition +internal class ConditionPluginScoped : IInternalDisposableService, ICondition { [ServiceManager.ServiceDependency] private readonly Condition conditionService = Service.Get(); @@ -185,7 +168,7 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition public bool this[int flag] => this.conditionService[flag]; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.conditionService.ConditionChange -= this.ConditionChangedForward; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 40e632113..a0e16f0e2 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState +internal unsafe class GamepadState : IInternalDisposableService, IGamepadState { private readonly Hook? gamepadPoll; @@ -109,7 +109,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState /// /// Disposes this instance, alongside its hooks. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.Dispose(true); GC.SuppressFinalize(this); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6b67f1892..7dcca763b 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Command; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager +internal sealed class CommandManager : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -130,7 +130,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; } @@ -170,7 +170,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager +internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -193,7 +193,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM public ReadOnlyDictionary Commands => this.commandManagerService.Commands; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var command in this.pluginRegisteredCommands) { diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 162df9417..a021025b1 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Config; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable +internal sealed class GameConfig : IInternalDisposableService, IGameConfig { private readonly TaskCompletionSource tcsInitialization = new(); private readonly TaskCompletionSource tcsSystem = new(); @@ -195,7 +195,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { var ode = new ObjectDisposedException(nameof(GameConfig)); this.tcsInitialization.SetExceptionIfIncomplete(ode); @@ -248,7 +248,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig +internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig { [ServiceManager.ServiceDependency] private readonly GameConfig gameConfigService = Service.Get(); @@ -295,7 +295,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public GameConfigSection UiControl => this.gameConfigService.UiControl; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameConfigService.Changed -= this.ConfigChangedForward; this.initializationTask.ContinueWith( diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index c4bda0d19..e2e4aef15 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -13,7 +13,7 @@ namespace Dalamud.Game.DutyState; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class DutyState : IDisposable, IServiceType, IDutyState +internal unsafe class DutyState : IInternalDisposableService, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -62,7 +62,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState private bool CompletedThisTerritory { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.contentDirectorNetworkMessageHook.Dispose(); this.framework.Update -= this.FrameworkOnUpdateEvent; @@ -168,7 +168,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState +internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState { [ServiceManager.ServiceDependency] private readonly DutyState dutyStateService = Service.Get(); @@ -200,7 +200,7 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState public bool IsDutyStarted => this.dutyStateService.IsDutyStarted; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.dutyStateService.DutyStarted -= this.DutyStartedForward; this.dutyStateService.DutyWiped -= this.DutyWipedForward; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 6520ca5c8..252a02031 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class Framework : IDisposable, IServiceType, IFramework +internal sealed class Framework : IInternalDisposableService, IFramework { private static readonly ModuleLog Log = new("Framework"); @@ -274,7 +274,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.RunOnFrameworkThread(() => { @@ -469,7 +469,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework +internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); @@ -504,7 +504,7 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.frameworkService.Update -= this.OnUpdateForward; diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 02b52ee56..e0b90b382 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -29,7 +29,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { private static readonly ModuleLog Log = new("ChatGui"); @@ -109,7 +109,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.printMessageHook.Dispose(); this.populateItemLinkHook.Dispose(); @@ -409,7 +409,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui +internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui { [ServiceManager.ServiceDependency] private readonly ChatGui chatGuiService = Service.Get(); @@ -447,7 +447,7 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.chatGuiService.ChatMessage -= this.OnMessageForward; this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 65c9b2760..f136d017a 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -28,7 +28,7 @@ namespace Dalamud.Game.Gui.ContextMenu; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu +internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu { private static readonly ModuleLog Log = new("ContextMenu"); @@ -77,7 +77,7 @@ internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMe private IReadOnlyList? SubmenuItems { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var manager = RaptureAtkUnitManager.Instance(); var menu = manager->GetAddonByName("ContextMenu"); @@ -496,7 +496,7 @@ original: #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu +internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu { [ServiceManager.ServiceDependency] private readonly ContextMenu parentService = Service.Get(); @@ -514,7 +514,7 @@ internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu private object MenuItemsLock { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.parentService.OnMenuOpened -= this.OnMenuOpenedForward; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 993bb951f..dbf6fba3c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Gui.Dtr; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar +internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { private const uint BaseNodeId = 1000; @@ -101,7 +101,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); @@ -493,7 +493,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar +internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar { [ServiceManager.ServiceDependency] private readonly DtrBar dtrBarService = Service.Get(); @@ -501,7 +501,7 @@ internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar private readonly Dictionary pluginEntries = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 2383b4e53..9310529e4 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.Gui.FlyText; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui +internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui { /// /// The native function responsible for adding fly text to the UI. See . @@ -78,7 +78,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.createFlyTextHook.Dispose(); } @@ -277,7 +277,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui +internal class FlyTextGuiPluginScoped : IInternalDisposableService, IFlyTextGui { [ServiceManager.ServiceDependency] private readonly FlyTextGui flyTextGuiService = Service.Get(); @@ -294,7 +294,7 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a97e19a0a..9272aa824 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui +internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui { private static readonly ModuleLog Log = new("GameGui"); @@ -344,7 +344,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui /// /// Disables the hooks and submodules of this module. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); @@ -520,7 +520,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui +internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui { [ServiceManager.ServiceDependency] private readonly GameGui gameGuiService = Service.Get(); @@ -558,7 +558,7 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui public HoveredAction HoveredAction => this.gameGuiService.HoveredAction; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 4a8332d24..f19fe3b0a 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Gui.PartyFinder; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui +internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui { private readonly PartyFinderAddressResolver address; private readonly IntPtr memory; @@ -47,7 +47,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.receiveListingHook.Dispose(); @@ -131,7 +131,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui +internal class PartyFinderGuiPluginScoped : IInternalDisposableService, IPartyFinderGui { [ServiceManager.ServiceDependency] private readonly PartyFinderGui partyFinderGuiService = Service.Get(); @@ -148,7 +148,7 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 7491b7f13..2cf327007 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.Gui.Toast; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui +internal sealed partial class ToastGui : IInternalDisposableService, IToastGui { private const uint QuestToastCheckmarkMagic = 60081; @@ -73,7 +73,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui /// /// Disposes of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.showNormalToastHook.Dispose(); this.showQuestToastHook.Dispose(); @@ -383,7 +383,7 @@ internal sealed partial class ToastGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui +internal class ToastGuiPluginScoped : IInternalDisposableService, IToastGui { [ServiceManager.ServiceDependency] private readonly ToastGui toastGuiService = Service.Get(); @@ -408,7 +408,7 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui public event IToastGui.OnErrorToastDelegate? ErrorToast; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.toastGuiService.Toast -= this.ToastForward; this.toastGuiService.QuestToast -= this.QuestToastForward; diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs index 2f4ec28c0..5ab024012 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Internal; /// This class disables anti-debug functionality in the game client. /// [ServiceManager.EarlyLoadedService] -internal sealed partial class AntiDebug : IServiceType +internal sealed class AntiDebug : IInternalDisposableService { private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 }; private byte[] original; @@ -43,16 +43,25 @@ internal sealed partial class AntiDebug : IServiceType } } + /// Finalizes an instance of the class. + ~AntiDebug() => this.Disable(); + /// /// Gets a value indicating whether the anti-debugging is enabled. /// public bool IsEnabled { get; private set; } = false; + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// /// Enables the anti-debugging by overwriting code in memory. /// public void Enable() { + if (this.IsEnabled) + return; + this.original = new byte[this.nop.Length]; if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled) { @@ -73,6 +82,9 @@ internal sealed partial class AntiDebug : IServiceType /// public void Disable() { + if (!this.IsEnabled) + return; + if (this.debugCheckAddress != IntPtr.Zero && this.original != null) { Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}"); @@ -86,45 +98,3 @@ internal sealed partial class AntiDebug : IServiceType this.IsEnabled = false; } } - -/// -/// Implementing IDisposable. -/// -internal sealed partial class AntiDebug : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~AntiDebug() => this.Dispose(false); - - /// - /// Disposes of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes of managed and unmanaged resources. - /// - /// If this was disposed through calling Dispose() or from being finalized. - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - // If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded. - // If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the - // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it. - this.Disable(); - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 30fab6b1b..9f9328de1 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.Internal; /// This class implements in-game Dalamud options in the in-game System menu. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe partial class DalamudAtkTweaks : IServiceType +internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService { private readonly AtkValueChangeType atkValueChangeType; private readonly AtkValueSetString atkValueSetString; @@ -40,6 +40,8 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private readonly string locDalamudPlugins; private readonly string locDalamudSettings; + private bool disposed = false; + [ServiceManager.ServiceConstructor] private DalamudAtkTweaks(TargetSigScanner sigScanner) { @@ -69,6 +71,9 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); } + /// Finalizes an instance of the class. + ~DalamudAtkTweaks() => this.Dispose(false); + private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type); @@ -79,6 +84,26 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5); + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing) + { + this.hookAgentHudOpenSystemMenu.Dispose(); + this.hookUiModuleRequestMainCommand.Dispose(); + this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); + + // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; + } + + this.disposed = true; + } + /* private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { @@ -229,45 +254,3 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType } } } - -/// -/// Implements IDisposable. -/// -internal sealed partial class DalamudAtkTweaks : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~DalamudAtkTweaks() => this.Dispose(false); - - /// - /// Dispose of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose of managed and unmanaged resources. - /// - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - this.hookAgentHudOpenSystemMenu.Dispose(); - this.hookUiModuleRequestMainCommand.Dispose(); - this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); - - // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 1c7f3e3bf..3e3dbc685 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Inventory; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal class GameInventory : IDisposable, IServiceType +internal class GameInventory : IInternalDisposableService { private readonly List subscribersPendingChange = new(); private readonly List subscribers = new(); @@ -61,7 +61,7 @@ internal class GameInventory : IDisposable, IServiceType private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.subscribersPendingChange) { @@ -351,7 +351,7 @@ internal class GameInventory : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory +internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory { private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); @@ -406,7 +406,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameInventoryService.Unsubscribe(this); diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 4099f228e..954612af7 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Network; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork +internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -59,7 +59,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.processZonePacketDownHook.Dispose(); this.processZonePacketUpHook.Dispose(); @@ -145,7 +145,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork +internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork { [ServiceManager.ServiceDependency] private readonly GameNetwork gameNetworkService = Service.Get(); @@ -162,7 +162,7 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 8d5ec1344..2a46af3d3 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -26,7 +26,7 @@ namespace Dalamud.Game.Network.Internal; /// This class handles network notifications and uploading market board data. /// [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class NetworkHandlers : IDisposable, IServiceType +internal unsafe class NetworkHandlers : IInternalDisposableService { private readonly IMarketBoardUploader uploader; @@ -213,7 +213,7 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.disposing = true; this.Dispose(this.disposing); diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index 8439389ff..619c458c4 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.Network.Internal; /// This class enables TCP optimizations in the game socket for better performance. /// [ServiceManager.EarlyLoadedService] -internal sealed class WinSockHandlers : IDisposable, IServiceType +internal sealed class WinSockHandlers : IInternalDisposableService { private Hook ws2SocketHook; @@ -27,7 +27,7 @@ internal sealed class WinSockHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.ws2SocketHook?.Dispose(); } diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index fe2d9083e..5e49052ae 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -104,6 +104,10 @@ public class SigScanner : IDisposable, ISigScanner /// public ProcessModule Module { get; } + /// Gets or sets a value indicating whether this instance of is meant to be a + /// Dalamud service. + private protected bool IsService { get; set; } + private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize; /// @@ -309,13 +313,11 @@ public class SigScanner : IDisposable, ISigScanner } } - /// - /// Free the memory of the copied module search area on object disposal, if applicable. - /// + /// public void Dispose() { - this.Save(); - Marshal.FreeHGlobal(this.moduleCopyPtr); + if (!this.IsService) + this.DisposeCore(); } /// @@ -337,6 +339,15 @@ public class SigScanner : IDisposable, ISigScanner } } + /// + /// Free the memory of the copied module search area on object disposal, if applicable. + /// + private protected void DisposeCore() + { + this.Save(); + Marshal.FreeHGlobal(this.moduleCopyPtr); + } + /// /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location. /// diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 35c82562e..e169ea904 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TargetSigScanner : SigScanner, IServiceType +internal class TargetSigScanner : SigScanner, IPublicDisposableService { /// /// Initializes a new instance of the class. @@ -26,4 +26,14 @@ internal class TargetSigScanner : SigScanner, IServiceType : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) { } + + /// + void IInternalDisposableService.DisposeService() + { + if (this.IsService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.IsService = true; } diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 9958385b9..1138d4e07 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -21,7 +21,7 @@ namespace Dalamud.Hooking.Internal; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable +internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternalDisposableService { private readonly LocalPlugin plugin; private readonly SigScanner scanner; @@ -83,7 +83,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT => this.HookFromAddress(this.scanner.ScanText(signature), detour, backend); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray(); if (notDisposed.Length != 0) diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 9c288a276..c8cdf3a46 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -14,7 +14,7 @@ namespace Dalamud.Hooking.Internal; /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes. /// [ServiceManager.EarlyLoadedService] -internal class HookManager : IDisposable, IServiceType +internal class HookManager : IInternalDisposableService { /// /// Logger shared with . @@ -74,7 +74,7 @@ internal class HookManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { RevertHooks(); TrackedHooks.Clear(); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 91020f898..a2253eb23 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Hooking.WndProcHook; /// Manages WndProc hooks for game main window and extra ImGui viewport windows. /// [ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable +internal sealed class WndProcHookManager : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); @@ -56,7 +56,7 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable public event WndProcEventDelegate? PostWndProc; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (this.dispatchMessageWHook.IsDisposed) return; diff --git a/Dalamud/IServiceType.cs b/Dalamud/IServiceType.cs index 973795faf..3a5dde880 100644 --- a/Dalamud/IServiceType.cs +++ b/Dalamud/IServiceType.cs @@ -6,3 +6,20 @@ public interface IServiceType { } + +/// , but for . +/// Use this to prevent services from accidentally being disposed by plugins or using clauses. +internal interface IInternalDisposableService : IServiceType +{ + /// Disposes the service. + void DisposeService(); +} + +/// An which happens to be public and needs to expose +/// . +internal interface IPublicDisposableService : IInternalDisposableService, IDisposable +{ + /// Marks that only should respond, + /// while suppressing . + void MarkDisposeOnlyFromService(); +} diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 151ef28a0..adc0ebff7 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Interface.DragDrop; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType +internal partial class DragDropManager : IInternalDisposableService, IDragDropManager { private nint windowHandlePtr = nint.Zero; @@ -56,6 +56,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. public IReadOnlyList Directories { get; private set; } = Array.Empty(); + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// Enable external drag and drop. public void Enable() { @@ -99,10 +102,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService this.ServiceAvailable = false; } - /// - public void Dispose() - => this.Disable(); - /// public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) { diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 9420fe42c..7636f22b6 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -93,7 +93,7 @@ public sealed class SingleFontChooserDialog : IDisposable /// Initializes a new instance of the class. /// A new instance of created using /// as its auto-rebuild mode. - /// The passed instance of will be disposed after use. If you pass an atlas + /// The passed instance of will be disposed after use. If you pass an atlas /// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing /// this font chooser. Consider using for automatic /// handling of font atlas derived from a , or even for automatic diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index caf014885..64040011e 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -34,7 +34,7 @@ namespace Dalamud.Interface.Internal; /// This class handles CJK IME. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class DalamudIme : IDisposable, IServiceType +internal sealed unsafe class DalamudIme : IInternalDisposableService { private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; @@ -200,7 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.Draw; this.ReleaseUnmanagedResources(); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1a07cd6ae..ec18fbb69 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -46,7 +46,7 @@ namespace Dalamud.Interface.Internal; /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. /// [ServiceManager.EarlyLoadedService] -internal class DalamudInterface : IDisposable, IServiceType +internal class DalamudInterface : IInternalDisposableService { private const float CreditsDarkeningMaxAlpha = 0.8f; @@ -209,7 +209,7 @@ internal class DalamudInterface : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.OnDraw; diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index bbf665405..9fa21a31b 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -35,7 +35,7 @@ namespace Dalamud.Interface.Internal; /// /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiClipboardFunctionProvider : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); private readonly nint clipboardUserDataOriginal; @@ -75,7 +75,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (!this.clipboardUserData.IsAllocated) return; diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs index f2d6ed244..139dd96e2 100644 --- a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// Change push_texture_id to only have one condition. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService { private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0; private const int CImGuiImDrawListAddRectFilled = 0x59FD0; @@ -69,7 +69,7 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl ImDrawFlags flags); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.hookImDrawListAddPolyline.Dispose(); this.hookImDrawListAddRectFilled.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48ad653d2..be14b882b 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; @@ -51,7 +52,7 @@ namespace Dalamud.Interface.Internal; /// This class manages interaction with the ImGui interface. /// [ServiceManager.BlockingEarlyLoadedService] -internal class InterfaceManager : IDisposable, IServiceType +internal class InterfaceManager : IInternalDisposableService { /// /// The default font size, in points. @@ -69,10 +70,13 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly SwapChainVtableResolver address = new(); - private readonly Hook setCursorHook; private RawDX11Scene? scene; + private Hook? setCursorHook; private Hook? presentHook; private Hook? resizeBuffersHook; @@ -87,8 +91,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.setCursorHook = Hook.FromImport( - null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -233,25 +235,45 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Dispose of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { - if (Service.GetNullable() is { } framework) - framework.RunOnFrameworkThread(Disposer).Wait(); - else - Disposer(); + // Unload hooks from the framework thread if possible. + // We're currently off the framework thread, as this function can only be called from + // ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread. + // The functions being unhooked are mostly called from the main thread, so unhooking from the main thread when + // possible would avoid any chance of unhooking a function that currently is being called. + // If unloading is initiated from "Unload Dalamud" /xldev menu, then the framework would still be running, as + // Framework.Destroy has never been called and thus Framework.IsFrameworkUnloading cannot be true, and this + // function will actually run the destroy from the framework thread. + // Otherwise, as Framework.IsFrameworkUnloading should have been set, this code should run immediately. + this.framework.RunOnFrameworkThread(ClearHooks).Wait(); + + // Below this point, hooks are guaranteed to be no longer called. + + // A font resource lock outlives the parent handle and the owner atlas. It should be disposed. + Interlocked.Exchange(ref this.defaultFontResourceLock, null)?.Dispose(); + + // Font handles become invalid after disposing the atlas, but just to be safe. + this.DefaultFontHandle?.Dispose(); + this.DefaultFontHandle = null; + + this.MonoFontHandle?.Dispose(); + this.MonoFontHandle = null; + + this.IconFontHandle?.Dispose(); + this.IconFontHandle = null; + + Interlocked.Exchange(ref this.dalamudAtlas, null)?.Dispose(); + Interlocked.Exchange(ref this.scene, null)?.Dispose(); - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas - this.defaultFontResourceLock = null; - this.dalamudAtlas?.Dispose(); - this.scene?.Dispose(); return; - void Disposer() + void ClearHooks() { - this.setCursorHook.Dispose(); - this.presentHook?.Dispose(); - this.resizeBuffersHook?.Dispose(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); + Interlocked.Exchange(ref this.presentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); } } @@ -693,7 +715,6 @@ internal class InterfaceManager : IDisposable, IServiceType "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] private void ContinueConstruction( TargetSigScanner sigScanner, - Framework framework, FontAtlasFactory fontAtlasFactory) { this.dalamudAtlas = fontAtlasFactory @@ -731,7 +752,7 @@ internal class InterfaceManager : IDisposable, IServiceType this.DefaultFontHandle.ImFontChanged += (_, font) => { var fontLocked = font.NewRef(); - Service.Get().RunOnFrameworkThread( + this.framework.RunOnFrameworkThread( () => { // Update the ImGui default font. @@ -765,6 +786,7 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Error(ex, "Could not enable immersive mode"); } + this.setCursorHook = Hook.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); @@ -808,7 +830,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed + return this.setCursorHook?.IsDisposed is not false ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9f90ea1ad..74ce91e5e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider +internal class TextureManager : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; @@ -268,7 +268,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs index c19f56654..5cede00cf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -74,7 +74,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.standardEnabled = false; if (!this.rawEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -105,7 +105,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.rawEnabled = false; if (!this.standardEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -135,7 +135,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget { if (ImGui.Button("Disable##all-disable")) { - this.scoped?.Dispose(); + ((IInternalDisposableService)this.scoped)?.DisposeService(); this.scoped = null; this.standardEnabled = this.rawEnabled = false; } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 29adbb3e5..97744b1a7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows; /// A cache for plugin icons and images. /// [ServiceManager.EarlyLoadedService] -internal class PluginImageCache : IDisposable, IServiceType +internal class PluginImageCache : IInternalDisposableService { /// /// Maximum plugin image width. @@ -136,7 +136,7 @@ internal class PluginImageCache : IDisposable, IServiceType this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancelToken.Cancel(); this.downloadQueue.CompleteAdding(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 883fcbbfc..b3d330075 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -204,12 +204,12 @@ internal sealed partial class FontAtlasFactory { while (this.IsBuildInProgress) await Task.Delay(100); - this.Garbage.Dispose(); + this.Clear(); }); } else { - this.Garbage.Dispose(); + this.Clear(); } return newRefCount; @@ -227,6 +227,20 @@ internal sealed partial class FontAtlasFactory var axisSubstance = this.Substances.OfType().Single(); return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; } + + public void Clear() + { + try + { + this.Garbage.Dispose(); + } + catch (Exception e) + { + Log.Error( + e, + $"Disposing {nameof(FontAtlasBuiltData)} of {this.Owner?.Name ?? "???"}."); + } + } } private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback @@ -547,13 +561,13 @@ internal sealed partial class FontAtlasFactory { if (this.buildIndex != rebuildIndex) { - data.ExplicitDisposeIgnoreExceptions(); + data.Release(); return; } var prevBuiltData = this.builtData; this.builtData = data; - prevBuiltData.ExplicitDisposeIgnoreExceptions(); + prevBuiltData?.Release(); this.buildTask = EmptyTask; fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count)); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 3e0fd1394..7fa41487a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -31,7 +31,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// [ServiceManager.BlockingEarlyLoadedService] internal sealed partial class FontAtlasFactory - : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable + : IInternalDisposableService, GamePrebakedFontHandle.IGameFontTextureProvider { private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); @@ -161,7 +161,7 @@ internal sealed partial class FontAtlasFactory this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancellationTokenSource.Cancel(); this.scopedFinalizer.Dispose(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 15e2803da..0e26145f0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; @@ -291,11 +292,15 @@ internal abstract class FontHandle : IFontHandle { if (disposing) { + if (Interlocked.Exchange(ref this.manager, null) is not { } managerToDisassociate) + return; + if (this.pushedFonts.Count > 0) Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); - this.Manager.FreeFontHandle(this); - this.manager = null; + + managerToDisassociate.FreeFontHandle(this); this.Disposed?.InvokeSafely(); + this.Disposed = null; this.ImFontChanged = null; } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 1f9a5bc76..6fbc0b4f3 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -193,7 +193,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu +internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleScreenMenu { [ServiceManager.ServiceDependency] private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); @@ -204,7 +204,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 2053d9354..03132a530 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -17,6 +17,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -605,6 +606,10 @@ public sealed class UiBuilder : IDisposable } } + /// Clean up resources allocated by this instance of . + /// Dalamud internal use only. + internal void DisposeInternal() => this.scopedFinalizer.Dispose(); + /// /// Open the registered configuration UI, if it exists. /// diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 01c18a8b2..9fcf1af3c 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -96,6 +96,17 @@ internal class ServiceScopeImpl : IServiceScope /// public void Dispose() { - foreach (var createdObject in this.scopeCreatedObjects.OfType()) createdObject.Dispose(); + foreach (var createdObject in this.scopeCreatedObjects) + { + switch (createdObject) + { + case IInternalDisposableService d: + d.DisposeService(); + break; + case IDisposable d: + d.Dispose(); + break; + } + } } } diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index b65f0efa7..9ecabe6c7 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -13,7 +13,7 @@ namespace Dalamud.Logging.Internal; /// Class responsible for tracking asynchronous tasks. /// [ServiceManager.EarlyLoadedService] -internal class TaskTracker : IDisposable, IServiceType +internal class TaskTracker : IInternalDisposableService { private static readonly ModuleLog Log = new("TT"); private static readonly List TrackedTasksInternal = new(); @@ -119,7 +119,7 @@ internal class TaskTracker : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.scheduleAndStartHook?.Dispose(); diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 924b4885d..0c044f2c2 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -17,7 +17,7 @@ namespace Dalamud.Logging; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; @@ -53,12 +53,6 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable /// public ILogger Logger { get; } - /// - public void Dispose() - { - GC.SuppressFinalize(this); - } - /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 4379a698f..23c6e3899 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -12,7 +12,7 @@ namespace Dalamud.Networking.Http; /// awareness. /// [ServiceManager.BlockingEarlyLoadedService] -internal class HappyHttpClient : IDisposable, IServiceType +internal class HappyHttpClient : IInternalDisposableService { /// /// Initializes a new instance of the class. @@ -58,7 +58,7 @@ internal class HappyHttpClient : IDisposable, IServiceType public HappyEyeballsCallback SharedHappyEyeballsCallback { get; } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.SharedHttpClient.Dispose(); this.SharedHappyEyeballsCallback.Dispose(); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 5e103ecbe..135cf89ea 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -452,26 +452,28 @@ public sealed class DalamudPluginInterface : IDisposable #endregion - /// - /// Unregister your plugin and dispose all references. - /// + /// void IDisposable.Dispose() { - this.UiBuilder.ExplicitDispose(); - Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); - Service.Get().LocalizationChanged -= this.OnLocalizationChanged; - Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } - /// - /// Obsolete implicit dispose implementation. Should not be used. - /// - [Obsolete("Do not dispose \"DalamudPluginInterface\".", true)] + /// This function will do nothing. Dalamud will dispose this object on plugin unload. + [Obsolete("This function will do nothing. Dalamud will dispose this object on plugin unload.", true)] public void Dispose() { // ignored } + /// Unregister the plugin and dispose all references. + /// Dalamud internal use only. + internal void DisposeInternal() + { + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); + Service.Get().LocalizationChanged -= this.OnLocalizationChanged; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; + this.UiBuilder.DisposeInternal(); + } + /// /// Dispatch the active plugins changed event. /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 6bdf73036..b815ac036 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -55,7 +55,7 @@ namespace Dalamud.Plugin.Internal; [InherentDependency] #pragma warning restore SA1015 -internal partial class PluginManager : IDisposable, IServiceType +internal partial class PluginManager : IInternalDisposableService { /// /// Default time to wait between plugin unload and plugin assembly unload. @@ -370,7 +370,7 @@ internal partial class PluginManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var disposablePlugins = this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); @@ -410,7 +410,16 @@ internal partial class PluginManager : IDisposable, IServiceType // Now that we've waited enough, dispose the whole plugin. // Since plugins should have been unloaded above, this should be done quickly. foreach (var plugin in disposablePlugins) - plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log); + { + try + { + plugin.Dispose(); + } + catch (Exception e) + { + Log.Error(e, $"Error disposing {plugin.Name}"); + } + } } this.assemblyLocationMonoHook?.Dispose(); diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 7001e4d7b..eebb87aaa 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Internal.Profiles; /// Service responsible for profile-related chat commands. /// [ServiceManager.EarlyLoadedService] -internal class ProfileCommandHandler : IServiceType, IDisposable +internal class ProfileCommandHandler : IInternalDisposableService { private readonly CommandManager cmd; private readonly ProfileManager profileManager; @@ -69,7 +69,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cmd.RemoveHandler("/xlenablecollection"); this.cmd.RemoveHandler("/xldisablecollection"); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 0f65bafb2..911bc436d 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -240,7 +240,7 @@ internal class LocalPlugin : IDisposable this.instance = null; } - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); @@ -426,7 +426,7 @@ internal class LocalPlugin : IDisposable if (this.instance == null) { this.State = PluginState.LoadError; - this.DalamudInterface.ExplicitDispose(); + this.DalamudInterface.DisposeInternal(); Log.Error( $"Error while loading {this.Name}, failed to bind and call the plugin constructor"); return; @@ -499,7 +499,7 @@ internal class LocalPlugin : IDisposable this.instance = null; - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index acd7c2b6f..845a65d6e 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -175,7 +176,8 @@ internal static class ServiceManager foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); - Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); + + CheckServiceTypeContracts(serviceType); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); @@ -514,6 +516,44 @@ internal static class ServiceManager return ServiceKind.ProvidedService; } + /// Validate service type contracts, and throws exceptions accordingly. + /// An instance of that is supposed to be a service type. + /// Does nothing on non-debug builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckServiceTypeContracts(Type serviceType) + { +#if DEBUG + try + { + if (!serviceType.IsAssignableTo(typeof(IServiceType))) + throw new InvalidOperationException($"Non-{nameof(IServiceType)} passed."); + if (serviceType.GetServiceKind() == ServiceKind.None) + throw new InvalidOperationException("Service type is not specified."); + + var isServiceDisposable = + serviceType.IsAssignableTo(typeof(IInternalDisposableService)); + var isAnyDisposable = + isServiceDisposable + || serviceType.IsAssignableTo(typeof(IDisposable)) + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + if (isAnyDisposable && !isServiceDisposable) + { + throw new InvalidOperationException( + $"A service must be an {nameof(IInternalDisposableService)} without specifying " + + $"{nameof(IDisposable)} nor {nameof(IAsyncDisposable)} if it is purely meant to be a service, " + + $"or an {nameof(IPublicDisposableService)} if it also is allowed to be constructed not as a " + + $"service to be used elsewhere and has to offer {nameof(IDisposable)} or " + + $"{nameof(IAsyncDisposable)}. See {nameof(ReliableFileStorage)} for an example of " + + $"{nameof(IPublicDisposableService)}."); + } + } + catch (Exception e) + { + throw new InvalidOperationException($"{serviceType.Name}: {e.Message}"); + } +#endif + } + /// /// Indicates that this constructor will be called for early initialization. /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 08f592826..ed03749d5 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -65,6 +65,12 @@ internal static class Service where T : IServiceType None, } + /// Does nothing. + /// Used to invoke the static ctor. + public static void Nop() + { + } + /// /// Sets the type in the service locator to the given object. /// @@ -72,6 +78,8 @@ internal static class Service where T : IServiceType public static void Provide(T obj) { ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + if (obj is IPublicDisposableService pds) + pds.MarkDisposeOnlyFromService(); instanceTcs.SetResult(obj); } @@ -297,23 +305,26 @@ internal static class Service where T : IServiceType if (!instanceTcs.Task.IsCompletedSuccessfully) return; - var instance = instanceTcs.Task.Result; - if (instance is IDisposable disposable) + switch (instanceTcs.Task.Result) { - ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); - try - { - disposable.Dispose(); - ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); - } - catch (Exception e) - { - ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); - } - } - else - { - ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + case IInternalDisposableService d: + ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); + try + { + d.DisposeService(); + ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); + } + catch (Exception e) + { + ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); + } + + break; + + default: + ServiceManager.CheckServiceTypeContracts(typeof(T)); + ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + break; } instanceTcs = new TaskCompletionSource(); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 68be78352..4f53460fb 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Storage.Assets; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager +internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamudAssetManager { private const int DownloadAttemptCount = 10; private const int RenameAttemptCount = 10; @@ -67,7 +67,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Where(x => x.GetAttribute()?.Required is true) .Select(this.CreateStreamAsync) .Select(x => x.ToContentDisposedTask())) - .ContinueWith(_ => loadTimings.Dispose()), + .ContinueWith( + r => + { + loadTimings.Dispose(); + return r; + }) + .Unwrap(), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); Task.WhenAll( @@ -83,7 +89,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.syncRoot) { diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index a013e95b5..eab93269e 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -22,17 +22,22 @@ namespace Dalamud.Storage; /// This is not an early-loaded service, as it is needed before they are initialized. /// [ServiceManager.ProvidedService] -public class ReliableFileStorage : IServiceType, IDisposable +[Api10ToDo("Make internal and IInternalDisposableService, and remove #pragma guard from the caller.")] +public class ReliableFileStorage : IPublicDisposableService { private static readonly ModuleLog Log = new("VFS"); private readonly object syncRoot = new(); + private SQLiteConnection? db; + private bool isService; /// /// Initializes a new instance of the class. /// /// Path to the VFS. + [Obsolete("Dalamud internal use only.", false)] + [Api10ToDo("Make internal, and remove #pragma guard from the caller.")] public ReliableFileStorage(string vfsDbPath) { var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); @@ -60,7 +65,7 @@ public class ReliableFileStorage : IServiceType, IDisposable } } } - + /// /// Check if a file exists. /// This will return true if the file does not exist on the filesystem, but in the transparent backup. @@ -288,9 +293,20 @@ public class ReliableFileStorage : IServiceType, IDisposable /// public void Dispose() { - this.db?.Dispose(); + if (!this.isService) + this.DisposeCore(); } + /// + void IInternalDisposableService.DisposeService() + { + if (this.isService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.isService = true; + /// /// Replace possible non-portable parts of a path with portable versions. /// @@ -312,6 +328,8 @@ public class ReliableFileStorage : IServiceType, IDisposable this.db.CreateTable(); } + private void DisposeCore() => this.db?.Dispose(); + private class DbFile { [PrimaryKey] diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 8ac891e0a..64d31048f 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -70,7 +70,16 @@ public static class DisposeSafety r => { if (!r.IsCompletedSuccessfully) - return ignoreAllExceptions ? Task.CompletedTask : r; + { + if (ignoreAllExceptions) + { + _ = r.Exception; + return Task.CompletedTask; + } + + return r; + } + try { r.Result.Dispose(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 65196b3ee..43355ac2c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -19,7 +19,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -638,42 +637,6 @@ public static class Util if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); } - - /// - /// Dispose this object. - /// - /// The object to dispose. - /// The type of object to dispose. - internal static void ExplicitDispose(this T obj) where T : IDisposable - { - obj.Dispose(); - } - - /// - /// Dispose this object. - /// - /// The object to dispose. - /// Log message to print, if specified and an error occurs. - /// Module logger, if any. - /// The type of object to dispose. - internal static void ExplicitDisposeIgnoreExceptions( - this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable - { - try - { - obj.Dispose(); - } - catch (Exception e) - { - if (logMessage == null) - return; - - if (moduleLog != null) - moduleLog.Error(e, logMessage); - else - Log.Error(e, logMessage); - } - } /// /// Gets a random, inoffensive, human-friendly string. From e52c2755cb6bb66eeea67718c249fa07afdf6144 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Mar 2024 01:02:36 +0900 Subject: [PATCH 41/41] Fix CreateImGuiRangesFrom to omit null char (#1709) * Fix CreateImGuiRangesFrom to omit null char UnicodeRanges.BasicLatin is [0, 127], but ImGui stops reading the glyph range list on encountering a zero. Fixed that by ensuring that 0 never appears in the glyph range list. * Make problems explicit --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 11 +++++++ .../FontAtlasFactory.Implementation.cs | 29 +++++++++++++++++-- Dalamud/Interface/UiBuilder.cs | 13 +++++++-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 7 +++-- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index a79ab099d..2feac8849 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -85,6 +85,10 @@ public interface IFontAtlas : IDisposable /// Creates a new from game's built-in fonts. /// Font to use. /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. /// This function does not throw. will be populated instead, if /// the build procedure has failed. can be used regardless of the state of the font /// handle. @@ -93,6 +97,13 @@ public interface IFontAtlas : IDisposable /// Creates a new IFontHandle using your own callbacks. /// Callback for . /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. + /// Consider calling to + /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language + /// users. /// /// Consider calling to /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index b3d330075..3c175ae3c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -35,6 +35,9 @@ internal sealed partial class FontAtlasFactory /// public const string EllipsisCodepoints = "\u2026\u0085"; + /// Marker for tasks on whether it's being called inside a font build cycle. + public static readonly AsyncLocal IsBuildInProgressForTask = new(); + /// /// If set, disables concurrent font build operation. /// @@ -427,11 +430,28 @@ internal sealed partial class FontAtlasFactory } /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + public IFontHandle NewGameFontHandle(GameFontStyle style) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewGameFontHandle)} may not be called during {nameof(this.BuildStepChange)}, the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.gameFontHandleManager.NewFontHandle(style); + } /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewDelegateFontHandle)} may not be called during {nameof(this.BuildStepChange)} or the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + } /// public void BuildFontsOnNextFrame() @@ -630,6 +650,8 @@ internal sealed partial class FontAtlasFactory FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; + + IsBuildInProgressForTask.Value = true; try { res = new(this, scale); @@ -754,6 +776,7 @@ internal sealed partial class FontAtlasFactory // ReSharper disable once ConstantConditionalAccessQualifier toolkit?.Dispose(); this.buildQueued = false; + IsBuildInProgressForTask.Value = false; } unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 03132a530..2c2ca9725 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -516,9 +516,16 @@ public sealed class UiBuilder : IDisposable /// Handle to the game font which may or may not be available for use yet. [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) + { + var prevValue = FontAtlasFactory.IsBuildInProgressForTask.Value; + FontAtlasFactory.IsBuildInProgressForTask.Value = false; + var v = new GameFontHandle( + (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); + FontAtlasFactory.IsBuildInProgressForTask.Value = prevValue; + return v; + } /// /// Call this to queue a rebuild of the font atlas.
    diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index f02effe1d..639b0315d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -493,12 +493,13 @@ public static class ImGuiHelpers /// The range array that can be used for . public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => ranges - .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .Select(x => (First: Math.Max(x.FirstCodePoint, 1), Last: x.FirstCodePoint + x.Length)) + .Where(x => x.First <= ushort.MaxValue && x.First <= x.Last) .SelectMany( x => new[] { - (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), - (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + (ushort)Math.Min(x.First, ushort.MaxValue), + (ushort)Math.Min(x.Last, ushort.MaxValue), }) .Append((ushort)0) .ToArray();