mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Implement INotificationManager
This commit is contained in:
parent
8e5a84792e
commit
3ba395bd70
12 changed files with 1064 additions and 307 deletions
109
Dalamud/Interface/ImGuiNotification/IActiveNotification.cs
Normal file
109
Dalamud/Interface/ImGuiNotification/IActiveNotification.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an active notification.
|
||||||
|
/// </summary>
|
||||||
|
public interface IActiveNotification : INotification
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The counter for <see cref="Id"/> field.
|
||||||
|
/// </summary>
|
||||||
|
private static long idCounter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked upon dismissing the notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded.
|
||||||
|
/// </remarks>
|
||||||
|
event NotificationDismissedDelegate Dismiss;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked upon clicking on the notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is not applicable when <see cref="INotification.Interactible"/> is set to <c>false</c>.
|
||||||
|
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
|
||||||
|
/// Refer to <see cref="IsDismissed"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event Action<IActiveNotification> Click;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the mouse enters the notification window.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is applicable regardless of <see cref="INotification.Interactible"/>.
|
||||||
|
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
|
||||||
|
/// Refer to <see cref="IsDismissed"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event Action<IActiveNotification> MouseEnter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the mouse leaves the notification window.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is applicable regardless of <see cref="INotification.Interactible"/>.
|
||||||
|
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
|
||||||
|
/// Refer to <see cref="IsDismissed"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event Action<IActiveNotification> MouseLeave;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked upon drawing the action bar of the notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is applicable regardless of <see cref="INotification.Interactible"/>.
|
||||||
|
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
|
||||||
|
/// Refer to <see cref="IsDismissed"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event Action<IActiveNotification> DrawActions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of this notification.
|
||||||
|
/// </summary>
|
||||||
|
long Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the mouse cursor is on the notification window.
|
||||||
|
/// </summary>
|
||||||
|
bool IsMouseHovered { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the notification has been dismissed.
|
||||||
|
/// This includes when the hide animation is being played.
|
||||||
|
/// </summary>
|
||||||
|
bool IsDismissed { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clones this notification as a <see cref="Notification"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new instance of <see cref="Notification"/>.</returns>
|
||||||
|
Notification CloneNotification();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dismisses this notification.
|
||||||
|
/// </summary>
|
||||||
|
void DismissNow();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the notification data.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Call <see cref="UpdateIcon"/> to update the icon using the new <see cref="INotification.IconCreator"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="newNotification">The new notification entry.</param>
|
||||||
|
void Update(INotification newNotification);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the icon again using <see cref="INotification.IconCreator"/>.
|
||||||
|
/// </summary>
|
||||||
|
void UpdateIcon();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a new value to use for <see cref="Id"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The new value.</returns>
|
||||||
|
internal static long CreateNewId() => Interlocked.Increment(ref idCounter);
|
||||||
|
}
|
||||||
75
Dalamud/Interface/ImGuiNotification/INotification.cs
Normal file
75
Dalamud/Interface/ImGuiNotification/INotification.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a notification.
|
||||||
|
/// </summary>
|
||||||
|
public interface INotification
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the content body of the notification.
|
||||||
|
/// </summary>
|
||||||
|
string Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the title of the notification.
|
||||||
|
/// </summary>
|
||||||
|
string? Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the notification.
|
||||||
|
/// </summary>
|
||||||
|
NotificationType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the icon creator function for the notification.<br />
|
||||||
|
/// Currently <see cref="IDalamudTextureWrap"/>, <see cref="SeIconChar"/>, and <see cref="FontAwesomeIcon"/> types
|
||||||
|
/// are accepted.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The icon created by the task returned will be owned by Dalamud,
|
||||||
|
/// i.e. it will be <see cref="IDisposable.Dispose"/>d automatically as needed.<br />
|
||||||
|
/// If <c>null</c> is supplied for this property or <see cref="Task.IsCompletedSuccessfully"/> of the returned task
|
||||||
|
/// is <c>false</c>, then the corresponding icon with <see cref="Type"/> will be used.<br />
|
||||||
|
/// Use <see cref="Task.FromResult{TResult}"/> if you have an instance of <see cref="IDalamudTextureWrap"/> that you
|
||||||
|
/// can transfer ownership to Dalamud and is available for use right away.
|
||||||
|
/// </remarks>
|
||||||
|
Func<Task<object>>? IconCreator { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expiry.
|
||||||
|
/// </summary>
|
||||||
|
DateTime Expiry { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this notification may be interacted.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set this value to <c>true</c> if you want to respond to user inputs from
|
||||||
|
/// <see cref="IActiveNotification.DrawActions"/>.
|
||||||
|
/// Note that the close buttons for notifications are always provided and interactible.
|
||||||
|
/// </remarks>
|
||||||
|
bool Interactible { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether clicking on the notification window counts as dismissing the notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property has no effect if <see cref="Interactible"/> is <c>false</c>.
|
||||||
|
/// </remarks>
|
||||||
|
bool ClickIsDismiss { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the new duration for this notification if mouse cursor is on the notification window.
|
||||||
|
/// If set to <see cref="TimeSpan.Zero"/> or less, then this feature is turned off.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property is applicable regardless of <see cref="Interactible"/>.
|
||||||
|
/// </remarks>
|
||||||
|
TimeSpan HoverExtendDuration { get; }
|
||||||
|
}
|
||||||
35
Dalamud/Interface/ImGuiNotification/Notification.cs
Normal file
35
Dalamud/Interface/ImGuiNotification/Notification.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a blueprint for a notification.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record Notification : INotification
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NotificationType Type { get; set; } = NotificationType.None;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Func<Task<object>>? IconCreator { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Interactible { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool ClickIsDismiss { get; set; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the reason of dismissal for a notification.
|
||||||
|
/// </summary>
|
||||||
|
public enum NotificationDismissReason
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The notification is dismissed because the expiry specified from <see cref="INotification.Expiry"/> is met.
|
||||||
|
/// </summary>
|
||||||
|
Timeout = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The notification is dismissed because the user clicked on the close button on a notification window.
|
||||||
|
/// </summary>
|
||||||
|
Manual = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The notification is dismissed from calling <see cref="IActiveNotification.DismissNow"/>.
|
||||||
|
/// </summary>
|
||||||
|
Programmatical = 3,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate representing the dismissal of an active notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">The notification being dismissed.</param>
|
||||||
|
/// <param name="dismissReason">The reason of dismissal.</param>
|
||||||
|
public delegate void NotificationDismissedDelegate(
|
||||||
|
IActiveNotification notification,
|
||||||
|
NotificationDismissReason dismissReason);
|
||||||
508
Dalamud/Interface/Internal/Notifications/ActiveNotification.cs
Normal file
508
Dalamud/Interface/Internal/Notifications/ActiveNotification.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an active notification.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ActiveNotification : IActiveNotification, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Easing showEasing;
|
||||||
|
private readonly Easing hideEasing;
|
||||||
|
|
||||||
|
private Notification underlyingNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ActiveNotification"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="underlyingNotification">The underlying notification.</param>
|
||||||
|
/// <param name="initiatorPlugin">The initiator plugin. Use <c>null</c> if originated by Dalamud.</param>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event NotificationDismissedDelegate? Dismiss;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<IActiveNotification>? Click;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<IActiveNotification>? DrawActions;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<IActiveNotification>? MouseEnter;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<IActiveNotification>? MouseLeave;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public long Id { get; } = IActiveNotification.CreateNewId();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tick of creating this notification.
|
||||||
|
/// </summary>
|
||||||
|
public long CreatedAt { get; } = Environment.TickCount64;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Content => this.underlyingNotification.Content;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string? Title => this.underlyingNotification.Title;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NotificationType Type => this.underlyingNotification.Type;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Func<Task<object>>? IconCreator => this.underlyingNotification.IconCreator;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime Expiry => this.underlyingNotification.Expiry;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Interactible => this.underlyingNotification.Interactible;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsMouseHovered { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsDismissed => this.hideEasing.IsRunning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the plugin that initiated this notification.
|
||||||
|
/// </summary>
|
||||||
|
public LocalPlugin? InitiatorPlugin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icon of this notification.
|
||||||
|
/// </summary>
|
||||||
|
public Task<object>? IconTask { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default color of the notification.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default icon of the notification.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default title of the notification.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.ClearIconTask();
|
||||||
|
this.underlyingNotification.IconCreator = null;
|
||||||
|
this.Dismiss = null;
|
||||||
|
this.Click = null;
|
||||||
|
this.DrawActions = null;
|
||||||
|
this.InitiatorPlugin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Notification CloneNotification() => this.underlyingNotification with { };
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dismisses this notification. Multiple calls will be ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">The reason of dismissal.</param>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates animations.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns><c>true</c> if the notification is over.</returns>
|
||||||
|
public bool UpdateAnimations()
|
||||||
|
{
|
||||||
|
this.showEasing.Update();
|
||||||
|
this.hideEasing.Update();
|
||||||
|
return this.hideEasing.IsRunning && this.hideEasing.IsDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws this notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxWidth">The maximum width of the notification window.</param>
|
||||||
|
/// <param name="offsetY">The offset from the bottom.</param>
|
||||||
|
/// <returns>The height of the notification.</returns>
|
||||||
|
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<NotificationManager>.Get();
|
||||||
|
var interfaceManager = Service<InterfaceManager>.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void UpdateIcon()
|
||||||
|
{
|
||||||
|
this.ClearIconTask();
|
||||||
|
this.IconTask = this.IconCreator?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes non-Dalamud invocation targets from events.
|
||||||
|
/// </summary>
|
||||||
|
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>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.Notifications;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constants for drawing notification windows.
|
||||||
|
/// </summary>
|
||||||
|
internal static class NotificationConstants
|
||||||
|
{
|
||||||
|
// ..............................[X]
|
||||||
|
// ..[i]..title title title title ..
|
||||||
|
// .. by this_plugin ..
|
||||||
|
// .. ..
|
||||||
|
// .. body body body body ..
|
||||||
|
// .. some more wrapped body ..
|
||||||
|
// .. ..
|
||||||
|
// .. action buttons ..
|
||||||
|
// .................................
|
||||||
|
|
||||||
|
/// <summary>The string to show in place of this_plugin if the notification is shown by Dalamud.</summary>
|
||||||
|
public const string DefaultInitiator = "Dalamud";
|
||||||
|
|
||||||
|
/// <summary>The size of the icon.</summary>
|
||||||
|
public const float IconSize = 32;
|
||||||
|
|
||||||
|
/// <summary>The background opacity of a notification window.</summary>
|
||||||
|
public const float BackgroundOpacity = 0.82f;
|
||||||
|
|
||||||
|
/// <summary>Duration of show animation.</summary>
|
||||||
|
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
|
||||||
|
|
||||||
|
/// <summary>Default duration of the notification.</summary>
|
||||||
|
public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
|
/// <summary>Default duration of the notification.</summary>
|
||||||
|
public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
|
/// <summary>Duration of hide animation.</summary>
|
||||||
|
public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
|
||||||
|
|
||||||
|
/// <summary>Text color for the close button [X].</summary>
|
||||||
|
public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f);
|
||||||
|
|
||||||
|
/// <summary>Text color for the title.</summary>
|
||||||
|
public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
|
/// <summary>Text color for the name of the initiator.</summary>
|
||||||
|
public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f);
|
||||||
|
|
||||||
|
/// <summary>Text color for the body.</summary>
|
||||||
|
public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f);
|
||||||
|
|
||||||
|
/// <summary>Gets the scaled padding of the window (dot(.) in the above diagram).</summary>
|
||||||
|
public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
/// <summary>Gets the distance from the right bottom border of the viewport
|
||||||
|
/// to the right bottom border of a notification window.
|
||||||
|
/// </summary>
|
||||||
|
public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
/// <summary>Gets the scaled gap between two notification windows.</summary>
|
||||||
|
public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
/// <summary>Gets the scaled gap between components.</summary>
|
||||||
|
public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
/// <summary>Gets the scaled size of the icon.</summary>
|
||||||
|
public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
/// <summary>Gets the scaled size of the close button.</summary>
|
||||||
|
public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
using System;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
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.Interface.Utility;
|
||||||
using Dalamud.Utility;
|
using Dalamud.IoC;
|
||||||
using ImGuiNET;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Internal.Types;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal.Notifications;
|
namespace Dalamud.Interface.Internal.Notifications;
|
||||||
|
|
||||||
|
|
@ -14,51 +17,66 @@ namespace Dalamud.Interface.Internal.Notifications;
|
||||||
/// Class handling notifications/toasts in ImGui.
|
/// Class handling notifications/toasts in ImGui.
|
||||||
/// Ported from https://github.com/patrickcjk/imgui-notify.
|
/// Ported from https://github.com/patrickcjk/imgui-notify.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[InterfaceVersion("1.0")]
|
||||||
[ServiceManager.EarlyLoadedService]
|
[ServiceManager.EarlyLoadedService]
|
||||||
internal class NotificationManager : IServiceType
|
internal class NotificationManager : INotificationManager, IServiceType, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly List<ActiveNotification> notifications = new();
|
||||||
/// Value indicating the bottom-left X padding.
|
private readonly ConcurrentBag<ActiveNotification> pendingNotifications = new();
|
||||||
/// </summary>
|
|
||||||
internal const float NotifyPaddingX = 20.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating the bottom-left Y padding.
|
|
||||||
/// </summary>
|
|
||||||
internal const float NotifyPaddingY = 20.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating the Y padding between each message.
|
|
||||||
/// </summary>
|
|
||||||
internal const float NotifyPaddingMessageY = 10.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating the fade-in and out duration.
|
|
||||||
/// </summary>
|
|
||||||
internal const int NotifyFadeInOutTime = 500;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating the default time until the notification is dismissed.
|
|
||||||
/// </summary>
|
|
||||||
internal const int NotifyDefaultDismiss = 3000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating the maximum opacity.
|
|
||||||
/// </summary>
|
|
||||||
internal const float NotifyOpacity = 0.82f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Value indicating default window flags for the notifications.
|
|
||||||
/// </summary>
|
|
||||||
internal const ImGuiWindowFlags NotifyToastFlags =
|
|
||||||
ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoInputs |
|
|
||||||
ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing;
|
|
||||||
|
|
||||||
private readonly List<Notification> notifications = new();
|
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[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 })));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the handle to AXIS fonts, sized for use as an icon.</summary>
|
||||||
|
public IFontHandle IconAxisFontHandle { get; }
|
||||||
|
|
||||||
|
/// <summary>Gets the handle to FontAwesome fonts, sized for use as an icon.</summary>
|
||||||
|
public IFontHandle IconFontAwesomeFontHandle { get; }
|
||||||
|
|
||||||
|
private IFontAtlas PrivateAtlas { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IActiveNotification AddNotification(Notification notification)
|
||||||
|
{
|
||||||
|
var an = new ActiveNotification(notification, null);
|
||||||
|
this.pendingNotifications.Add(an);
|
||||||
|
return an;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a notification originating from a plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">The notification.</param>
|
||||||
|
/// <param name="plugin">The source plugin.</param>
|
||||||
|
/// <returns>The new notification.</returns>
|
||||||
|
public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin)
|
||||||
|
{
|
||||||
|
var an = new ActiveNotification(notification, plugin);
|
||||||
|
this.pendingNotifications.Add(an);
|
||||||
|
return an;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -67,252 +85,77 @@ internal class NotificationManager : IServiceType
|
||||||
/// <param name="content">The content of the notification.</param>
|
/// <param name="content">The content of the notification.</param>
|
||||||
/// <param name="title">The title of the notification.</param>
|
/// <param name="title">The title of the notification.</param>
|
||||||
/// <param name="type">The type of the notification.</param>
|
/// <param name="type">The type of the notification.</param>
|
||||||
/// <param name="msDelay">The time the notification should be displayed for.</param>
|
public void AddNotification(
|
||||||
public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss)
|
string content,
|
||||||
{
|
string? title = null,
|
||||||
this.notifications.Add(new Notification
|
NotificationType type = NotificationType.None) =>
|
||||||
{
|
this.AddNotification(
|
||||||
Content = content,
|
new()
|
||||||
Title = title,
|
{
|
||||||
NotificationType = type,
|
Content = content,
|
||||||
DurationMs = msDelay,
|
Title = title,
|
||||||
});
|
Type = type,
|
||||||
}
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw all currently queued notifications.
|
/// Draw all currently queued notifications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
var viewportSize = ImGuiHelpers.MainViewport.Size;
|
var viewportSize = ImGuiHelpers.MainViewport.WorkSize;
|
||||||
var height = 0f;
|
var height = 0f;
|
||||||
|
|
||||||
for (var i = 0; i < this.notifications.Count; i++)
|
while (this.pendingNotifications.TryTake(out var newNotification))
|
||||||
{
|
this.notifications.Add(newNotification);
|
||||||
var tn = this.notifications.ElementAt(i);
|
|
||||||
|
|
||||||
if (tn.GetPhase() == Notification.Phase.Expired)
|
var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3);
|
||||||
{
|
|
||||||
this.notifications.RemoveAt(i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
/// <summary>
|
||||||
iconColor.W = opacity;
|
/// Plugin-scoped version of a <see cref="NotificationManager"/> service.
|
||||||
|
/// </summary>
|
||||||
|
[PluginInterface]
|
||||||
|
[InterfaceVersion("1.0")]
|
||||||
|
[ServiceManager.ScopedService]
|
||||||
|
#pragma warning disable SA1015
|
||||||
|
[ResolveVia<INotificationManager>]
|
||||||
|
#pragma warning restore SA1015
|
||||||
|
internal class NotificationManagerPluginScoped : INotificationManager, IServiceType, IDisposable
|
||||||
|
{
|
||||||
|
private readonly LocalPlugin localPlugin;
|
||||||
|
private readonly ConcurrentDictionary<IActiveNotification, int> notifications = new();
|
||||||
|
|
||||||
var windowName = $"##NOTIFY{i}";
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly NotificationManager notificationManagerService = Service<NotificationManager>.Get();
|
||||||
|
|
||||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
[ServiceManager.ServiceConstructor]
|
||||||
ImGui.SetNextWindowBgAlpha(opacity);
|
private NotificationManagerPluginScoped(LocalPlugin localPlugin) =>
|
||||||
ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One);
|
this.localPlugin = localPlugin;
|
||||||
ImGui.Begin(windowName, NotifyToastFlags);
|
|
||||||
|
|
||||||
ImGui.PushTextWrapPos(viewportSize.X / 3.0f);
|
/// <inheritdoc/>
|
||||||
|
public IActiveNotification AddNotification(Notification notification)
|
||||||
var wasTitleRendered = false;
|
{
|
||||||
|
var an = this.notificationManagerService.AddNotification(notification, this.localPlugin);
|
||||||
if (!tn.Icon.IsNullOrEmpty())
|
_ = this.notifications.TryAdd(an, 0);
|
||||||
{
|
an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _);
|
||||||
wasTitleRendered = true;
|
return an;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Container class for notifications.
|
public void Dispose()
|
||||||
/// </summary>
|
|
||||||
internal class Notification
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
while (!this.notifications.IsEmpty)
|
||||||
/// Possible notification phases.
|
|
||||||
/// </summary>
|
|
||||||
internal enum Phase
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
foreach (var n in this.notifications.Keys)
|
||||||
/// Phase indicating fade-in.
|
|
||||||
/// </summary>
|
|
||||||
FadeIn,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Phase indicating waiting until fade-out.
|
|
||||||
/// </summary>
|
|
||||||
Wait,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Phase indicating fade-out.
|
|
||||||
/// </summary>
|
|
||||||
FadeOut,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Phase indicating that the notification has expired.
|
|
||||||
/// </summary>
|
|
||||||
Expired,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the notification.
|
|
||||||
/// </summary>
|
|
||||||
internal NotificationType NotificationType { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the title of the notification.
|
|
||||||
/// </summary>
|
|
||||||
internal string? Title { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the content of the notification.
|
|
||||||
/// </summary>
|
|
||||||
internal string Content { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the duration of the notification in milliseconds.
|
|
||||||
/// </summary>
|
|
||||||
internal uint DurationMs { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the creation time of the notification.
|
|
||||||
/// </summary>
|
|
||||||
internal DateTime CreationTime { get; init; } = DateTime.Now;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the default color of the notification.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="NotificationType"/> is set to an out-of-range value.</exception>
|
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the icon of the notification.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="NotificationType"/> is set to an out-of-range value.</exception>
|
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the default title of the notification.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="NotificationType"/> is set to an out-of-range value.</exception>
|
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the elapsed time since creating the notification.
|
|
||||||
/// </summary>
|
|
||||||
internal TimeSpan ElapsedTime => DateTime.Now - this.CreationTime;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the phase of the notification.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The phase of the notification.</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the opacity of the notification.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The opacity, in a range from 0 to 1.</returns>
|
|
||||||
internal float GetFadePercent()
|
|
||||||
{
|
|
||||||
var phase = this.GetPhase();
|
|
||||||
var elapsed = this.ElapsedTime.TotalMilliseconds;
|
|
||||||
|
|
||||||
if (phase == Phase.FadeIn)
|
|
||||||
{
|
{
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,32 +44,66 @@ internal class ImGuiWidget : IDataWindowWidget
|
||||||
|
|
||||||
if (ImGui.Button("Add random notification"))
|
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.";
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -9,12 +10,14 @@ using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.Gui;
|
using Dalamud.Game.Gui;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
using Dalamud.Interface.GameFonts;
|
using Dalamud.Interface.GameFonts;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||||
using Dalamud.Interface.Internal.Notifications;
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
using Dalamud.Interface.ManagedFontAtlas;
|
using Dalamud.Interface.ManagedFontAtlas;
|
||||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||||
using Dalamud.Plugin.Internal.Types;
|
using Dalamud.Plugin.Internal.Types;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using ImGuiScene;
|
using ImGuiScene;
|
||||||
|
|
@ -29,11 +32,13 @@ namespace Dalamud.Interface;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UiBuilder : IDisposable
|
public sealed class UiBuilder : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly LocalPlugin localPlugin;
|
||||||
private readonly Stopwatch stopwatch;
|
private readonly Stopwatch stopwatch;
|
||||||
private readonly HitchDetector hitchDetector;
|
private readonly HitchDetector hitchDetector;
|
||||||
private readonly string namespaceName;
|
private readonly string namespaceName;
|
||||||
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
|
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
|
||||||
private readonly Framework framework = Service<Framework>.Get();
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
private readonly ConcurrentDictionary<IActiveNotification, int> notifications = new();
|
||||||
|
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||||
|
|
@ -52,8 +57,10 @@ public sealed class UiBuilder : IDisposable
|
||||||
/// You do not have to call this manually.
|
/// You do not have to call this manually.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="namespaceName">The plugin namespace.</param>
|
/// <param name="namespaceName">The plugin namespace.</param>
|
||||||
internal UiBuilder(string namespaceName)
|
/// <param name="localPlugin">The relevant local plugin.</param>
|
||||||
|
internal UiBuilder(string namespaceName, LocalPlugin localPlugin)
|
||||||
{
|
{
|
||||||
|
this.localPlugin = localPlugin;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.stopwatch = new Stopwatch();
|
this.stopwatch = new Stopwatch();
|
||||||
|
|
@ -556,22 +563,46 @@ public sealed class UiBuilder : IDisposable
|
||||||
/// <param name="title">The title of the notification.</param>
|
/// <param name="title">The title of the notification.</param>
|
||||||
/// <param name="type">The type of the notification.</param>
|
/// <param name="type">The type of the notification.</param>
|
||||||
/// <param name="msDelay">The time the notification should be displayed for.</param>
|
/// <param name="msDelay">The time the notification should be displayed for.</param>
|
||||||
public void AddNotification(
|
[Obsolete($"Use {nameof(INotificationManager)}.", false)]
|
||||||
string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000)
|
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||||
|
public async void AddNotification(
|
||||||
|
string content,
|
||||||
|
string? title = null,
|
||||||
|
NotificationType type = NotificationType.None,
|
||||||
|
uint msDelay = 3000)
|
||||||
{
|
{
|
||||||
Service<NotificationManager>
|
var nm = await Service<NotificationManager>.GetAsync();
|
||||||
.GetAsync()
|
var an = nm.AddNotification(
|
||||||
.ContinueWith(task =>
|
new()
|
||||||
{
|
{
|
||||||
if (task.IsCompletedSuccessfully)
|
Content = content,
|
||||||
task.Result.AddNotification(content, title, type, msDelay);
|
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 _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unregister the UiBuilder. Do not call this in plugin code.
|
/// Unregister the UiBuilder. Do not call this in plugin code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Open the registered configuration UI, if it exists.
|
/// Open the registered configuration UI, if it exists.
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
||||||
var dataManager = Service<DataManager>.Get();
|
var dataManager = Service<DataManager>.Get();
|
||||||
var localization = Service<Localization>.Get();
|
var localization = Service<Localization>.Get();
|
||||||
|
|
||||||
this.UiBuilder = new UiBuilder(plugin.Name);
|
this.UiBuilder = new UiBuilder(plugin.Name, plugin);
|
||||||
|
|
||||||
this.configs = Service<PluginManager>.Get().PluginConfigs;
|
this.configs = Service<PluginManager>.Get().PluginConfigs;
|
||||||
this.Reason = reason;
|
this.Reason = reason;
|
||||||
|
|
|
||||||
16
Dalamud/Plugin/Services/INotificationManager.cs
Normal file
16
Dalamud/Plugin/Services/INotificationManager.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
||||||
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manager for notifications provided by Dalamud using ImGui.
|
||||||
|
/// </summary>
|
||||||
|
public interface INotificationManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">The new notification.</param>
|
||||||
|
/// <returns>The added notification.</returns>
|
||||||
|
IActiveNotification AddNotification(Notification notification);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue