#nullable enable using System; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using UVC.UIToolkit.Util; namespace UVC.UIToolkit { /// /// Alert 팝업 컴포넌트. /// 사용자에게 중요한 정보를 알리거나 확인을 요청하는 모달 대화상자입니다. /// /// /// Alert(경고창/대화상자)란? /// /// Alert는 사용자의 주의를 끌기 위해 화면 위에 표시되는 모달 대화상자입니다. /// 배경이 어두워지고 Alert가 닫힐 때까지 다른 작업을 할 수 없습니다. /// 중요한 정보 전달, 사용자 확인 요청, 에러 알림 등에 사용됩니다. /// /// /// Alert vs Toast 차이: /// /// Alert - 사용자가 버튼을 클릭해야 닫힘 (확인 필요) /// Toast - 일정 시간 후 자동으로 사라짐 (단순 알림) /// /// /// Alert 타입 (AlertType): /// /// Info - 일반 정보 (파란색) /// Success - 성공 메시지 (녹색) /// Warning - 경고 메시지 (주황색) /// Error - 오류 메시지 (빨간색) /// Confirm - 확인/취소 버튼이 있는 대화상자 /// /// /// 사용 전 초기화: /// /// Alert를 표시하기 전에 UTKAlert.SetRoot(rootVisualElement)로 루트를 설정해야 합니다. /// /// /// 동기 vs 비동기 호출: /// /// 동기: Show() - 콜백 함수로 결과 처리 /// 비동기: ShowAsync() - await로 결과 대기 /// /// /// 실제 활용 예시: /// /// 저장 완료, 전송 성공 등 알림 /// 삭제 확인, 변경 저장 확인 /// 네트워크 오류, 파일 없음 등 에러 표시 /// 저장하지 않고 나갈지 경고 /// /// /// /// C# 코드에서 사용: /// /// // 초기화 (root 설정 필요) /// UTKAlert.SetRoot(rootVisualElement); /// /// // 기본 알림 /// await UTKAlert.ShowInfoAsync("알림", "작업이 완료되었습니다."); /// /// // 확인 대화상자 /// bool confirmed = await UTKAlert.ShowConfirmAsync("삭제 확인", "정말 삭제하시겠습니까?"); /// if (confirmed) { /// // 삭제 실행 /// } /// /// // 타입별 알림 /// await UTKAlert.ShowSuccessAsync("성공", "저장되었습니다."); /// await UTKAlert.ShowErrorAsync("오류", "파일을 찾을 수 없습니다."); /// await UTKAlert.ShowWarningAsync("경고", "변경사항이 저장되지 않았습니다."); /// /// UXML에서 사용: /// /// /// /// /// /// /// [UxmlElement] public partial class UTKAlert : VisualElement, IDisposable { #region Constants private const string USS_PATH = "UIToolkit/Modal/UTKAlert"; #endregion #region Fields private static VisualElement? _root; private bool _disposed; private UTKModalBlocker? _blocker; private Label? _titleLabel; private Label? _messageLabel; private VisualElement? _buttonContainer; private string _title = ""; private string _message = ""; private AlertType _alertType = AlertType.Info; private string _confirmLabel = "OK"; private string _cancelLabel = "Cancel"; private EventCallback? _keyDownCallback; private VisualElement? _keyEventTarget; #endregion #region Events /// 닫힘 이벤트 public event Action? OnClosed; /// 확인 이벤트 public event Action? OnConfirm; /// 취소 이벤트 public event Action? OnCancel; #endregion #region Properties /// 제목 [UxmlAttribute("title")] public string Title { get => _title; set { _title = value; if (_titleLabel != null) { _titleLabel.text = value; _titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex; } } } /// 메시지 [UxmlAttribute("message")] public string Message { get => _message; set { _message = value; if (_messageLabel != null) _messageLabel.text = value; } } /// 알림 유형 [UxmlAttribute("type")] public AlertType Type { get => _alertType; set { _alertType = value; UpdateType(); } } #endregion #region Enums public enum AlertType { Info, Success, Warning, Error, Confirm } #endregion #region Constructor public UTKAlert() { UTKThemeManager.Instance.ApplyThemeToElement(this); var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SubscribeToThemeChanges(); } public UTKAlert(string title, string message, AlertType type = AlertType.Info) : this() { Title = title; Message = message; Type = type; } #endregion #region Static Methods /// /// 기본 루트 요소 설정. /// 모든 Alert는 이 루트의 panel.visualTree에 표시됩니다. /// /// Alert를 표시할 기본 루트 요소 public static void SetRoot(VisualElement root) { _root = root; } /// /// 기본 루트 요소 반환 /// public static VisualElement? GetRoot() => _root; private static void ValidateRoot() { if (_root == null) { throw new InvalidOperationException("UTKAlert.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다."); } } #endregion #region Static Factory /// /// Info 알림 표시 /// public static UTKAlert ShowInfo(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return Show(title, message, AlertType.Info, onClose, closeOnBlockerClick, confirmLabel); } /// /// Success 알림 표시 /// public static UTKAlert ShowSuccess(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return Show(title, message, AlertType.Success, onClose, closeOnBlockerClick, confirmLabel); } /// /// Warning 알림 표시 /// public static UTKAlert ShowWarning(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return Show(title, message, AlertType.Warning, onClose, closeOnBlockerClick, confirmLabel); } /// /// Error 알림 표시 /// public static UTKAlert ShowError(string title, string message, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return Show(title, message, AlertType.Error, onClose, closeOnBlockerClick, confirmLabel); } /// /// Confirm 대화상자 표시 /// public static UTKAlert ShowConfirm(string title, string message, Action? onConfirm, Action? onCancel = null, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel") { var alert = Show(title, message, AlertType.Confirm, null, closeOnBlockerClick, confirmLabel, cancelLabel); alert.OnConfirm = onConfirm; alert.OnCancel = onCancel; return alert; } /// /// 알림 표시 /// /// 제목 /// 메시지 /// 알림 유형 /// 닫힘 콜백 /// 배경 클릭 시 닫힘 여부 /// 확인 버튼 레이블 /// 취소 버튼 레이블 (Confirm 타입에서만 사용) public static UTKAlert Show(string title, string message, AlertType type = AlertType.Info, Action? onClose = null, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel") { ValidateRoot(); var alert = new UTKAlert(title, message, type); alert.OnClosed = onClose; alert._confirmLabel = confirmLabel; alert._cancelLabel = cancelLabel; alert.UpdateButtons(); alert._blocker = UTKModalBlocker.Show(_root!, 0.5f, closeOnBlockerClick); if (closeOnBlockerClick) { alert._blocker.OnBlockerClicked += alert.Close; } else { // closeOnBlockerClick이 false일 때 blocker 클릭 시 Alert로 포커스 유지 alert._blocker.OnBlockerClicked += () => alert.Focus(); } // panel.visualTree에 직접 추가 var root = _root!.panel?.visualTree ?? _root!; root.Add(alert); // 중앙 정렬 alert.style.position = Position.Absolute; alert.style.left = Length.Percent(50); alert.style.top = Length.Percent(50); alert.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50)); // ESC 키 이벤트 등록 alert.RegisterEscapeKey(_root!); // Alert에 포커스 설정 (키보드 이벤트 수신을 위해) alert.focusable = true; alert.schedule.Execute(() => alert.Focus()); return alert; } #endregion #region Static Factory Async /// /// Info 알림 표시 (비동기) /// public static UniTask ShowInfoAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return ShowAsync(title, message, AlertType.Info, closeOnBlockerClick, confirmLabel); } /// /// Success 알림 표시 (비동기) /// public static UniTask ShowSuccessAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return ShowAsync(title, message, AlertType.Success, closeOnBlockerClick, confirmLabel); } /// /// Warning 알림 표시 (비동기) /// public static UniTask ShowWarningAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return ShowAsync(title, message, AlertType.Warning, closeOnBlockerClick, confirmLabel); } /// /// Error 알림 표시 (비동기) /// public static UniTask ShowErrorAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK") { return ShowAsync(title, message, AlertType.Error, closeOnBlockerClick, confirmLabel); } /// /// Confirm 대화상자 표시 (비동기) /// /// 확인 버튼 클릭 시 true, 취소 버튼 클릭 시 false public static UniTask ShowConfirmAsync(string title, string message, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel") { var tcs = new UniTaskCompletionSource(); var alert = Show(title, message, AlertType.Confirm, null, closeOnBlockerClick, confirmLabel, cancelLabel); alert.OnConfirm = () => tcs.TrySetResult(true); alert.OnCancel = () => tcs.TrySetResult(false); alert.OnClosed = () => tcs.TrySetResult(false); return tcs.Task; } /// /// 알림 표시 (비동기) /// public static UniTask ShowAsync(string title, string message, AlertType type, bool closeOnBlockerClick = false, string confirmLabel = "OK", string cancelLabel = "Cancel") { var tcs = new UniTaskCompletionSource(); var alert = Show(title, message, type, () => tcs.TrySetResult(), closeOnBlockerClick, confirmLabel, cancelLabel); return tcs.Task; } #endregion #region UI Creation private void CreateUI() { AddToClassList("utk-alert"); // Title (상단 왼쪽) _titleLabel = new Label { name = "title" }; _titleLabel.AddToClassList("utk-alert__title"); Add(_titleLabel); // Content (가운데 메시지) var contentContainer = new VisualElement { name = "content" }; contentContainer.AddToClassList("utk-alert__content"); Add(contentContainer); _messageLabel = new Label { name = "message" }; _messageLabel.AddToClassList("utk-alert__message"); contentContainer.Add(_messageLabel); // Buttons _buttonContainer = new VisualElement { name = "buttons" }; _buttonContainer.AddToClassList("utk-alert__buttons"); Add(_buttonContainer); 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); } #endregion #region Methods private void UpdateType() { RemoveFromClassList("utk-alert--info"); RemoveFromClassList("utk-alert--success"); RemoveFromClassList("utk-alert--warning"); RemoveFromClassList("utk-alert--error"); RemoveFromClassList("utk-alert--confirm"); var typeClass = _alertType switch { AlertType.Success => "utk-alert--success", AlertType.Warning => "utk-alert--warning", AlertType.Error => "utk-alert--error", AlertType.Confirm => "utk-alert--confirm", _ => "utk-alert--info" }; AddToClassList(typeClass); // 버튼 업데이트 UpdateButtons(); } private void UpdateButtons() { if (_buttonContainer == null) return; _buttonContainer.Clear(); if (_alertType == AlertType.Confirm) { var cancelBtn = new UTKButton(_cancelLabel, "", UTKButton.ButtonVariant.Normal); cancelBtn.AddToClassList("utk-alert__btn"); cancelBtn.OnClicked += () => { OnCancel?.Invoke(); Close(); }; _buttonContainer.Add(cancelBtn); var confirmBtn = new UTKButton(_confirmLabel, "", UTKButton.ButtonVariant.Primary); confirmBtn.AddToClassList("utk-alert__btn"); confirmBtn.OnClicked += () => { OnConfirm?.Invoke(); Close(); }; _buttonContainer.Add(confirmBtn); } else { var okBtn = new UTKButton(_confirmLabel, "", UTKButton.ButtonVariant.Primary); okBtn.AddToClassList("utk-alert__btn"); okBtn.OnClicked += Close; _buttonContainer.Add(okBtn); } UTKChildAnnotator.AnnotateChild(_buttonContainer); } /// /// ESC 키 이벤트 등록 /// private void RegisterEscapeKey(VisualElement target) { _keyEventTarget = target; _keyDownCallback = evt => { if (evt.keyCode == KeyCode.Escape) { evt.StopPropagation(); HandleEscapeKey(); } }; // 패널에 키 이벤트 등록 (포커스와 관계없이 캡처) _keyEventTarget.RegisterCallback(_keyDownCallback, TrickleDown.TrickleDown); } /// /// ESC 키 해제 /// private void UnregisterEscapeKey() { if (_keyDownCallback != null && _keyEventTarget != null) { _keyEventTarget.UnregisterCallback(_keyDownCallback, TrickleDown.TrickleDown); _keyDownCallback = null; _keyEventTarget = null; } } /// /// ESC 키 처리 /// private void HandleEscapeKey() { if (_alertType == AlertType.Confirm) { // Confirm 타입: 취소 처리 OnCancel?.Invoke(); } Close(); } /// /// 알림 닫기 /// public void Close() { UnregisterEscapeKey(); OnClosed?.Invoke(); RemoveFromHierarchy(); if (_blocker != null) { _blocker.OnBlockerClicked -= Close; _blocker.Hide(); } _blocker = null; } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UnregisterCallback(OnAttachToPanelForTheme); UnregisterCallback(OnDetachFromPanelForTheme); OnClosed = null; OnConfirm = null; OnCancel = null; _blocker = null; } #endregion } }