UIToolkit 기본 UI 개발 중
This commit is contained in:
332
Assets/Scripts/UVC/UIToolkit/Modal/UTKAlert.cs
Normal file
332
Assets/Scripts/UVC/UIToolkit/Modal/UTKAlert.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.Modal;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Alert 팝업 컴포넌트.
|
||||
/// 사용자에게 중요한 정보를 알립니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKAlert : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string USS_PATH = "UIToolkit/Modal/UTKAlert";
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private UTKModalBlocker? _blocker;
|
||||
private VisualElement? _iconContainer;
|
||||
private Label? _iconLabel;
|
||||
private Label? _titleLabel;
|
||||
private Label? _messageLabel;
|
||||
private VisualElement? _buttonContainer;
|
||||
|
||||
private string _title = "";
|
||||
private string _message = "";
|
||||
private AlertType _alertType = AlertType.Info;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>닫힘 이벤트</summary>
|
||||
public event Action? OnClosed;
|
||||
/// <summary>확인 이벤트</summary>
|
||||
public event Action? OnConfirm;
|
||||
/// <summary>취소 이벤트</summary>
|
||||
public event Action? OnCancel;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>제목</summary>
|
||||
[UxmlAttribute]
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
if (_titleLabel != null)
|
||||
{
|
||||
_titleLabel.text = value;
|
||||
_titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>메시지</summary>
|
||||
[UxmlAttribute]
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set
|
||||
{
|
||||
_message = value;
|
||||
if (_messageLabel != null) _messageLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>알림 유형</summary>
|
||||
[UxmlAttribute]
|
||||
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<StyleSheet>(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 Factory
|
||||
/// <summary>
|
||||
/// Info 알림 표시
|
||||
/// </summary>
|
||||
public static UTKAlert ShowInfo(VisualElement parent, string title, string message, Action? onClose = null)
|
||||
{
|
||||
return Show(parent, title, message, AlertType.Info, onClose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Success 알림 표시
|
||||
/// </summary>
|
||||
public static UTKAlert ShowSuccess(VisualElement parent, string title, string message, Action? onClose = null)
|
||||
{
|
||||
return Show(parent, title, message, AlertType.Success, onClose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning 알림 표시
|
||||
/// </summary>
|
||||
public static UTKAlert ShowWarning(VisualElement parent, string title, string message, Action? onClose = null)
|
||||
{
|
||||
return Show(parent, title, message, AlertType.Warning, onClose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error 알림 표시
|
||||
/// </summary>
|
||||
public static UTKAlert ShowError(VisualElement parent, string title, string message, Action? onClose = null)
|
||||
{
|
||||
return Show(parent, title, message, AlertType.Error, onClose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirm 대화상자 표시
|
||||
/// </summary>
|
||||
public static UTKAlert ShowConfirm(VisualElement parent, string title, string message, Action? onConfirm, Action? onCancel = null)
|
||||
{
|
||||
var alert = Show(parent, title, message, AlertType.Confirm, null);
|
||||
alert.OnConfirm = onConfirm;
|
||||
alert.OnCancel = onCancel;
|
||||
return alert;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 알림 표시
|
||||
/// </summary>
|
||||
public static UTKAlert Show(VisualElement parent, string title, string message, AlertType type, Action? onClose)
|
||||
{
|
||||
var alert = new UTKAlert(title, message, type);
|
||||
alert.OnClosed = onClose;
|
||||
|
||||
alert._blocker = UTKModalBlocker.Show(parent, 0.5f, true);
|
||||
alert._blocker.OnBlockerClicked += alert.Close;
|
||||
alert._blocker.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));
|
||||
|
||||
return alert;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Creation
|
||||
private void CreateUI()
|
||||
{
|
||||
AddToClassList("utk-alert");
|
||||
|
||||
_iconContainer = new VisualElement { name = "icon-container" };
|
||||
_iconContainer.AddToClassList("utk-alert__icon-container");
|
||||
Add(_iconContainer);
|
||||
|
||||
_iconLabel = new Label { name = "icon" };
|
||||
_iconLabel.AddToClassList("utk-alert__icon");
|
||||
_iconContainer.Add(_iconLabel);
|
||||
|
||||
var contentContainer = new VisualElement { name = "content" };
|
||||
contentContainer.AddToClassList("utk-alert__content");
|
||||
Add(contentContainer);
|
||||
|
||||
_titleLabel = new Label { name = "title" };
|
||||
_titleLabel.AddToClassList("utk-alert__title");
|
||||
contentContainer.Add(_titleLabel);
|
||||
|
||||
_messageLabel = new Label { name = "message" };
|
||||
_messageLabel.AddToClassList("utk-alert__message");
|
||||
contentContainer.Add(_messageLabel);
|
||||
|
||||
_buttonContainer = new VisualElement { name = "buttons" };
|
||||
_buttonContainer.AddToClassList("utk-alert__buttons");
|
||||
Add(_buttonContainer);
|
||||
|
||||
UpdateType();
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
RegisterCallback<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
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);
|
||||
|
||||
// 아이콘 업데이트
|
||||
if (_iconLabel != null)
|
||||
{
|
||||
_iconLabel.text = _alertType switch
|
||||
{
|
||||
AlertType.Success => "✓",
|
||||
AlertType.Warning => "⚠",
|
||||
AlertType.Error => "✕",
|
||||
AlertType.Confirm => "?",
|
||||
_ => "ℹ"
|
||||
};
|
||||
}
|
||||
|
||||
// 버튼 업데이트
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private void UpdateButtons()
|
||||
{
|
||||
if (_buttonContainer == null) return;
|
||||
|
||||
_buttonContainer.Clear();
|
||||
|
||||
if (_alertType == AlertType.Confirm)
|
||||
{
|
||||
var cancelBtn = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal);
|
||||
cancelBtn.AddToClassList("utk-alert__btn");
|
||||
cancelBtn.OnClicked += () =>
|
||||
{
|
||||
OnCancel?.Invoke();
|
||||
Close();
|
||||
};
|
||||
_buttonContainer.Add(cancelBtn);
|
||||
|
||||
var confirmBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
|
||||
confirmBtn.AddToClassList("utk-alert__btn");
|
||||
confirmBtn.OnClicked += () =>
|
||||
{
|
||||
OnConfirm?.Invoke();
|
||||
Close();
|
||||
};
|
||||
_buttonContainer.Add(confirmBtn);
|
||||
}
|
||||
else
|
||||
{
|
||||
var okBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
|
||||
okBtn.AddToClassList("utk-alert__btn");
|
||||
okBtn.OnClicked += Close;
|
||||
_buttonContainer.Add(okBtn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 알림 닫기
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
OnClosed?.Invoke();
|
||||
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;
|
||||
OnClosed = null;
|
||||
OnConfirm = null;
|
||||
OnCancel = null;
|
||||
_blocker = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKAlert.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKAlert.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 969edca0103007147b72d7f0020eea98
|
||||
1085
Assets/Scripts/UVC/UIToolkit/Modal/UTKColorPicker.cs
Normal file
1085
Assets/Scripts/UVC/UIToolkit/Modal/UTKColorPicker.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb6c13fd5d2027542a2c6de9c1c33344
|
||||
572
Assets/Scripts/UVC/UIToolkit/Modal/UTKDatePicker.cs
Normal file
572
Assets/Scripts/UVC/UIToolkit/Modal/UTKDatePicker.cs
Normal file
@@ -0,0 +1,572 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.Locale;
|
||||
using UVC.UIToolkit.Input;
|
||||
|
||||
namespace UVC.UIToolkit.Modal
|
||||
{
|
||||
/// <summary>
|
||||
/// UIToolkit 기반 날짜/시간 피커 모달
|
||||
/// 캘린더 그리드 + 시간 선택 지원
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKDatePicker : VisualElement, IDisposable
|
||||
{
|
||||
#region Enums
|
||||
public enum PickerMode
|
||||
{
|
||||
DateOnly,
|
||||
DateAndTime
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
private const string UXML_PATH = "UIToolkit/Modal/UTKDatePicker";
|
||||
private const string USS_PATH = "UIToolkit/Modal/UTKDatePicker";
|
||||
private const int DAYS_IN_GRID = 42; // 6 rows x 7 columns
|
||||
private static readonly string[] DAY_NAME_KEYS = { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" };
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private PickerMode _mode;
|
||||
private DateTime _selectedDate = DateTime.Today;
|
||||
private DateTime _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
|
||||
|
||||
private UTKModalBlocker? _blocker;
|
||||
private readonly List<Button> _dayButtons = new();
|
||||
|
||||
// UI 요소
|
||||
private Label? _titleLabel;
|
||||
private Button? _closeButton;
|
||||
private Label? _yearMonthLabel;
|
||||
private Button? _prevYearButton;
|
||||
private Button? _prevMonthButton;
|
||||
private Button? _nextMonthButton;
|
||||
private Button? _nextYearButton;
|
||||
private VisualElement? _dayNamesRow;
|
||||
private VisualElement? _timeRow;
|
||||
private UTKNumberStepper? _hourStepper;
|
||||
private UTKNumberStepper? _minuteStepper;
|
||||
private Button? _cancelButton;
|
||||
private Button? _confirmButton;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>날짜가 선택되었을 때 발생</summary>
|
||||
public event Action<DateTime>? OnDateSelected;
|
||||
|
||||
/// <summary>피커가 닫힐 때 발생</summary>
|
||||
public event Action? OnClosed;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public UTKDatePicker()
|
||||
{
|
||||
// 테마 스타일시트 적용
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
// 컴포넌트 스타일시트 로드
|
||||
var styleSheet = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (styleSheet != null)
|
||||
{
|
||||
styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||||
if (visualTree != null)
|
||||
{
|
||||
visualTree.CloneTree(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateUI();
|
||||
}
|
||||
|
||||
QueryElements();
|
||||
SetupEvents();
|
||||
SubscribeToThemeChanges();
|
||||
|
||||
// 캘린더 초기화
|
||||
UpdateCalendar();
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
|
||||
// 패널에서 분리될 때 이벤트 구독 해제
|
||||
RegisterCallback<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// 날짜 피커를 표시합니다.
|
||||
/// </summary>
|
||||
public static UTKDatePicker Show(
|
||||
VisualElement parent,
|
||||
DateTime initialDate,
|
||||
PickerMode mode = PickerMode.DateOnly,
|
||||
string title = "Select Date")
|
||||
{
|
||||
var picker = new UTKDatePicker();
|
||||
picker._mode = mode;
|
||||
picker._selectedDate = initialDate;
|
||||
picker._displayMonth = new DateTime(initialDate.Year, initialDate.Month, 1);
|
||||
|
||||
// 블로커 추가
|
||||
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
|
||||
picker._blocker.OnBlockerClicked += picker.Close;
|
||||
|
||||
// 피커 추가
|
||||
parent.Add(picker);
|
||||
|
||||
// UI 초기화
|
||||
picker.SetTitle(title);
|
||||
picker.SetTimeVisible(mode == PickerMode.DateAndTime);
|
||||
picker.UpdateTimeFields();
|
||||
picker.UpdateCalendar();
|
||||
picker.CenterOnScreen();
|
||||
|
||||
return picker;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
public void SetDate(DateTime date)
|
||||
{
|
||||
_selectedDate = date;
|
||||
_displayMonth = new DateTime(date.Year, date.Month, 1);
|
||||
UpdateTimeFields();
|
||||
UpdateCalendar();
|
||||
}
|
||||
|
||||
public DateTime GetDate()
|
||||
{
|
||||
if (_mode == PickerMode.DateAndTime)
|
||||
{
|
||||
int hour = _hourStepper?.Value ?? 0;
|
||||
int minute = _minuteStepper?.Value ?? 0;
|
||||
return new DateTime(
|
||||
_selectedDate.Year,
|
||||
_selectedDate.Month,
|
||||
_selectedDate.Day,
|
||||
hour,
|
||||
minute,
|
||||
0
|
||||
);
|
||||
}
|
||||
return _selectedDate.Date;
|
||||
}
|
||||
|
||||
public void PreviousYear() => NavigateMonth(-12);
|
||||
public void NextYear() => NavigateMonth(12);
|
||||
public void PreviousMonth() => NavigateMonth(-1);
|
||||
public void NextMonth() => NavigateMonth(1);
|
||||
|
||||
public void Close()
|
||||
{
|
||||
OnClosed?.Invoke();
|
||||
_blocker?.Hide();
|
||||
RemoveFromHierarchy();
|
||||
Dispose();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods - UI Creation
|
||||
private void CreateUI()
|
||||
{
|
||||
AddToClassList("utk-date-picker");
|
||||
|
||||
// Header
|
||||
var header = new VisualElement { name = "header" };
|
||||
header.AddToClassList("utk-date-picker__header");
|
||||
|
||||
_titleLabel = new Label("Select Date") { name = "title" };
|
||||
_titleLabel.AddToClassList("utk-date-picker__title");
|
||||
|
||||
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // X
|
||||
_closeButton.AddToClassList("utk-date-picker__close-btn");
|
||||
|
||||
header.Add(_titleLabel);
|
||||
header.Add(_closeButton);
|
||||
Add(header);
|
||||
|
||||
// Navigation
|
||||
var navRow = new VisualElement { name = "nav-row" };
|
||||
navRow.AddToClassList("utk-date-picker__nav-row");
|
||||
|
||||
var leftNav = new VisualElement();
|
||||
leftNav.AddToClassList("utk-date-picker__nav-group");
|
||||
|
||||
_prevYearButton = CreateNavButton("prev-year", "\u00AB"); // <<
|
||||
_prevYearButton.AddToClassList("utk-date-picker__nav-btn--prev-year");
|
||||
|
||||
_prevMonthButton = CreateNavButton("prev-month", "\u2039"); // <
|
||||
|
||||
leftNav.Add(_prevYearButton);
|
||||
leftNav.Add(_prevMonthButton);
|
||||
|
||||
_yearMonthLabel = new Label { name = "year-month" };
|
||||
_yearMonthLabel.AddToClassList("utk-date-picker__year-month-label");
|
||||
|
||||
var rightNav = new VisualElement();
|
||||
rightNav.AddToClassList("utk-date-picker__nav-group");
|
||||
|
||||
_nextMonthButton = CreateNavButton("next-month", "\u203A"); // >
|
||||
|
||||
_nextYearButton = CreateNavButton("next-year", "\u00BB"); // >>
|
||||
_nextYearButton.AddToClassList("utk-date-picker__nav-btn--next-year");
|
||||
|
||||
rightNav.Add(_nextMonthButton);
|
||||
rightNav.Add(_nextYearButton);
|
||||
|
||||
navRow.Add(leftNav);
|
||||
navRow.Add(_yearMonthLabel);
|
||||
navRow.Add(rightNav);
|
||||
Add(navRow);
|
||||
|
||||
// Day names row
|
||||
_dayNamesRow = new VisualElement { name = "day-names" };
|
||||
_dayNamesRow.AddToClassList("utk-date-picker__day-names");
|
||||
|
||||
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
|
||||
{
|
||||
var dayText = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
|
||||
var label = new Label(dayText) { name = $"day-name-{i}" };
|
||||
label.AddToClassList("utk-date-picker__day-name");
|
||||
|
||||
// 요일별 색상 클래스 (0=일요일, 6=토요일)
|
||||
if (i == 0) label.AddToClassList("utk-date-picker__day-name--sunday");
|
||||
else if (i == 6) label.AddToClassList("utk-date-picker__day-name--saturday");
|
||||
|
||||
_dayNamesRow.Add(label);
|
||||
}
|
||||
Add(_dayNamesRow);
|
||||
|
||||
// Calendar grid - 6 rows
|
||||
for (int row = 0; row < 6; row++)
|
||||
{
|
||||
var rowContainer = new VisualElement { name = $"calendar-row-{row}" };
|
||||
rowContainer.AddToClassList("utk-date-picker__calendar-row");
|
||||
|
||||
for (int col = 0; col < 7; col++)
|
||||
{
|
||||
var dayButton = new Button { name = $"day-{row * 7 + col}" };
|
||||
dayButton.AddToClassList("utk-date-picker__day-btn");
|
||||
|
||||
int index = row * 7 + col;
|
||||
dayButton.clicked += () => OnDayClicked(index);
|
||||
|
||||
_dayButtons.Add(dayButton);
|
||||
rowContainer.Add(dayButton);
|
||||
}
|
||||
Add(rowContainer);
|
||||
}
|
||||
|
||||
// Time row (optional)
|
||||
_timeRow = new VisualElement { name = "time-row" };
|
||||
_timeRow.AddToClassList("utk-date-picker__time-row");
|
||||
|
||||
var timeLabel = new Label("시간:");
|
||||
timeLabel.AddToClassList("utk-date-picker__time-label");
|
||||
|
||||
_hourStepper = new UTKNumberStepper(0, 23, 0, 1) { name = "hour-stepper" };
|
||||
_hourStepper.AddToClassList("utk-date-picker__time-stepper");
|
||||
_hourStepper.WrapAround = true;
|
||||
|
||||
var colonLabel = new Label(":");
|
||||
colonLabel.AddToClassList("utk-date-picker__time-separator");
|
||||
|
||||
_minuteStepper = new UTKNumberStepper(0, 59, 0, 1) { name = "minute-stepper" };
|
||||
_minuteStepper.AddToClassList("utk-date-picker__time-stepper");
|
||||
_minuteStepper.WrapAround = true;
|
||||
|
||||
// Tab 키로 시간 <-> 분 이동
|
||||
_hourStepper.OnTabPressed += () => _minuteStepper?.Focus();
|
||||
_minuteStepper.OnShiftTabPressed += () => _hourStepper?.Focus();
|
||||
|
||||
_timeRow.Add(timeLabel);
|
||||
_timeRow.Add(_hourStepper);
|
||||
_timeRow.Add(colonLabel);
|
||||
_timeRow.Add(_minuteStepper);
|
||||
Add(_timeRow);
|
||||
|
||||
// Buttons
|
||||
var buttonRow = new VisualElement { name = "button-row" };
|
||||
buttonRow.AddToClassList("utk-date-picker__button-row");
|
||||
|
||||
_cancelButton = new Button { name = "cancel-btn", text = "Cancel" };
|
||||
_cancelButton.AddToClassList("utk-date-picker__cancel-btn");
|
||||
|
||||
_confirmButton = new Button { name = "confirm-btn", text = "OK" };
|
||||
_confirmButton.AddToClassList("utk-date-picker__confirm-btn");
|
||||
|
||||
buttonRow.Add(_cancelButton);
|
||||
buttonRow.Add(_confirmButton);
|
||||
Add(buttonRow);
|
||||
}
|
||||
|
||||
private Button CreateNavButton(string name, string text)
|
||||
{
|
||||
var btn = new Button { name = name, text = text };
|
||||
btn.AddToClassList("utk-date-picker__nav-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void QueryElements()
|
||||
{
|
||||
_titleLabel ??= this.Q<Label>("title");
|
||||
_closeButton ??= this.Q<Button>("close-btn");
|
||||
_yearMonthLabel ??= this.Q<Label>("year-month");
|
||||
_prevYearButton ??= this.Q<Button>("prev-year");
|
||||
_prevMonthButton ??= this.Q<Button>("prev-month");
|
||||
_nextMonthButton ??= this.Q<Button>("next-month");
|
||||
_nextYearButton ??= this.Q<Button>("next-year");
|
||||
_dayNamesRow ??= this.Q<VisualElement>("day-names");
|
||||
_timeRow ??= this.Q<VisualElement>("time-row");
|
||||
_hourStepper ??= this.Q<UTKNumberStepper>("hour-stepper");
|
||||
_minuteStepper ??= this.Q<UTKNumberStepper>("minute-stepper");
|
||||
_cancelButton ??= this.Q<Button>("cancel-btn");
|
||||
_confirmButton ??= this.Q<Button>("confirm-btn");
|
||||
|
||||
// Day buttons 쿼리
|
||||
if (_dayButtons.Count == 0)
|
||||
{
|
||||
for (int i = 0; i < DAYS_IN_GRID; i++)
|
||||
{
|
||||
var btn = this.Q<Button>($"day-{i}");
|
||||
if (btn != null)
|
||||
{
|
||||
_dayButtons.Add(btn);
|
||||
int index = i;
|
||||
btn.clicked += () => OnDayClicked(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
{
|
||||
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
|
||||
_cancelButton?.RegisterCallback<ClickEvent>(_ => Close());
|
||||
_confirmButton?.RegisterCallback<ClickEvent>(_ => Confirm());
|
||||
|
||||
_prevYearButton?.RegisterCallback<ClickEvent>(_ => PreviousYear());
|
||||
_prevMonthButton?.RegisterCallback<ClickEvent>(_ => PreviousMonth());
|
||||
_nextMonthButton?.RegisterCallback<ClickEvent>(_ => NextMonth());
|
||||
_nextYearButton?.RegisterCallback<ClickEvent>(_ => NextYear());
|
||||
|
||||
// 언어 변경 이벤트 구독
|
||||
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(string newLanguage)
|
||||
{
|
||||
UpdateDayNameLabels();
|
||||
}
|
||||
|
||||
private void UpdateDayNameLabels()
|
||||
{
|
||||
if (_dayNamesRow == null) return;
|
||||
|
||||
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
|
||||
{
|
||||
var label = _dayNamesRow.Q<Label>($"day-name-{i}");
|
||||
if (label != null)
|
||||
{
|
||||
label.text = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods - Logic
|
||||
private void NavigateMonth(int months)
|
||||
{
|
||||
_displayMonth = _displayMonth.AddMonths(months);
|
||||
UpdateCalendar();
|
||||
}
|
||||
|
||||
private void UpdateCalendar()
|
||||
{
|
||||
if (_yearMonthLabel != null)
|
||||
{
|
||||
_yearMonthLabel.text = $"{_displayMonth.Year}년 {_displayMonth.Month:D2}월";
|
||||
}
|
||||
|
||||
// 첫째 날의 요일
|
||||
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
||||
int startDayOfWeek = (int)firstDay.DayOfWeek; // 0=Sunday
|
||||
|
||||
// 해당 월의 일 수
|
||||
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
||||
|
||||
DateTime today = DateTime.Today;
|
||||
|
||||
for (int i = 0; i < DAYS_IN_GRID; i++)
|
||||
{
|
||||
var btn = _dayButtons[i];
|
||||
|
||||
// 동적 클래스 초기화
|
||||
btn.RemoveFromClassList("utk-date-picker__day-btn--sunday");
|
||||
btn.RemoveFromClassList("utk-date-picker__day-btn--saturday");
|
||||
btn.RemoveFromClassList("utk-date-picker__day-btn--today");
|
||||
btn.RemoveFromClassList("utk-date-picker__day-btn--selected");
|
||||
btn.RemoveFromClassList("utk-date-picker__day-btn--other-month");
|
||||
|
||||
int dayNumber = i - startDayOfWeek + 1;
|
||||
|
||||
if (dayNumber < 1 || dayNumber > daysInMonth)
|
||||
{
|
||||
// 이번 달이 아닌 날
|
||||
btn.text = "";
|
||||
btn.SetEnabled(false);
|
||||
btn.AddToClassList("utk-date-picker__day-btn--other-month");
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.text = dayNumber.ToString();
|
||||
btn.SetEnabled(true);
|
||||
|
||||
DateTime currentDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayNumber);
|
||||
DayOfWeek dow = currentDate.DayOfWeek;
|
||||
|
||||
// 선택된 날짜인지 확인
|
||||
bool isSelected = currentDate.Date == _selectedDate.Date;
|
||||
|
||||
// 오늘인지 확인
|
||||
bool isToday = currentDate.Date == today;
|
||||
|
||||
// 선택 상태 클래스
|
||||
if (isSelected)
|
||||
{
|
||||
btn.AddToClassList("utk-date-picker__day-btn--selected");
|
||||
}
|
||||
|
||||
// 오늘 클래스 (선택되지 않은 경우에만)
|
||||
if (isToday && !isSelected)
|
||||
{
|
||||
btn.AddToClassList("utk-date-picker__day-btn--today");
|
||||
}
|
||||
|
||||
// 요일별 클래스 (선택되지 않은 경우에만)
|
||||
if (!isSelected)
|
||||
{
|
||||
if (dow == DayOfWeek.Sunday)
|
||||
{
|
||||
btn.AddToClassList("utk-date-picker__day-btn--sunday");
|
||||
}
|
||||
else if (dow == DayOfWeek.Saturday)
|
||||
{
|
||||
btn.AddToClassList("utk-date-picker__day-btn--saturday");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDayClicked(int index)
|
||||
{
|
||||
// 첫째 날의 요일
|
||||
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
||||
int startDayOfWeek = (int)firstDay.DayOfWeek;
|
||||
|
||||
int dayNumber = index - startDayOfWeek + 1;
|
||||
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
||||
|
||||
if (dayNumber >= 1 && dayNumber <= daysInMonth)
|
||||
{
|
||||
// 시간 유지
|
||||
int hour = _hourStepper?.Value ?? _selectedDate.Hour;
|
||||
int minute = _minuteStepper?.Value ?? _selectedDate.Minute;
|
||||
|
||||
_selectedDate = new DateTime(
|
||||
_displayMonth.Year,
|
||||
_displayMonth.Month,
|
||||
dayNumber,
|
||||
hour,
|
||||
minute,
|
||||
0
|
||||
);
|
||||
|
||||
UpdateCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
OnDateSelected?.Invoke(GetDate());
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SetTitle(string title)
|
||||
{
|
||||
if (_titleLabel != null)
|
||||
_titleLabel.text = title;
|
||||
}
|
||||
|
||||
private void SetTimeVisible(bool visible)
|
||||
{
|
||||
if (_timeRow != null)
|
||||
_timeRow.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void UpdateTimeFields()
|
||||
{
|
||||
if (_hourStepper != null)
|
||||
_hourStepper.SetValue(_selectedDate.Hour, notify: false);
|
||||
|
||||
if (_minuteStepper != null)
|
||||
_minuteStepper.SetValue(_selectedDate.Minute, notify: false);
|
||||
}
|
||||
|
||||
private void CenterOnScreen()
|
||||
{
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
var parent = this.parent;
|
||||
if (parent == null) return;
|
||||
|
||||
float parentWidth = parent.resolvedStyle.width;
|
||||
float parentHeight = parent.resolvedStyle.height;
|
||||
float selfWidth = resolvedStyle.width;
|
||||
float selfHeight = resolvedStyle.height;
|
||||
|
||||
style.left = (parentWidth - selfWidth) / 2;
|
||||
style.top = (parentHeight - selfHeight) / 2;
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 언어 변경 이벤트 구독 해제
|
||||
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
|
||||
|
||||
_dayButtons.Clear();
|
||||
|
||||
OnDateSelected = null;
|
||||
OnClosed = null;
|
||||
|
||||
_blocker = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKDatePicker.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKDatePicker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d01e3cea7bbfd8442a4576a7059802a2
|
||||
285
Assets/Scripts/UVC/UIToolkit/Modal/UTKModal.cs
Normal file
285
Assets/Scripts/UVC/UIToolkit/Modal/UTKModal.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.Modal;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 모달 창 컴포넌트.
|
||||
/// 사용자 정의 콘텐츠를 포함할 수 있는 모달 대화상자입니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKModal : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string USS_PATH = "UIToolkit/Modal/UTKModal";
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private UTKModalBlocker? _blocker;
|
||||
private VisualElement? _header;
|
||||
private Label? _titleLabel;
|
||||
private Button? _closeButton;
|
||||
private VisualElement? _content;
|
||||
private VisualElement? _footer;
|
||||
|
||||
private string _title = "Modal";
|
||||
private bool _showCloseButton = true;
|
||||
private bool _closeOnBackdropClick = true;
|
||||
private ModalSize _size = ModalSize.Medium;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>닫힘 이벤트</summary>
|
||||
public event Action? OnClosed;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>모달 제목</summary>
|
||||
[UxmlAttribute]
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
if (_titleLabel != null) _titleLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>닫기 버튼 표시 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => _showCloseButton;
|
||||
set
|
||||
{
|
||||
_showCloseButton = value;
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>배경 클릭 시 닫기 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool CloseOnBackdropClick
|
||||
{
|
||||
get => _closeOnBackdropClick;
|
||||
set => _closeOnBackdropClick = value;
|
||||
}
|
||||
|
||||
/// <summary>모달 크기</summary>
|
||||
[UxmlAttribute]
|
||||
public ModalSize Size
|
||||
{
|
||||
get => _size;
|
||||
set
|
||||
{
|
||||
_size = value;
|
||||
UpdateSize();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>콘텐츠 컨테이너</summary>
|
||||
public VisualElement? ContentContainer => _content;
|
||||
|
||||
/// <summary>푸터 컨테이너</summary>
|
||||
public VisualElement? FooterContainer => _footer;
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
public enum ModalSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
FullScreen
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public UTKModal()
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (uss != null)
|
||||
{
|
||||
styleSheets.Add(uss);
|
||||
}
|
||||
|
||||
CreateUI();
|
||||
SetupEvents();
|
||||
SubscribeToThemeChanges();
|
||||
}
|
||||
|
||||
public UTKModal(string title, ModalSize size = ModalSize.Medium) : this()
|
||||
{
|
||||
Title = title;
|
||||
Size = size;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// 모달 표시
|
||||
/// </summary>
|
||||
public static UTKModal Show(VisualElement parent, string title, ModalSize size = ModalSize.Medium)
|
||||
{
|
||||
var modal = new UTKModal(title, size);
|
||||
|
||||
modal._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
|
||||
if (modal._closeOnBackdropClick)
|
||||
{
|
||||
modal._blocker.OnBlockerClicked += modal.Close;
|
||||
}
|
||||
modal._blocker.Add(modal);
|
||||
|
||||
// 중앙 정렬
|
||||
modal.style.position = Position.Absolute;
|
||||
modal.style.left = Length.Percent(50);
|
||||
modal.style.top = Length.Percent(50);
|
||||
modal.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
|
||||
|
||||
return modal;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Creation
|
||||
private void CreateUI()
|
||||
{
|
||||
AddToClassList("utk-modal");
|
||||
|
||||
// Header
|
||||
_header = new VisualElement { name = "header" };
|
||||
_header.AddToClassList("utk-modal__header");
|
||||
hierarchy.Add(_header);
|
||||
|
||||
_titleLabel = new Label { name = "title", text = _title };
|
||||
_titleLabel.AddToClassList("utk-modal__title");
|
||||
_header.Add(_titleLabel);
|
||||
|
||||
_closeButton = new Button { name = "close-btn", text = "✕" };
|
||||
_closeButton.AddToClassList("utk-modal__close-btn");
|
||||
_header.Add(_closeButton);
|
||||
|
||||
// Content
|
||||
_content = new VisualElement { name = "content" };
|
||||
_content.AddToClassList("utk-modal__content");
|
||||
hierarchy.Add(_content);
|
||||
|
||||
// Footer
|
||||
_footer = new VisualElement { name = "footer" };
|
||||
_footer.AddToClassList("utk-modal__footer");
|
||||
hierarchy.Add(_footer);
|
||||
|
||||
UpdateSize();
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
{
|
||||
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
|
||||
|
||||
RegisterCallback<KeyDownEvent>(evt =>
|
||||
{
|
||||
if (evt.keyCode == KeyCode.Escape)
|
||||
{
|
||||
Close();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
RegisterCallback<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void UpdateSize()
|
||||
{
|
||||
RemoveFromClassList("utk-modal--small");
|
||||
RemoveFromClassList("utk-modal--medium");
|
||||
RemoveFromClassList("utk-modal--large");
|
||||
RemoveFromClassList("utk-modal--fullscreen");
|
||||
|
||||
var sizeClass = _size switch
|
||||
{
|
||||
ModalSize.Small => "utk-modal--small",
|
||||
ModalSize.Large => "utk-modal--large",
|
||||
ModalSize.FullScreen => "utk-modal--fullscreen",
|
||||
_ => "utk-modal--medium"
|
||||
};
|
||||
AddToClassList(sizeClass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 콘텐츠 추가
|
||||
/// </summary>
|
||||
public new void Add(VisualElement element)
|
||||
{
|
||||
_content?.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 푸터에 요소 추가
|
||||
/// </summary>
|
||||
public void AddToFooter(VisualElement element)
|
||||
{
|
||||
_footer?.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 푸터 표시/숨김
|
||||
/// </summary>
|
||||
public void SetFooterVisible(bool visible)
|
||||
{
|
||||
if (_footer != null)
|
||||
{
|
||||
_footer.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달 닫기
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
OnClosed?.Invoke();
|
||||
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;
|
||||
OnClosed = null;
|
||||
_blocker = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKModal.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKModal.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b126ba0e77214b54796e203a14746d45
|
||||
173
Assets/Scripts/UVC/UIToolkit/Modal/UTKModalBlocker.cs
Normal file
173
Assets/Scripts/UVC/UIToolkit/Modal/UTKModalBlocker.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit.Modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 모달 뒤의 배경을 차단하고 클릭 시 닫힘 처리를 담당하는 컴포넌트
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKModalBlocker : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string USS_CLASS = "utk-modal-blocker";
|
||||
private const float DEFAULT_OPACITY = 0.7f;
|
||||
private const int FADE_DURATION_MS = 200;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private bool _closeOnClick;
|
||||
private float _targetOpacity;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// 블로커가 클릭되었을 때 발생
|
||||
/// </summary>
|
||||
public event Action? OnBlockerClicked;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// UTKModalBlocker 생성자
|
||||
/// </summary>
|
||||
public UTKModalBlocker()
|
||||
{
|
||||
AddToClassList(USS_CLASS);
|
||||
|
||||
// 기본 스타일 설정
|
||||
style.position = Position.Absolute;
|
||||
style.left = 0;
|
||||
style.top = 0;
|
||||
style.right = 0;
|
||||
style.bottom = 0;
|
||||
style.backgroundColor = new Color(0, 0, 0, 0);
|
||||
|
||||
// 클릭 이벤트 등록
|
||||
RegisterCallback<ClickEvent>(OnClick);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// 모달 블로커를 생성하고 표시합니다.
|
||||
/// </summary>
|
||||
/// <param name="root">블로커를 추가할 루트 요소</param>
|
||||
/// <param name="opacity">배경 투명도 (0~1)</param>
|
||||
/// <param name="closeOnClick">클릭 시 자동 닫힘 여부</param>
|
||||
/// <returns>생성된 UTKModalBlocker 인스턴스</returns>
|
||||
public static UTKModalBlocker Show(VisualElement root, float opacity = DEFAULT_OPACITY, bool closeOnClick = false)
|
||||
{
|
||||
var blocker = new UTKModalBlocker
|
||||
{
|
||||
_closeOnClick = closeOnClick,
|
||||
_targetOpacity = Mathf.Clamp01(opacity)
|
||||
};
|
||||
|
||||
root.Add(blocker);
|
||||
blocker.FadeIn();
|
||||
|
||||
return blocker;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 블로커를 숨기고 제거합니다.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
FadeOut(() =>
|
||||
{
|
||||
RemoveFromHierarchy();
|
||||
Dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달 콘텐츠를 블로커 앞에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="modalContent">모달 콘텐츠 요소</param>
|
||||
public void ShowModal(VisualElement modalContent)
|
||||
{
|
||||
if (modalContent == null)
|
||||
return;
|
||||
|
||||
// 모달 콘텐츠를 블로커 다음에 추가
|
||||
var parent = this.parent;
|
||||
if (parent != null)
|
||||
{
|
||||
int blockerIndex = parent.IndexOf(this);
|
||||
parent.Insert(blockerIndex + 1, modalContent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 블로커를 맨 위로 가져옵니다.
|
||||
/// </summary>
|
||||
public void BringToFront()
|
||||
{
|
||||
this.BringToFront();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private void OnClick(ClickEvent evt)
|
||||
{
|
||||
// 블로커 자체가 클릭된 경우에만 처리 (버블링된 이벤트 무시)
|
||||
if (evt.target != this)
|
||||
return;
|
||||
|
||||
OnBlockerClicked?.Invoke();
|
||||
|
||||
if (_closeOnClick)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void FadeIn()
|
||||
{
|
||||
// 시작 투명도
|
||||
style.backgroundColor = new Color(0, 0, 0, 0);
|
||||
|
||||
// 애니메이션
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
style.transitionDuration = new StyleList<TimeValue>(new List<TimeValue> { new TimeValue(FADE_DURATION_MS, TimeUnit.Millisecond) });
|
||||
style.transitionProperty = new StyleList<StylePropertyName>(new List<StylePropertyName> { new StylePropertyName("background-color") });
|
||||
style.backgroundColor = new Color(0, 0, 0, _targetOpacity);
|
||||
});
|
||||
}
|
||||
|
||||
private void FadeOut(Action? onComplete = null)
|
||||
{
|
||||
style.transitionDuration = new StyleList<TimeValue>(new List<TimeValue> { new TimeValue(FADE_DURATION_MS, TimeUnit.Millisecond) });
|
||||
style.transitionProperty = new StyleList<StylePropertyName>(new List<StylePropertyName> { new StylePropertyName("background-color") });
|
||||
style.backgroundColor = new Color(0, 0, 0, 0);
|
||||
|
||||
// 페이드 아웃 완료 후 콜백
|
||||
schedule.Execute(() => onComplete?.Invoke()).ExecuteLater(FADE_DURATION_MS);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
UnregisterCallback<ClickEvent>(OnClick);
|
||||
OnBlockerClicked = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21cd16195edffc849a96bb6a83934c4c
|
||||
350
Assets/Scripts/UVC/UIToolkit/Modal/UTKNotification.cs
Normal file
350
Assets/Scripts/UVC/UIToolkit/Modal/UTKNotification.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 알림 창 컴포넌트.
|
||||
/// 화면 모서리에 표시되는 알림 메시지입니다.
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>닫힘 이벤트</summary>
|
||||
public event Action? OnClosed;
|
||||
/// <summary>액션 클릭 이벤트</summary>
|
||||
public event Action<string>? OnActionClicked;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>제목</summary>
|
||||
[UxmlAttribute]
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
if (_titleLabel != null) _titleLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>메시지</summary>
|
||||
[UxmlAttribute]
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set
|
||||
{
|
||||
_message = value;
|
||||
if (_messageLabel != null) _messageLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>알림 유형</summary>
|
||||
[UxmlAttribute]
|
||||
public NotificationType Type
|
||||
{
|
||||
get => _type;
|
||||
set
|
||||
{
|
||||
_type = value;
|
||||
UpdateType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>표시 시간 (밀리초, 0이면 수동 닫기)</summary>
|
||||
[UxmlAttribute]
|
||||
public int Duration
|
||||
{
|
||||
get => _duration;
|
||||
set => _duration = value;
|
||||
}
|
||||
|
||||
/// <summary>표시 위치</summary>
|
||||
[UxmlAttribute]
|
||||
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();
|
||||
}
|
||||
|
||||
public UTKNotification(string title, string message, NotificationType type = NotificationType.Info) : this()
|
||||
{
|
||||
Title = title;
|
||||
Message = message;
|
||||
Type = type;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// Info 알림 표시
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Success 알림 표시
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning 알림 표시
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error 알림 표시
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 알림 표시
|
||||
/// </summary>
|
||||
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");
|
||||
_header.Add(_iconLabel);
|
||||
|
||||
_titleLabel = new Label { name = "title" };
|
||||
_titleLabel.AddToClassList("utk-notification__title");
|
||||
_header.Add(_titleLabel);
|
||||
|
||||
_closeButton = new Button { name = "close-btn", text = "✕" };
|
||||
_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<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#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 => "✓",
|
||||
NotificationType.Warning => "⚠",
|
||||
NotificationType.Error => "✕",
|
||||
_ => "ℹ"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 알림 닫기
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b1d71d12b91f24585078c70006df42
|
||||
297
Assets/Scripts/UVC/UIToolkit/Modal/UTKPanel.cs
Normal file
297
Assets/Scripts/UVC/UIToolkit/Modal/UTKPanel.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 패널 컨테이너 컴포넌트.
|
||||
/// 콘텐츠를 그룹화하고 시각적으로 구분합니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKPanel : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string USS_PATH = "UIToolkit/Modal/UTKPanel";
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private VisualElement? _header;
|
||||
private Label? _titleLabel;
|
||||
private VisualElement? _headerActions;
|
||||
private VisualElement? _content;
|
||||
private VisualElement? _footer;
|
||||
|
||||
private string _title = "";
|
||||
private bool _showHeader = true;
|
||||
private bool _showFooter;
|
||||
private PanelVariant _variant = PanelVariant.Default;
|
||||
private bool _isCollapsible;
|
||||
private bool _isCollapsed;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>접힘 상태 변경 이벤트</summary>
|
||||
public event Action<bool>? OnCollapsedChanged;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>패널 제목</summary>
|
||||
[UxmlAttribute]
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
if (_titleLabel != null) _titleLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>헤더 표시 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool ShowHeader
|
||||
{
|
||||
get => _showHeader;
|
||||
set
|
||||
{
|
||||
_showHeader = value;
|
||||
if (_header != null)
|
||||
{
|
||||
_header.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>푸터 표시 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool ShowFooter
|
||||
{
|
||||
get => _showFooter;
|
||||
set
|
||||
{
|
||||
_showFooter = value;
|
||||
if (_footer != null)
|
||||
{
|
||||
_footer.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>패널 스타일</summary>
|
||||
[UxmlAttribute]
|
||||
public PanelVariant Variant
|
||||
{
|
||||
get => _variant;
|
||||
set
|
||||
{
|
||||
_variant = value;
|
||||
UpdateVariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>접기 가능 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool IsCollapsible
|
||||
{
|
||||
get => _isCollapsible;
|
||||
set
|
||||
{
|
||||
_isCollapsible = value;
|
||||
EnableInClassList("utk-panel--collapsible", value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>접힘 상태</summary>
|
||||
[UxmlAttribute]
|
||||
public bool IsCollapsed
|
||||
{
|
||||
get => _isCollapsed;
|
||||
set => SetCollapsed(value, true);
|
||||
}
|
||||
|
||||
/// <summary>콘텐츠 영역</summary>
|
||||
public VisualElement? ContentContainer => _content;
|
||||
|
||||
/// <summary>푸터 영역</summary>
|
||||
public VisualElement? FooterContainer => _footer;
|
||||
|
||||
/// <summary>헤더 액션 영역</summary>
|
||||
public VisualElement? HeaderActionsContainer => _headerActions;
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
public enum PanelVariant
|
||||
{
|
||||
Default,
|
||||
Elevated,
|
||||
Outlined,
|
||||
Flat
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public UTKPanel()
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (uss != null)
|
||||
{
|
||||
styleSheets.Add(uss);
|
||||
}
|
||||
|
||||
CreateUI();
|
||||
SetupEvents();
|
||||
SubscribeToThemeChanges();
|
||||
}
|
||||
|
||||
public UTKPanel(string title) : this()
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Creation
|
||||
private void CreateUI()
|
||||
{
|
||||
AddToClassList("utk-panel");
|
||||
|
||||
// Header
|
||||
_header = new VisualElement { name = "header" };
|
||||
_header.AddToClassList("utk-panel__header");
|
||||
hierarchy.Add(_header);
|
||||
|
||||
_titleLabel = new Label { name = "title" };
|
||||
_titleLabel.AddToClassList("utk-panel__title");
|
||||
_header.Add(_titleLabel);
|
||||
|
||||
_headerActions = new VisualElement { name = "header-actions" };
|
||||
_headerActions.AddToClassList("utk-panel__header-actions");
|
||||
_header.Add(_headerActions);
|
||||
|
||||
// Content
|
||||
_content = new VisualElement { name = "content" };
|
||||
_content.AddToClassList("utk-panel__content");
|
||||
hierarchy.Add(_content);
|
||||
|
||||
// Footer
|
||||
_footer = new VisualElement { name = "footer" };
|
||||
_footer.AddToClassList("utk-panel__footer");
|
||||
_footer.style.display = DisplayStyle.None;
|
||||
hierarchy.Add(_footer);
|
||||
|
||||
UpdateVariant();
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
{
|
||||
_header?.RegisterCallback<ClickEvent>(OnHeaderClick);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
RegisterCallback<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
private void OnHeaderClick(ClickEvent evt)
|
||||
{
|
||||
if (!_isCollapsible) return;
|
||||
SetCollapsed(!_isCollapsed, true);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>
|
||||
/// 접힘 상태 설정
|
||||
/// </summary>
|
||||
public void SetCollapsed(bool collapsed, bool notify)
|
||||
{
|
||||
if (_isCollapsed == collapsed) return;
|
||||
|
||||
_isCollapsed = collapsed;
|
||||
EnableInClassList("utk-panel--collapsed", collapsed);
|
||||
|
||||
if (_content != null)
|
||||
{
|
||||
_content.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
if (_footer != null && _showFooter)
|
||||
{
|
||||
_footer.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
if (notify)
|
||||
{
|
||||
OnCollapsedChanged?.Invoke(collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVariant()
|
||||
{
|
||||
RemoveFromClassList("utk-panel--default");
|
||||
RemoveFromClassList("utk-panel--elevated");
|
||||
RemoveFromClassList("utk-panel--outlined");
|
||||
RemoveFromClassList("utk-panel--flat");
|
||||
|
||||
var variantClass = _variant switch
|
||||
{
|
||||
PanelVariant.Elevated => "utk-panel--elevated",
|
||||
PanelVariant.Outlined => "utk-panel--outlined",
|
||||
PanelVariant.Flat => "utk-panel--flat",
|
||||
_ => "utk-panel--default"
|
||||
};
|
||||
AddToClassList(variantClass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 콘텐츠 추가
|
||||
/// </summary>
|
||||
public new void Add(VisualElement element)
|
||||
{
|
||||
_content?.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 헤더 액션 추가
|
||||
/// </summary>
|
||||
public void AddHeaderAction(VisualElement element)
|
||||
{
|
||||
_headerActions?.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 푸터에 요소 추가
|
||||
/// </summary>
|
||||
public void AddToFooter(VisualElement element)
|
||||
{
|
||||
_footer?.Add(element);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
OnCollapsedChanged = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKPanel.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKPanel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e437e0457f225ff4db4735261757835b
|
||||
274
Assets/Scripts/UVC/UIToolkit/Modal/UTKToast.cs
Normal file
274
Assets/Scripts/UVC/UIToolkit/Modal/UTKToast.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 토스트 알림 컴포넌트.
|
||||
/// 화면 하단에 일시적으로 표시되는 알림입니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKToast : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string USS_PATH = "UIToolkit/Modal/UTKToast";
|
||||
private const int DEFAULT_DURATION_MS = 3000;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
private Label? _iconLabel;
|
||||
private Label? _messageLabel;
|
||||
private Button? _closeButton;
|
||||
|
||||
private string _message = "";
|
||||
private ToastType _type = ToastType.Info;
|
||||
private int _duration = DEFAULT_DURATION_MS;
|
||||
private bool _showCloseButton;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>닫힘 이벤트</summary>
|
||||
public event Action? OnClosed;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>메시지</summary>
|
||||
[UxmlAttribute]
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set
|
||||
{
|
||||
_message = value;
|
||||
if (_messageLabel != null) _messageLabel.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>토스트 유형</summary>
|
||||
[UxmlAttribute]
|
||||
public ToastType Type
|
||||
{
|
||||
get => _type;
|
||||
set
|
||||
{
|
||||
_type = value;
|
||||
UpdateType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>표시 시간 (밀리초)</summary>
|
||||
[UxmlAttribute]
|
||||
public int Duration
|
||||
{
|
||||
get => _duration;
|
||||
set => _duration = value;
|
||||
}
|
||||
|
||||
/// <summary>닫기 버튼 표시 여부</summary>
|
||||
[UxmlAttribute]
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => _showCloseButton;
|
||||
set
|
||||
{
|
||||
_showCloseButton = value;
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
public enum ToastType
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public UTKToast()
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (uss != null)
|
||||
{
|
||||
styleSheets.Add(uss);
|
||||
}
|
||||
|
||||
CreateUI();
|
||||
SubscribeToThemeChanges();
|
||||
}
|
||||
|
||||
public UTKToast(string message, ToastType type = ToastType.Info) : this()
|
||||
{
|
||||
Message = message;
|
||||
Type = type;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// Info 토스트 표시
|
||||
/// </summary>
|
||||
public static UTKToast ShowInfo(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
|
||||
{
|
||||
return Show(parent, message, ToastType.Info, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Success 토스트 표시
|
||||
/// </summary>
|
||||
public static UTKToast ShowSuccess(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
|
||||
{
|
||||
return Show(parent, message, ToastType.Success, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning 토스트 표시
|
||||
/// </summary>
|
||||
public static UTKToast ShowWarning(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
|
||||
{
|
||||
return Show(parent, message, ToastType.Warning, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error 토스트 표시
|
||||
/// </summary>
|
||||
public static UTKToast ShowError(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
|
||||
{
|
||||
return Show(parent, message, ToastType.Error, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토스트 표시
|
||||
/// </summary>
|
||||
public static UTKToast Show(VisualElement parent, string message, ToastType type, int duration = DEFAULT_DURATION_MS)
|
||||
{
|
||||
var toast = new UTKToast(message, type);
|
||||
toast._duration = duration;
|
||||
|
||||
parent.Add(toast);
|
||||
|
||||
// 하단 중앙 정렬
|
||||
toast.style.position = Position.Absolute;
|
||||
toast.style.left = Length.Percent(50);
|
||||
toast.style.bottom = 20;
|
||||
toast.style.translate = new Translate(Length.Percent(-50), 0);
|
||||
|
||||
// 자동 닫기
|
||||
if (duration > 0)
|
||||
{
|
||||
toast.AutoCloseAsync(duration).Forget();
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Creation
|
||||
private void CreateUI()
|
||||
{
|
||||
AddToClassList("utk-toast");
|
||||
|
||||
_iconLabel = new Label { name = "icon" };
|
||||
_iconLabel.AddToClassList("utk-toast__icon");
|
||||
Add(_iconLabel);
|
||||
|
||||
_messageLabel = new Label { name = "message" };
|
||||
_messageLabel.AddToClassList("utk-toast__message");
|
||||
Add(_messageLabel);
|
||||
|
||||
_closeButton = new Button { name = "close-btn", text = "✕" };
|
||||
_closeButton.AddToClassList("utk-toast__close-btn");
|
||||
_closeButton.style.display = DisplayStyle.None;
|
||||
_closeButton.RegisterCallback<ClickEvent>(_ => Close());
|
||||
Add(_closeButton);
|
||||
|
||||
UpdateType();
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
RegisterCallback<DetachFromPanelEvent>(_ =>
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void UpdateType()
|
||||
{
|
||||
RemoveFromClassList("utk-toast--info");
|
||||
RemoveFromClassList("utk-toast--success");
|
||||
RemoveFromClassList("utk-toast--warning");
|
||||
RemoveFromClassList("utk-toast--error");
|
||||
|
||||
var typeClass = _type switch
|
||||
{
|
||||
ToastType.Success => "utk-toast--success",
|
||||
ToastType.Warning => "utk-toast--warning",
|
||||
ToastType.Error => "utk-toast--error",
|
||||
_ => "utk-toast--info"
|
||||
};
|
||||
AddToClassList(typeClass);
|
||||
|
||||
if (_iconLabel != null)
|
||||
{
|
||||
_iconLabel.text = _type switch
|
||||
{
|
||||
ToastType.Success => "✓",
|
||||
ToastType.Warning => "⚠",
|
||||
ToastType.Error => "✕",
|
||||
_ => "ℹ"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid AutoCloseAsync(int delayMs)
|
||||
{
|
||||
await UniTask.Delay(delayMs);
|
||||
if (!_disposed)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토스트 닫기
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
OnClosed?.Invoke();
|
||||
RemoveFromHierarchy();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
OnClosed = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKToast.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Modal/UTKToast.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f578f7e9af679ed42922a5edf6d35618
|
||||
Reference in New Issue
Block a user