Add progressbar

This commit is contained in:
Soreepeong 2024-02-25 23:42:01 +09:00
parent 199722d29a
commit 04c6be5671
6 changed files with 698 additions and 286 deletions

View file

@ -1,28 +1,24 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification;
/// <summary> /// <summary>Represents an active notification.</summary>
/// Represents an active notification.
/// </summary>
public interface IActiveNotification : INotification public interface IActiveNotification : INotification
{ {
/// <summary> /// <summary>The counter for <see cref="Id"/> field.</summary>
/// The counter for <see cref="Id"/> field.
/// </summary>
private static long idCounter; private static long idCounter;
/// <summary> /// <summary>Invoked upon dismissing the notification.</summary>
/// Invoked upon dismissing the notification. /// <remarks>The event callback will not be called,
/// </summary> /// if a user interacts with the notification after the plugin is unloaded.</remarks>
/// <remarks>
/// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded.
/// </remarks>
event NotificationDismissedDelegate Dismiss; event NotificationDismissedDelegate Dismiss;
/// <summary> /// <summary>Invoked upon clicking on the notification.</summary>
/// Invoked upon clicking on the notification.
/// </summary>
/// <remarks> /// <remarks>
/// This event is not applicable when <see cref="INotification.Interactible"/> is set to <c>false</c>. /// 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. /// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
@ -30,9 +26,7 @@ public interface IActiveNotification : INotification
/// </remarks> /// </remarks>
event Action<IActiveNotification> Click; event Action<IActiveNotification> Click;
/// <summary> /// <summary>Invoked when the mouse enters the notification window.</summary>
/// Invoked when the mouse enters the notification window.
/// </summary>
/// <remarks> /// <remarks>
/// This event is applicable regardless of <see cref="INotification.Interactible"/>. /// 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. /// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
@ -40,9 +34,7 @@ public interface IActiveNotification : INotification
/// </remarks> /// </remarks>
event Action<IActiveNotification> MouseEnter; event Action<IActiveNotification> MouseEnter;
/// <summary> /// <summary>Invoked when the mouse leaves the notification window.</summary>
/// Invoked when the mouse leaves the notification window.
/// </summary>
/// <remarks> /// <remarks>
/// This event is applicable regardless of <see cref="INotification.Interactible"/>. /// 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. /// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
@ -50,9 +42,7 @@ public interface IActiveNotification : INotification
/// </remarks> /// </remarks>
event Action<IActiveNotification> MouseLeave; event Action<IActiveNotification> MouseLeave;
/// <summary> /// <summary>Invoked upon drawing the action bar of the notification.</summary>
/// Invoked upon drawing the action bar of the notification.
/// </summary>
/// <remarks> /// <remarks>
/// This event is applicable regardless of <see cref="INotification.Interactible"/>. /// 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. /// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
@ -60,50 +50,60 @@ public interface IActiveNotification : INotification
/// </remarks> /// </remarks>
event Action<IActiveNotification> DrawActions; event Action<IActiveNotification> DrawActions;
/// <summary> /// <inheritdoc cref="INotification.Content"/>
/// Gets the ID of this notification. new string Content { get; set; }
/// </summary>
/// <inheritdoc cref="INotification.Title"/>
new string? Title { get; set; }
/// <inheritdoc cref="INotification.Type"/>
new NotificationType Type { get; set; }
/// <inheritdoc cref="INotification.IconCreator"/>
new Func<Task<object>>? IconCreator { get; set; }
/// <inheritdoc cref="INotification.Expiry"/>
new DateTime Expiry { get; set; }
/// <inheritdoc cref="INotification.Interactible"/>
new bool Interactible { get; set; }
/// <inheritdoc cref="INotification.HoverExtendDuration"/>
new TimeSpan HoverExtendDuration { get; set; }
/// <inheritdoc cref="INotification.Progress"/>
new float Progress { get; set; }
/// <summary>Gets the ID of this notification.</summary>
long Id { get; } long Id { get; }
/// <summary> /// <summary>Gets a value indicating whether the mouse cursor is on the notification window.</summary>
/// Gets a value indicating whether the mouse cursor is on the notification window.
/// </summary>
bool IsMouseHovered { get; } bool IsMouseHovered { get; }
/// <summary> /// <summary>Gets a value indicating whether the notification has been dismissed.</summary>
/// Gets a value indicating whether the notification has been dismissed. /// <remarks>This includes when the hide animation is being played.</remarks>
/// This includes when the hide animation is being played.
/// </summary>
bool IsDismissed { get; } bool IsDismissed { get; }
/// <summary> /// <summary>Clones this notification as a <see cref="Notification"/>.</summary>
/// Clones this notification as a <see cref="Notification"/>.
/// </summary>
/// <returns>A new instance of <see cref="Notification"/>.</returns> /// <returns>A new instance of <see cref="Notification"/>.</returns>
Notification CloneNotification(); Notification CloneNotification();
/// <summary> /// <summary>Dismisses this notification.</summary>
/// Dismisses this notification.
/// </summary>
void DismissNow(); void DismissNow();
/// <summary> /// <summary>Updates the notification data.</summary>
/// Updates the notification data.
/// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="UpdateIcon"/> to update the icon using the new <see cref="INotification.IconCreator"/>. /// Call <see cref="UpdateIcon"/> to update the icon using the new <see cref="INotification.IconCreator"/>.
/// If <see cref="IsDismissed"/> is <c>true</c>, then this function is a no-op.
/// </remarks> /// </remarks>
/// <param name="newNotification">The new notification entry.</param> /// <param name="newNotification">The new notification entry.</param>
void Update(INotification newNotification); void Update(INotification newNotification);
/// <summary> /// <summary>Loads the icon again using <see cref="INotification.IconCreator"/>.</summary>
/// Loads the icon again using <see cref="INotification.IconCreator"/>. /// <remarks>If <see cref="IsDismissed"/> is <c>true</c>, then this function is a no-op.</remarks>
/// </summary>
void UpdateIcon(); void UpdateIcon();
/// <summary> /// <summary>Generates a new value to use for <see cref="Id"/>.</summary>
/// Generates a new value to use for <see cref="Id"/>.
/// </summary>
/// <returns>The new value.</returns> /// <returns>The new value.</returns>
internal static long CreateNewId() => Interlocked.Increment(ref idCounter); internal static long CreateNewId() => Interlocked.Increment(ref idCounter);
} }

