Dalamud/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs
2024-02-28 17:11:30 +09:00

411 lines
13 KiB
C#

using System.Numerics;
using System.Runtime.Loader;
using System.Threading;
using Dalamud.Interface.Animation;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Represents an active notification.</summary>
internal sealed partial class ActiveNotification : IActiveNotification
{
private readonly Notification underlyingNotification;
private readonly Easing showEasing;
private readonly Easing hideEasing;
private readonly Easing progressEasing;
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>
private float progressBefore;
/// <summary>Used for calculating correct dismissal progressbar animation (left edge).</summary>
private float prevProgressL;
/// <summary>Used for calculating correct dismissal progressbar animation (right edge).</summary>
private float prevProgressR;
/// <summary>New progress value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private float? newProgress;
/// <summary>New minimized value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private bool? newMinimized;
/// <summary>Initializes a new instance of the <see cref="ActiveNotification"/> class.</summary>
/// <param name="underlyingNotification">The underlying notification.</param>
/// <param name="initiatorPlugin">The initiator plugin. Use <c>null</c> if originated by Dalamud.</param>
public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin)
{
this.underlyingNotification = underlyingNotification with { };
this.initiatorPlugin = initiatorPlugin;
this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration);
this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration);
this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration);
this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration);
this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now;
this.showEasing.Start();
this.progressEasing.Start();
}
/// <inheritdoc/>
public event NotificationDismissedDelegate? Dismiss;
/// <inheritdoc/>
public event Action<IActiveNotification>? Click;
/// <inheritdoc/>
public event Action<IActiveNotification>? DrawActions;
/// <inheritdoc/>
public long Id { get; } = IActiveNotification.CreateNewId();
/// <inheritdoc/>
public DateTime CreatedAt { get; }
/// <inheritdoc/>
public string Content
{
get => this.underlyingNotification.Content;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Content = value;
}
}
/// <inheritdoc/>
public string? Title
{
get => this.underlyingNotification.Title;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Title = value;
}
}
/// <inheritdoc/>
public string? MinimizedText
{
get => this.underlyingNotification.MinimizedText;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.MinimizedText = value;
}
}
/// <inheritdoc/>
public NotificationType Type
{
get => this.underlyingNotification.Type;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Type = value;
}
}
/// <inheritdoc/>
public INotificationIcon? Icon
{
get => this.underlyingNotification.Icon;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.Icon = value;
}
}
/// <inheritdoc/>
public DateTime HardExpiry
{
get => this.underlyingNotification.HardExpiry;
set
{
if (this.underlyingNotification.HardExpiry == value || this.IsDismissed)
return;
this.underlyingNotification.HardExpiry = value;
this.lastInterestTime = DateTime.Now;
}
}
/// <inheritdoc/>
public TimeSpan InitialDuration
{
get => this.underlyingNotification.InitialDuration;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.InitialDuration = value;
this.lastInterestTime = DateTime.Now;
}
}
/// <inheritdoc/>
public TimeSpan ExtensionDurationSinceLastInterest
{
get => this.underlyingNotification.ExtensionDurationSinceLastInterest;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.ExtensionDurationSinceLastInterest = value;
this.lastInterestTime = DateTime.Now;
}
}
/// <inheritdoc/>
public DateTime EffectiveExpiry { get; private set; }
/// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry
{
get => this.underlyingNotification.ShowIndeterminateIfNoExpiry;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.ShowIndeterminateIfNoExpiry = value;
}
}
/// <inheritdoc/>
public bool Minimized
{
get => this.newMinimized ?? this.underlyingNotification.Minimized;
set
{
if (this.IsDismissed)
return;
this.newMinimized = value;
}
}
/// <inheritdoc/>
public bool UserDismissable
{
get => this.underlyingNotification.UserDismissable;
set
{
if (this.IsDismissed)
return;
this.underlyingNotification.UserDismissable = value;
}
}
/// <inheritdoc/>
public float Progress
{
get => this.newProgress ?? this.underlyingNotification.Progress;
set
{
if (this.IsDismissed)
return;
this.newProgress = value;
}
}
/// <inheritdoc/>
public bool IsDismissed => this.hideEasing.IsRunning;
/// <summary>Gets the eased progress.</summary>
private float ProgressEased
{
get
{
var underlyingProgress = this.underlyingNotification.Progress;
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone)
return underlyingProgress;
var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
return this.progressBefore + (state * (underlyingProgress - this.progressBefore));
}
}
/// <summary>Gets the string for the initiator field.</summary>
private string InitiatorString =>
this.initiatorPlugin is not { } plugin
? NotificationConstants.DefaultInitiator
: this.isInitiatorUnloaded
? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name)
: plugin.Name;
/// <summary>Gets the effective text to display when minimized.</summary>
private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" ");
/// <inheritdoc/>
public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical);
/// <summary>Dismisses this notification. Multiple calls will be ignored.</summary>
/// <param name="reason">The reason of dismissal.</param>
public void DismissNow(NotificationDismissReason reason)
{
if (this.hideEasing.IsRunning)
return;
this.hideEasing.Start();
try
{
this.Dismiss?.Invoke(this, reason);
}
catch (Exception e)
{
Log.Error(
e,
$"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}");
}
}
/// <inheritdoc/>
public void ExtendBy(TimeSpan extension)
{
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.hideEasing.Update();
this.progressEasing.Update();
if (this.expandoEasing.IsRunning)
{
this.expandoEasing.Update();
if (this.expandoEasing.IsDone)
this.expandoEasing.Stop();
}
if (this.newProgress is { } newProgressValue)
{
if (Math.Abs(this.underlyingNotification.Progress - newProgressValue) > float.Epsilon)
{
this.progressBefore = this.ProgressEased;
this.underlyingNotification.Progress = newProgressValue;
this.progressEasing.Restart();
this.progressEasing.Update();
}
this.newProgress = null;
}
if (this.newMinimized is { } newMinimizedValue)
{
if (this.underlyingNotification.Minimized != newMinimizedValue)
{
this.underlyingNotification.Minimized = newMinimizedValue;
this.expandoEasing.Restart();
this.expandoEasing.Update();
}
this.newMinimized = null;
}
if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone)
return false;
this.DisposeInternal();
return true;
}
/// <summary>Clears the resources associated with this instance of <see cref="ActiveNotification"/>.</summary>
internal void DisposeInternal()
{
if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose)
wrapToDispose.Dispose();
this.Dismiss = null;
this.Click = null;
this.DrawActions = null;
this.initiatorPlugin = null;
}
}