From 0040f611253add1cd7aabaffd26f12fbb95bb056 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 19:51:00 +0900 Subject: [PATCH] Make notifications minimizable, remove interactable --- .../ImGuiNotification/IActiveNotification.cs | 48 +- .../ImGuiNotification/INotification.cs | 101 +-- .../Internal/ActiveNotification.cs | 685 +++++++++++------- .../IconSource/FontAwesomeIconIconSource.cs | 31 +- .../IconSource/SeIconCharIconSource.cs | 25 +- .../Internal/NotificationManager.cs | 15 +- .../ImGuiNotification/Notification.cs | 64 +- .../NotificationConstants.cs | 31 + .../NotificationDismissReason.cs | 2 +- .../NotificationUtilities.cs | 42 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 66 +- Dalamud/Interface/UiBuilder.cs | 2 +- 12 files changed, 688 insertions(+), 424 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index e6355cd90..504c6d6d5 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,7 +1,5 @@ using System.Threading; -using Dalamud.Interface.Internal.Notifications; - namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -17,7 +15,6 @@ public interface IActiveNotification : INotification /// Invoked upon clicking on the notification. /// - /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -25,7 +22,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse enters the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -33,7 +29,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse leaves the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -41,42 +36,18 @@ public interface IActiveNotification : INotification /// Invoked upon drawing the action bar of the notification. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// event Action DrawActions; - /// - new string Content { get; set; } - - /// - new string? Title { get; set; } - - /// - new NotificationType Type { get; set; } - - /// - new DateTime Expiry { get; set; } - - /// - new bool ShowIndeterminateIfNoExpiry { get; set; } - - /// - new bool Interactable { get; set; } - - /// - new bool UserDismissable { get; set; } - - /// - new TimeSpan HoverExtendDuration { get; set; } - - /// - new float Progress { get; set; } - /// Gets the ID of this notification. long Id { get; } + /// Gets the effective expiry time. + /// Contains if the notification does not expire. + DateTime EffectiveExpiry { get; } + /// Gets a value indicating whether the mouse cursor is on the notification window. bool IsMouseHovered { get; } @@ -87,16 +58,15 @@ public interface IActiveNotification : INotification /// Dismisses this notification. void DismissNow(); + /// Extends this notifiation. + /// The extension time. + /// This does not override . + void ExtendBy(TimeSpan extension); + /// Loads the icon again using the same . /// If is true, then this function is a no-op. void UpdateIcon(); - /// Disposes the previous icon source, take ownership of the new icon source, - /// and calls . - /// Thew new icon source. - /// If is true, then this function is a no-op. - void UpdateIconSource(INotificationIconSource? newIconSource); - /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 9d6167a95..8f5a30e79 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -6,63 +5,69 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. public interface INotification : IDisposable { - /// Gets the content body of the notification. - string Content { get; } + /// Gets or sets the content body of the notification. + string Content { get; set; } - /// Gets the title of the notification. - string? Title { get; } + /// Gets or sets the title of the notification. + string? Title { get; set; } - /// Gets the type of the notification. - NotificationType Type { get; } + /// Gets or sets the text to display when the notification is minimized. + string? MinimizedText { get; set; } - /// Gets the icon source. + /// Gets or sets the type of the notification. + NotificationType Type { get; set; } + + /// Gets or sets the icon source. /// - /// The assigned value will be disposed upon the call on this instance of - /// .
- ///
- /// The following icon sources are currently available.
- ///
    - ///
  • - ///
  • - ///
  • - ///
  • - ///
  • - ///
  • - ///
