This commit is contained in:
Soreepeong 2024-02-27 23:20:08 +09:00
parent e44180d4a2
commit a7d5380796
24 changed files with 1056 additions and 1412 deletions

View file

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents an active notification.</summary> /// <summary>Represents an active notification.</summary>
@ -20,20 +22,6 @@ public interface IActiveNotification : INotification
/// </remarks> /// </remarks>
event Action<IActiveNotification> Click; event Action<IActiveNotification> Click;
/// <summary>Invoked when the mouse enters the notification window.</summary>
/// <remarks>
/// 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>
/// 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> /// <summary>Invoked upon drawing the action bar of the notification.</summary>
/// <remarks> /// <remarks>
/// 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.
@ -44,16 +32,13 @@ public interface IActiveNotification : INotification
/// <summary>Gets the ID of this notification.</summary> /// <summary>Gets the ID of this notification.</summary>
long Id { get; } long Id { get; }
/// <summary>Gets the time of creating this notification.</summary>
DateTime CreatedAt { get; }
/// <summary>Gets the effective expiry time.</summary> /// <summary>Gets the effective expiry time.</summary>
/// <remarks>Contains <see cref="DateTime.MaxValue"/> if the notification does not expire.</remarks> /// <remarks>Contains <see cref="DateTime.MaxValue"/> if the notification does not expire.</remarks>
DateTime EffectiveExpiry { get; } DateTime EffectiveExpiry { get; }
/// <summary>Gets a value indicating whether the mouse cursor is on the notification window.</summary>
bool IsHovered { get; }
/// <summary>Gets a value indicating whether the notification window is focused.</summary>
bool IsFocused { get; }
/// <summary>Gets a value indicating whether the notification has been dismissed.</summary> /// <summary>Gets a value indicating whether the notification has been dismissed.</summary>
/// <remarks>This includes when the hide animation is being played.</remarks> /// <remarks>This includes when the hide animation is being played.</remarks>
bool IsDismissed { get; } bool IsDismissed { get; }
@ -66,9 +51,16 @@ public interface IActiveNotification : INotification
/// <remarks>This does not override <see cref="INotification.HardExpiry"/>.</remarks> /// <remarks>This does not override <see cref="INotification.HardExpiry"/>.</remarks>
void ExtendBy(TimeSpan extension); void ExtendBy(TimeSpan extension);
/// <summary>Loads the icon again using the same <see cref="INotification.IconSource"/>.</summary> /// <summary>Sets the icon from <see cref="IDalamudTextureWrap"/>, overriding the icon .</summary>
/// <remarks>If <see cref="IsDismissed"/> is <c>true</c>, then this function is a no-op.</remarks> /// <param name="textureWrap">The new texture wrap to use, or null to clear and revert back to the icon specified
void UpdateIcon(); /// from <see cref="INotification.Icon"/>.</param>
/// <remarks>
/// <para>The texture passed will be disposed when the notification is dismissed or a new different texture is set
/// via another call to this function. You do not have to dispose it yourself.</para>
/// <para>If <see cref="IsDismissed"/> is <c>true</c>, then calling this function will simply dispose the passed
/// <paramref name="textureWrap"/> without actually updating the icon.</para>
/// </remarks>
void SetIconTexture(IDalamudTextureWrap? textureWrap);
/// <summary>Generates a new value to use for <see cref="Id"/>.</summary> /// <summary>Generates a new value to use for <see cref="Id"/>.</summary>
/// <returns>The new value.</returns> /// <returns>The new value.</returns>

View file

