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.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.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class NotificationManager : IServiceType
|
||||
internal class NotificationManager : INotificationManager, IServiceType, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Value indicating the bottom-left X padding.
|
||||
/// </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();
|
||||
private readonly List<ActiveNotification> notifications = new();
|
||||
private readonly ConcurrentBag<ActiveNotification> 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 })));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
|
@ -67,252 +85,77 @@ internal class NotificationManager : IServiceType
|
|||
/// <param name="content">The content of the notification.</param>
|
||||
/// <param name="title">The title 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(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,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Draw all currently queued notifications.
|
||||
/// </summary>
|
||||
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;
|
||||
/// <summary>
|
||||
/// 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();
|
||||
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();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container class for notifications.
|
||||
/// </summary>
|
||||
internal class Notification
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible notification phases.
|
||||
/// </summary>
|
||||
internal enum Phase
|
||||
while (!this.notifications.IsEmpty)
|
||||
{
|
||||
/// <summary>
|
||||
/// 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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// </summary>
|
||||
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<InterfaceManager>.Get();
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
private readonly ConcurrentDictionary<IActiveNotification, int> notifications = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
|
@ -52,8 +57,10 @@ public sealed class UiBuilder : IDisposable
|
|||
/// You do not have to call this manually.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
this.stopwatch = new Stopwatch();
|
||||
|
|
@ -556,22 +563,46 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <param name="title">The title 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(
|
||||
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<NotificationManager>
|
||||
.GetAsync()
|
||||
.ContinueWith(task =>
|
||||
var nm = await Service<NotificationManager>.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 _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister the UiBuilder. Do not call this in plugin code.
|
||||
/// </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>
|
||||
/// Open the registered configuration UI, if it exists.
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
var dataManager = Service<DataManager>.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.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