443 lines
14 KiB
C#
443 lines
14 KiB
C#
#nullable enable
|
|
using System;
|
|
using Cysharp.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using UVC.UIToolkit.Util;
|
|
|
|
namespace UVC.UIToolkit
|
|
{
|
|
/// <summary>
|
|
/// 알림 창 컴포넌트.
|
|
/// 화면 모서리에 표시되는 알림 메시지입니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>사용 전 초기화:</b></para>
|
|
/// <para>
|
|
/// Notification을 표시하기 전에 <c>UTKNotification.SetRoot(rootVisualElement)</c>로 루트를 설정해야 합니다.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <para><b>C# 코드에서 사용:</b></para>
|
|
/// <code>
|
|
/// // 초기화 (root 설정 필요)
|
|
/// UTKNotification.SetRoot(rootVisualElement);
|
|
///
|
|
/// // 기본 알림
|
|
/// UTKNotification.Show("알림", "새로운 메시지가 있습니다.",
|
|
/// NotificationType.Info, NotificationPosition.TopRight);
|
|
///
|
|
/// // 타입별 알림
|
|
/// UTKNotification.ShowSuccess("성공", "저장되었습니다.");
|
|
/// UTKNotification.ShowError("오류", "실패했습니다.");
|
|
/// UTKNotification.ShowWarning("경고", "주의가 필요합니다.");
|
|
/// </code>
|
|
/// <para><b>UXML에서 사용:</b></para>
|
|
/// <code>
|
|
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
|
|
/// <!-- Notification은 주로 C# 코드로 동적 생성합니다 -->
|
|
/// <utk:UTKNotification title="알림" message="메시지" type="Info" />
|
|
/// </ui:UXML>
|
|
/// </code>
|
|
/// </example>
|
|
[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
|
|
/// <summary>닫힘 이벤트</summary>
|
|
public event Action? OnClosed;
|
|
/// <summary>액션 클릭 이벤트</summary>
|
|
public event Action<string>? OnActionClicked;
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary>제목</summary>
|
|
[UxmlAttribute("title")]
|
|
public string Title
|
|
{
|
|
get => _title;
|
|
set
|
|
{
|
|
_title = value;
|
|
if (_titleLabel != null) _titleLabel.text = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>메시지</summary>
|
|
[UxmlAttribute("message")]
|
|
public string Message
|
|
{
|
|
get => _message;
|
|
set
|
|
{
|
|
_message = value;
|
|
if (_messageLabel != null) _messageLabel.text = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>알림 유형</summary>
|
|
[UxmlAttribute("type")]
|
|
public NotificationType Type
|
|
{
|
|
get => _type;
|
|
set
|
|
{
|
|
_type = value;
|
|
UpdateType();
|
|
}
|
|
}
|
|
|
|
/// <summary>표시 시간 (밀리초, 0이면 수동 닫기)</summary>
|
|
[UxmlAttribute("duration")]
|
|
public int Duration
|
|
{
|
|
get => _duration;
|
|
set => _duration = value;
|
|
}
|
|
|
|
/// <summary>표시 위치</summary>
|
|
[UxmlAttribute("position")]
|
|
public NotificationPosition Position
|
|
{
|
|
get => _position;
|
|
set => _position = value;
|
|
}
|
|
|
|
/// <summary>액션 컨테이너</summary>
|
|
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<StyleSheet>(USS_PATH);
|
|
if (uss != null)
|
|
{
|
|
styleSheets.Add(uss);
|
|
}
|
|
|
|
CreateUI();
|
|
SubscribeToThemeChanges();
|
|
|
|
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
|
|
// Unity 6의 소스 생성기는 Deserialize에서 필드에 직접 값을 할당하므로
|
|
// AttachToPanelEvent를 사용하여 패널에 연결된 후 UI를 갱신
|
|
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
|
}
|
|
|
|
public UTKNotification(string title, string message, NotificationType type = NotificationType.Info) : this()
|
|
{
|
|
Title = title;
|
|
Message = message;
|
|
Type = type;
|
|
}
|
|
#endregion
|
|
|
|
#region Static Methods
|
|
/// <summary>
|
|
/// 기본 루트 요소 설정.
|
|
/// 모든 Notification은 이 루트의 panel.visualTree에 표시됩니다.
|
|
/// </summary>
|
|
/// <param name="root">Notification을 표시할 기본 루트 요소</param>
|
|
public static void SetRoot(VisualElement root)
|
|
{
|
|
_root = root;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기본 루트 요소 반환
|
|
/// </summary>
|
|
public static VisualElement? GetRoot() => _root;
|
|
|
|
private static void ValidateRoot()
|
|
{
|
|
if (_root == null)
|
|
{
|
|
throw new InvalidOperationException("UTKNotification.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다.");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Static Factory
|
|
/// <summary>
|
|
/// Info 알림 표시
|
|
/// </summary>
|
|
public static UTKNotification ShowInfo(string title, string message, int duration = DEFAULT_DURATION_MS)
|
|
{
|
|
return Show(title, message, NotificationType.Info, NotificationPosition.TopRight, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Success 알림 표시
|
|
/// </summary>
|
|
public static UTKNotification ShowSuccess(string title, string message, int duration = DEFAULT_DURATION_MS)
|
|
{
|
|
return Show(title, message, NotificationType.Success, NotificationPosition.TopRight, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Warning 알림 표시
|
|
/// </summary>
|
|
public static UTKNotification ShowWarning(string title, string message, int duration = DEFAULT_DURATION_MS)
|
|
{
|
|
return Show(title, message, NotificationType.Warning, NotificationPosition.TopRight, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Error 알림 표시
|
|
/// </summary>
|
|
public static UTKNotification ShowError(string title, string message, int duration = DEFAULT_DURATION_MS)
|
|
{
|
|
return Show(title, message, NotificationType.Error, NotificationPosition.TopRight, duration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 알림 표시
|
|
/// </summary>
|
|
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<ClickEvent>(_ => 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<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
RegisterCallback<DetachFromPanelEvent>(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<AttachToPanelEvent>(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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 액션 버튼 추가
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 알림 닫기
|
|
/// </summary>
|
|
public void Close()
|
|
{
|
|
OnClosed?.Invoke();
|
|
RemoveFromHierarchy();
|
|
}
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
|
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
|
OnClosed = null;
|
|
OnActionClicked = null;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|