@ -1,9 +1,10 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents a notification.</summary> /// <summary>Represents a notification.</summary>
public interface INotification : IDisposable public interface INotification
{ {
/// <summary>Gets or sets the content body of the notification.</summary> /// <summary>Gets or sets the content body of the notification.</summary>
string Content { get; set; } string Content { get; set; }
@ -18,22 +19,13 @@ public interface INotification : IDisposable
NotificationType Type { get; set; } NotificationType Type { get; set; }
/// <summary>Gets or sets the icon source.</summary> /// <summary>Gets or sets the icon source.</summary>
/// <remarks> /// <remarks>Use <see cref="IActiveNotification.SetIconTexture"/> to use a texture, after calling
/// <para>Assigning a new value that does not equal to the previous value will dispose the old value. The ownership /// <see cref="INotificationManager.AddNotification"/>.</remarks>
/// of the new value is transferred to this <see cref="INotification"/>. <b>Even if the assignment throws an INotificationIcon? Icon { get; set; }
/// exception</b>, the ownership is transferred, causing the value to be disposed. Assignment should not throw an
/// exception though, so wrapping the assignment in try...catch block is not required.</para>
/// <para>The assigned value will be disposed upon the <see cref="IDisposable.Dispose"/> call on this instance of
/// <see cref="INotification"/>, unless the same value is assigned, in which case it will do nothing.</para>
/// <para>If this <see cref="INotification"/> is an <see cref="IActiveNotification"/>, then updating this property
/// will change the icon being displayed (calls <see cref="IActiveNotification.UpdateIcon"/>), unless
/// <see cref="IActiveNotification.IsDismissed"/> is <c>true</c>.</para>
/// </remarks>
INotificationIconSource? IconSource { get; set; }
/// <summary>Gets or sets the hard expiry.</summary> /// <summary>Gets or sets the hard expiry.</summary>
/// <remarks> /// <remarks>
/// Setting this value will override <see cref="InitialDuration"/> and <see cref="DurationSinceLastInterest"/>, in that /// Setting this value will override <see cref="InitialDuration"/> and <see cref="ExtensionDurationSinceLastInterest"/>, in that
/// the notification will be dismissed when this expiry expires.<br /> /// the notification will be dismissed when this expiry expires.<br />
/// Set to <see cref="DateTime.MaxValue"/> to make only <see cref="InitialDuration"/> take effect.<br /> /// Set to <see cref="DateTime.MaxValue"/> to make only <see cref="InitialDuration"/> take effect.<br />
/// If neither <see cref="HardExpiry"/> nor <see cref="InitialDuration"/> is not MaxValue, then the notification /// If neither <see cref="HardExpiry"/> nor <see cref="InitialDuration"/> is not MaxValue, then the notification
@ -45,7 +37,8 @@ public interface INotification : IDisposable
/// <summary>Gets or sets the initial duration.</summary> /// <summary>Gets or sets the initial duration.</summary>
/// <remarks>Set to <see cref="TimeSpan.MaxValue"/> to make only <see cref="HardExpiry"/> take effect.</remarks> /// <remarks>Set to <see cref="TimeSpan.MaxValue"/> to make only <see cref="HardExpiry"/> take effect.</remarks>
/// <remarks>Updating this value will reset the dismiss timer.</remarks> /// <remarks>Updating this value will reset the dismiss timer, but the remaining duration will still be calculated
/// based on <see cref="IActiveNotification.CreatedAt"/>.</remarks>
TimeSpan InitialDuration { get; set; } TimeSpan InitialDuration { get; set; }
/// <summary>Gets or sets the new duration for this notification once the mouse cursor leaves the window and the /// <summary>Gets or sets the new duration for this notification once the mouse cursor leaves the window and the
@ -55,7 +48,7 @@ public interface INotification : IDisposable
/// notification or focusing on it will not make the notification stay.<br /> /// notification or focusing on it will not make the notification stay.<br />
/// Updating this value will reset the dismiss timer. /// Updating this value will reset the dismiss timer.
/// </remarks> /// </remarks>
TimeSpan DurationSinceLastInterest { get; set; } TimeSpan ExtensionDurationSinceLastInterest { get; set; }
/// <summary>Gets or sets a value indicating whether to show an indeterminate expiration animation if /// <summary>Gets or sets a value indicating whether to show an indeterminate expiration animation if
/// <see cref="HardExpiry"/> is set to <see cref="DateTime.MaxValue"/>.</summary> /// <see cref="HardExpiry"/> is set to <see cref="DateTime.MaxValue"/>.</summary>

View file

@ -0,0 +1,54 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.Text;
using Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Icon source for <see cref="INotification"/>.</summary>
/// <remarks>Plugins implementing this interface are left to their own on managing the resources contained by the
/// instance of their implementation of <see cref="INotificationIcon"/>. In other words, they should not expect to have
/// <see cref="IDisposable.Dispose"/> called if their implementation is an <see cref="IDisposable"/>. Dalamud will not
/// call <see cref="IDisposable.Dispose"/> on any instance of <see cref="INotificationIcon"/>. On plugin unloads, the
/// icon may be reverted back to the default, if the instance of <see cref="INotificationIcon"/> is not provided by
/// Dalamud.</remarks>
public interface INotificationIcon
{
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an
/// <see cref="SeIconChar"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an
/// <see cref="FontAwesomeIcon"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from a texture
/// file shipped as a part of the game resources.</summary>
/// <param name="gamePath">The path to a texture file in the game virtual file system.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an image
/// file from the file system.</summary>
/// <param name="filePath">The path to an image file in the file system.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath);
/// <summary>Draws the icon.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="color">The foreground color.</param>
/// <returns><c>true</c> if anything has been drawn.</returns>
bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color);
}

View file

@ -1,88 +0,0 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface.ImGuiNotification.Internal.IconSource;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Icon source for <see cref="INotification"/>.</summary>
/// <remarks>Plugins should NOT implement this interface.</remarks>
public interface INotificationIconSource : ICloneable, IDisposable
{
/// <summary>The internal interface.</summary>
internal interface IInternal : INotificationIconSource
{
/// <summary>Materializes the icon resource.</summary>
/// <returns>The materialized resource.</returns>
INotificationMaterializedIcon Materialize();
}
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="SeIconChar"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="FontAwesomeIcon"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="wrap">The texture wrap.</param>
/// <param name="takeOwnership">
/// If <c>true</c>, this class will own the passed <paramref name="wrap"/>, and you <b>must not</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// If <c>false</c>, this class will create a new reference of the passed wrap, and you <b>should</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// In both cases, the returned object must be disposed after use.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown or <paramref name="wrap"/> is <c>null</c>, the default icon will be displayed
/// instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) =>
new TextureWrapIconSource(wrap, takeOwnership);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="Func{TResult}"/> returning a <see cref="Task{TResult}"/> resulting in an
/// <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="wrapTaskFunc">The function that returns a task that results a texture wrap.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown or <paramref name="wrapTaskFunc"/> is <c>null</c>, the default icon will be
/// displayed instead.<br />
/// Use <see cref="Task.FromResult{TResult}"/> if you will have a wrap available without waiting.<br />
/// <paramref name="wrapTaskFunc"/> should not contain a reference to a resource; if it does, the resource will be
/// released when all instances of <see cref="INotificationIconSource"/> derived from the returned object are freed
/// by the garbage collector, which will result in non-deterministic resource releases.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(Func<Task<IDalamudTextureWrap?>?>? wrapTaskFunc) =>
new TextureWrapTaskIconSource(wrapTaskFunc);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from a texture
/// file shipped as a part of the game resources.</summary>
/// <param name="gamePath">The path to a texture file in the game virtual file system.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an image
/// file from the file system.</summary>
/// <param name="filePath">The path to an image file in the file system.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath);
/// <inheritdoc cref="ICloneable.Clone"/>
new INotificationIconSource Clone();
/// <inheritdoc/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,16 +0,0 @@
using System.Numerics;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents a materialized icon.</summary>
internal interface INotificationMaterializedIcon : IDisposable
{
/// <summary>Draws the icon.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="color">The foreground color.</param>
/// <param name="initiatorPlugin">The initiator plugin.</param>
void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin);
}

View file

@ -0,0 +1,494 @@
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Represents an active notification.</summary>
internal sealed partial class ActiveNotification
{
/// <summary>Draws this notification.</summary>
/// <param name="width">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 width, float offsetY)
{
var opacity =
Math.Clamp(
(float)(this.hideEasing.IsRunning
? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value)
: (this.showEasing.IsDone ? 1 : this.showEasing.Value)),
0f,
1f);
if (opacity <= 0)
return 0;
var actionWindowHeight =
// Content
ImGui.GetTextLineHeight() +
// Top and bottom padding
(NotificationConstants.ScaledWindowPadding * 2);
var viewport = ImGuiHelpers.MainViewport;
var viewportPos = viewport.WorkPos;
var viewportSize = viewport.WorkSize;
ImGui.PushID(this.Id.GetHashCode());
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f);
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding));
unsafe
{
ImGui.PushStyleColor(
ImGuiCol.WindowBg,
*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4(
1f,
1f,
1f,
NotificationConstants.BackgroundOpacity));
}
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(
(viewportPos + viewportSize) -
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
ImGuiCond.Always,
Vector2.One);
ImGui.SetNextWindowSizeConstraints(
new(width, actionWindowHeight),
new(
width,
!this.underlyingNotification.Minimized || this.expandoEasing.IsRunning
? float.MaxValue
: actionWindowHeight));
ImGui.Begin(
$"##NotifyMainWindow{this.Id}",
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput;
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
var warrantsExtension =
this.ExtensionDurationSinceLastInterest > TimeSpan.Zero
&& (isHovered || isTakingKeyboardInput);
this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension);
if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry)
this.DismissNow(NotificationDismissReason.Timeout);
if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension)
this.lastInterestTime = DateTime.Now;
this.DrawWindowBackgroundProgressBar();
this.DrawTopBar(width, actionWindowHeight, isHovered);
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
{
this.DrawContentArea(width, actionWindowHeight);
}
else if (this.expandoEasing.IsRunning)
{
if (this.underlyingNotification.Minimized)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value));
else
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value);
this.DrawContentArea(width, actionWindowHeight);
ImGui.PopStyleVar();
}
if (isTakingKeyboardInput)
this.DrawKeyboardInputIndicator();
this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension);
if (ImGui.IsWindowHovered())
{
if (this.Click is null)
{
if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
this.DismissNow(NotificationDismissReason.Manual);
}
else
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Right)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
this.Click.InvokeSafely(this);
}
}
var windowSize = ImGui.GetWindowSize();
ImGui.End();
ImGui.PopStyleColor();
ImGui.PopStyleVar(3);
ImGui.PopID();
return windowSize.Y;
}
/// <summary>Calculates the effective expiry, taking ImGui window state into account.</summary>
/// <param name="warrantsExtension">Notification will not dismiss while this paramter is <c>true</c>.</param>
/// <returns>The calculated effective expiry.</returns>
/// <remarks>Expected to be called BETWEEN <see cref="ImGui.Begin(string)"/> and <see cref="ImGui.End"/>.</remarks>
private DateTime CalculateEffectiveExpiry(ref bool warrantsExtension)
{
DateTime expiry;
var initialDuration = this.InitialDuration;
var expiryInitial =
initialDuration == TimeSpan.MaxValue
? DateTime.MaxValue
: this.CreatedAt + initialDuration;
var extendDuration = this.ExtensionDurationSinceLastInterest;
if (warrantsExtension)
{
expiry = DateTime.MaxValue;
}
else
{
var expiryExtend =
extendDuration == TimeSpan.MaxValue
? DateTime.MaxValue
: this.lastInterestTime + extendDuration;
expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend;
if (expiry < this.extendedExpiry)
expiry = this.extendedExpiry;
}
var he = this.HardExpiry;
if (he < expiry)
{
expiry = he;
warrantsExtension = false;
}
return expiry;
}
private void DrawWindowBackgroundProgressBar()
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.ProgressWaveLoopDuration) /
NotificationConstants.ProgressWaveLoopDuration);
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
var colorElapsed =
elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
: ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) /
NotificationConstants.ProgressWaveLoopMaxColorTimeRatio;
elapsed = Math.Clamp(elapsed, 0f, 1f);
colorElapsed = Math.Clamp(colorElapsed, 0f, 1f);
colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f));
var progress = Math.Clamp(this.ProgressEased, 0f, 1f);
if (progress >= 1f)
elapsed = colorElapsed = 0f;
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var rb = windowPos + windowSize;
var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed };
var rp = windowPos + windowSize with { X = windowSize.X * progress };
ImGui.PushClipRect(windowPos, rb, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos,
midp,
ImGui.GetColorU32(
Vector4.Lerp(
NotificationConstants.BackgroundProgressColorMin,
NotificationConstants.BackgroundProgressColorMax,
colorElapsed)));
ImGui.GetWindowDrawList().AddRectFilled(
midp with { Y = 0 },
rp,
ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin));
ImGui.PopClipRect();
}
private void DrawKeyboardInputIndicator()
{
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRect(
windowPos,
windowPos + windowSize,
ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)),
0f,
ImDrawFlags.None,
NotificationConstants.FocusIndicatorThickness);
ImGui.PopClipRect();
}
private void DrawTopBar(float width, float height, bool drawActionButtons)
{
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var rtOffset = new Vector2(width, 0);
using (Service<InterfaceManager>.Get().IconFontHandle?.Push())
{
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
if (this.UserDismissable)
{
if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons))
this.DismissNow(NotificationDismissReason.Manual);
rtOffset.X -= height;
}
if (this.underlyingNotification.Minimized)
{
if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons))
this.Minimized = false;
}
else
{
if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons))
this.Minimized = true;
}
rtOffset.X -= height;
ImGui.PopClipRect();
}
float relativeOpacity;
if (this.expandoEasing.IsRunning)
{
relativeOpacity =
this.underlyingNotification.Minimized
? 1f - (float)this.expandoEasing.Value
: (float)this.expandoEasing.Value;
}
else
{
relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f;
}
if (drawActionButtons)
ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false);
else
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
if (relativeOpacity > 0)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity);
ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding));
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(
ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)
? this.CreatedAt.FormatAbsoluteDateTime()
: this.CreatedAt.FormatRelativeDateTime());
ImGui.PopStyleColor();
ImGui.PopStyleVar();
}
if (relativeOpacity < 1)
{
rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity));
var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding);
this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding)));
ltOffset.X = height;
var agoText = this.CreatedAt.FormatRelativeDateTimeShort();
var agoSize = ImGui.CalcTextSize(agoText);
rtOffset.X -= agoSize.X;
ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding });
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(agoText);
ImGui.PopStyleColor();
rtOffset.X -= NotificationConstants.ScaledWindowPadding;
ImGui.PushClipRect(
windowPos + ltOffset with { Y = 0 },
windowPos + rtOffset with { Y = height },
true);
ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding });
ImGui.TextUnformatted(this.EffectiveMinimizedText);
ImGui.PopClipRect();
ImGui.PopStyleVar();
}
ImGui.PopClipRect();
}
private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size, bool drawActionButtons)
{
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
if (!drawActionButtons)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f);
ImGui.PushStyleColor(ImGuiCol.Button, 0);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor);
ImGui.SetCursorPos(rt - new Vector2(size, 0));
var r = ImGui.Button(icon.ToIconString(), new(size));
ImGui.PopStyleColor(2);
if (!drawActionButtons)
ImGui.PopStyleVar();
ImGui.PopStyleVar();
return r;
}
private void DrawContentArea(float width, float actionWindowHeight)
{
var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize;
var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding;
var textColumnOffset = new Vector2(textColumnX, actionWindowHeight);
this.DrawIcon(
new(NotificationConstants.ScaledWindowPadding, actionWindowHeight),
new(NotificationConstants.ScaledIconSize));
textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth);
textColumnOffset.Y += NotificationConstants.ScaledComponentGap;
this.DrawContentBody(textColumnOffset, textColumnWidth);
}
private void DrawIcon(Vector2 minCoord, Vector2 size)
{
var maxCoord = minCoord + size;
var iconColor = this.Type.ToColor();
if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap))
return;
if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true)
return;
if (NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
this.Type.ToChar(),
Service<NotificationManager>.Get().IconFontAwesomeFontHandle,
iconColor))
return;
if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.initiatorPlugin))
return;
NotificationUtilities.DrawIconFromDalamudLogo(minCoord, maxCoord);
}
private float DrawTitle(Vector2 minCoord, float width)
{
ImGui.PushTextWrapPos(minCoord.X + width);
ImGui.SetCursorPos(minCoord);
if ((this.Title ?? this.Type.ToTitle()) is { } title)
{
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor);
ImGui.TextUnformatted(title);
ImGui.PopStyleColor();
}
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor);
ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() });
ImGui.TextUnformatted(this.InitiatorString);
ImGui.PopStyleColor();
ImGui.PopTextWrapPos();
return ImGui.GetCursorPosY() - minCoord.Y;
}
private void DrawContentBody(Vector2 minCoord, float width)
{
ImGui.SetCursorPos(minCoord);
ImGui.PushTextWrapPos(minCoord.X + width);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor);
ImGui.TextUnformatted(this.Content);
ImGui.PopStyleColor();
ImGui.PopTextWrapPos();
if (this.DrawActions is not null)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap);
try
{
this.DrawActions.Invoke(this);
}
catch
{
// ignore
}
}
}
private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension)
{
float barL, barR;
if (this.IsDismissed)
{
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f;
barL = midpoint - (length * v);
barR = midpoint + (length * v);
}
else if (warrantsExtension)
{
barL = 0f;
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else if (effectiveExpiry == DateTime.MaxValue)
{
if (this.ShowIndeterminateIfNoExpiry)
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.IndeterminateProgressbarLoopDuration) /
NotificationConstants.IndeterminateProgressbarLoopDuration);
barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3);
barR = Math.Min(elapsed, 2f / 3) / (2f / 3);
barL = MathF.Pow(barL, 3);
barR = 1f - MathF.Pow(1f - barR, 3);
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else
{
this.prevProgressL = barL = 0f;
this.prevProgressR = barR = 1f;
}
}
else
{
barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds /
(effectiveExpiry - this.lastInterestTime).TotalMilliseconds);
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
barR = Math.Clamp(barR, 0f, 1f);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos + new Vector2(
windowSize.X * barL,
windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight),
windowPos + windowSize with { X = windowSize.X * barR },
ImGui.GetColorU32(this.Type.ToColor()));
ImGui.PopClipRect();
}
}

