mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-13 12:14:16 +01:00
* Switch INotificationManager and ITitleScreenMenu to use ISharedImmediateTexture #1879 * Remove SetIconTexture Remove some remarks that no longer apply * Cleanup StyleCop warnings
352 lines
12 KiB
C#
352 lines
12 KiB
C#
using System.Runtime.Loader;
|
|
|
|
using Dalamud.Configuration.Internal;
|
|
using Dalamud.Interface.Animation;
|
|
using Dalamud.Interface.Animation.EasingFunctions;
|
|
using Dalamud.Interface.Textures;
|
|
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 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 long Id { get; } = IActiveNotification.CreateNewId();
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime CreatedAt { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public string Content
|
|
{
|
|
get => this.underlyingNotification.Content;
|
|
set => this.underlyingNotification.Content = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string? Title
|
|
{
|
|
get => this.underlyingNotification.Title;
|
|
set => this.underlyingNotification.Title = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool RespectUiHidden
|
|
{
|
|
get => this.underlyingNotification.RespectUiHidden;
|
|
set => this.underlyingNotification.RespectUiHidden = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string? MinimizedText
|
|
{
|
|
get => this.underlyingNotification.MinimizedText;
|
|
set => this.underlyingNotification.MinimizedText = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public NotificationType Type
|
|
{
|
|
get => this.underlyingNotification.Type;
|
|
set => this.underlyingNotification.Type = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public INotificationIcon? Icon
|
|
{
|
|
get => this.underlyingNotification.Icon;
|
|
set => this.underlyingNotification.Icon = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ISharedImmediateTexture? IconTexture
|
|
{
|
|
get => this.underlyingNotification.IconTexture;
|
|
set => this.underlyingNotification.IconTexture = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime HardExpiry
|
|
{
|
|
get => this.underlyingNotification.HardExpiry;
|
|
set
|
|
{
|
|
if (this.underlyingNotification.HardExpiry == value)
|
|
return;
|
|
this.underlyingNotification.HardExpiry = value;
|
|
this.lastInterestTime = DateTime.Now;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public TimeSpan InitialDuration
|
|
{
|
|
get => this.underlyingNotification.InitialDuration;
|
|
set
|
|
{
|
|
this.underlyingNotification.InitialDuration = value;
|
|
this.lastInterestTime = DateTime.Now;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public TimeSpan ExtensionDurationSinceLastInterest
|
|
{
|
|
get => this.underlyingNotification.ExtensionDurationSinceLastInterest;
|
|
set
|
|
{
|
|
this.underlyingNotification.ExtensionDurationSinceLastInterest = value;
|
|
this.lastInterestTime = DateTime.Now;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime EffectiveExpiry { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public NotificationDismissReason? DismissReason { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public bool ShowIndeterminateIfNoExpiry
|
|
{
|
|
get => this.underlyingNotification.ShowIndeterminateIfNoExpiry;
|
|
set => this.underlyingNotification.ShowIndeterminateIfNoExpiry = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool Minimized
|
|
{
|
|
get => this.newMinimized ?? this.underlyingNotification.Minimized;
|
|
set => this.newMinimized = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool UserDismissable
|
|
{
|
|
get => this.underlyingNotification.UserDismissable;
|
|
set => this.underlyingNotification.UserDismissable = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public float Progress
|
|
{
|
|
get => this.newProgress ?? this.underlyingNotification.Progress;
|
|
set => this.newProgress = value;
|
|
}
|
|
|
|
private static bool ReducedMotions => Service<DalamudConfiguration>.Get().ReduceMotions ?? false;
|
|
|
|
/// <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 || ReducedMotions)
|
|
return underlyingProgress;
|
|
|
|
var state = ReducedMotions ? 1f : 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.DismissReason is not null)
|
|
return;
|
|
|
|
this.DismissReason = reason;
|
|
this.hideEasing.Start();
|
|
this.InvokeDismiss();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void ExtendBy(TimeSpan extension)
|
|
{
|
|
var newExpiry = DateTime.Now + extension;
|
|
if (this.extendedExpiry < newExpiry)
|
|
this.extendedExpiry = newExpiry;
|
|
}
|
|
|
|
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
|
|
/// <remarks>
|
|
/// This is done to prevent references of plugins being unloaded from outliving the plugin itself.
|
|
/// Anything that can contain plugin-provided types and functions count, which effectively means that events and
|
|
/// interface/object-typed fields need to be scrubbed.
|
|
/// As a notification can be marked as non-user-dismissable, in which case after removing event handlers there will
|
|
/// be no way to remove the notification, we force the notification to become user-dismissable, and reset the expiry
|
|
/// to the default duration on unload.
|
|
/// </remarks>
|
|
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;
|
|
|
|
if (this.IconTexture is { } previousTexture && !IsOwnedByDalamud(previousTexture.GetType()))
|
|
this.IconTexture = 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()
|
|
{
|
|
this.Dismiss = null;
|
|
this.Click = null;
|
|
this.DrawActions = null;
|
|
this.initiatorPlugin = null;
|
|
}
|
|
|
|
private void LogEventInvokeError(Exception exception, string message) =>
|
|
Log.Error(
|
|
exception,
|
|
$"[{nameof(ActiveNotification)}:{this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}] {message}");
|
|
}
|