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