View file

@ -6,31 +6,21 @@ using Dalamud.Interface.Internal.Notifications;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification;
/// <summary> /// <summary>Represents a notification.</summary>
/// Represents a notification.
/// </summary>
public interface INotification public interface INotification
{ {
/// <summary> /// <summary>Gets the content body of the notification.</summary>
/// Gets the content body of the notification.
/// </summary>
string Content { get; } string Content { get; }
/// <summary> /// <summary>Gets the title of the notification.</summary>
/// Gets the title of the notification.
/// </summary>
string? Title { get; } string? Title { get; }
/// <summary> /// <summary>Gets the type of the notification.</summary>
/// Gets the type of the notification.
/// </summary>
NotificationType Type { get; } NotificationType Type { get; }
/// <summary> /// <summary>Gets the icon creator function for the notification.<br />
/// Gets the icon creator function for the notification.<br />
/// Currently <see cref="IDalamudTextureWrap"/>, <see cref="SeIconChar"/>, and <see cref="FontAwesomeIcon"/> types /// Currently <see cref="IDalamudTextureWrap"/>, <see cref="SeIconChar"/>, and <see cref="FontAwesomeIcon"/> types
/// are accepted. /// are accepted.</summary>
/// </summary>
/// <remarks> /// <remarks>
/// The icon created by the task returned will be owned by Dalamud, /// 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 /> /// i.e. it will be <see cref="IDisposable.Dispose"/>d automatically as needed.<br />
@ -41,35 +31,30 @@ public interface INotification
/// </remarks> /// </remarks>
Func<Task<object>>? IconCreator { get; } Func<Task<object>>? IconCreator { get; }
/// <summary> /// <summary>Gets the expiry.</summary>
/// Gets the expiry. /// <remarks>Set to <see cref="DateTime.MaxValue"/> to make the notification not have an expiry time
/// </summary> /// (sticky, indeterminate, permanent, or persistent).</remarks>
DateTime Expiry { get; } DateTime Expiry { get; }
/// <summary> /// <summary>Gets a value indicating whether this notification may be interacted.</summary>
/// Gets a value indicating whether this notification may be interacted.
/// </summary>
/// <remarks> /// <remarks>
/// Set this value to <c>true</c> if you want to respond to user inputs from /// Set this value to <c>true</c> if you want to respond to user inputs from
/// <see cref="IActiveNotification.DrawActions"/>. /// <see cref="IActiveNotification.DrawActions"/>.
/// Note that the close buttons for notifications are always provided and interactible. /// Note that the close buttons for notifications are always provided and interactible.
/// If set to <c>true</c>, then clicking on the notification itself will be interpreted as user-initiated dismissal,
/// unless <see cref="IActiveNotification.Click"/> is set.
/// </remarks> /// </remarks>
bool Interactible { get; } bool Interactible { get; }
/// <summary> /// <summary>Gets the new duration for this notification if mouse cursor is on the notification window.</summary>
/// Gets a value indicating whether clicking on the notification window counts as dismissing the notification.
/// </summary>
/// <remarks> /// <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. /// 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"/>. /// This property is applicable regardless of <see cref="Interactible"/>.
/// </remarks> /// </remarks>
TimeSpan HoverExtendDuration { get; } TimeSpan HoverExtendDuration { get; }
/// <summary>Gets the progress for the progress bar of the notification.
/// The progress should either be in the range between 0 and 1 or be a negative value.
/// Specifying a negative value will show an indeterminate progress bar.</summary>
float Progress { get; }
} }

View file

