#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
}
}