+ /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership + /// of the new value is transferred to this . Even if the assignment throws an + /// exception, 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. + /// The assigned value will be disposed upon the call on this instance of + /// , unless the same value is assigned, in which case it will do nothing. + /// If this is an , then updating this property + /// will change the icon being displayed (calls ), unless + /// is true. ///
- INotificationIconSource? IconSource { get; } + INotificationIconSource? IconSource { get; set; } - /// Gets the expiry. - /// Set to to make the notification not have an expiry time - /// (sticky, indeterminate, permanent, or persistent). - DateTime Expiry { get; } - - /// Gets a value indicating whether to show an indeterminate expiration animation if - /// is set to . - bool ShowIndeterminateIfNoExpiry { get; } - - /// Gets a value indicating whether this notification may be interacted. + /// Gets or sets the hard expiry. /// - /// Set this value to true if you want to respond to user inputs from - /// . - /// Note that the close buttons for notifications are always provided and interactable. - /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, - /// unless is set or is unset. + /// Setting this value will override and , in that + /// the notification will be dismissed when this expiry expires.
+ /// Set to to make only take effect.
+ /// If neither nor is not MaxValue, then the notification + /// will not expire after a set time. It must be explicitly dismissed by the user of via calling + /// .
+ /// Updating this value will reset the dismiss timer. ///
- bool Interactable { get; } + DateTime HardExpiry { get; set; } - /// Gets a value indicating whether the user can dismiss the notification by themselves. + /// Gets or sets the initial duration. + /// Set to to make only take effect. + /// Updating this value will reset the dismiss timer. + TimeSpan InitialDuration { get; set; } + + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window. + /// + /// If set to or less, then this feature is turned off, and hovering the mouse on the + /// notification will not make the notification stay.
+ /// Updating this value will reset the dismiss timer. + ///
+ TimeSpan HoverExtendDuration { get; set; } + + /// Gets or sets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; set; } + + /// Gets or sets a value indicating whether the notification has been minimized. + bool Minimized { get; set; } + + /// Gets or sets a value indicating whether the user can dismiss the notification by themselves. /// Consider adding a cancel button to . - bool UserDismissable { get; } + bool UserDismissable { get; set; } - /// Gets the new duration for this notification if mouse cursor is on the notification window. - /// - /// If set to or less, then this feature is turned off. - /// This property is applicable regardless of . - /// - TimeSpan HoverExtendDuration { get; } - - /// Gets the progress for the background progress bar of the notification. + /// Gets or sets the progress for the background progress bar of the notification. /// The progress should be in the range between 0 and 1. - float Progress { get; } + float Progress { get; set; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a71c35c49..a89ebeb0b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -25,6 +25,7 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing showEasing; private readonly Easing hideEasing; private readonly Easing progressEasing; + private readonly Easing expandoEasing; /// The progress before for the progress bar animation with . private float progressBefore; @@ -38,6 +39,9 @@ internal sealed class ActiveNotification : IActiveNotification /// New progress value to be updated on next call to . private float? newProgress; + /// New minimized value to be updated on next call to . + private bool? newMinimized; + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. @@ -51,6 +55,7 @@ internal sealed class ActiveNotification : IActiveNotification 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.showEasing.Start(); this.progressEasing.Start(); @@ -88,9 +93,12 @@ internal sealed class ActiveNotification : IActiveNotification public DateTime CreatedAt { get; } = DateTime.Now; /// Gets the time of starting to count the timer for the expiration. - public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; + public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; - /// + /// Gets the extended expiration time from . + public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + + /// public string Content { get => this.underlyingNotification.Content; @@ -102,7 +110,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// public string? Title { get => this.underlyingNotification.Title; @@ -114,7 +122,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// + public string? MinimizedText + { + get => this.underlyingNotification.MinimizedText; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.MinimizedText = value; + } + } + + /// public NotificationType Type { get => this.underlyingNotification.Type; @@ -127,22 +147,98 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource => this.underlyingNotification.IconSource; - - /// - public DateTime Expiry + public INotificationIconSource? IconSource { - get => this.underlyingNotification.Expiry; + get => this.underlyingNotification.IconSource; set { - if (this.underlyingNotification.Expiry == value || this.IsDismissed) + if (this.IsDismissed) + { + value?.Dispose(); return; - this.underlyingNotification.Expiry = value; - this.ExpiryRelativeToTime = DateTime.Now; + } + + this.underlyingNotification.IconSource = value; + this.UpdateIcon(); } } - /// + /// + public DateTime HardExpiry + { + get => this.underlyingNotification.HardExpiry; + set + { + if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) + return; + this.underlyingNotification.HardExpiry = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan InitialDuration + { + get => this.underlyingNotification.InitialDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.InitialDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan HoverExtendDuration + { + get => this.underlyingNotification.HoverExtendDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.HoverExtendDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public DateTime EffectiveExpiry + { + get + { + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + DateTime expiry; + var hoverExtendDuration = this.HoverExtendDuration; + if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + hoverExtendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.HoverRelativeToTime + hoverExtendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.ExtendedExpiry) + expiry = this.ExtendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + expiry = he; + return expiry; + } + } + + /// public bool ShowIndeterminateIfNoExpiry { get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; @@ -154,19 +250,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool Interactable + /// + public bool Minimized { - get => this.underlyingNotification.Interactable; + get => this.newMinimized ?? this.underlyingNotification.Minimized; set { if (this.IsDismissed) return; - this.underlyingNotification.Interactable = value; + this.newMinimized = value; } } - /// + /// public bool UserDismissable { get => this.underlyingNotification.UserDismissable; @@ -178,19 +274,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public TimeSpan HoverExtendDuration - { - get => this.underlyingNotification.HoverExtendDuration; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.HoverExtendDuration = value; - } - } - - /// + /// public float Progress { get => this.newProgress ?? this.underlyingNotification.Progress; @@ -198,7 +282,6 @@ internal sealed class ActiveNotification : IActiveNotification { if (this.IsDismissed) return; - this.newProgress = value; } } @@ -244,13 +327,13 @@ internal sealed class ActiveNotification : IActiveNotification }; /// Gets the default icon of the notification. - private string? DefaultIconString => this.Type switch + private char? DefaultIconChar => this.Type switch { NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), _ => null, }; @@ -273,6 +356,9 @@ internal sealed class ActiveNotification : IActiveNotification ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) : initiatorPlugin.Name; + /// Gets the effective text to display when minimized. + private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); + /// public void Dispose() { @@ -314,16 +400,38 @@ internal sealed class ActiveNotification : IActiveNotification this.showEasing.Update(); this.hideEasing.Update(); this.progressEasing.Update(); - - if (this.newProgress is { } p) + if (this.expandoEasing.IsRunning) { - this.progressBefore = this.ProgressEased; - this.underlyingNotification.Progress = p; - this.progressEasing.Restart(); - this.progressEasing.Update(); + 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; + } + return this.hideEasing.IsRunning && this.hideEasing.IsDone; } @@ -333,12 +441,9 @@ internal sealed class ActiveNotification : IActiveNotification /// The height of the notification. public float Draw(float maxWidth, float offsetY) { - if (!this.IsDismissed - && DateTime.Now > this.Expiry - && (this.HoverExtendDuration <= TimeSpan.Zero || !this.IsMouseHovered)) - { + var effectiveExpiry = this.EffectiveExpiry; + if (!this.IsDismissed && DateTime.Now > effectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); - } var opacity = Math.Clamp( @@ -375,6 +480,12 @@ internal sealed class ActiveNotification : IActiveNotification 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; @@ -384,6 +495,7 @@ internal sealed class ActiveNotification : IActiveNotification 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( @@ -402,83 +514,49 @@ internal sealed class ActiveNotification : IActiveNotification new Vector2(0, offsetY), ImGuiCond.Always, Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); - ImGui.PushStyleVar( - ImGuiStyleVar.WindowPadding, - new Vector2(NotificationConstants.ScaledWindowPadding, 0)); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); ImGui.Begin( $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactable - ? ImGuiWindowFlags.None - : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationMainWindowContent(width); + 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.PopStyleVar(); - - offsetY += windowSize.Y; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight)); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.Begin( - $"##NotifyActionWindow{this.Id}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - - this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationActionWindowContent(interfaceManager, width); - windowSize.Y += actionWindowHeight; - windowPos.Y -= actionWindowHeight; - hovered |= ImGui.IsWindowHovered(); - - ImGui.End(); - ImGui.PopStyleVar(); ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + ImGui.PopStyleVar(3); ImGui.PopID(); - 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); - } - } - if (windowPos.X <= ImGui.GetIO().MousePos.X && windowPos.Y <= ImGui.GetIO().MousePos.Y && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X @@ -489,19 +567,28 @@ internal sealed class ActiveNotification : IActiveNotification this.IsMouseHovered = true; this.MouseEnter.InvokeSafely(this); } + + if (this.HoverExtendDuration > TimeSpan.Zero) + this.HoverRelativeToTime = 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.IsMouseHovered) { - if (this.HoverExtendDuration > TimeSpan.Zero) - { - var newExpiry = DateTime.Now + this.HoverExtendDuration; - if (newExpiry > this.Expiry) - { - this.underlyingNotification.Expiry = newExpiry; - this.ExpiryRelativeToTime = DateTime.Now; - } - } - this.IsMouseHovered = false; this.MouseLeave.InvokeSafely(this); } @@ -509,6 +596,14 @@ internal sealed class ActiveNotification : IActiveNotification return windowSize.Y; } + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.ExtendedExpiry < newExpiry) + this.ExtendedExpiry = newExpiry; + } + /// public void UpdateIcon() { @@ -518,17 +613,6 @@ internal sealed class ActiveNotification : IActiveNotification this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } - /// - public void UpdateIconSource(INotificationIconSource? newIconSource) - { - if (this.IsDismissed || this.underlyingNotification.IconSource == newIconSource) - return; - - this.underlyingNotification.IconSource?.Dispose(); - this.underlyingNotification.IconSource = newIconSource; - this.UpdateIcon(); - } - /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -539,14 +623,13 @@ internal sealed class ActiveNotification : IActiveNotification this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - this.Interactable = true; this.IsInitiatorUnloaded = true; this.UserDismissable = true; this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.Expiry > newMaxExpiry) - this.Expiry = newMaxExpiry; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; return; @@ -617,23 +700,209 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PopClipRect(); } - private void DrawNotificationMainWindowContent(float width) + private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) { - var basePos = ImGui.GetCursorPos(); + 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.IsMouseHovered) + 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.IsMouseHovered + ? 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); + if (!this.IsMouseHovered) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!this.IsMouseHovered) + 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( - basePos, - basePos + new Vector2(NotificationConstants.ScaledIconSize)); - basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; - width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); - this.DrawTitle(basePos, basePos + new Vector2(width, 0)); - basePos.Y = ImGui.GetCursorPosY(); - this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); - // Intention was to have left, right, and bottom have the window padding and top have the component gap, - // but as ImGui only allows horz/vert padding, we add the extra bottom padding. - // Top padding is zero, as the action window will add the padding. - ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); + 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.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) { @@ -643,7 +912,14 @@ internal sealed class ActiveNotification : IActiveNotification barL = midpoint - (length * v); barR = midpoint + (length * v); } - else if (this.Expiry == DateTime.MaxValue) + else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) { if (this.ShowIndeterminateIfNoExpiry) { @@ -663,17 +939,10 @@ internal sealed class ActiveNotification : IActiveNotification this.prevProgressR = barR = 1f; } } - else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } else { - barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; @@ -692,112 +961,4 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } - - private void DrawIcon(Vector2 minCoord, Vector2 maxCoord) - { - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconString = this.DefaultIconString; - if (!string.IsNullOrWhiteSpace(defaultIconString)) - { - FontAwesomeIconIconSource.DrawIconStatic(defaultIconString, minCoord, maxCoord, this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.PushTextWrapPos(maxCoord.X); - - 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(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - } - - private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(maxCoord.X); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width) - { - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsMouseHovered - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - - this.DrawCloseButton( - interfaceManager, - new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding), - NotificationConstants.ScaledWindowPadding); - } - - private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) - { - if (!this.UserDismissable) - return; - - using (interfaceManager.IconFontHandle?.Push()) - { - var str = FontAwesomeIcon.Times.ToIconString(); - var textSize = ImGui.CalcTextSize(str); - var size = Math.Max(textSize.X, textSize.Y); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - if (!this.IsMouseHovered) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); - if (ImGui.Button(str, new(size + (pad * 2)))) - this.DismissNow(NotificationDismissReason.Manual); - - ImGui.PopStyleColor(2); - if (!this.IsMouseHovered) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - } - } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs index 86a6f835c..cfe790851 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs @@ -2,8 +2,6 @@ using System.Numerics; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -27,35 +25,22 @@ internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal /// public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - /// Draws the icon. - /// The icon string. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - internal static void DrawIconStatic(string iconString, Vector2 minCoord, Vector2 maxCoord, Vector4 color) - { - using (Service.Get().IconFontAwesomeFontHandle.Push()) - { - var size = ImGui.CalcTextSize(iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); - } - } - private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(FontAwesomeIcon c) => this.iconString = c.ToIconString(); + public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); public void Dispose() { } public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - DrawIconStatic(this.iconString, minCoord, maxCoord, color); + NotificationUtilities.DrawIconString( + Service.Get().IconFontAwesomeFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs index 83fd0bef6..19fe8e948 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs @@ -3,8 +3,6 @@ using System.Numerics; using Dalamud.Game.Text; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -30,25 +28,20 @@ internal class SeIconCharIconSource : INotificationIconSource.IInternal private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(SeIconChar c) => this.iconString = c.ToIconString(); + public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); public void Dispose() { } - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) - { - using (Service.Get().IconAxisFontHandle.Push()) - { - var size = ImGui.CalcTextSize(this.iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(this.iconString); - ImGui.PopStyleColor(); - } - } + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawIconString( + Service.Get().IconAxisFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index fdea6146a..b457539a3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -106,14 +106,15 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - this.notifications.RemoveAll(static x => - { - if (!x.UpdateAnimations()) - return false; + this.notifications.RemoveAll( + static x => + { + if (!x.UpdateAnimations()) + return false; - x.Dispose(); - return true; - }); + x.Dispose(); + return true; + }); foreach (var tn in this.notifications) height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 9c89dc305..dd1d87c42 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,3 +1,5 @@ +using System.Threading; + using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -5,40 +7,88 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { + private INotificationIconSource? iconSource; + + /// Initializes a new instance of the class. + public Notification() + { + } + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(INotification notification) => this.CopyValuesFrom(notification); + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// public string Content { get; set; } = string.Empty; /// public string? Title { get; set; } + /// + public string? MinimizedText { get; set; } + /// public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource { get; set; } + public INotificationIconSource? IconSource + { + get => this.iconSource; + set + { + var prevSource = Interlocked.Exchange(ref this.iconSource, value); + if (prevSource != value) + prevSource?.Dispose(); + } + } /// - public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + public DateTime HardExpiry { get; set; } = DateTime.MaxValue; + + /// + public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + + /// + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; /// - public bool Interactable { get; set; } = true; + public bool Minimized { get; set; } = true; /// public bool UserDismissable { get; set; } = true; - /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; - /// public float Progress { get; set; } = 1f; /// public void Dispose() { - this.IconSource?.Dispose(); + // Assign to the property; it will take care of disposing this.IconSource = null; } + + /// Copy values from the given instance of . + /// The instance of to copy from. + 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.HoverExtendDuration = copyFrom.HoverExtendDuration; + this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; + this.Minimized = copyFrom.Minimized; + this.UserDismissable = copyFrom.UserDismissable; + this.Progress = copyFrom.Progress; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 800531f39..08ef8aebd 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using Dalamud.Interface.Utility; @@ -56,6 +57,9 @@ public static class NotificationConstants /// Duration of progress change animation. internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of expando animation. + internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -92,6 +96,16 @@ public static class NotificationConstants (TimeSpan.MinValue, "just now"), }; + /// Gets the relative time format strings. + private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort = + { + (TimeSpan.FromDays(1), "{0:%d}d"), + (TimeSpan.FromHours(1), "{0:%h}h"), + (TimeSpan.FromMinutes(1), "{0:%m}m"), + (TimeSpan.FromSeconds(1), "{0:%s}s"), + (TimeSpan.MinValue, "now"), + }; + /// Gets the scaled padding of the window (dot(.) in the above diagram). internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -137,4 +151,21 @@ public static class NotificationConstants /// When. /// The formatted string. internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + + /// Formats an instance of as a relative time. + /// When. + /// The formatted string. + internal static string FormatRelativeDateTimeShort(this DateTime when) + { + var ts = DateTime.Now - when; + foreach (var (minSpan, formatString) in RelativeFormatStringsShort) + { + if (ts < minSpan) + continue; + return string.Format(formatString, ts); + } + + Debug.Assert(false, "must not reach here"); + return "???"; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs index 47e52b142..2c9d6d2a4 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.ImGuiNotification; /// Specifies the reason of dismissal for a notification. public enum NotificationDismissReason { - /// The notification is dismissed because the expiry specified from is + /// The notification is dismissed because the expiry specified from is /// met. Timeout = 1, diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 9b3602b68..016e9b793 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using Dalamud.Game.Text; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; @@ -19,22 +21,56 @@ public static class NotificationUtilities [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => INotificationIconSource.From(wrap, takeOwnership); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => INotificationIconSource.FromFile(fileInfo.FullName); + /// Draws an icon string. + /// The font handle to use. + /// The icon character. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + internal static unsafe void DrawIconString( + IFontHandle fontHandleLarge, + char c, + Vector2 minCoord, + Vector2 maxCoord, + Vector4 color) + { + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); + using (fontHandleLarge.Push()) + { + var font = ImGui.GetFont(); + ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var size = glyph.XY1 - glyph.XY0; + var smallerSizeDim = Math.Min(size.X, size.Y); + var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; + size *= scale; + var pos = ((minCoord + maxCoord) - size) / 2; + pos += ImGui.GetWindowPos(); + ImGui.GetWindowDrawList().AddImage( + font.ContainerAtlas.Textures[glyph.TextureIndex].TexID, + pos, + pos + size, + glyph.UV0, + glyph.UV1, + ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); + } + } + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index ae3f16576..4d3807417 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; @@ -64,6 +63,10 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.SameLine(); ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + ImGui.Checkbox("##manualMinimizedText", ref this.notificationTemplate.ManualMinimizedText); + ImGui.SameLine(); + ImGui.InputText("MinimizedText##minimizedText", ref this.notificationTemplate.MinimizedText, 255); + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); ImGui.SameLine(); ImGui.Combo( @@ -107,10 +110,16 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.Combo( - "Duration", - ref this.notificationTemplate.DurationInt, - NotificationTemplate.DurationTitles, - NotificationTemplate.DurationTitles.Length); + "Initial Duration", + ref this.notificationTemplate.InitialDurationInt, + NotificationTemplate.InitialDurationTitles, + NotificationTemplate.InitialDurationTitles.Length); + + ImGui.Combo( + "Hover Extend Duration", + ref this.notificationTemplate.HoverExtendDurationInt, + NotificationTemplate.HoverExtendDurationTitles, + NotificationTemplate.HoverExtendDurationTitles.Length); ImGui.Combo( "Progress", @@ -118,7 +127,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); - ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); @@ -141,18 +150,26 @@ internal class ImGuiWidget : IDataWindowWidget if (this.notificationTemplate.ManualType) type = (NotificationType)this.notificationTemplate.TypeInt; - var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt]; - var n = notifications.AddNotification( new() { Content = text, Title = title, + MinimizedText = this.notificationTemplate.ManualMinimizedText + ? this.notificationTemplate.MinimizedText + : null, Type = type, ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, - Interactable = this.notificationTemplate.Interactable, + Minimized = this.notificationTemplate.Minimized, UserDismissable = this.notificationTemplate.UserDismissable, - Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, + InitialDuration = + this.notificationTemplate.InitialDurationInt == 0 + ? TimeSpan.MaxValue + : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], + HoverExtendDuration = + this.notificationTemplate.HoverExtendDurationInt == 0 + ? TimeSpan.Zero + : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], Progress = this.notificationTemplate.ProgressMode switch { 0 => 1f, @@ -220,7 +237,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDisplayDuration); + n.InitialDuration = NotificationConstants.DefaultDisplayDuration; }); break; } @@ -324,7 +342,7 @@ internal class ImGuiWidget : IDataWindowWidget nameof(NotificationType.Info), }; - public static readonly string[] DurationTitles = + public static readonly string[] InitialDurationTitles = { "Infinite", "1 seconds", @@ -332,9 +350,17 @@ internal class ImGuiWidget : IDataWindowWidget "10 seconds", }; + public static readonly string[] HoverExtendDurationTitles = + { + "Disable", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + public static readonly TimeSpan[] Durations = { - TimeSpan.MaxValue, + TimeSpan.Zero, TimeSpan.FromSeconds(1), NotificationConstants.DefaultDisplayDuration, TimeSpan.FromSeconds(10), @@ -344,14 +370,17 @@ internal class ImGuiWidget : IDataWindowWidget public string Content; public bool ManualTitle; public string Title; + public bool ManualMinimizedText; + public string MinimizedText; public int IconSourceInt; public string IconSourceText; public int IconSourceAssetInt; public bool ManualType; public int TypeInt; - public int DurationInt; + public int InitialDurationInt; + public int HoverExtendDurationInt; public bool ShowIndeterminateIfNoExpiry; - public bool Interactable; + public bool Minimized; public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -362,14 +391,17 @@ internal class ImGuiWidget : IDataWindowWidget this.Content = string.Empty; this.ManualTitle = false; this.Title = string.Empty; + this.ManualMinimizedText = false; + this.MinimizedText = string.Empty; this.IconSourceInt = 0; this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; this.IconSourceAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; - this.DurationInt = 2; + this.InitialDurationInt = 2; + this.HoverExtendDurationInt = 2; this.ShowIndeterminateIfNoExpiry = true; - this.Interactable = true; + this.Minimized = true; this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1237c9c1f..417d77e7d 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -579,7 +579,7 @@ public sealed class UiBuilder : IDisposable Content = content, Title = title, Type = type, - Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), + InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, true, this.localPlugin);