honor notification window focus

This commit is contained in:
Soreepeong 2024-02-26 20:04:33 +09:00
parent 0040f61125
commit e44180d4a2
6 changed files with 74 additions and 34 deletions

View file

@ -49,7 +49,10 @@ public interface IActiveNotification : INotification
DateTime EffectiveExpiry { get; } DateTime EffectiveExpiry { get; }
/// <summary>Gets a value indicating whether the mouse cursor is on the notification window.</summary> /// <summary>Gets a value indicating whether the mouse cursor is on the notification window.</summary>
bool IsMouseHovered { get; } 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>

View file

@ -33,7 +33,7 @@ public interface INotification : IDisposable
/// <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="HoverExtendDuration"/>, in that /// Setting this value will override <see cref="InitialDuration"/> and <see cref="DurationSinceLastInterest"/>, 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
@ -48,13 +48,14 @@ public interface INotification : IDisposable
/// <remarks>Updating this value will reset the dismiss timer.</remarks> /// <remarks>Updating this value will reset the dismiss timer.</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.</summary> /// <summary>Gets or sets the new duration for this notification once the mouse cursor leaves the window and the
/// window is no longer focused.</summary>
/// <remarks> /// <remarks>
/// If set to <see cref="TimeSpan.Zero"/> or less, then this feature is turned off, and hovering the mouse on the /// If set to <see cref="TimeSpan.Zero"/> or less, then this feature is turned off, and hovering the mouse on the
/// notification 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 HoverExtendDuration { get; set; } TimeSpan DurationSinceLastInterest { 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