View file

@ -1,24 +1,21 @@
using System.Numerics; using System.Numerics;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Threading;
using Dalamud.Interface.Animation; using Dalamud.Interface.Animation;
using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification.Internal.IconSource;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET;
using Serilog; using Serilog;
namespace Dalamud.Interface.ImGuiNotification.Internal; namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Represents an active notification.</summary> /// <summary>Represents an active notification.</summary>
internal sealed class ActiveNotification : IActiveNotification internal sealed partial class ActiveNotification : IActiveNotification
{ {
private readonly Notification underlyingNotification; private readonly Notification underlyingNotification;
@ -27,6 +24,21 @@ internal sealed class ActiveNotification : IActiveNotification
private readonly Easing progressEasing; private readonly Easing progressEasing;
private readonly Easing expandoEasing; private readonly Easing expandoEasing;
/// <summary>Gets the time of starting to count the timer for the expiration.</summary>
private DateTime lastInterestTime;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
private DateTime extendedExpiry;
/// <summary>The icon texture to use if specified; otherwise, icon will be used from <see cref="Icon"/>.</summary>
private IDalamudTextureWrap? iconTextureWrap;
/// <summary>The plugin that initiated this notification.</summary>
private LocalPlugin? initiatorPlugin;
/// <summary>Whether <see cref="initiatorPlugin"/> has been unloaded.</summary>
private bool isInitiatorUnloaded;
/// <summary>The progress before for the progress bar animation with <see cref="progressEasing"/>.</summary> /// <summary>The progress before for the progress bar animation with <see cref="progressEasing"/>.</summary>
private float progressBefore; private float progressBefore;
@ -36,10 +48,10 @@ internal sealed class ActiveNotification : IActiveNotification
/// <summary>Used for calculating correct dismissal progressbar animation (right edge).</summary> /// <summary>Used for calculating correct dismissal progressbar animation (right edge).</summary>
private float prevProgressR; private float prevProgressR;
/// <summary>New progress value to be updated on next call to <see cref="UpdateAnimations"/>.</summary> /// <summary>New progress value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private float? newProgress; private float? newProgress;
/// <summary>New minimized value to be updated on next call to <see cref="UpdateAnimations"/>.</summary> /// <summary>New minimized value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private bool? newMinimized; private bool? newMinimized;
/// <summary>Initializes a new instance of the <see cref="ActiveNotification"/> class.</summary> /// <summary>Initializes a new instance of the <see cref="ActiveNotification"/> class.</summary>
@ -47,28 +59,16 @@ internal sealed class ActiveNotification : IActiveNotification
/// <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)
{ {
this.underlyingNotification = underlyingNotification with this.underlyingNotification = underlyingNotification with { };
{ this.initiatorPlugin = initiatorPlugin;
IconSource = underlyingNotification.IconSource?.Clone(),
};
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.ProgressChangeAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration);
this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration);
this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now;
this.showEasing.Start(); this.showEasing.Start();
this.progressEasing.Start(); this.progressEasing.Start();
try
{
this.UpdateIcon();
}
catch (Exception e)
{
// Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the
// error accordingly.
Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored.");
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -80,23 +80,11 @@ internal sealed class ActiveNotification : IActiveNotification
/// <inheritdoc/> /// <inheritdoc/>
public event Action<IActiveNotification>? DrawActions; public event Action<IActiveNotification>? DrawActions;
/// <inheritdoc/>
public event Action<IActiveNotification>? MouseEnter;
/// <inheritdoc/>
public event Action<IActiveNotification>? MouseLeave;
/// <inheritdoc/> /// <inheritdoc/>
public long Id { get; } = IActiveNotification.CreateNewId(); public long Id { get; } = IActiveNotification.CreateNewId();
/// <summary>Gets the time of creating this notification.</summary> /// <inheritdoc/>
public DateTime CreatedAt { get; } = DateTime.Now; public DateTime CreatedAt { get; }
/// <summary>Gets the time of starting to count the timer for the expiration.</summary>
public DateTime LastInterestTime { get; private set; } = DateTime.Now;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
public DateTime ExtendedExpiry { get; private set; } = DateTime.Now;
/// <inheritdoc/> /// <inheritdoc/>
public string Content public string Content
@ -147,19 +135,14 @@ internal sealed class ActiveNotification : IActiveNotification
} }
/// <inheritdoc/> /// <inheritdoc/>
public INotificationIconSource? IconSource public INotificationIcon? Icon
{ {
get => this.underlyingNotification.IconSource; get => this.underlyingNotification.Icon;
set set
{ {
if (this.IsDismissed) if (this.IsDismissed)
{
value?.Dispose();
return; return;
} this.underlyingNotification.Icon = value;
this.underlyingNotification.IconSource = value;
this.UpdateIcon();
} }
} }
@ -172,7 +155,7 @@ internal sealed class ActiveNotification : IActiveNotification
if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) if (this.underlyingNotification.HardExpiry == value || this.IsDismissed)
return; return;
this.underlyingNotification.HardExpiry = value; this.underlyingNotification.HardExpiry = value;
this.LastInterestTime = DateTime.Now; this.lastInterestTime = DateTime.Now;
} }
} }
@ -185,58 +168,25 @@ internal sealed class ActiveNotification : IActiveNotification
if (this.IsDismissed) if (this.IsDismissed)
return; return;
this.underlyingNotification.InitialDuration = value; this.underlyingNotification.InitialDuration = value;
this.LastInterestTime = DateTime.Now; this.lastInterestTime = DateTime.Now;
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan DurationSinceLastInterest public TimeSpan ExtensionDurationSinceLastInterest
{ {
get => this.underlyingNotification.DurationSinceLastInterest; get => this.underlyingNotification.ExtensionDurationSinceLastInterest;
set set
{ {
if (this.IsDismissed) if (this.IsDismissed)
return; return;
this.underlyingNotification.DurationSinceLastInterest = value; this.underlyingNotification.ExtensionDurationSinceLastInterest = value;
this.LastInterestTime = DateTime.Now; this.lastInterestTime = DateTime.Now;
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public DateTime EffectiveExpiry public DateTime EffectiveExpiry { get; private set; }
{
get
{
var initialDuration = this.InitialDuration;
var expiryInitial =
initialDuration == TimeSpan.MaxValue
? DateTime.MaxValue
: this.CreatedAt + initialDuration;
DateTime expiry;
var hoverExtendDuration = this.DurationSinceLastInterest;
if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused))
{
expiry = DateTime.MaxValue;
}
else
{
var expiryExtend =
hoverExtendDuration == TimeSpan.MaxValue
? DateTime.MaxValue
: this.LastInterestTime + hoverExtendDuration;
expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend;
if (expiry < this.ExtendedExpiry)
expiry = this.ExtendedExpiry;
}
var he = this.HardExpiry;
if (he < expiry)
expiry = he;
return expiry;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry public bool ShowIndeterminateIfNoExpiry
@ -286,24 +236,9 @@ internal sealed class ActiveNotification : IActiveNotification
} }
} }
/// <inheritdoc/>
public bool IsHovered { get; private set; }
/// <inheritdoc/>
public bool IsFocused { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsDismissed => this.hideEasing.IsRunning; public bool IsDismissed => this.hideEasing.IsRunning;
/// <summary>Gets a value indicating whether <see cref="InitiatorPlugin"/> has been unloaded.</summary>
public bool IsInitiatorUnloaded { get; private set; }
/// <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 INotificationMaterializedIcon? MaterializedIcon { get; set; }
/// <summary>Gets the eased progress.</summary> /// <summary>Gets the eased progress.</summary>
private float ProgressEased private float ProgressEased
{ {
@ -318,61 +253,17 @@ internal sealed class ActiveNotification : IActiveNotification
} }
} }
/// <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 char? DefaultIconChar => this.Type switch
{
NotificationType.None => null,
NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(),
NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(),
NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(),
NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(),
_ => null,
};
/// <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,
};
/// <summary>Gets the string for the initiator field.</summary> /// <summary>Gets the string for the initiator field.</summary>
private string InitiatorString => private string InitiatorString =>
this.InitiatorPlugin is not { } initiatorPlugin this.initiatorPlugin is not { } plugin
? NotificationConstants.DefaultInitiator ? NotificationConstants.DefaultInitiator
: this.IsInitiatorUnloaded : this.isInitiatorUnloaded
? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) ? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name)
: initiatorPlugin.Name; : plugin.Name;
/// <summary>Gets the effective text to display when minimized.</summary> /// <summary>Gets the effective text to display when minimized.</summary>
private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" ");
/// <inheritdoc/>
public void Dispose()
{
this.ClearMaterializedIcon();
this.underlyingNotification.Dispose();
this.Dismiss = null;
this.Click = null;
this.DrawActions = null;
this.InitiatorPlugin = null;
}
/// <inheritdoc/> /// <inheritdoc/>
public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical);
@ -392,13 +283,78 @@ internal sealed class ActiveNotification : IActiveNotification
{ {
Log.Error( Log.Error(
e, e,
$"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}");
} }
} }
/// <summary>Updates animations.</summary> /// <inheritdoc/>
/// <returns><c>true</c> if the notification is over.</returns> public void ExtendBy(TimeSpan extension)
public bool UpdateAnimations() {
var newExpiry = DateTime.Now + extension;
if (this.extendedExpiry < newExpiry)
this.extendedExpiry = newExpiry;
}
/// <inheritdoc/>
public void SetIconTexture(IDalamudTextureWrap? textureWrap)
{
if (this.IsDismissed)
{
textureWrap?.Dispose();
return;
}
// After replacing, if the old texture is not the old texture, then dispose the old texture.
if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrap) is { } wrapToDispose &&
wrapToDispose != textureWrap)
{
wrapToDispose.Dispose();
}
}
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
internal void RemoveNonDalamudInvocations()
{
var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly);
this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss);
this.Click = RemoveNonDalamudInvocationsCore(this.Click);
this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions);
if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType()))
this.Icon = null;
this.isInitiatorUnloaded = true;
this.UserDismissable = true;
this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration;
var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDuration;
if (this.EffectiveExpiry > newMaxExpiry)
this.HardExpiry = newMaxExpiry;
return;
bool IsOwnedByDalamud(Type t) => AssemblyLoadContext.GetLoadContext(t.Assembly) == dalamudContext;
T? RemoveNonDalamudInvocationsCore<T>(T? @delegate) where T : Delegate
{
if (@delegate is null)
return null;
foreach (var il in @delegate.GetInvocationList())
{
if (il.Target is { } target && !IsOwnedByDalamud(target.GetType()))
@delegate = (T)Delegate.Remove(@delegate, il);
}
return @delegate;
}
}
/// <summary>Updates the state of this notification, and release the relevant resource if this notification is no
/// longer in use.</summary>
/// <returns><c>true</c> if the notification is over and relevant resources are released.</returns>
/// <remarks>Intended to be called from the main thread only.</remarks>
internal bool UpdateOrDisposeInternal()
{ {
this.showEasing.Update(); this.showEasing.Update();
this.hideEasing.Update(); this.hideEasing.Update();
@ -435,555 +391,21 @@ internal sealed class ActiveNotification : IActiveNotification
this.newMinimized = null; this.newMinimized = null;
} }
return this.hideEasing.IsRunning && this.hideEasing.IsDone; if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone)
return false;
this.DisposeInternal();
return true;
} }
/// <summary>Draws this notification.</summary> /// <summary>Clears the resources associated with this instance of <see cref="ActiveNotification"/>.</summary>
/// <param name="maxWidth">The maximum width of the notification window.</param> internal void DisposeInternal()
/// <param name="offsetY">The offset from the bottom.</param>
/// <returns>The height of the notification.</returns>
public float Draw(float maxWidth, float offsetY)
{ {
var effectiveExpiry = this.EffectiveExpiry; if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose)
if (!this.IsDismissed && DateTime.Now > effectiveExpiry) wrapToDispose.Dispose();
this.DismissNow(NotificationDismissReason.Timeout); this.Dismiss = null;
this.Click = null;
var opacity = this.DrawActions = null;
Math.Clamp( this.initiatorPlugin = null;
(float)(this.hideEasing.IsRunning
? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value)
: (this.showEasing.IsDone ? 1 : this.showEasing.Value)),
0f,
1f);
if (opacity <= 0)
return 0;
var interfaceManager = Service<InterfaceManager>.Get();
var unboundedWidth = ImGui.CalcTextSize(this.Content).X;
float closeButtonHorizontalSpaceReservation;
using (interfaceManager.IconFontHandle?.Push())
{
closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X;
closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding;
}
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.InitiatorString).X);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation);
unboundedWidth = Math.Max(
unboundedWidth,
ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation);
unboundedWidth += NotificationConstants.ScaledWindowPadding * 3;
unboundedWidth += NotificationConstants.ScaledIconSize;
var actionWindowHeight =
// Content
ImGui.GetTextLineHeight() +
// Top and bottom padding
(NotificationConstants.ScaledWindowPadding * 2);
var width = Math.Min(maxWidth, unboundedWidth);
var viewport = ImGuiHelpers.MainViewport;
var viewportPos = viewport.WorkPos;
var viewportSize = viewport.WorkSize;
ImGui.PushID(this.Id.GetHashCode());
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f);
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding));
unsafe
{
ImGui.PushStyleColor(
ImGuiCol.WindowBg,
*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4(
1f,
1f,
1f,
NotificationConstants.BackgroundOpacity));
}
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(
(viewportPos + viewportSize) -
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
ImGuiCond.Always,
Vector2.One);
ImGui.SetNextWindowSizeConstraints(
new(width, actionWindowHeight),
new(
width,
!this.underlyingNotification.Minimized || this.expandoEasing.IsRunning
? float.MaxValue
: actionWindowHeight));
ImGui.Begin(
$"##NotifyMainWindow{this.Id}",
ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking);
this.IsFocused = ImGui.IsWindowFocused();
if (this.IsFocused)
this.LastInterestTime = DateTime.Now;
this.DrawWindowBackgroundProgressBar();
this.DrawFocusIndicator();
this.DrawTopBar(interfaceManager, width, actionWindowHeight);
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
{
this.DrawContentArea(width, actionWindowHeight);
}
else if (this.expandoEasing.IsRunning)
{
if (this.underlyingNotification.Minimized)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value));
else
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value);
this.DrawContentArea(width, actionWindowHeight);
ImGui.PopStyleVar();
}
this.DrawExpiryBar(effectiveExpiry);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var hovered = ImGui.IsWindowHovered();
ImGui.End();
ImGui.PopStyleColor();
ImGui.PopStyleVar(3);
ImGui.PopID();
if (windowPos.X <= ImGui.GetIO().MousePos.X
&& windowPos.Y <= ImGui.GetIO().MousePos.Y
&& ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X
&& ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y)
{
if (!this.IsHovered)
{
this.IsHovered = true;
this.MouseEnter.InvokeSafely(this);
}
if (this.DurationSinceLastInterest > TimeSpan.Zero)
this.LastInterestTime = DateTime.Now;
if (hovered)
{
if (this.Click is null)
{
if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
this.DismissNow(NotificationDismissReason.Manual);
}
else
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Right)
|| ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
this.Click.InvokeSafely(this);
}
}
}
else if (this.IsHovered)
{
this.IsHovered = false;
this.MouseLeave.InvokeSafely(this);
}
return windowSize.Y;
}
/// <inheritdoc/>
public void ExtendBy(TimeSpan extension)
{
var newExpiry = DateTime.Now + extension;
if (this.ExtendedExpiry < newExpiry)
this.ExtendedExpiry = newExpiry;
}
/// <inheritdoc/>
public void UpdateIcon()
{
if (this.IsDismissed)
return;
this.ClearMaterializedIcon();
this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize();
}
/// <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);
this.IsInitiatorUnloaded = true;
this.UserDismissable = true;
this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration;
var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration;
if (this.EffectiveExpiry > newMaxExpiry)
this.HardExpiry = newMaxExpiry;
return;
T? RemoveNonDalamudInvocationsCore<T>(T? @delegate) where T : Delegate
{
if (@delegate is null)
return null;
foreach (var il in @delegate.GetInvocationList())
{
if (il.Target is { } target &&
AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext)
{
@delegate = (T)Delegate.Remove(@delegate, il);
}
}
return @delegate;
}
}
private void ClearMaterializedIcon()
{
this.MaterializedIcon?.Dispose();
this.MaterializedIcon = null;
}
private void DrawWindowBackgroundProgressBar()
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.ProgressWaveLoopDuration) /
NotificationConstants.ProgressWaveLoopDuration);
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
var colorElapsed =
elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
: ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) /
NotificationConstants.ProgressWaveLoopMaxColorTimeRatio;
elapsed = Math.Clamp(elapsed, 0f, 1f);
colorElapsed = Math.Clamp(colorElapsed, 0f, 1f);
colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f));
var progress = Math.Clamp(this.ProgressEased, 0f, 1f);
if (progress >= 1f)
elapsed = colorElapsed = 0f;
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var rb = windowPos + windowSize;
var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed };
var rp = windowPos + windowSize with { X = windowSize.X * progress };
ImGui.PushClipRect(windowPos, rb, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos,
midp,
ImGui.GetColorU32(
Vector4.Lerp(
NotificationConstants.BackgroundProgressColorMin,
NotificationConstants.BackgroundProgressColorMax,
colorElapsed)));
ImGui.GetWindowDrawList().AddRectFilled(
midp with { Y = 0 },
rp,
ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin));
ImGui.PopClipRect();
}
private void DrawFocusIndicator()
{
if (!this.IsFocused)
return;
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRect(
windowPos,
windowPos + windowSize,
ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)),
0f,
ImDrawFlags.None,
NotificationConstants.FocusIndicatorThickness);
ImGui.PopClipRect();
}
private void DrawTopBar(InterfaceManager interfaceManager, float width, float height)
{
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var rtOffset = new Vector2(width, 0);
using (interfaceManager.IconFontHandle?.Push())
{
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
if (this.UserDismissable)
{
if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height))
this.DismissNow(NotificationDismissReason.Manual);
rtOffset.X -= height;
}
if (this.underlyingNotification.Minimized)
{
if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height))
this.Minimized = false;
}
else
{
if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height))
this.Minimized = true;
}
rtOffset.X -= height;
ImGui.PopClipRect();
}
float relativeOpacity;
if (this.expandoEasing.IsRunning)
{
relativeOpacity =
this.underlyingNotification.Minimized
? 1f - (float)this.expandoEasing.Value
: (float)this.expandoEasing.Value;
}
else
{
relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f;
}
if (this.IsHovered || this.IsFocused)
ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false);
else
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
if (relativeOpacity > 0)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity);
ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding));
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(
this.IsHovered || this.IsFocused
? this.CreatedAt.FormatAbsoluteDateTime()
: this.CreatedAt.FormatRelativeDateTime());
ImGui.PopStyleColor();
ImGui.PopStyleVar();
}
if (relativeOpacity < 1)
{
rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity));
var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding);
this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding)));
ltOffset.X = height;
var agoText = this.CreatedAt.FormatRelativeDateTimeShort();
var agoSize = ImGui.CalcTextSize(agoText);
rtOffset.X -= agoSize.X;
ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding });
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(agoText);
ImGui.PopStyleColor();
rtOffset.X -= NotificationConstants.ScaledWindowPadding;
ImGui.PushClipRect(
windowPos + ltOffset with { Y = 0 },
windowPos + rtOffset with { Y = height },
true);
ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding });
ImGui.TextUnformatted(this.EffectiveMinimizedText);
ImGui.PopClipRect();
ImGui.PopStyleVar();
}
ImGui.PopClipRect();
}
private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size)
{
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
var alphaPush = !this.IsHovered && !this.IsFocused;
if (alphaPush)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f);
ImGui.PushStyleColor(ImGuiCol.Button, 0);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor);
ImGui.SetCursorPos(rt - new Vector2(size, 0));
var r = ImGui.Button(icon.ToIconString(), new(size));
ImGui.PopStyleColor(2);
if (alphaPush)
ImGui.PopStyleVar();
ImGui.PopStyleVar();
return r;
}
private void DrawContentArea(float width, float actionWindowHeight)
{
var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize;
var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding;
var textColumnOffset = new Vector2(textColumnX, actionWindowHeight);
this.DrawIcon(
new(NotificationConstants.ScaledWindowPadding, actionWindowHeight),
new(NotificationConstants.ScaledIconSize));
textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth);
textColumnOffset.Y += NotificationConstants.ScaledComponentGap;
this.DrawContentBody(textColumnOffset, textColumnWidth);
}
private void DrawIcon(Vector2 minCoord, Vector2 size)
{
var maxCoord = minCoord + size;
if (this.MaterializedIcon is not null)
{
this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin);
return;
}
var defaultIconChar = this.DefaultIconChar;
if (defaultIconChar is not null)
{
NotificationUtilities.DrawIconString(
Service<NotificationManager>.Get().IconFontAwesomeFontHandle,
defaultIconChar.Value,
minCoord,
maxCoord,
this.DefaultIconColor);
return;
}
TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon(
minCoord,
maxCoord,
this.DefaultIconColor,
this.InitiatorPlugin);
}
private float DrawTitle(Vector2 minCoord, float width)
{
ImGui.PushTextWrapPos(minCoord.X + width);
ImGui.SetCursorPos(minCoord);
if ((this.Title ?? this.DefaultTitle) is { } title)
{
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor);
ImGui.TextUnformatted(title);
ImGui.PopStyleColor();
}
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor);
ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() });
ImGui.TextUnformatted(this.InitiatorString);
ImGui.PopStyleColor();
ImGui.PopTextWrapPos();
return ImGui.GetCursorPosY() - minCoord.Y;
}
private void DrawContentBody(Vector2 minCoord, float width)
{
ImGui.SetCursorPos(minCoord);
ImGui.PushTextWrapPos(minCoord.X + width);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor);
ImGui.TextUnformatted(this.Content);
ImGui.PopStyleColor();
ImGui.PopTextWrapPos();
if (this.DrawActions is not null)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap);
try
{
this.DrawActions.Invoke(this);
}
catch
{
// ignore
}
}
}
private void DrawExpiryBar(DateTime effectiveExpiry)
{
float barL, barR;
if (this.IsDismissed)
{
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f;
barL = midpoint - (length * v);
barR = midpoint + (length * v);
}
else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused))
{
barL = 0f;
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else if (effectiveExpiry == DateTime.MaxValue)
{
if (this.ShowIndeterminateIfNoExpiry)
{
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.IndeterminateProgressbarLoopDuration) /
NotificationConstants.IndeterminateProgressbarLoopDuration);
barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3);
barR = Math.Min(elapsed, 2f / 3) / (2f / 3);
barL = MathF.Pow(barL, 3);
barR = 1f - MathF.Pow(1f - barR, 3);
this.prevProgressL = barL;
this.prevProgressR = barR;
}
else
{
this.prevProgressL = barL = 0f;
this.prevProgressR = barR = 1f;
}
}
else
{
barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds /
(effectiveExpiry - this.LastInterestTime).TotalMilliseconds);
barR = 1f;
this.prevProgressL = barL;
this.prevProgressR = barR;
}
barR = Math.Clamp(barR, 0f, 1f);
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
ImGui.PushClipRect(windowPos, windowPos + windowSize, false);
ImGui.GetWindowDrawList().AddRectFilled(
windowPos + new Vector2(
windowSize.X * barL,
windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight),
windowPos + windowSize with { X = windowSize.X * barR },
ImGui.GetColorU32(this.DefaultIconColor));
ImGui.PopClipRect();
} }
} }

