From a03db8b35da11f1304e7a69a64a4485bffe43ce3 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Mon, 16 Aug 2021 03:36:45 +0200
Subject: [PATCH] feat: add ImGui notifications/toasts
---
.../Interface/Internal/InterfaceManager.cs | 6 +
Dalamud/Interface/Internal/Notifications.cs | 345 ++++++++++++++++++
.../Interface/Internal/Windows/DataWindow.cs | 32 ++
.../Internal/Windows/PluginInstallerWindow.cs | 10 +-
4 files changed, 392 insertions(+), 1 deletion(-)
create mode 100644 Dalamud/Interface/Internal/Notifications.cs
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 58d9054d4..3c8f63f25 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -148,6 +148,11 @@ namespace Dalamud.Interface.Internal
///
public static ImFontPtr MonoFont { get; private set; }
+ ///
+ /// Gets the manager for notifications/toasts.
+ ///
+ public Notifications Notifications { get; init; } = new();
+
///
/// Gets or sets an action that is exexuted when fonts are rebuilt.
///
@@ -626,6 +631,7 @@ namespace Dalamud.Interface.Internal
this.lastWantCapture = this.LastImGuiIoPtr.WantCaptureMouse;
this.Draw?.Invoke();
+ this.Notifications.Draw();
}
}
}
diff --git a/Dalamud/Interface/Internal/Notifications.cs b/Dalamud/Interface/Internal/Notifications.cs
new file mode 100644
index 000000000..4e64a366b
--- /dev/null
+++ b/Dalamud/Interface/Internal/Notifications.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Internal;
+using Dalamud.Utility;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal
+{
+ ///
+ /// Class handling notifications/toasts in ImGui.
+ /// Ported from https://github.com/patrickcjk/imgui-notify.
+ ///
+ internal class Notifications
+ {
+ ///
+ /// Value indicating the bottom-left X padding.
+ ///
+ internal const float NotifyPaddingX = 20.0f;
+
+ ///
+ /// Value indicating the bottom-left Y padding.
+ ///
+ internal const float NotifyPaddingY = 20.0f;
+
+ ///
+ /// Value indicating the Y padding between each message.
+ ///
+ internal const float NotifyPaddingMessageY = 10.0f;
+
+ ///
+ /// Value indicating the fade-in and out duration.
+ ///
+ internal const int NotifyFadeInOutTime = 500;
+
+ ///
+ /// Value indicating the default time until the notification is dismissed.
+ ///
+ internal const int NotifyDefaultDismiss = 3000;
+
+ ///
+ /// Value indicating the maximum opacity.
+ ///
+ internal const float NotifyOpacity = 0.82f;
+
+ ///
+ /// Value indicating default window flags for the notifications.
+ ///
+ internal const ImGuiWindowFlags NotifyToastFlags =
+ ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoInputs |
+ ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing;
+
+ private readonly List notifications = new();
+
+ ///
+ /// Add a notification to the notification queue.
+ ///
+ /// The content of the notification.
+ /// The title of the notification.
+ /// The type of the notification.
+ /// The time the notification should be displayed for.
+ public void AddNotification(
+ string content, string title = null, Notification.Type type = Notification.Type.None, int msDelay = NotifyDefaultDismiss)
+ {
+ this.notifications.Add(new Notification
+ {
+ Content = content,
+ Title = title,
+ NotificationType = type,
+ DurationMs = msDelay,
+ });
+ }
+
+ ///
+ /// Draw all currently queued notifications.
+ ///
+ public void Draw()
+ {
+ var viewportSize = ImGuiHelpers.MainViewport.Size;
+ var height = 0f;
+
+ for (var i = 0; i < this.notifications.Count; i++)
+ {
+ var tn = this.notifications.ElementAt(i);
+
+ if (tn.GetPhase() == Notification.Phase.Expired)
+ {
+ this.notifications.RemoveAt(i);
+ continue;
+ }
+
+ var opacity = tn.GetFadePercent();
+
+ var iconColor = tn.Color;
+ iconColor.W = opacity;
+
+ var windowName = $"##NOTIFY{i}";
+
+ ImGuiHelpers.ForceNextWindowMainViewport();
+ ImGui.SetNextWindowBgAlpha(opacity);
+ ImGui.SetNextWindowPos(new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One);
+ ImGui.Begin(windowName, NotifyToastFlags);
+
+ ImGui.PushTextWrapPos(viewportSize.X / 3.0f);
+
+ var wasTitleRendered = false;
+
+ if (!tn.Icon.IsNullOrEmpty())
+ {
+ wasTitleRendered = true;
+ ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.TextColored(iconColor, tn.Icon);
+ ImGui.PopFont();
+ }
+
+ var textColor = ImGuiColors.DalamudWhite;
+ textColor.W = opacity;
+
+ ImGui.PushStyleColor(ImGuiCol.Text, textColor);
+
+ if (!tn.Title.IsNullOrEmpty())
+ {
+ if (!tn.Icon.IsNullOrEmpty())
+ {
+ ImGui.SameLine();
+ }
+
+ ImGui.TextUnformatted(tn.Title);
+ wasTitleRendered = true;
+ }
+ else if (!tn.DefaultTitle.IsNullOrEmpty())
+ {
+ if (!tn.Icon.IsNullOrEmpty())
+ {
+ ImGui.SameLine();
+ }
+
+ ImGui.TextUnformatted(tn.DefaultTitle);
+ wasTitleRendered = true;
+ }
+
+ if (wasTitleRendered && !tn.Content.IsNullOrEmpty())
+ {
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5.0f);
+ }
+
+ if (!tn.Content.IsNullOrEmpty())
+ {
+ if (wasTitleRendered)
+ {
+ ImGui.Separator();
+ }
+
+ ImGui.TextUnformatted(tn.Content);
+ }
+
+ ImGui.PopStyleColor();
+
+ ImGui.PopTextWrapPos();
+
+ height += ImGui.GetWindowHeight() + NotifyPaddingMessageY;
+
+ ImGui.End();
+ }
+ }
+
+ ///
+ /// Container class for notifications.
+ ///
+ internal class Notification
+ {
+ ///
+ /// Possible notification types.
+ ///
+ public enum Type
+ {
+ ///
+ /// No special type.
+ ///
+ None,
+
+ ///
+ /// Type indicating success.
+ ///
+ Success,
+
+ ///
+ /// Type indicating a warning.
+ ///
+ Warning,
+
+ ///
+ /// Type indicating an error.
+ ///
+ Error,
+
+ ///
+ /// Type indicating generic information.
+ ///
+ Info,
+ }
+
+ ///
+ /// Possible notification phases.
+ ///
+ internal enum Phase
+ {
+ ///
+ /// Phase indicating fade-in.
+ ///
+ FadeIn,
+
+ ///
+ /// Phase indicating waiting until fade-out.
+ ///
+ Wait,
+
+ ///
+ /// Phase indicating fade-out.
+ ///
+ FadeOut,
+
+ ///
+ /// Phase indicating that the notification has expired.
+ ///
+ Expired,
+ }
+
+ ///
+ /// Gets the type of the notification.
+ ///
+ internal Type NotificationType { get; init; }
+
+ ///
+ /// Gets the title of the notification.
+ ///
+ internal string Title { get; init; }
+
+ ///
+ /// Gets the content of the notification.
+ ///
+ internal string Content { get; init; }
+
+ ///
+ /// Gets the duration of the notification in milliseconds.
+ ///
+ internal int DurationMs { get; init; }
+
+ ///
+ /// Gets the creation time of the notification.
+ ///
+ internal DateTime CreationTime { get; init; } = DateTime.Now;
+
+ ///
+ /// Gets the default color of the notification.
+ ///
+ /// Thrown when is set to an out-of-range value.
+ internal Vector4 Color => this.NotificationType switch
+ {
+ Type.None => ImGuiColors.DalamudWhite,
+ Type.Success => ImGuiColors.HealerGreen,
+ Type.Warning => ImGuiColors.DalamudOrange,
+ Type.Error => ImGuiColors.DalamudRed,
+ Type.Info => ImGuiColors.TankBlue,
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+
+ ///
+ /// Gets the icon of the notification.
+ ///
+ /// Thrown when is set to an out-of-range value.
+ internal string? Icon => this.NotificationType switch
+ {
+ Type.None => null,
+ Type.Success => FontAwesomeIcon.CheckCircle.ToIconString(),
+ Type.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(),
+ Type.Error => FontAwesomeIcon.TimesCircle.ToIconString(),
+ Type.Info => FontAwesomeIcon.InfoCircle.ToIconString(),
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+
+ ///
+ /// Gets the default title of the notification.
+ ///
+ /// Thrown when is set to an out-of-range value.
+ internal string? DefaultTitle => this.NotificationType switch
+ {
+ Type.None => null,
+ Type.Success => Type.Success.ToString(),
+ Type.Warning => Type.Warning.ToString(),
+ Type.Error => Type.Error.ToString(),
+ Type.Info => Type.Info.ToString(),
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+
+ ///
+ /// Gets the elapsed time since creating the notification.
+ ///
+ internal TimeSpan ElapsedTime => DateTime.Now - this.CreationTime;
+
+ ///
+ /// Gets the phase of the notification.
+ ///
+ /// The phase of the notification.
+ internal Phase GetPhase()
+ {
+ var elapsed = (int)this.ElapsedTime.TotalMilliseconds;
+
+ if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime)
+ return Phase.Expired;
+ else if (elapsed > NotifyFadeInOutTime + this.DurationMs)
+ return Phase.FadeOut;
+ else if (elapsed > NotifyFadeInOutTime)
+ return Phase.Wait;
+ else
+ return Phase.FadeIn;
+ }
+
+ ///
+ /// Gets the opacity of the notification.
+ ///
+ /// The opacity, in a range from 0 to 1.
+ internal float GetFadePercent()
+ {
+ var phase = this.GetPhase();
+ var elapsed = this.ElapsedTime.TotalMilliseconds;
+
+ if (phase == Phase.FadeIn)
+ {
+ return (float)elapsed / NotifyFadeInOutTime * NotifyOpacity;
+ }
+ else if (phase == Phase.FadeOut)
+ {
+ return (1.0f - (((float)elapsed - NotifyFadeInOutTime - this.DurationMs) /
+ NotifyFadeInOutTime)) * NotifyOpacity;
+ }
+
+ return 1.0f * NotifyOpacity;
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs
index 27ee74d9e..d43500dc0 100644
--- a/Dalamud/Interface/Internal/Windows/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs
@@ -1018,6 +1018,38 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.Button("THIS IS A BUTTON###hoverTestButton");
this.dalamud.InterfaceManager.OverrideGameCursor = !ImGui.IsItemHovered();
+
+ ImGui.Separator();
+
+ if (ImGui.Button("Add random notification"))
+ {
+ var rand = new Random();
+
+ var title = rand.Next(0, 5) switch
+ {
+ 0 => "This is a toast",
+ 1 => "Truly, a toast",
+ 2 => "I am testing this toast",
+ 3 => "I hope this looks right",
+ 4 => "Good stuff",
+ 5 => "Nice",
+ _ => null,
+ };
+
+ var type = rand.Next(0, 4) switch
+ {
+ 0 => Notifications.Notification.Type.Error,
+ 1 => Notifications.Notification.Type.Warning,
+ 2 => Notifications.Notification.Type.Info,
+ 3 => Notifications.Notification.Type.Success,
+ 4 => Notifications.Notification.Type.None,
+ _ => Notifications.Notification.Type.None,
+ };
+
+ var text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla.";
+
+ this.dalamud.InterfaceManager.Notifications.AddNotification(text, title, type);
+ }
}
private void DrawTex()
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
index c0071e3a4..32ab7a868 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
@@ -315,6 +315,11 @@ namespace Dalamud.Interface.Internal.Windows
if (this.updatePluginCount > 0)
{
this.dalamud.PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
+ this.dalamud.InterfaceManager.Notifications.AddNotification($"Updates for {this.updatePluginCount} of your plugins were installed.", "Updates installed!", Notifications.Notification.Type.Success);
+ }
+ else if (this.updatePluginCount == 0)
+ {
+ this.dalamud.InterfaceManager.Notifications.AddNotification("No updates were found.", "No updates", Notifications.Notification.Type.Info);
}
}
});
@@ -682,7 +687,10 @@ namespace Dalamud.Interface.Internal.Windows
{
// There is no need to set as Complete for an individual plugin installation
this.installStatus = OperationStatus.Idle;
- this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name));
+ if (this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)))
+ {
+ this.dalamud.InterfaceManager.Notifications.AddNotification($"The plugin {manifest.Name} was successfully installed.", "Plugin installed!", Notifications.Notification.Type.Success);
+ }
});
}
}