@ -93,7 +93,7 @@ internal sealed class ActiveNotification : IActiveNotification
public DateTime CreatedAt { get; } = DateTime.Now; public DateTime CreatedAt { get; } = DateTime.Now;
/// <summary>Gets the time of starting to count the timer for the expiration.</summary> /// <summary>Gets the time of starting to count the timer for the expiration.</summary>
public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; public DateTime LastInterestTime { get; private set; } = DateTime.Now;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary> /// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; public DateTime ExtendedExpiry { get; private set; } = DateTime.Now;
@ -172,7 +172,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.HoverRelativeToTime = DateTime.Now; this.LastInterestTime = DateTime.Now;
} }
} }
@ -185,20 +185,20 @@ internal sealed class ActiveNotification : IActiveNotification
if (this.IsDismissed) if (this.IsDismissed)
return; return;
this.underlyingNotification.InitialDuration = value; this.underlyingNotification.InitialDuration = value;
this.HoverRelativeToTime = DateTime.Now; this.LastInterestTime = DateTime.Now;
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan HoverExtendDuration public TimeSpan DurationSinceLastInterest
{ {
get => this.underlyingNotification.HoverExtendDuration; get => this.underlyingNotification.DurationSinceLastInterest;
set set
{ {
if (this.IsDismissed) if (this.IsDismissed)
return; return;
this.underlyingNotification.HoverExtendDuration = value; this.underlyingNotification.DurationSinceLastInterest = value;
this.HoverRelativeToTime = DateTime.Now; this.LastInterestTime = DateTime.Now;
} }
} }
@ -214,8 +214,8 @@ internal sealed class ActiveNotification : IActiveNotification
: this.CreatedAt + initialDuration; : this.CreatedAt + initialDuration;
DateTime expiry; DateTime expiry;
var hoverExtendDuration = this.HoverExtendDuration; var hoverExtendDuration = this.DurationSinceLastInterest;
if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused))
{ {
expiry = DateTime.MaxValue; expiry = DateTime.MaxValue;
} }
@ -224,7 +224,7 @@ internal sealed class ActiveNotification : IActiveNotification
var expiryExtend = var expiryExtend =
hoverExtendDuration == TimeSpan.MaxValue hoverExtendDuration == TimeSpan.MaxValue
? DateTime.MaxValue ? DateTime.MaxValue
: this.HoverRelativeToTime + hoverExtendDuration; : this.LastInterestTime + hoverExtendDuration;
expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend;
if (expiry < this.ExtendedExpiry) if (expiry < this.ExtendedExpiry)
@ -287,7 +287,10 @@ internal sealed class ActiveNotification : IActiveNotification
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMouseHovered { get; private set; } 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;
@ -529,8 +532,12 @@ internal sealed class ActiveNotification : IActiveNotification
ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoDocking); ImGuiWindowFlags.NoDocking);
this.IsFocused = ImGui.IsWindowFocused();
if (this.IsFocused)
this.LastInterestTime = DateTime.Now;
this.DrawWindowBackgroundProgressBar(); this.DrawWindowBackgroundProgressBar();
this.DrawFocusIndicator();
this.DrawTopBar(interfaceManager, width, actionWindowHeight); this.DrawTopBar(interfaceManager, width, actionWindowHeight);
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
{ {
@ -562,14 +569,14 @@ internal sealed class ActiveNotification : IActiveNotification
&& ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X
&& ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y)
{ {
if (!this.IsMouseHovered) if (!this.IsHovered)
{ {
this.IsMouseHovered = true; this.IsHovered = true;
this.MouseEnter.InvokeSafely(this); this.MouseEnter.InvokeSafely(this);
} }
if (this.HoverExtendDuration > TimeSpan.Zero) if (this.DurationSinceLastInterest > TimeSpan.Zero)
this.HoverRelativeToTime = DateTime.Now; this.LastInterestTime = DateTime.Now;
if (hovered) if (hovered)
{ {
@ -587,9 +594,9 @@ internal sealed class ActiveNotification : IActiveNotification
} }
} }
} }
else if (this.IsMouseHovered) else if (this.IsHovered)
{ {
this.IsMouseHovered = false; this.IsHovered = false;
this.MouseLeave.InvokeSafely(this); this.MouseLeave.InvokeSafely(this);
} }
@ -625,7 +632,7 @@ internal sealed class ActiveNotification : IActiveNotification
this.IsInitiatorUnloaded = true; this.IsInitiatorUnloaded = true;
this.UserDismissable = true; this.UserDismissable = true;
this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration;
var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration;
if (this.EffectiveExpiry > newMaxExpiry) if (this.EffectiveExpiry > newMaxExpiry)
@ -700,6 +707,23 @@ internal sealed class ActiveNotification : IActiveNotification
ImGui.PopClipRect(); 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) private void DrawTopBar(InterfaceManager interfaceManager, float width, float height)
{ {
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
@ -744,7 +768,7 @@ internal sealed class ActiveNotification : IActiveNotification
relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f;
} }
if (this.IsMouseHovered) if (this.IsHovered || this.IsFocused)
ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false);
else else
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
@ -755,7 +779,7 @@ internal sealed class ActiveNotification : IActiveNotification
ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding));
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted( ImGui.TextUnformatted(
this.IsMouseHovered this.IsHovered || this.IsFocused
? this.CreatedAt.FormatAbsoluteDateTime() ? this.CreatedAt.FormatAbsoluteDateTime()
: this.CreatedAt.FormatRelativeDateTime()); : this.CreatedAt.FormatRelativeDateTime());
ImGui.PopStyleColor(); ImGui.PopStyleColor();
@ -799,7 +823,8 @@ internal sealed class ActiveNotification : IActiveNotification
private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size)
{ {
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
if (!this.IsMouseHovered) var alphaPush = !this.IsHovered && !this.IsFocused;
if (alphaPush)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f);
ImGui.PushStyleColor(ImGuiCol.Button, 0); ImGui.PushStyleColor(ImGuiCol.Button, 0);
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor);
@ -808,7 +833,7 @@ internal sealed class ActiveNotification : IActiveNotification
var r = ImGui.Button(icon.ToIconString(), new(size)); var r = ImGui.Button(icon.ToIconString(), new(size));
ImGui.PopStyleColor(2); ImGui.PopStyleColor(2);
if (!this.IsMouseHovered) if (alphaPush)
ImGui.PopStyleVar(); ImGui.PopStyleVar();
ImGui.PopStyleVar(); ImGui.PopStyleVar();
return r; return r;
@ -912,7 +937,7 @@ internal sealed class ActiveNotification : IActiveNotification
barL = midpoint - (length * v); barL = midpoint - (length * v);
barR = midpoint + (length * v); barR = midpoint + (length * v);
} }
else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused))
{ {
barL = 0f; barL = 0f;
barR = 1f; barR = 1f;
@ -942,7 +967,7 @@ internal sealed class ActiveNotification : IActiveNotification
else else
{ {
barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds /
(effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); (effectiveExpiry - this.LastInterestTime).TotalMilliseconds);
barR = 1f; barR = 1f;
this.prevProgressL = barL; this.prevProgressL = barL;
this.prevProgressR = barR; this.prevProgressR = barR;

View file

@ -53,7 +53,7 @@ public sealed record Notification : INotification
public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration;
/// <inheritdoc/> /// <inheritdoc/>
public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration;
/// <inheritdoc/> /// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry { get; set; } = true; public bool ShowIndeterminateIfNoExpiry { get; set; } = true;
@ -85,7 +85,7 @@ public sealed record Notification : INotification
this.IconSource = copyFrom.IconSource?.Clone(); this.IconSource = copyFrom.IconSource?.Clone();
this.HardExpiry = copyFrom.HardExpiry; this.HardExpiry = copyFrom.HardExpiry;
this.InitialDuration = copyFrom.InitialDuration; this.InitialDuration = copyFrom.InitialDuration;
this.HoverExtendDuration = copyFrom.HoverExtendDuration; this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest;
this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry;
this.Minimized = copyFrom.Minimized; this.Minimized = copyFrom.Minimized;
this.UserDismissable = copyFrom.UserDismissable; this.UserDismissable = copyFrom.UserDismissable;

View file

@ -60,6 +60,9 @@ public static class NotificationConstants
/// <summary>Duration of expando animation.</summary> /// <summary>Duration of expando animation.</summary>
internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <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);
/// <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); internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f);
@ -126,6 +129,9 @@ public static class NotificationConstants
/// <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); internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the thickness of the focus indicator rectangle.</summary>
internal 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)"; internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)";

View file

@ -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],
HoverExtendDuration = DurationSinceLastInterest =
this.notificationTemplate.HoverExtendDurationInt == 0 this.notificationTemplate.HoverExtendDurationInt == 0
? TimeSpan.Zero ? TimeSpan.Zero
: NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt],
@ -246,10 +246,12 @@ internal class ImGuiWidget : IDataWindowWidget
if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable)
{ {
var nclick = 0; var nclick = 0;
var testString = "input";
n.Click += _ => nclick++; n.Click += _ => nclick++;
n.DrawActions += an => n.DrawActions += an =>
{ {
if (ImGui.Button("Update in place")) if (ImGui.Button("Update"))
{ {
NewRandom(out title, out type, out progress); NewRandom(out title, out type, out progress);
an.Title = title; an.Title = title;
@ -257,7 +259,10 @@ internal class ImGuiWidget : IDataWindowWidget
an.Progress = progress; an.Progress = progress;
} }
if (an.IsMouseHovered) ImGui.SameLine();
ImGui.InputText("##input", ref testString, 255);
if (an.IsHovered)
{ {
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Dismiss")) if (ImGui.Button("Dismiss"))