View file

@ -1,49 +0,0 @@
using System.IO;
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of a texture from a file as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class FilePathIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="FilePathIconSource"/> class.</summary>
/// <param name="filePath">The path to a .tex file inside the game resources.</param>
public FilePathIconSource(string filePath) => this.FilePath = filePath;
/// <summary>Gets the path to a .tex file inside the game resources.</summary>
public string FilePath { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() =>
new MaterializedIcon(this.FilePath);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private readonly FileInfo fileInfo;
public MaterializedIcon(string filePath) => this.fileInfo = new(filePath);
public void Dispose()
{
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawTexture(
Service<TextureManager>.Get().GetTextureFromFile(this.fileInfo),
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,46 +0,0 @@
using System.Numerics;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of <see cref="FontAwesomeIcon"/> as the icon of a notification.</summary>
internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="FontAwesomeIconIconSource"/> class.</summary>
/// <param name="iconChar">The character.</param>
public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar;
/// <summary>Gets the icon character.</summary>
public FontAwesomeIcon IconChar { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private readonly char iconChar;
public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar();
public void Dispose()
{
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawIconString(
Service<NotificationManager>.Get().IconFontAwesomeFontHandle,
this.iconChar,
minCoord,
maxCoord,
color);
}
}

View file

@ -1,50 +0,0 @@
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of a game-shipped texture as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class GamePathIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="GamePathIconSource"/> class.</summary>
/// <param name="gamePath">The path to a .tex file inside the game resources.</param>
/// <remarks>Use <see cref="ITextureProvider.GetIconPath"/> to get the game path from icon IDs.</remarks>
public GamePathIconSource(string gamePath) => this.GamePath = gamePath;
/// <summary>Gets the path to a .tex file inside the game resources.</summary>
public string GamePath { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() =>
new MaterializedIcon(this.GamePath);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private readonly string gamePath;
public MaterializedIcon(string gamePath) => this.gamePath = gamePath;
public void Dispose()
{
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawTexture(
Service<TextureManager>.Get().GetTextureFromGame(this.gamePath),
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,47 +0,0 @@
using System.Numerics;
using Dalamud.Game.Text;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of <see cref="SeIconChar"/> as the icon of a notification.</summary>
internal class SeIconCharIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="SeIconCharIconSource"/> class.</summary>
/// <param name="c">The character.</param>
public SeIconCharIconSource(SeIconChar c) => this.IconChar = c;
/// <summary>Gets the icon character.</summary>
public SeIconChar IconChar { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private readonly char iconChar;
public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar();
public void Dispose()
{
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawIconString(
Service<NotificationManager>.Get().IconAxisFontHandle,
this.iconChar,
minCoord,
maxCoord,
color);
}
}

View file

@ -1,62 +0,0 @@
using System.Numerics;
using System.Threading;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of future <see cref="IDalamudTextureWrap"/> as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class TextureWrapIconSource : INotificationIconSource.IInternal
{
private IDalamudTextureWrap? wrap;
/// <summary>Initializes a new instance of the <see cref="TextureWrapIconSource"/> class.</summary>
/// <param name="wrap">The texture wrap to handle over the ownership.</param>
/// <param name="takeOwnership">
/// If <c>true</c>, this class will own the passed <paramref name="wrap"/>, and you <b>must not</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// If <c>false</c>, this class will create a new reference of the passed wrap, and you <b>should</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// In both cases, this class must be disposed after use.</param>
public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) =>
this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource();
/// <summary>Gets the underlying texture wrap.</summary>
public IDalamudTextureWrap? Wrap => this.wrap;
/// <inheritdoc/>
public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false);
/// <inheritdoc/>
public void Dispose()
{
if (Interlocked.Exchange(ref this.wrap, null) is { } w)
w.Dispose();
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() =>
new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource());
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private IDalamudTextureWrap? wrap;
public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap;
public void Dispose()
{
if (Interlocked.Exchange(ref this.wrap, null) is { } w)
w.Dispose();
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawTexture(
this.wrap,
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,71 +0,0 @@
using System.Numerics;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of future <see cref="IDalamudTextureWrap"/> as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal
{
/// <summary>Gets the default materialized icon, for the purpose of displaying the plugin icon.</summary>
internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null);
/// <summary>Initializes a new instance of the <see cref="TextureWrapTaskIconSource"/> class.</summary>
/// <param name="taskFunc">The function.</param>
public TextureWrapTaskIconSource(Func<Task<IDalamudTextureWrap?>?>? taskFunc) =>
this.TextureWrapTaskFunc = taskFunc;
/// <summary>Gets the function that returns a task resulting in a new instance of <see cref="IDalamudTextureWrap"/>.
/// </summary>
/// <remarks>Dalamud will take ownership of the result. Do not call <see cref="IDisposable.Dispose"/>.</remarks>
public Func<Task<IDalamudTextureWrap?>?>? TextureWrapTaskFunc { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() =>
new MaterializedIcon(this.TextureWrapTaskFunc);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private Task<IDalamudTextureWrap>? task;
public MaterializedIcon(Func<Task<IDalamudTextureWrap?>?>? taskFunc)
{
try
{
this.task = taskFunc?.Invoke();
}
catch (Exception e)
{
Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture.");
this.task = null;
}
}
public void Dispose()
{
this.task?.ToContentDisposedTask(true);
this.task = null;
}
public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) =>
NotificationUtilities.DrawTexture(
this.task?.IsCompletedSuccessfully is true ? this.task.Result : null,
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,12 +1,14 @@
using System.Diagnostics; using System.Diagnostics;
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Constants for drawing notification windows.</summary> /// <summary>Constants for drawing notification windows.</summary>
public static class NotificationConstants internal static class NotificationConstants
{ {
// .............................[..] // .............................[..]
// ..when.......................[XX] // ..when.......................[XX]
@ -20,69 +22,74 @@ public static class NotificationConstants
// .. action buttons .. // .. action buttons ..
// ................................. // .................................
/// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3);
/// <summary>Default duration of the notification, after the mouse cursor leaves the notification window.</summary>
public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3);
/// <summary>The string to show in place of this_plugin if the notification is shown by Dalamud.</summary> /// <summary>The string to show in place of this_plugin if the notification is shown by Dalamud.</summary>
internal const string DefaultInitiator = "Dalamud"; public const string DefaultInitiator = "Dalamud";
/// <summary>The string to measure size of, to decide the width of notification windows.</summary>
public const string NotificationWidthMeasurementString =
"The width of this text will decide the width\n" +
"of the notification window.";
/// <summary>The ratio of maximum notification window width w.r.t. main viewport width.</summary>
public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3;
/// <summary>The size of the icon.</summary> /// <summary>The size of the icon.</summary>
internal const float IconSize = 32; public const float IconSize = 32;
/// <summary>The background opacity of a notification window.</summary> /// <summary>The background opacity of a notification window.</summary>
internal const float BackgroundOpacity = 0.82f; public const float BackgroundOpacity = 0.82f;
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary> /// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
internal const float IndeterminateProgressbarLoopDuration = 2000f; public const float IndeterminateProgressbarLoopDuration = 2000f;
/// <summary>The duration of the progress wave animation in milliseconds.</summary> /// <summary>The duration of the progress wave animation in milliseconds.</summary>
internal const float ProgressWaveLoopDuration = 2000f; public const float ProgressWaveLoopDuration = 2000f;
/// <summary>The time ratio of a progress wave loop where the animation is idle.</summary> /// <summary>The time ratio of a progress wave loop where the animation is idle.</summary>
internal const float ProgressWaveIdleTimeRatio = 0.5f; public const float ProgressWaveIdleTimeRatio = 0.5f;
/// <summary>The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. /// <summary>The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque.
/// </summary> /// </summary>
internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3);
/// <summary>Duration of show animation.</summary> /// <summary>Duration of show animation.</summary>
internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of hide animation.</summary> /// <summary>Duration of hide animation.</summary>
internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of progress change animation.</summary> /// <summary>Duration of progress change animation.</summary>
internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200);
/// <summary>Duration of expando animation.</summary> /// <summary>Duration of expando animation.</summary>
internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Text color for the rectangular border when the notification is focused.</summary> /// <summary>Text color for the rectangular border when the notification is focused.</summary>
internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); public static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f);
/// <summary>Text color for the when.</summary> /// <summary>Text color for the when.</summary>
internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f);
/// <summary>Text color for the close button [X].</summary> /// <summary>Text color for the close button [X].</summary>
internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f);
/// <summary>Text color for the title.</summary> /// <summary>Text color for the title.</summary>
internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f);
/// <summary>Text color for the name of the initiator.</summary> /// <summary>Text color for the name of the initiator.</summary>
internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f);
/// <summary>Text color for the body.</summary> /// <summary>Text color for the body.</summary>
internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f);
/// <summary>Color for the background progress bar (determinate progress only).</summary> /// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f);
/// <summary>Color for the background progress bar (determinate progress only).</summary> /// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f);
/// <summary>Gets the relative time format strings.</summary> /// <summary>Gets the relative time format strings.</summary>
private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings =
@ -110,35 +117,35 @@ public static class NotificationConstants
}; };
/// <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>
internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the distance from the right bottom border of the viewport /// <summary>Gets the distance from the right bottom border of the viewport
/// to the right bottom border of a notification window. /// to the right bottom border of a notification window.
/// </summary> /// </summary>
internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled gap between two notification windows.</summary> /// <summary>Gets the scaled gap between two notification windows.</summary>
internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled gap between components.</summary> /// <summary>Gets the scaled gap between components.</summary>
internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled size of the icon.</summary> /// <summary>Gets the scaled size of the icon.</summary>
internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale);
/// <summary>Gets the height of the expiry progress bar.</summary> /// <summary>Gets the height of the expiry progress bar.</summary>
internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the thickness of the focus indicator rectangle.</summary> /// <summary>Gets the thickness of the focus indicator rectangle.</summary>
internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the string format of the initiator name field, if the initiator is unloaded.</summary> /// <summary>Gets the string format of the initiator name field, if the initiator is unloaded.</summary>
internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; public static string UnloadedInitiatorNameFormat => "{0} (unloaded)";
/// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary> /// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary>
/// <param name="when">When.</param> /// <param name="when">When.</param>
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
internal static string FormatRelativeDateTime(this DateTime when) public static string FormatRelativeDateTime(this DateTime when)
{ {
var ts = DateTime.Now - when; var ts = DateTime.Now - when;
foreach (var (minSpan, formatString) in RelativeFormatStrings) foreach (var (minSpan, formatString) in RelativeFormatStrings)
@ -156,12 +163,12 @@ public static class NotificationConstants
/// <summary>Formats an instance of <see cref="DateTime"/> as an absolute time.</summary> /// <summary>Formats an instance of <see cref="DateTime"/> as an absolute time.</summary>
/// <param name="when">When.</param> /// <param name="when">When.</param>
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}";
/// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary> /// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary>
/// <param name="when">When.</param> /// <param name="when">When.</param>
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
internal static string FormatRelativeDateTimeShort(this DateTime when) public static string FormatRelativeDateTimeShort(this DateTime when)
{ {
var ts = DateTime.Now - when; var ts = DateTime.Now - when;
foreach (var (minSpan, formatString) in RelativeFormatStringsShort) foreach (var (minSpan, formatString) in RelativeFormatStringsShort)
@ -174,4 +181,43 @@ public static class NotificationConstants
Debug.Assert(false, "must not reach here"); Debug.Assert(false, "must not reach here");
return "???"; return "???";
} }
/// <summary>Gets the color corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding color.</returns>
public static Vector4 ToColor(this NotificationType type) => type switch
{
NotificationType.None => ImGuiColors.DalamudWhite,
NotificationType.Success => ImGuiColors.HealerGreen,
NotificationType.Warning => ImGuiColors.DalamudOrange,
NotificationType.Error => ImGuiColors.DalamudRed,
NotificationType.Info => ImGuiColors.TankBlue,
_ => ImGuiColors.DalamudWhite,
};
/// <summary>Gets the <see cref="FontAwesomeIcon"/> char value corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding char, or null.</returns>
public static char ToChar(this NotificationType type) => type switch
{
NotificationType.None => '\0',
NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(),
NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(),
NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(),
NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(),
_ => '\0',
};
/// <summary>Gets the localized title string corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding title.</returns>
public static string? ToTitle(this NotificationType type) => type switch
{
NotificationType.None => null,
NotificationType.Success => NotificationType.Success.ToString(),
NotificationType.Warning => NotificationType.Warning.ToString(),
NotificationType.Error => NotificationType.Error.ToString(),
NotificationType.Info => NotificationType.Info.ToString(),
_ => null,
};
} }