@ -28,8 +28,8 @@ public sealed record Notification : INotification
public bool Interactible { get; set; } public bool Interactible { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool ClickIsDismiss { get; set; } = true; public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration;
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; public float Progress { get; set; } = 1f;
} }

View file

@ -20,19 +20,25 @@ using Serilog;
namespace Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.Internal.Notifications;
/// <summary> /// <summary>Represents an active notification.</summary>
/// Represents an active notification.
/// </summary>
internal sealed class ActiveNotification : IActiveNotification, IDisposable internal sealed class ActiveNotification : IActiveNotification, IDisposable
{ {
private readonly Notification underlyingNotification;
private readonly Easing showEasing; private readonly Easing showEasing;
private readonly Easing hideEasing; private readonly Easing hideEasing;
private readonly Easing progressEasing;
private Notification underlyingNotification; /// <summary>The progress before for the progress bar animation with <see cref="progressEasing"/>.</summary>
private float progressBefore;
/// <summary> /// <summary>Used for calculating correct dismissal progressbar animation (left edge).</summary>
/// Initializes a new instance of the <see cref="ActiveNotification"/> class. private float prevProgressL;
/// </summary>
/// <summary>Used for calculating correct dismissal progressbar animation (right edge).</summary>
private float prevProgressR;
/// <summary>Initializes a new instance of the <see cref="ActiveNotification"/> class.</summary>
/// <param name="underlyingNotification">The underlying notification.</param> /// <param name="underlyingNotification">The underlying notification.</param>
/// <param name="initiatorPlugin">The initiator plugin. Use <c>null</c> if originated by Dalamud.</param> /// <param name="initiatorPlugin">The initiator plugin. Use <c>null</c> if originated by Dalamud.</param>
public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin)
@ -41,8 +47,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
this.InitiatorPlugin = initiatorPlugin; this.InitiatorPlugin = initiatorPlugin;
this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration);
this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration);
this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration);
this.showEasing.Start(); this.showEasing.Start();
this.progressEasing.Start();
this.UpdateIcon(); this.UpdateIcon();
} }
@ -64,39 +72,111 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public long Id { get; } = IActiveNotification.CreateNewId(); public long Id { get; } = IActiveNotification.CreateNewId();
/// <summary> /// <summary>Gets the time of creating this notification.</summary>
/// Gets the time of creating this notification.
/// </summary>
public DateTime CreatedAt { get; } = DateTime.Now; public DateTime CreatedAt { get; } = DateTime.Now;
/// <summary> /// <summary>Gets the time of starting to count the timer for the expiration.</summary>
/// Gets the time of starting to count the timer for the expiration.
/// </summary>
public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now;
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Content"/>
public string Content => this.underlyingNotification.Content; public string Content
{
get => this.underlyingNotification.Content;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Content = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Title"/>
public string? Title => this.underlyingNotification.Title; public string? Title
{
get => this.underlyingNotification.Title;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Title = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Type"/>
public NotificationType Type => this.underlyingNotification.Type; public NotificationType Type
{
get => this.underlyingNotification.Type;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Type = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.IconCreator"/>
public Func<Task<object>>? IconCreator => this.underlyingNotification.IconCreator; public Func<Task<object>>? IconCreator
{
get => this.underlyingNotification.IconCreator;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.IconCreator = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Expiry"/>
public DateTime Expiry => this.underlyingNotification.Expiry; public DateTime Expiry
{
get => this.underlyingNotification.Expiry;
set
{
if (this.underlyingNotification.Expiry == value || this.IsDismissed)
return;
this.underlyingNotification.Expiry = value;
this.ExpiryRelativeToTime = DateTime.Now;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Interactible"/>
public bool Interactible => this.underlyingNotification.Interactible; public bool Interactible
{
get => this.underlyingNotification.Interactible;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Interactible = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.HoverExtendDuration"/>
public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss; public TimeSpan HoverExtendDuration
{
get => this.underlyingNotification.HoverExtendDuration;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.HoverExtendDuration = value;
}
}
/// <inheritdoc/> /// <inheritdoc cref="IActiveNotification.Progress"/>
public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration; public float Progress
{
get => this.underlyingNotification.Progress;
set
{
if (this.IsDismissed)
return;
this.progressBefore = this.ProgressEased;
this.underlyingNotification.Progress = value;
this.progressEasing.Restart();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMouseHovered { get; private set; } public bool IsMouseHovered { get; private set; }
@ -104,19 +184,32 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public bool IsDismissed => this.hideEasing.IsRunning; public bool IsDismissed => this.hideEasing.IsRunning;
/// <summary> /// <summary>Gets a value indicating whether <see cref="InitiatorPlugin"/> has been unloaded.</summary>
/// Gets or sets the plugin that initiated this notification. public bool IsInitiatorUnloaded { get; private set; }
/// </summary>
/// <summary>Gets or sets the plugin that initiated this notification.</summary>
public LocalPlugin? InitiatorPlugin { get; set; } public LocalPlugin? InitiatorPlugin { get; set; }
/// <summary> /// <summary>Gets or sets the icon of this notification.</summary>
/// Gets or sets the icon of this notification.
/// </summary>
public Task<object>? IconTask { get; set; } public Task<object>? IconTask { get; set; }
/// <summary> /// <summary>Gets the eased progress.</summary>
/// Gets the default color of the notification. private float ProgressEased
/// </summary> {
get
{
if (this.Progress < 0)
return 0f;
if (Math.Abs(this.Progress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone)
return this.Progress;
var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
return this.progressBefore + (state * (this.Progress - this.progressBefore));
}
}
/// <summary>Gets the default color of the notification.</summary>
private Vector4 DefaultIconColor => this.Type switch private Vector4 DefaultIconColor => this.Type switch
{ {
NotificationType.None => ImGuiColors.DalamudWhite, NotificationType.None => ImGuiColors.DalamudWhite,
@ -127,9 +220,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
_ => ImGuiColors.DalamudWhite, _ => ImGuiColors.DalamudWhite,
}; };
/// <summary> /// <summary>Gets the default icon of the notification.</summary>
/// Gets the default icon of the notification.
/// </summary>
private string? DefaultIconString => this.Type switch private string? DefaultIconString => this.Type switch
{ {
NotificationType.None => null, NotificationType.None => null,
@ -140,9 +231,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
_ => null, _ => null,
}; };
/// <summary> /// <summary>Gets the default title of the notification.</summary>
/// Gets the default title of the notification.
/// </summary>
private string? DefaultTitle => this.Type switch private string? DefaultTitle => this.Type switch
{ {
NotificationType.None => null, NotificationType.None => null,
@ -153,6 +242,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
_ => null, _ => null,
}; };
/// <summary>Gets the string for the initiator field.</summary>
private string InitiatorString =>
this.InitiatorPlugin is not { } initiatorPlugin
? NotificationConstants.DefaultInitiator
: this.IsInitiatorUnloaded
? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name)
: initiatorPlugin.Name;
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
@ -170,9 +267,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical);
/// <summary> /// <summary>Dismisses this notification. Multiple calls will be ignored.</summary>
/// Dismisses this notification. Multiple calls will be ignored.
/// </summary>
/// <param name="reason">The reason of dismissal.</param> /// <param name="reason">The reason of dismissal.</param>
public void DismissNow(NotificationDismissReason reason) public void DismissNow(NotificationDismissReason reason)
{ {
@ -192,20 +287,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
} }
} }
/// <summary> /// <summary>Updates animations.</summary>
/// Updates animations.
/// </summary>
/// <returns><c>true</c> if the notification is over.</returns> /// <returns><c>true</c> if the notification is over.</returns>
public bool UpdateAnimations() public bool UpdateAnimations()
{ {
this.showEasing.Update(); this.showEasing.Update();
this.hideEasing.Update(); this.hideEasing.Update();
this.progressEasing.Update();
return this.hideEasing.IsRunning && this.hideEasing.IsDone; return this.hideEasing.IsRunning && this.hideEasing.IsDone;
} }
/// <summary> /// <summary>Draws this notification.</summary>
/// Draws this notification.
/// </summary>
/// <param name="maxWidth">The maximum width of the notification window.</param> /// <param name="maxWidth">The maximum width of the notification window.</param>
/// <param name="offsetY">The offset from the bottom.</param> /// <param name="offsetY">The offset from the bottom.</param>
/// <returns>The height of the notification.</returns> /// <returns>The height of the notification.</returns>
@ -230,13 +322,29 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
var notificationManager = Service<NotificationManager>.Get(); var notificationManager = Service<NotificationManager>.Get();
var interfaceManager = Service<InterfaceManager>.Get(); var interfaceManager = Service<InterfaceManager>.Get();
var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; var unboundedWidth = ImGui.CalcTextSize(this.Content).X;
float closeButtonHorizontalSpaceReservation;
using (interfaceManager.IconFontHandle?.Push())
{
closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X;
closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding;
}
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.InitiatorString).X);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation);
unboundedWidth += NotificationConstants.ScaledWindowPadding * 3;
unboundedWidth += NotificationConstants.ScaledIconSize; unboundedWidth += NotificationConstants.ScaledIconSize;
unboundedWidth += Math.Max(
Math.Max(
ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X,
ImGui.CalcTextSize(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator).X),
ImGui.CalcTextSize(this.Content).X);
var width = Math.Min(maxWidth, unboundedWidth); var width = Math.Min(maxWidth, unboundedWidth);
@ -244,16 +352,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
var viewportPos = viewport.WorkPos; var viewportPos = viewport.WorkPos;
var viewportSize = viewport.WorkSize; 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.PushID(this.Id.GetHashCode());
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding));
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f);
unsafe unsafe
@ -267,67 +366,88 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
NotificationConstants.BackgroundOpacity)); NotificationConstants.BackgroundOpacity));
} }
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(
(viewportPos + viewportSize) -
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
ImGuiCond.Always,
Vector2.One);
ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue));
ImGui.PushStyleVar(
ImGuiStyleVar.WindowPadding,
new Vector2(NotificationConstants.ScaledWindowPadding, 0));
ImGui.Begin( ImGui.Begin(
$"##NotifyWindow{this.Id}", $"##NotifyMainWindow{this.Id}",
ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDecoration |
(this.Interactible ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs) | (this.Interactible
? ImGuiWindowFlags.None
: ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) |
ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoFocusOnAppearing); ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
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);
}
this.DrawNotificationMainWindowContent(notificationManager, width);
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
var hovered = ImGui.IsWindowHovered();
float expiryRatio;
if (this.IsDismissed)
{
expiryRatio = 0f;
}
else if (this.Expiry == DateTime.MaxValue || (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered))
{
expiryRatio = 1f;
}
else
{
expiryRatio = (float)((this.Expiry - DateTime.Now).TotalMilliseconds /
(this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds);
}
expiryRatio = Math.Clamp(expiryRatio, 0f, 1f);
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos + new Vector2(0, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight),
windowPos + windowSize with { X = windowSize.X * expiryRatio },
ImGui.GetColorU32(this.DefaultIconColor));
ImGui.PopClipRect();
ImGui.End(); ImGui.End();
ImGui.PopStyleVar();
if (!this.IsDismissed) offsetY += windowSize.Y;
this.DrawCloseButton(interfaceManager, windowPos);
var actionWindowHeight =
// Content
ImGui.GetTextLineHeight() +
// Top and bottom padding
(NotificationConstants.ScaledWindowPadding * 2);
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(
(viewportPos + viewportSize) -
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
ImGuiCond.Always,
Vector2.One);
ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGui.Begin(
$"##NotifyActionWindow{this.Id}",
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
this.DrawNotificationActionWindowContent(interfaceManager, width);
windowSize.Y += actionWindowHeight;
windowPos.Y -= actionWindowHeight;
hovered |= ImGui.IsWindowHovered();
ImGui.End();
ImGui.PopStyleVar();
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopStyleVar(3); ImGui.PopStyleVar(2);
ImGui.PopID(); ImGui.PopID();
if (hovered)
{
if (this.Click is null)
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
this.DismissNow(NotificationDismissReason.Manual);
}
else
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Right)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
this.Click.InvokeSafely(this);
}
}
if (windowPos.X <= ImGui.GetIO().MousePos.X if (windowPos.X <= ImGui.GetIO().MousePos.X
&& windowPos.Y <= ImGui.GetIO().MousePos.Y && windowPos.Y <= ImGui.GetIO().MousePos.Y
&& ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X
@ -361,31 +481,28 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public void Update(INotification newNotification) public void Update(INotification newNotification)
{ {
this.underlyingNotification.Content = newNotification.Content; if (this.IsDismissed)
this.underlyingNotification.Title = newNotification.Title; return;
this.underlyingNotification.Type = newNotification.Type; this.Content = newNotification.Content;
this.underlyingNotification.IconCreator = newNotification.IconCreator; this.Title = newNotification.Title;
if (this.underlyingNotification.Expiry != newNotification.Expiry) this.Type = newNotification.Type;
{ this.IconCreator = newNotification.IconCreator;
this.underlyingNotification.Expiry = newNotification.Expiry; this.Expiry = newNotification.Expiry;
this.ExpiryRelativeToTime = DateTime.Now; this.Interactible = newNotification.Interactible;
} this.HoverExtendDuration = newNotification.HoverExtendDuration;
this.Progress = newNotification.Progress;
this.underlyingNotification.Interactible = newNotification.Interactible;
this.underlyingNotification.ClickIsDismiss = newNotification.ClickIsDismiss;
this.underlyingNotification.HoverExtendDuration = newNotification.HoverExtendDuration;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void UpdateIcon() public void UpdateIcon()
{ {
if (this.IsDismissed)
return;
this.ClearIconTask(); this.ClearIconTask();
this.IconTask = this.IconCreator?.Invoke(); this.IconTask = this.IconCreator?.Invoke();
} }
/// <summary> /// <summary>Removes non-Dalamud invocation targets from events.</summary>
/// Removes non-Dalamud invocation targets from events.
/// </summary>
public void RemoveNonDalamudInvocations() public void RemoveNonDalamudInvocations()
{ {
var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly);
@ -395,6 +512,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter);
this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave);
this.underlyingNotification.Interactible = false;
this.IsInitiatorUnloaded = true;
var now = DateTime.Now;
var newMaxExpiry = now + NotificationConstants.DefaultDisplayDuration;
if (this.underlyingNotification.Expiry > newMaxExpiry)
{
this.underlyingNotification.Expiry = newMaxExpiry;
this.ExpiryRelativeToTime = now;
}
return; return;
T? RemoveNonDalamudInvocationsCore<T>(T? @delegate) where T : Delegate T? RemoveNonDalamudInvocationsCore<T>(T? @delegate) where T : Delegate
@ -426,6 +554,84 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
this.IconTask = null; this.IconTask = null;
} }
private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width)
{
var basePos = ImGui.GetCursorPos();
this.DrawIcon(
notificationManager,
basePos,
basePos + new Vector2(NotificationConstants.ScaledIconSize));
basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding;
width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2);
this.DrawTitle(basePos, basePos + new Vector2(width, 0));
basePos.Y = ImGui.GetCursorPosY();
this.DrawContentBody(basePos, basePos + new Vector2(width, 0));
// Intention was to have left, right, and bottom have the window padding and top have the component gap,
// but as ImGui only allows horz/vert padding, we add the extra bottom padding.
// Top padding is zero, as the action window will add the padding.
ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding));
float progressL, progressR;
if (this.IsDismissed)
{
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f;
progressL = midpoint - (length * v);
progressR = midpoint + (length * v);
}
else if (this.Expiry == DateTime.MaxValue)
{
if (this.Progress >= 0)
{
progressL = 0f;
progressR = this.ProgressEased;
}
else
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.IndeterminateProgressbarLoopDuration) /
NotificationConstants.IndeterminateProgressbarLoopDuration);
progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3);
progressR = Math.Min(elapsed, 2f / 3) / (2f / 3);
progressL = MathF.Pow(progressL, 3);
progressR = 1f - MathF.Pow(1f - progressR, 3);
}
this.prevProgressL = progressL;
this.prevProgressR = progressR;
}
else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)
{
progressL = 0f;
progressR = 1f;
this.prevProgressL = progressL;
this.prevProgressR = progressR;
}
else
{
progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds /
(this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds);
progressR = 1f;
this.prevProgressL = progressL;
this.prevProgressR = progressR;
}
progressR = Math.Clamp(progressR, 0f, 1f);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos + new Vector2(
windowSize.X * progressL,
windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight),
windowPos + windowSize with { X = windowSize.X * progressR },
ImGui.GetColorU32(this.DefaultIconColor));
ImGui.PopClipRect();
}
private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord)
{ {
string? iconString = null; string? iconString = null;
@ -486,8 +692,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
ImGui.SetCursorPos(pos); ImGui.SetCursorPos(pos);
ImGui.Image(iconTexture.ImGuiHandle, size); ImGui.Image(iconTexture.ImGuiHandle, size);
} }
else if (fontHandle is not null) else
{ {
// Just making it extremely sure
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (fontHandle is null || iconString is null)
// ReSharper disable once HeuristicUnreachableCode
return;
using (fontHandle.Push()) using (fontHandle.Push())
{ {
var size = ImGui.CalcTextSize(iconString); var size = ImGui.CalcTextSize(iconString);
@ -514,47 +726,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor);
ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() });
ImGui.TextUnformatted(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator); ImGui.TextUnformatted(this.InitiatorString);
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopTextWrapPos(); ImGui.PopTextWrapPos();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap);
} }
private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 screenCoord)
{
using (interfaceManager.IconFontHandle?.Push())
{
var str = FontAwesomeIcon.Times.ToIconString();
var size = NotificationConstants.ScaledCloseButtonMinSize;
var textSize = ImGui.CalcTextSize(str);
size = Math.Max(size, Math.Max(textSize.X, textSize.Y));
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f);
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGui.PushStyleColor(ImGuiCol.Button, 0);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor);
// ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(screenCoord, ImGuiCond.Always, new(1, 0));
ImGui.SetNextWindowSizeConstraints(new(size), new(size));
ImGui.Begin(
$"##CloseButtonWindow{this.Id}",
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBringToFrontOnFocus |
ImGuiWindowFlags.NoFocusOnAppearing);
if (ImGui.Button(str, new(size)))
this.DismissNow();
ImGui.End();
ImGui.PopStyleColor(2);
ImGui.PopStyleVar(3);
}
}
private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord)
{ {
ImGui.SetCursorPos(minCoord); ImGui.SetCursorPos(minCoord);
@ -576,4 +754,44 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable
} }
} }
} }
private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width)
{
ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding));
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(
this.IsMouseHovered
? this.CreatedAt.FormatAbsoluteDateTime()
: this.CreatedAt.FormatRelativeDateTime());
ImGui.PopStyleColor();
this.DrawCloseButton(
interfaceManager,
new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding),
NotificationConstants.ScaledWindowPadding);
}
private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad)
{
using (interfaceManager.IconFontHandle?.Push())
{
var str = FontAwesomeIcon.Times.ToIconString();
var textSize = ImGui.CalcTextSize(str);
var size = Math.Max(textSize.X, textSize.Y);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
if (!this.IsMouseHovered)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f);
ImGui.PushStyleColor(ImGuiCol.Button, 0);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor);
ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad));
if (ImGui.Button(str, new(size + (pad * 2))))
this.DismissNow();
ImGui.PopStyleColor(2);
if (!this.IsMouseHovered)
ImGui.PopStyleVar();
ImGui.PopStyleVar();
}
}
} }

