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); + } }); } }