View file

@ -0,0 +1,34 @@
using System.IO;
using System.Numerics;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of a texture from a file as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class FilePathNotificationIcon : INotificationIcon
{
private readonly FileInfo fileInfo;
/// <summary>Initializes a new instance of the <see cref="FilePathNotificationIcon"/> class.</summary>
/// <param name="filePath">The path to a .tex file inside the game resources.</param>
public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath);
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromFile(this.fileInfo));
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName);
/// <inheritdoc/>
public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})";
}

View file

@ -0,0 +1,31 @@
using System.Numerics;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of <see cref="FontAwesomeIcon"/> as the icon of a notification.</summary>
internal class FontAwesomeIconNotificationIcon : INotificationIcon
{
private readonly char iconChar;
/// <summary>Initializes a new instance of the <see cref="FontAwesomeIconNotificationIcon"/> class.</summary>
/// <param name="iconChar">The character.</param>
public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
this.iconChar,
Service<NotificationManager>.Get().IconFontAwesomeFontHandle,
color);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar);
/// <inheritdoc/>
public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})";
}

View file

@ -0,0 +1,34 @@
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of a game-shipped texture as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class GamePathNotificationIcon : INotificationIcon
{
private readonly string gamePath;
/// <summary>Initializes a new instance of the <see cref="GamePathNotificationIcon"/> class.</summary>
/// <param name="gamePath">The path to a .tex file inside the game resources.</param>
/// <remarks>Use <see cref="ITextureProvider.GetIconPath"/> to get the game path from icon IDs.</remarks>
public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromGame(this.gamePath));
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath);
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})";
}

