using System; using System.Collections.Generic; using JetBrains.Annotations; using Unity.Cloud.Collaborate.Assets; using Unity.Cloud.Collaborate.UserInterface; using UnityEditor; using UnityEngine.UIElements; namespace Unity.Cloud.Collaborate.Components { [UsedImplicitly] internal class AlertBox : VisualElement { /// <summary> /// Describes the severity of the alert. Used to set the icon. /// </summary> public enum AlertLevel { Info, Warning, Alert } public const string UssClassName = "alert-box"; public const string IconUssClassName = UssClassName + "__icon"; public const string TextUssClassName = UssClassName + "__text"; public const string ButtonUssClassName = UssClassName + "__button"; static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(AlertBox)}.uxml"; static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(AlertBox)}.uss"; readonly Button m_Button; readonly VisualElement m_Icon; readonly TextElement m_Text; // Uss classes to set which icon is displayed. const string k_UssIconInfo = "icon-info"; const string k_UssIconWarning = "icon-warning"; const string k_UssIconAlert = "icon-alert"; /// <summary> /// Queue of alerts to be displayed. /// </summary> readonly SortedSet<AlertEntry> m_AlertEntryList; /// <summary> /// Initialize the box and hide it. /// </summary> public AlertBox() { // Get the layout AddToClassList(UssClassName); AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this); styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath)); // Initialise fields m_Icon = this.Q<VisualElement>(className: IconUssClassName); m_Text = this.Q<TextElement>(className: TextUssClassName); m_Button = this.Q<Button>(className: ButtonUssClassName); m_AlertEntryList = new SortedSet<AlertEntry>(); // No alerts to display, so this hides the box. UpdateAlertBox(); } /// <summary> /// Queue an alert to be displayed. Displayed immediately if there is currently none. If there is an existing /// alert with the same name, it will be replaced with the latest one. /// </summary> /// <param name="id">Unique ID for the queued alert.</param> /// <param name="level">Level of important of the alert.</param> /// <param name="message">Message to be displayed.</param> /// <param name="button">Info to populate optional button.</param> public void QueueAlert([NotNull] string id, AlertLevel level, [NotNull] string message, (string text, Action action)? button = null) { // Search for existing alert. var oldActive = m_AlertEntryList.Count == 0 ? (AlertEntry?)null : m_AlertEntryList.Max; // Create new alert entry. var entry = new AlertEntry(id, level, message, button == null ? (AlertEntry.AlertButton?)null : new AlertEntry.AlertButton { Text = button.Value.text, Action = button.Value.action }); m_AlertEntryList.Add(entry); UpdateAlertBox(oldActive?.Button?.Action); } /// <summary> /// Remove existing alert. If current active one, switch to next one, or hide if none queued. /// </summary> /// <param name="id">Unique ID for the alert.</param> public void DequeueAlert([NotNull] string id) { var oldAlert = m_AlertEntryList.Max; m_AlertEntryList.RemoveWhere(e => e.Id == id); UpdateAlertBox(oldAlert.Button?.Action); } /// <summary> /// Display alert at the front of the queue, or hide if there are none. /// </summary> void UpdateAlertBox(Action previousButtonAction = null) { // Remove old event if it exists. if (previousButtonAction != null) { m_Button.clickable.clicked -= previousButtonAction; } if (m_AlertEntryList.Count == 0) { m_Button.text = string.Empty; m_Button.AddToClassList(UiConstants.ussHidden); AddToClassList(UiConstants.ussHidden); } else { var activeAlert = m_AlertEntryList.Max; m_Text.text = activeAlert.Message; // Update state of optional button if (activeAlert.Button?.Action != null) { m_Button.clickable.clicked += activeAlert.Button.Value.Action; m_Button.text = activeAlert.Button.Value.Text; m_Button.RemoveFromClassList(UiConstants.ussHidden); } else { m_Button.text = string.Empty; m_Button.AddToClassList(UiConstants.ussHidden); } SetAlertLevel(activeAlert.Level); RemoveFromClassList(UiConstants.ussHidden); } } /// <summary> /// Set the icon to the given severity level. /// </summary> /// <param name="level">Level of severity to make the icon.</param> void SetAlertLevel(AlertLevel level) { // Remove old level m_Icon.RemoveFromClassList(k_UssIconInfo); m_Icon.RemoveFromClassList(k_UssIconWarning); m_Icon.RemoveFromClassList(k_UssIconAlert); // Set new one switch (level) { case AlertLevel.Info: m_Icon.AddToClassList(k_UssIconInfo); break; case AlertLevel.Warning: m_Icon.AddToClassList(k_UssIconWarning); break; case AlertLevel.Alert: m_Icon.AddToClassList(k_UssIconAlert); break; default: throw new ArgumentOutOfRangeException(nameof(level), level, null); } } struct AlertEntry : IComparable<AlertEntry> { public readonly string Id; public readonly AlertLevel Level; public readonly string Message; public AlertButton? Button; public AlertEntry(string id, AlertLevel level, string message, AlertButton? button) { Id = id; Level = level; Message = message; Button = button; } public struct AlertButton { public string Text; public Action Action; } public override int GetHashCode() { return Id.GetHashCode(); } public override bool Equals(object obj) { return obj is AlertEntry other && Id == other.Id; } public int CompareTo(AlertEntry other) { var value = Level.CompareTo(other.Level); // If same level, compare by id. return value != 0 ? value : string.Compare(Id, other.Id, StringComparison.Ordinal); } } [UsedImplicitly] public new class UxmlFactory : UxmlFactory<AlertBox> { } } }