#nullable enable using System; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using UVC.UIToolkit.Util; namespace UVC.UIToolkit { /// /// 알림 창 컴포넌트. /// 화면 모서리에 표시되는 알림 메시지입니다. /// /// /// 사용 전 초기화: /// /// Notification을 표시하기 전에 UTKNotification.SetRoot(rootVisualElement)로 루트를 설정해야 합니다. /// /// /// /// C# 코드에서 사용: /// /// // 초기화 (root 설정 필요) /// UTKNotification.SetRoot(rootVisualElement); /// /// // 기본 알림 /// UTKNotification.Show("알림", "새로운 메시지가 있습니다.", /// NotificationType.Info, NotificationPosition.TopRight); /// /// // 타입별 알림 /// UTKNotification.ShowSuccess("성공", "저장되었습니다."); /// UTKNotification.ShowError("오류", "실패했습니다."); /// UTKNotification.ShowWarning("경고", "주의가 필요합니다."); /// /// UXML에서 사용: /// /// /// /// /// /// /// [UxmlElement] public partial class UTKNotification : VisualElement, IDisposable { #region Constants private const string USS_PATH = "UIToolkit/Modal/UTKNotification"; private const int DEFAULT_DURATION_MS = 5000; #endregion #region Fields private static VisualElement? _root; private bool _disposed; private VisualElement? _header; private UTKLabel? _iconLabel; private Label? _titleLabel; private UTKButton? _closeButton; private Label? _messageLabel; private VisualElement? _actions; private string _title = ""; private string _message = ""; private NotificationType _type = NotificationType.Info; private int _duration = DEFAULT_DURATION_MS; private NotificationPosition _position = NotificationPosition.TopRight; #endregion #region Events /// 닫힘 이벤트 public event Action? OnClosed; /// 액션 클릭 이벤트 public event Action? OnActionClicked; #endregion #region Properties /// 제목 [UxmlAttribute("title")] public string Title { get => _title; set { _title = value; if (_titleLabel != null) _titleLabel.text = value; } } /// 메시지 [UxmlAttribute("message")] public string Message { get => _message; set { _message = value; if (_messageLabel != null) _messageLabel.text = value; } } /// 알림 유형 [UxmlAttribute("type")] public NotificationType Type { get => _type; set { _type = value; UpdateType(); } } /// 표시 시간 (밀리초, 0이면 수동 닫기) [UxmlAttribute("duration")] public int Duration { get => _duration; set => _duration = value; } /// 표시 위치 [UxmlAttribute("position")] public NotificationPosition Position { get => _position; set => _position = value; } /// 액션 컨테이너 public VisualElement? ActionsContainer => _actions; #endregion #region Enums public enum NotificationType { Info, Success, Warning, Error } public enum NotificationPosition { TopLeft, TopRight, BottomLeft, BottomRight } #endregion #region Constructor public UTKNotification() { UTKThemeManager.Instance.ApplyThemeToElement(this); var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SubscribeToThemeChanges(); // UXML에서 로드될 때 속성이 설정된 후 UI 갱신 // Unity 6의 소스 생성기는 Deserialize에서 필드에 직접 값을 할당하므로 // AttachToPanelEvent를 사용하여 패널에 연결된 후 UI를 갱신 RegisterCallback(OnAttachToPanel); } public UTKNotification(string title, string message, NotificationType type = NotificationType.Info) : this() { Title = title; Message = message; Type = type; } #endregion #region Static Methods /// /// 기본 루트 요소 설정. /// 모든 Notification은 이 루트의 panel.visualTree에 표시됩니다. /// /// Notification을 표시할 기본 루트 요소 public static void SetRoot(VisualElement root) { _root = root; } /// /// 기본 루트 요소 반환 /// public static VisualElement? GetRoot() => _root; private static void ValidateRoot() { if (_root == null) { throw new InvalidOperationException("UTKNotification.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다."); } } #endregion #region Static Factory /// /// Info 알림 표시 /// public static UTKNotification ShowInfo(string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(title, message, NotificationType.Info, NotificationPosition.TopRight, duration); } /// /// Success 알림 표시 /// public static UTKNotification ShowSuccess(string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(title, message, NotificationType.Success, NotificationPosition.TopRight, duration); } /// /// Warning 알림 표시 /// public static UTKNotification ShowWarning(string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(title, message, NotificationType.Warning, NotificationPosition.TopRight, duration); } /// /// Error 알림 표시 /// public static UTKNotification ShowError(string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(title, message, NotificationType.Error, NotificationPosition.TopRight, duration); } /// /// 알림 표시 /// public static UTKNotification Show(string title, string message, NotificationType type, NotificationPosition position, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); var notification = new UTKNotification(title, message, type); notification._duration = duration; notification._position = position; // panel.visualTree에 직접 추가 var root = _root!.panel?.visualTree ?? _root!; root.Add(notification); // 위치 설정 notification.style.position = UnityEngine.UIElements.Position.Absolute; notification.ApplyPosition(position); // 자동 닫기 if (duration > 0) { notification.AutoCloseAsync(duration).Forget(); } return notification; } #endregion #region UI Creation private void CreateUI() { AddToClassList("utk-notification"); // Header _header = new VisualElement { name = "header" }; _header.AddToClassList("utk-notification__header"); Add(_header); _iconLabel = new UTKLabel(); _iconLabel.IconSize = 16; _iconLabel.AddToClassList("utk-notification__icon"); _header.Add(_iconLabel); _titleLabel = new Label { name = "title" }; _titleLabel.AddToClassList("utk-notification__title"); _header.Add(_titleLabel); _closeButton = new UTKButton("close-btn", UTKMaterialIcons.Close); _closeButton.Variant = UTKButton.ButtonVariant.Text; _closeButton.IconOnly = true; _closeButton.IconSize = 16; _closeButton.AddToClassList("utk-notification__close-btn"); _closeButton.RegisterCallback(_ => Close()); _header.Add(_closeButton); // Message _messageLabel = new Label { name = "message" }; _messageLabel.AddToClassList("utk-notification__message"); Add(_messageLabel); // Actions _actions = new VisualElement { name = "actions" }; _actions.AddToClassList("utk-notification__actions"); Add(_actions); UpdateType(); } private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(OnAttachToPanelForTheme); RegisterCallback(OnDetachFromPanelForTheme); } private void OnAttachToPanelForTheme(AttachToPanelEvent evt) { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; UTKThemeManager.Instance.ApplyThemeToElement(this); } private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt) { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; } private void OnThemeChanged(UTKTheme theme) { UTKThemeManager.Instance.ApplyThemeToElement(this); } private void OnAttachToPanel(AttachToPanelEvent evt) { // UXML 속성이 설정된 후 한 번만 UI 갱신 UnregisterCallback(OnAttachToPanel); if (_titleLabel != null) _titleLabel.text = _title; if (_messageLabel != null) _messageLabel.text = _message; UpdateType(); } #endregion #region Methods private void UpdateType() { RemoveFromClassList("utk-notification--info"); RemoveFromClassList("utk-notification--success"); RemoveFromClassList("utk-notification--warning"); RemoveFromClassList("utk-notification--error"); var typeClass = _type switch { NotificationType.Success => "utk-notification--success", NotificationType.Warning => "utk-notification--warning", NotificationType.Error => "utk-notification--error", _ => "utk-notification--info" }; AddToClassList(typeClass); if (_iconLabel != null) { _iconLabel.SetMaterialIcon(_type switch { NotificationType.Success => UTKMaterialIcons.CheckCircle, NotificationType.Warning => UTKMaterialIcons.Warning, NotificationType.Error => UTKMaterialIcons.Error, _ => UTKMaterialIcons.Info }); } } private void ApplyPosition(NotificationPosition position) { const int offset = 16; switch (position) { case NotificationPosition.TopLeft: style.top = offset; style.left = offset; break; case NotificationPosition.TopRight: style.top = offset; style.right = offset; break; case NotificationPosition.BottomLeft: style.bottom = offset; style.left = offset; break; case NotificationPosition.BottomRight: style.bottom = offset; style.right = offset; break; } } private async UniTaskVoid AutoCloseAsync(int delayMs) { await UniTask.Delay(delayMs); if (!_disposed) { Close(); } } /// /// 액션 버튼 추가 /// public void AddAction(string label, string actionId) { if (_actions == null) return; var btn = new UTKButton(label, "", UTKButton.ButtonVariant.Ghost); btn.Size = UTKButton.ButtonSize.Small; btn.OnClicked += () => OnActionClicked?.Invoke(actionId); _actions.Add(btn); UTKChildAnnotator.AnnotateChild(_actions); } /// /// 알림 닫기 /// public void Close() { OnClosed?.Invoke(); RemoveFromHierarchy(); } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UnregisterCallback(OnAttachToPanelForTheme); UnregisterCallback(OnDetachFromPanelForTheme); OnClosed = null; OnActionClicked = null; } #endregion } }