View file

@ -0,0 +1,33 @@
using System.Numerics;
using Dalamud.Game.Text;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of <see cref="SeIconChar"/> as the icon of a notification.</summary>
internal class SeIconCharNotificationIcon : INotificationIcon
{
private readonly SeIconChar iconChar;
/// <summary>Initializes a new instance of the <see cref="SeIconCharNotificationIcon"/> class.</summary>
/// <param name="c">The character.</param>
public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
(char)this.iconChar,
Service<NotificationManager>.Get().IconAxisFontHandle,
color);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar);
/// <inheritdoc/>
public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})";
}

View file

@ -11,6 +11,8 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiNotification.Internal; namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Class handling notifications/toasts in ImGui.</summary> /// <summary>Class handling notifications/toasts in ImGui.</summary>
@ -41,6 +43,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
/// <summary>Gets the handle to FontAwesome fonts, sized for use as an icon.</summary> /// <summary>Gets the handle to FontAwesome fonts, sized for use as an icon.</summary>
public IFontHandle IconFontAwesomeFontHandle { get; } public IFontHandle IconFontAwesomeFontHandle { get; }
/// <summary>Gets the private atlas for use with notification windows.</summary>
private IFontAtlas PrivateAtlas { get; } private IFontAtlas PrivateAtlas { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -48,17 +51,16 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
{ {
this.PrivateAtlas.Dispose(); this.PrivateAtlas.Dispose();
foreach (var n in this.pendingNotifications) foreach (var n in this.pendingNotifications)
n.Dispose(); n.DisposeInternal();
foreach (var n in this.notifications) foreach (var n in this.notifications)
n.Dispose(); n.DisposeInternal();
this.pendingNotifications.Clear(); this.pendingNotifications.Clear();
this.notifications.Clear(); this.notifications.Clear();
} }
/// <inheritdoc/> /// <inheritdoc/>
public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) public IActiveNotification AddNotification(Notification notification)
{ {
using var disposer = disposeNotification ? notification : null;
var an = new ActiveNotification(notification, null); var an = new ActiveNotification(notification, null);
this.pendingNotifications.Add(an); this.pendingNotifications.Add(an);
return an; return an;
@ -66,13 +68,10 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
/// <summary>Adds a notification originating from a plugin.</summary> /// <summary>Adds a notification originating from a plugin.</summary>
/// <param name="notification">The notification.</param> /// <param name="notification">The notification.</param>
/// <param name="disposeNotification">Dispose <paramref name="notification"/> when this function returns.</param>
/// <param name="plugin">The source plugin.</param> /// <param name="plugin">The source plugin.</param>
/// <returns>The added notification.</returns> /// <returns>The added notification.</returns>
/// <remarks><paramref name="disposeNotification"/> will be honored even on exceptions.</remarks> public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin)
public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin)
{ {
using var disposer = disposeNotification ? notification : null;
var an = new ActiveNotification(notification, plugin); var an = new ActiveNotification(notification, plugin);
this.pendingNotifications.Add(an); this.pendingNotifications.Add(an);
return an; return an;
@ -92,8 +91,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
Content = content, Content = content,
Title = title, Title = title,
Type = type, Type = type,
}, });
true);
/// <summary>Draw all currently queued notifications.</summary> /// <summary>Draw all currently queued notifications.</summary>
public void Draw() public void Draw()
@ -104,19 +102,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
while (this.pendingNotifications.TryTake(out var newNotification)) while (this.pendingNotifications.TryTake(out var newNotification))
this.notifications.Add(newNotification); this.notifications.Add(newNotification);
var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X;
width += NotificationConstants.ScaledWindowPadding * 3;
width += NotificationConstants.ScaledIconSize;
width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth);
this.notifications.RemoveAll( this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal());
static x =>
{
if (!x.UpdateAnimations())
return false;
x.Dispose();
return true;
});
foreach (var tn in this.notifications) foreach (var tn in this.notifications)
height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap;
} }
} }
@ -140,9 +133,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT
this.localPlugin = localPlugin; this.localPlugin = localPlugin;
/// <inheritdoc/> /// <inheritdoc/>
public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) public IActiveNotification AddNotification(Notification notification)
{ {
var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); var an = this.notificationManagerService.AddNotification(notification, this.localPlugin);
_ = this.notifications.TryAdd(an, 0); _ = this.notifications.TryAdd(an, 0);
an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _);
return an; return an;