View file

@ -9,7 +9,9 @@ namespace Dalamud.Interface.Internal.Notifications;
/// </summary> /// </summary>
internal static class NotificationConstants internal static class NotificationConstants
{ {
// ..............................[X] // .............................[..]
// ..when.......................[XX]
// .. ..
// ..[i]..title title title title .. // ..[i]..title title title title ..
// .. by this_plugin .. // .. by this_plugin ..
// .. .. // .. ..
@ -28,6 +30,9 @@ internal static class NotificationConstants
/// <summary>The background opacity of a notification window.</summary> /// <summary>The background opacity of a notification window.</summary>
public const float BackgroundOpacity = 0.82f; public const float BackgroundOpacity = 0.82f;
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
public const float IndeterminateProgressbarLoopDuration = 2000f;
/// <summary>Duration of show animation.</summary> /// <summary>Duration of show animation.</summary>
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
@ -40,6 +45,12 @@ internal static class NotificationConstants
/// <summary>Duration of hide animation.</summary> /// <summary>Duration of hide animation.</summary>
public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of hide animation.</summary>
public static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200);
/// <summary>Text color for the when.</summary>
public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f);
/// <summary>Text color for the close button [X].</summary> /// <summary>Text color for the close button [X].</summary>
public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f);
@ -52,6 +63,21 @@ internal static class NotificationConstants
/// <summary>Text color for the body.</summary> /// <summary>Text color for the body.</summary>
public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f);
/// <summary>Gets the relative time format strings.</summary>
private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings =
{
(TimeSpan.FromDays(7), null),
(TimeSpan.FromDays(2), "{0:%d} days ago"),
(TimeSpan.FromDays(1), "yesterday"),
(TimeSpan.FromHours(2), "{0:%h} hours ago"),
(TimeSpan.FromHours(1), "an hour ago"),
(TimeSpan.FromMinutes(2), "{0:%m} minutes ago"),
(TimeSpan.FromMinutes(1), "a minute ago"),
(TimeSpan.FromSeconds(2), "{0:%s} seconds ago"),
(TimeSpan.FromSeconds(1), "a second ago"),
(TimeSpan.MinValue, "just now"),
};
/// <summary>Gets the scaled padding of the window (dot(.) in the above diagram).</summary> /// <summary>Gets the scaled padding of the window (dot(.) in the above diagram).</summary>
public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
@ -69,9 +95,36 @@ internal static class NotificationConstants
/// <summary>Gets the scaled size of the icon.</summary> /// <summary>Gets the scaled size of the icon.</summary>
public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); 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);
/// <summary>Gets the height of the expiry progress bar.</summary> /// <summary>Gets the height of the expiry progress bar.</summary>
public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the string format of the initiator name field, if the initiator is unloaded.</summary>
public static string UnloadedInitiatorNameFormat => "{0} (unloaded)";
/// <summary>
/// Formats an instance of <see cref="DateTime"/> as a relative time.
/// </summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
public static string FormatRelativeDateTime(this DateTime when)
{
var ts = DateTime.Now - when;
foreach (var (minSpan, formatString) in RelativeFormatStrings)
{
if (ts < minSpan)
continue;
if (formatString is null)
break;
return string.Format(formatString, ts);
}
return when.FormatAbsoluteDateTime();
}
/// <summary>
/// Formats an instance of <see cref="DateTime"/> as an absolute time.
/// </summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}";
} }

