#nullable enable using System; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 알림 창 컴포넌트. /// 화면 모서리에 표시되는 알림 메시지입니다. /// /// /// C# 코드에서 사용: /// /// // 초기화 (root 설정 필요) /// UTKNotification.Initialize(rootVisualElement); /// /// // 기본 알림 /// UTKNotification.Show("알림", "새로운 메시지가 있습니다."); /// /// // 타입별 알림 /// UTKNotification.ShowSuccess("성공", "저장되었습니다."); /// UTKNotification.ShowError("오류", "실패했습니다."); /// UTKNotification.ShowWarning("경고", "주의가 필요합니다."); /// /// // 위치 설정 /// UTKNotification.Show("알림", "메시지", position: NotificationPosition.BottomRight); /// /// // 액션 버튼 있는 알림 /// UTKNotification.Show("알림", "메시지", actions: new[] { /// ("확인", () => Debug.Log("확인")), /// ("취소", () => Debug.Log("취소")) /// }); /// /// 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 bool _disposed; private VisualElement? _header; private Label? _iconLabel; private Label? _titleLabel; private Button? _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] public string Title { get => _title; set { _title = value; if (_titleLabel != null) _titleLabel.text = value; } } /// 메시지 [UxmlAttribute] public string Message { get => _message; set { _message = value; if (_messageLabel != null) _messageLabel.text = value; } } /// 알림 유형 [UxmlAttribute] public NotificationType Type { get => _type; set { _type = value; UpdateType(); } } /// 표시 시간 (밀리초, 0이면 수동 닫기) [UxmlAttribute] public int Duration { get => _duration; set => _duration = value; } /// 표시 위치 [UxmlAttribute] 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 Factory /// /// Info 알림 표시 /// public static UTKNotification ShowInfo(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, title, message, NotificationType.Info, NotificationPosition.TopRight, duration); } /// /// Success 알림 표시 /// public static UTKNotification ShowSuccess(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, title, message, NotificationType.Success, NotificationPosition.TopRight, duration); } /// /// Warning 알림 표시 /// public static UTKNotification ShowWarning(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, title, message, NotificationType.Warning, NotificationPosition.TopRight, duration); } /// /// Error 알림 표시 /// public static UTKNotification ShowError(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, title, message, NotificationType.Error, NotificationPosition.TopRight, duration); } /// /// 알림 표시 /// public static UTKNotification Show(VisualElement parent, string title, string message, NotificationType type, NotificationPosition position, int duration = DEFAULT_DURATION_MS) { var notification = new UTKNotification(title, message, type); notification._duration = duration; notification._position = position; parent.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 Label { name = "icon" }; _iconLabel.AddToClassList("utk-notification__icon"); UTKMaterialIcons.ApplyIconStyle(_iconLabel, 20); _header.Add(_iconLabel); _titleLabel = new Label { name = "title" }; _titleLabel.AddToClassList("utk-notification__title"); _header.Add(_titleLabel); _closeButton = new Button { name = "close-btn", text = UTKMaterialIcons.Close }; _closeButton.AddToClassList("utk-notification__close-btn"); UTKMaterialIcons.ApplyIconStyle(_closeButton, 16); _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(_ => { 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.text = _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); } /// /// 알림 닫기 /// public void Close() { OnClosed?.Invoke(); RemoveFromHierarchy(); } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; OnClosed = null; OnActionClicked = null; } #endregion } }