View file

@ -1,5 +1,4 @@
using System.Threading; using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
namespace Dalamud.Interface.ImGuiNotification; namespace Dalamud.Interface.ImGuiNotification;
@ -7,20 +6,10 @@ namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents a blueprint for a notification.</summary> /// <summary>Represents a blueprint for a notification.</summary>
public sealed record Notification : INotification public sealed record Notification : INotification
{ {
private INotificationIconSource? iconSource; /// <summary>
/// Gets the default value for <see cref="InitialDuration"/> and <see cref="ExtensionDurationSinceLastInterest"/>.
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary> /// </summary>
public Notification() public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration;
{
}
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary>
/// <param name="notification">The instance of <see cref="INotification"/> to copy from.</param>
public Notification(INotification notification) => this.CopyValuesFrom(notification);
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary>
/// <param name="notification">The instance of <see cref="Notification"/> to copy from.</param>
public Notification(Notification notification) => this.CopyValuesFrom(notification);
/// <inheritdoc/> /// <inheritdoc/>
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
@ -35,25 +24,16 @@ public sealed record Notification : INotification
public NotificationType Type { get; set; } = NotificationType.None; public NotificationType Type { get; set; } = NotificationType.None;
/// <inheritdoc/> /// <inheritdoc/>
public INotificationIconSource? IconSource public INotificationIcon? Icon { get; set; }
{
get => this.iconSource;
set
{
var prevSource = Interlocked.Exchange(ref this.iconSource, value);
if (prevSource != value)
prevSource?.Dispose();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public DateTime HardExpiry { get; set; } = DateTime.MaxValue; public DateTime HardExpiry { get; set; } = DateTime.MaxValue;
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; public TimeSpan InitialDuration { get; set; } = DefaultDuration;
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration;
/// <inheritdoc/> /// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry { get; set; } = true; public bool ShowIndeterminateIfNoExpiry { get; set; } = true;
@ -66,29 +46,4 @@ public sealed record Notification : INotification
/// <inheritdoc/> /// <inheritdoc/>
public float Progress { get; set; } = 1f; public float Progress { get; set; } = 1f;
/// <inheritdoc/>
public void Dispose()
{
// Assign to the property; it will take care of disposing
this.IconSource = null;
}
/// <summary>Copy values from the given instance of <see cref="INotification"/>.</summary>
/// <param name="copyFrom">The instance of <see cref="INotification"/> to copy from.</param>
private void CopyValuesFrom(INotification copyFrom)
{
this.Content = copyFrom.Content;
this.Title = copyFrom.Title;
this.MinimizedText = copyFrom.MinimizedText;
this.Type = copyFrom.Type;
this.IconSource = copyFrom.IconSource?.Clone();
this.HardExpiry = copyFrom.HardExpiry;
this.InitialDuration = copyFrom.InitialDuration;
this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest;
this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry;
this.Minimized = copyFrom.Minimized;
this.UserDismissable = copyFrom.UserDismissable;
this.Progress = copyFrom.Progress;
}
} }

View file

@ -17,44 +17,47 @@ namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Utilities for implementing stuff under <see cref="ImGuiNotification"/>.</summary> /// <summary>Utilities for implementing stuff under <see cref="ImGuiNotification"/>.</summary>
public static class NotificationUtilities public static class NotificationUtilities
{ {
/// <inheritdoc cref="INotificationIconSource.From(SeIconChar)"/> /// <inheritdoc cref="INotificationIcon.From(SeIconChar)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => public static INotificationIcon ToIconSource(this SeIconChar iconChar) =>
INotificationIconSource.From(iconChar); INotificationIcon.From(iconChar);
/// <inheritdoc cref="INotificationIconSource.From(FontAwesomeIcon)"/> /// <inheritdoc cref="INotificationIcon.From(FontAwesomeIcon)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) =>
INotificationIconSource.From(iconChar); INotificationIcon.From(iconChar);
/// <inheritdoc cref="INotificationIconSource.From(IDalamudTextureWrap,bool)"/> /// <inheritdoc cref="INotificationIcon.FromFile(string)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => public static INotificationIcon ToIconSource(this FileInfo fileInfo) =>
INotificationIconSource.From(wrap, takeOwnership); INotificationIcon.FromFile(fileInfo.FullName);
/// <inheritdoc cref="INotificationIconSource.FromFile(string)"/> /// <summary>Draws an icon from an <see cref="IFontHandle"/> and a <see cref="char"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this FileInfo fileInfo) =>
INotificationIconSource.FromFile(fileInfo.FullName);
/// <summary>Draws an icon string.</summary>
/// <param name="fontHandleLarge">The font handle to use.</param>
/// <param name="c">The icon character.</param>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param> /// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param> /// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="c">The icon character.</param>
/// <param name="fontHandle">The font handle to use.</param>
/// <param name="color">The foreground color.</param> /// <param name="color">The foreground color.</param>
internal static unsafe void DrawIconString( /// <returns><c>true</c> if anything has been drawn.</returns>
IFontHandle fontHandleLarge, internal static unsafe bool DrawIconFrom(
char c,
Vector2 minCoord, Vector2 minCoord,
Vector2 maxCoord, Vector2 maxCoord,
char c,
IFontHandle fontHandle,
Vector4 color) Vector4 color)
{ {
if (c is '\0' or char.MaxValue)
return false;
var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X);
using (fontHandleLarge.Push()) using (fontHandle.Push())
{ {
var font = ImGui.GetFont(); var font = ImGui.GetFont();
ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; var glyphPtr = (ImGuiHelpers.ImFontGlyphReal*)font.FindGlyphNoFallback(c).NativePtr;
if (glyphPtr is null)
return false;
ref readonly var glyph = ref *glyphPtr;
var size = glyph.XY1 - glyph.XY0; var size = glyph.XY1 - glyph.XY0;
var smallerSizeDim = Math.Min(size.X, size.Y); var smallerSizeDim = Math.Min(size.X, size.Y);
var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f;
@ -69,67 +72,72 @@ public static class NotificationUtilities
glyph.UV1, glyph.UV1,
ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha }));
} }
return true;
} }
/// <summary>Draws the given texture, or the icon of the plugin if texture is <c>null</c>.</summary> /// <summary>Draws an icon from an instance of <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="texture">The texture.</param>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param> /// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param> /// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="initiatorPlugin">The initiator plugin.</param> /// <param name="texture">The texture.</param>
internal static void DrawTexture( /// <returns><c>true</c> if anything has been drawn.</returns>
IDalamudTextureWrap? texture, internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, IDalamudTextureWrap? texture)
Vector2 minCoord,
Vector2 maxCoord,
LocalPlugin? initiatorPlugin)
{ {
var handle = nint.Zero; if (texture is null)
var size = Vector2.Zero; return false;
if (texture is not null) try
{ {
try var handle = texture.ImGuiHandle;
var size = texture.Size;
if (size.X > maxCoord.X - minCoord.X)
size *= (maxCoord.X - minCoord.X) / size.X;
if (size.Y > maxCoord.Y - minCoord.Y)
size *= (maxCoord.Y - minCoord.Y) / size.Y;
ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2);
ImGui.Image(handle, size);
return true;
}
catch
{
return false;
}
}
/// <summary>Draws an icon from an instance of <see cref="LocalPlugin"/>.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="plugin">The plugin. Dalamud icon will be drawn if <c>null</c> is given.</param>
/// <returns><c>true</c> if anything has been drawn.</returns>
internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin)
{
var dam = Service<DalamudAssetManager>.Get();
if (plugin is null)
return false;
if (!Service<PluginImageCache>.Get().TryGetIcon(
plugin,
plugin.Manifest,
plugin.IsThirdParty,
out var texture) || texture is null)
{
texture = plugin switch
{ {
handle = texture.ImGuiHandle; { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon),
size = texture.Size; { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon),
} _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon),
catch };
{
// must have been disposed or something; ignore the texture
}
} }
if (handle == nint.Zero) return DrawIconFrom(minCoord, maxCoord, texture);
{ }
var dam = Service<DalamudAssetManager>.Get();
if (initiatorPlugin is null)
{
texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall);
}
else
{
if (!Service<PluginImageCache>.Get().TryGetIcon(
initiatorPlugin,
initiatorPlugin.Manifest,
initiatorPlugin.IsThirdParty,
out texture) || texture is null)
{
texture = initiatorPlugin switch
{
{ IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon),
{ IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon),
_ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon),
};
}
}
handle = texture.ImGuiHandle; /// <summary>Draws the Dalamud logo as an icon.</summary>
size = texture.Size; /// <param name="minCoord">The coordinates of the top left of the icon area.</param>
} /// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord)
if (size.X > maxCoord.X - minCoord.X) {
size *= (maxCoord.X - minCoord.X) / size.X; var dam = Service<DalamudAssetManager>.Get();
if (size.Y > maxCoord.Y - minCoord.Y) var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall);
size *= (maxCoord.Y - minCoord.Y) / size.Y; DrawIconFrom(minCoord, maxCoord, texture);
ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2);
ImGui.Image(handle, size);
} }
} }