View file

@ -1,5 +1,8 @@
using Dalamud.Interface.Internal.Notifications; using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using ImGuiNET; using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -9,6 +12,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary> /// </summary>
internal class ImGuiWidget : IDataWindowWidget internal class ImGuiWidget : IDataWindowWidget
{ {
private NotificationTemplate notificationTemplate;
/// <inheritdoc/> /// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "imgui" }; public string[]? CommandShortcuts { get; init; } = { "imgui" };
@ -22,6 +27,7 @@ internal class ImGuiWidget : IDataWindowWidget
public void Load() public void Load()
{ {
this.Ready = true; this.Ready = true;
this.notificationTemplate.Reset();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -38,51 +44,134 @@ internal class ImGuiWidget : IDataWindowWidget
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); ImGui.TextUnformatted(
$"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms");
ImGui.Separator(); ImGui.Separator();
if (ImGui.Button("Add random notification")) ImGui.Checkbox("##manualContent", ref this.notificationTemplate.ManualContent);
{ ImGui.SameLine();
const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; ImGui.InputText("Content##content", ref this.notificationTemplate.Content, 255);
ImGui.Checkbox("##manualTitle", ref this.notificationTemplate.ManualTitle);
ImGui.SameLine();
ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255);
ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType);
ImGui.SameLine();
ImGui.Combo(
"Type##type",
ref this.notificationTemplate.TypeInt,
NotificationTemplate.TypeTitles,
NotificationTemplate.TypeTitles.Length);
ImGui.Combo(
"Duration",
ref this.notificationTemplate.DurationInt,
NotificationTemplate.DurationTitles,
NotificationTemplate.DurationTitles.Length);
ImGui.Combo(
"Progress",
ref this.notificationTemplate.ProgressMode,
NotificationTemplate.ProgressModeTitles,
NotificationTemplate.ProgressModeTitles.Length);
ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible);
ImGui.Checkbox("Action Bar", ref this.notificationTemplate.ActionBar);
if (ImGui.Button("Add notification"))
{
var text =
"Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla.";
NewRandom(out var title, out var type, out var progress);
if (this.notificationTemplate.ManualTitle)
title = this.notificationTemplate.Title;
if (this.notificationTemplate.ManualContent)
text = this.notificationTemplate.Content;
if (this.notificationTemplate.ManualType)
type = (NotificationType)this.notificationTemplate.TypeInt;
var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt];
NewRandom(out var title, out var type);
var n = notifications.AddNotification( var n = notifications.AddNotification(
new() new()
{ {
Content = text, Content = text,
Title = title, Title = title,
Type = type, Type = type,
Interactible = true, Interactible = this.notificationTemplate.Interactible,
ClickIsDismiss = false, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration,
Expiry = DateTime.MaxValue, Progress = this.notificationTemplate.ProgressMode switch
{
0 => 1f,
1 => progress,
2 => 0f,
3 => 0f,
4 => -1f,
_ => 0.5f,
},
}); });
switch (this.notificationTemplate.ProgressMode)
var nclick = 0;
n.Click += _ => nclick++;
n.DrawActions += an =>
{ {
if (ImGui.Button("Update in place")) case 2:
{ Task.Run(
NewRandom(out title, out type); async () =>
an.Update(an.CloneNotification() with { Title = title, Type = type }); {
} for (var i = 0; i <= 10 && !n.IsDismissed; i++)
{
await Task.Delay(500);
n.Progress = i / 10f;
}
});
break;
case 3:
Task.Run(
async () =>
{
for (var i = 0; i <= 10 && !n.IsDismissed; i++)
{
await Task.Delay(500);
n.Progress = i / 10f;
}
if (an.IsMouseHovered) n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration;
});
break;
}
if (this.notificationTemplate.ActionBar)
{
var nclick = 0;
n.Click += _ => nclick++;
n.DrawActions += an =>
{ {
if (ImGui.Button("Update in place"))
{
NewRandom(out title, out type, out progress);
an.Title = title;
an.Type = type;
an.Progress = progress;
}
if (an.IsMouseHovered)
{
ImGui.SameLine();
if (ImGui.Button("Dismiss"))
an.DismissNow();
}
ImGui.AlignTextToFramePadding();
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Dismiss")) ImGui.TextUnformatted($"Clicked {nclick} time(s)");
an.DismissNow(); };
} }
ImGui.AlignTextToFramePadding();
ImGui.SameLine();
ImGui.TextUnformatted($"Clicked {nclick} time(s)");
};
} }
} }
private static void NewRandom(out string? title, out NotificationType type) private static void NewRandom(out string? title, out NotificationType type, out float progress)
{ {
var rand = new Random(); var rand = new Random();
@ -106,5 +195,72 @@ internal class ImGuiWidget : IDataWindowWidget
4 => NotificationType.None, 4 => NotificationType.None,
_ => NotificationType.None, _ => NotificationType.None,
}; };
if (rand.Next() % 2 == 0)
progress = -1;
else
progress = rand.NextSingle();
}
private struct NotificationTemplate
{
public static readonly string[] ProgressModeTitles =
{
"Default",
"Random",
"Increasing",
"Increasing & Auto Dismiss",
"Indeterminate",
};
public static readonly string[] TypeTitles =
{
nameof(NotificationType.None),
nameof(NotificationType.Success),
nameof(NotificationType.Warning),
nameof(NotificationType.Error),
nameof(NotificationType.Info),
};
public static readonly string[] DurationTitles =
{
"Infinite",
"1 seconds",
"3 seconds (default)",
"10 seconds",
};
public static readonly TimeSpan[] Durations =
{
TimeSpan.MaxValue,
TimeSpan.FromSeconds(1),
NotificationConstants.DefaultDisplayDuration,
TimeSpan.FromSeconds(10),
};
public bool ManualContent;
public string Content;
public bool ManualTitle;
public string Title;
public bool ManualType;
public int TypeInt;
public int DurationInt;
public bool Interactible;
public bool ActionBar;
public int ProgressMode;
public void Reset()
{
this.ManualContent = false;
this.Content = string.Empty;
this.ManualTitle = false;
this.Title = string.Empty;
this.ManualType = false;
this.TypeInt = (int)NotificationType.None;
this.DurationInt = 2;
this.Interactible = true;
this.ActionBar = true;
this.ProgressMode = 0;
}
} }
} }