View file

@ -116,7 +116,7 @@ internal class ImGuiWidget : IDataWindowWidget
NotificationTemplate.InitialDurationTitles.Length); NotificationTemplate.InitialDurationTitles.Length);
ImGui.Combo( ImGui.Combo(
"Hover Extend Duration", "Extension Duration",
ref this.notificationTemplate.HoverExtendDurationInt, ref this.notificationTemplate.HoverExtendDurationInt,
NotificationTemplate.HoverExtendDurationTitles, NotificationTemplate.HoverExtendDurationTitles,
NotificationTemplate.HoverExtendDurationTitles.Length); NotificationTemplate.HoverExtendDurationTitles.Length);
@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget
this.notificationTemplate.InitialDurationInt == 0 this.notificationTemplate.InitialDurationInt == 0
? TimeSpan.MaxValue ? TimeSpan.MaxValue
: NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt],
DurationSinceLastInterest = ExtensionDurationSinceLastInterest =
this.notificationTemplate.HoverExtendDurationInt == 0 this.notificationTemplate.HoverExtendDurationInt == 0
? TimeSpan.Zero ? TimeSpan.Zero
: NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt],
@ -179,41 +179,40 @@ internal class ImGuiWidget : IDataWindowWidget
4 => -1f, 4 => -1f,
_ => 0.5f, _ => 0.5f,
}, },
IconSource = this.notificationTemplate.IconSourceInt switch Icon = this.notificationTemplate.IconSourceInt switch
{ {
1 => INotificationIconSource.From( 1 => INotificationIcon.From(
(SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0
? 0 ? 0
: this.notificationTemplate.IconSourceText[0])), : this.notificationTemplate.IconSourceText[0])),
2 => INotificationIconSource.From( 2 => INotificationIcon.From(
(FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0
? 0 ? 0
: this.notificationTemplate.IconSourceText[0])), : this.notificationTemplate.IconSourceText[0])),
3 => INotificationIconSource.From( 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText),
Service<DalamudAssetManager>.Get().GetDalamudTextureWrap( 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText),
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[
this.notificationTemplate.IconSourceAssetInt])),
false),
4 => INotificationIconSource.From(
() =>
Service<DalamudAssetManager>.Get().GetDalamudTextureWrapAsync(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[
this.notificationTemplate.IconSourceAssetInt]))),
5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText),
6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText),
7 => INotificationIconSource.From(
Service<TextureManager>.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText),
false),
8 => INotificationIconSource.From(
Service<TextureManager>.Get().GetTextureFromFile(
new(this.notificationTemplate.IconSourceText)),
false),
_ => null, _ => null,
}, },
}, });
true);
var dam = Service<DalamudAssetManager>.Get();
var tm = Service<TextureManager>.Get();
switch (this.notificationTemplate.IconSourceInt)
{
case 5:
n.SetIconTexture(
dam.GetDalamudTextureWrap(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt])));
break;
case 6:
n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText));
break;
case 7:
n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText)));
break;
}
switch (this.notificationTemplate.ProgressMode) switch (this.notificationTemplate.ProgressMode)
{ {
case 2: case 2:
@ -237,8 +236,8 @@ internal class ImGuiWidget : IDataWindowWidget
n.Progress = i / 10f; n.Progress = i / 10f;
} }
n.ExtendBy(NotificationConstants.DefaultDisplayDuration); n.ExtendBy(NotificationConstants.DefaultDuration);
n.InitialDuration = NotificationConstants.DefaultDisplayDuration; n.InitialDuration = NotificationConstants.DefaultDuration;
}); });
break; break;
} }
@ -251,6 +250,10 @@ internal class ImGuiWidget : IDataWindowWidget
n.Click += _ => nclick++; n.Click += _ => nclick++;
n.DrawActions += an => n.DrawActions += an =>
{ {
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"{nclick}");
ImGui.SameLine();
if (ImGui.Button("Update")) if (ImGui.Button("Update"))
{ {
NewRandom(out title, out type, out progress); NewRandom(out title, out type, out progress);
@ -260,18 +263,11 @@ internal class ImGuiWidget : IDataWindowWidget
} }
ImGui.SameLine(); ImGui.SameLine();
ImGui.InputText("##input", ref testString, 255); if (ImGui.Button("Dismiss"))
an.DismissNow();
if (an.IsHovered)
{
ImGui.SameLine();
if (ImGui.Button("Dismiss"))
an.DismissNow();
}
ImGui.AlignTextToFramePadding();
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted($"Clicked {nclick} time(s)"); ImGui.InputText("##input", ref testString, 255);
}; };
} }
} }
@ -315,10 +311,9 @@ internal class ImGuiWidget : IDataWindowWidget
"None (use Type)", "None (use Type)",
"SeIconChar", "SeIconChar",
"FontAwesomeIcon", "FontAwesomeIcon",
"TextureWrap from DalamudAssets",
"TextureWrapTask from DalamudAssets",
"GamePath", "GamePath",
"FilePath", "FilePath",
"TextureWrap from DalamudAssets",
"TextureWrap from GamePath", "TextureWrap from GamePath",
"TextureWrap from FilePath", "TextureWrap from FilePath",
}; };
@ -367,7 +362,7 @@ internal class ImGuiWidget : IDataWindowWidget
{ {
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
NotificationConstants.DefaultDisplayDuration, NotificationConstants.DefaultDuration,
TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10),
}; };

View file

@ -581,7 +581,6 @@ public sealed class UiBuilder : IDisposable
Type = type, Type = type,
InitialDuration = TimeSpan.FromMilliseconds(msDelay), InitialDuration = TimeSpan.FromMilliseconds(msDelay),
}, },
true,
this.localPlugin); this.localPlugin);
_ = this.notifications.TryAdd(an, 0); _ = this.notifications.TryAdd(an, 0);
an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _);

View file

@ -2,21 +2,11 @@ using Dalamud.Interface.ImGuiNotification;
namespace Dalamud.Plugin.Services; namespace Dalamud.Plugin.Services;
/// <summary> /// <summary>Manager for notifications provided by Dalamud using ImGui.</summary>
/// Manager for notifications provided by Dalamud using ImGui.
/// </summary>
public interface INotificationManager public interface INotificationManager
{ {
/// <summary> /// <summary>Adds a notification.</summary>
/// Adds a notification.
/// </summary>
/// <param name="notification">The new notification.</param> /// <param name="notification">The new notification.</param>
/// <param name="disposeNotification">
/// Dispose <paramref name="notification"/> when this function returns, even if the function throws an exception.
/// Set to <c>false</c> to reuse <paramref name="notification"/> for multiple calls to this function, in which case,
/// you should call <see cref="IDisposable.Dispose"/> on the value supplied to <paramref name="notification"/> at a
/// later time.
/// </param>
/// <returns>The added notification.</returns> /// <returns>The added notification.</returns>
IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); IActiveNotification AddNotification(Notification notification);
} }