#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// Enum 선택 드롭다운 컴포넌트. /// 여러 옵션 중 하나를 선택할 수 있는 UI 컨트롤입니다. /// 선택된 항목은 체크 아이콘으로 표시됩니다. /// /// /// Enum 드롭다운이란? /// /// Enum 드롭다운은 C#의 열거형(Enum) 타입을 자동으로 드롭다운 목록으로 만들어주는 컴포넌트입니다. /// 문자열 목록을 수동으로 관리할 필요 없이, Enum 정의만으로 타입 안전한 선택 UI를 구현합니다. /// /// /// Enum이란? /// /// Enum(열거형)은 관련된 상수들을 묶어 명명된 값의 집합을 정의합니다. /// 예를 들어, 게임 난이도를 Easy, Normal, Hard로 정의할 수 있습니다. /// /// /// 문자열 드롭다운 vs Enum 드롭다운: /// /// 문자열: 유연하지만 오타 위험, 컴파일 타임 검증 불가 /// Enum: 타입 안전, 자동 완성 지원, 리팩토링 용이 /// /// /// 주요 속성: /// /// Label - 드롭다운 라벨 텍스트 /// Value - 현재 선택된 Enum 값 /// Placeholder - 선택되지 않았을 때 표시되는 텍스트 /// IsEnabled - 활성화/비활성화 상태 /// /// /// 주요 메서드: /// /// Init(Enum) - Enum 타입과 기본값으로 초기화 (필수!) /// SetValue(Enum, bool) - 값 설정 (notify: 이벤트 발생 여부) /// /// /// 주요 이벤트: /// /// OnValueChanged - 선택 변경 시 발생 (Enum 값 전달) /// /// /// UXML 사용 시 주의: /// /// UXML에서는 Enum 타입을 지정할 수 없으므로, 반드시 C# 코드에서 Init()을 호출해야 합니다. /// /// /// 실제 활용 예시: /// /// 게임 난이도 - Easy, Normal, Hard, Expert /// 정렬 방식 - Ascending, Descending /// 파일 형식 - PNG, JPG, GIF, BMP /// 품질 설정 - Low, Medium, High, Ultra /// 애니메이션 상태 - Idle, Walk, Run, Jump /// /// /// /// Enum 정의: /// /// // 게임 난이도 Enum /// public enum Difficulty { Easy, Normal, Hard, Expert } /// /// // 품질 설정 Enum /// public enum QualityLevel { Low, Medium, High, Ultra } /// /// C# 코드에서 사용: /// /// // 난이도 선택 드롭다운 /// var difficultyDropdown = new UTKEnumDropDown(); /// difficultyDropdown.Label = "난이도"; /// difficultyDropdown.Init(Difficulty.Normal); // 기본값: Normal /// /// // 값 변경 이벤트 /// difficultyDropdown.OnValueChanged += (value) => { /// var difficulty = (Difficulty)value; /// Debug.Log($"난이도 변경: {difficulty}"); /// ApplyDifficulty(difficulty); /// }; /// /// // 생성자로 한 번에 설정 /// var qualityDropdown = new UTKEnumDropDown("품질", QualityLevel.High); /// /// // 현재 값 읽기 (캐스팅 필요) /// var currentQuality = (QualityLevel)qualityDropdown.Value; /// /// // 프로그래밍으로 값 변경 /// difficultyDropdown.Value = Difficulty.Hard; /// /// UXML에서 사용: /// /// /// /// /// /// UXML 로드 후 C#에서 초기화: /// /// // UXML 로드 후 반드시 Init() 호출 /// var diffDropdown = root.Q("difficulty-dropdown"); /// diffDropdown.Init(Difficulty.Normal); /// /// var qualityDropdown = root.Q("quality-dropdown"); /// qualityDropdown.Init(QualityLevel.High); /// /// [UxmlElement] public partial class UTKEnumDropDown : VisualElement, IDisposable { #region Constants private const string USS_PATH = "UIToolkit/Dropdown/UTKEnumDropDownUss"; private const string DEFAULT_PLACEHOLDER = "Select"; #endregion #region Fields private bool _disposed; private Label? _labelElement; private VisualElement? _inputContainer; private Label? _displayLabel; private VisualElement? _dropdownIconContainer; private UTKLabel? _dropdownIcon; private VisualElement? _popupContainer; private ScrollView? _optionsScrollView; private string _label = ""; private Type? _enumType; private Enum? _value; private string _placeholder = DEFAULT_PLACEHOLDER; private bool _isEnabled = true; private bool _isPopupOpen; private readonly StyleSheet? _loadedUss; // 옵션 항목과 체크 아이콘 추적 private readonly List<(VisualElement container, UTKLabel checkIcon, Enum enumValue)> _optionItems = new(); #endregion #region Events /// 값 변경 이벤트 (선택된 Enum 값) public event Action? OnValueChanged; #endregion #region Properties /// 라벨 텍스트 [UxmlAttribute("label")] public string Label { get => _label; set { _label = value; if (_labelElement != null) _labelElement.text = value; } } /// 현재 선택된 Enum 값 public Enum? Value { get => _value; set { if (_value?.Equals(value) == true) return; _value = value; UpdateDisplayText(); UpdateCheckIcons(); } } /// 플레이스홀더 텍스트 (선택되지 않았을 때 표시) [UxmlAttribute("placeholder")] public string Placeholder { get => _placeholder; set { _placeholder = value; UpdateDisplayText(); } } /// 활성화 상태 [UxmlAttribute("is-enabled")] public bool IsEnabled { get => _isEnabled; set { _isEnabled = value; SetEnabled(value); EnableInClassList("utk-enum-dropdown--disabled", !value); } } #endregion #region Constructor public UTKEnumDropDown() : base() { UTKThemeManager.Instance.ApplyThemeToElement(this); _loadedUss = Resources.Load(USS_PATH); if (_loadedUss != null) { styleSheets.Add(_loadedUss); } CreateUI(); SubscribeToThemeChanges(); } public UTKEnumDropDown(string label, Enum defaultValue) : this() { Label = label; Init(defaultValue); } #endregion #region Setup private void CreateUI() { AddToClassList("utk-enum-dropdown"); // Label _labelElement = new Label(_label); _labelElement.AddToClassList("utk-enum-dropdown__label"); Add(_labelElement); // Input Container _inputContainer = new VisualElement(); _inputContainer.AddToClassList("utk-enum-dropdown__input"); Add(_inputContainer); // Display Label _displayLabel = new Label(_placeholder); _displayLabel.AddToClassList("utk-enum-dropdown__display"); _displayLabel.AddToClassList("utk-enum-dropdown__display--placeholder"); _inputContainer.Add(_displayLabel); // Icon Container _dropdownIconContainer = new VisualElement(); _dropdownIconContainer.AddToClassList("utk-enum-dropdown__icon-container"); _inputContainer.Add(_dropdownIconContainer); // Dropdown Icon _dropdownIcon = new UTKLabel(UTKMaterialIcons.ArrowDropDown, 24); _dropdownIcon.AddToClassList("utk-enum-dropdown__icon"); _dropdownIconContainer.Add(_dropdownIcon); // Popup Container _popupContainer = new VisualElement(); _popupContainer.AddToClassList("utk-enum-dropdown__popup"); _popupContainer.style.display = DisplayStyle.None; _popupContainer.style.position = Position.Absolute; // Options ScrollView _optionsScrollView = new ScrollView(ScrollViewMode.Vertical); _optionsScrollView.AddToClassList("utk-enum-dropdown__options"); _popupContainer.Add(_optionsScrollView); // Events _inputContainer.RegisterCallback(OnInputClicked); RegisterCallback(OnAttachedToPanel); RegisterCallback(OnDetachedFromPanel); } 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 OnAttachedToPanel(AttachToPanelEvent evt) { if (panel != null) { panel.visualTree.RegisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown); } } private void OnDetachedFromPanel(DetachFromPanelEvent evt) { if (panel != null) { panel.visualTree.UnregisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown); } } #endregion #region Public Methods /// /// Enum 타입과 기본값으로 초기화합니다. /// /// 기본값으로 설정할 Enum 값 public void Init(Enum defaultValue) { _enumType = defaultValue.GetType(); _value = defaultValue; RebuildOptions(); UpdateDisplayText(); } /// /// 현재 값을 특정 Enum 값으로 설정합니다. /// /// 설정할 Enum 값 /// 이벤트 발생 여부 public void SetValue(Enum? enumValue, bool notify = true) { if (_value?.Equals(enumValue) == true) return; _value = enumValue; UpdateDisplayText(); UpdateCheckIcons(); if (notify) { OnValueChanged?.Invoke(_value); } } #endregion #region Private Methods private void RebuildOptions() { if (_optionsScrollView == null || _enumType == null) return; // Clear existing options _optionsScrollView.Clear(); _optionItems.Clear(); // Get all enum values var enumValues = Enum.GetValues(_enumType); // Create option for each enum value foreach (Enum enumValue in enumValues) { var optionContainer = new VisualElement(); optionContainer.AddToClassList("utk-enum-dropdown__option"); // Check icon var checkIcon = new UTKLabel(UTKMaterialIcons.Check, 16); checkIcon.AddToClassList("utk-enum-dropdown__check-icon"); checkIcon.style.display = _value?.Equals(enumValue) == true ? DisplayStyle.Flex : DisplayStyle.None; optionContainer.Add(checkIcon); // Option label var optionLabel = new Label(enumValue.ToString()); optionLabel.AddToClassList("utk-enum-dropdown__option-label"); optionContainer.Add(optionLabel); // Click event optionContainer.RegisterCallback(evt => { evt.StopPropagation(); OnOptionSelected(enumValue); }); // 마우스 호버 이벤트 optionContainer.RegisterCallback(_ => { optionContainer.AddToClassList("utk-enum-dropdown__option--hover"); }); optionContainer.RegisterCallback(_ => { optionContainer.RemoveFromClassList("utk-enum-dropdown__option--hover"); }); _optionsScrollView.Add(optionContainer); _optionItems.Add((optionContainer, checkIcon, enumValue)); } } private void UpdateDisplayText() { if (_displayLabel == null) return; if (_value != null) { _displayLabel.text = _value.ToString(); _displayLabel.RemoveFromClassList("utk-enum-dropdown__display--placeholder"); } else { _displayLabel.text = _placeholder; _displayLabel.AddToClassList("utk-enum-dropdown__display--placeholder"); } } private void UpdateCheckIcons() { foreach (var (_, checkIcon, enumValue) in _optionItems) { checkIcon.style.display = _value?.Equals(enumValue) == true ? DisplayStyle.Flex : DisplayStyle.None; } } private void OnOptionSelected(Enum enumValue) { SetValue(enumValue, notify: true); ClosePopup(); } private void OnInputClicked(ClickEvent evt) { if (!_isEnabled) return; _isPopupOpen = !_isPopupOpen; if (_popupContainer != null) { if (_isPopupOpen) { OpenPopup(); } else { ClosePopup(); } } EnableInClassList("utk-enum-dropdown--open", _isPopupOpen); evt.StopPropagation(); } private void OpenPopup() { if (_popupContainer == null || _inputContainer == null || panel == null) return; // 아이콘 변경 if (_dropdownIcon is UTKLabel iconLabel) { iconLabel.SetMaterialIcon(UTKMaterialIcons.ArrowDropUp); } // 팝업을 루트로 이동 if (_popupContainer.parent != panel.visualTree) { _popupContainer.RemoveFromHierarchy(); panel.visualTree.Add(_popupContainer); UTKThemeManager.Instance.ApplyThemeToElement(_popupContainer); if (_loadedUss != null) { _popupContainer.styleSheets.Add(_loadedUss); } } // 드롭다운의 월드 위치 계산 var inputBounds = _inputContainer.worldBound; _popupContainer.style.position = Position.Absolute; _popupContainer.style.left = inputBounds.x; _popupContainer.style.top = inputBounds.yMax + 2; // 2px 간격 _popupContainer.style.width = inputBounds.width; _popupContainer.style.display = DisplayStyle.Flex; } private void ClosePopup() { _isPopupOpen = false; // 아이콘 복원 if (_dropdownIcon is UTKLabel iconLabel) { iconLabel.SetMaterialIcon(UTKMaterialIcons.ArrowDropDown); } if (_popupContainer != null) { _popupContainer.style.display = DisplayStyle.None; // 팝업을 원래 위치(드롭다운 내부)로 되돌림 if (_popupContainer.parent != this) { _popupContainer.RemoveFromHierarchy(); Add(_popupContainer); } } // 포커스 상태 클래스 제거 EnableInClassList("utk-enum-dropdown--open", false); } private void OnPanelPointerDown(PointerDownEvent evt) { if (!_isPopupOpen) return; var target = evt.target as VisualElement; if (target == null) return; // 입력 컨테이너 클릭 확인 if (_inputContainer != null && target.FindCommonAncestor(_inputContainer) == _inputContainer) { return; // 입력 영역 클릭이면 무시 } // 팝업 컨테이너 클릭 확인 if (_popupContainer != null && target.FindCommonAncestor(_popupContainer) == _popupContainer) { return; // 팝업 내부 클릭이면 무시 } // 외부 클릭이면 팝업 닫기 ClosePopup(); } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UnregisterCallback(OnAttachToPanelForTheme); UnregisterCallback(OnDetachFromPanelForTheme); OnValueChanged = null; _inputContainer?.UnregisterCallback(OnInputClicked); if (panel != null) { panel.visualTree.UnregisterCallback(OnPanelPointerDown); } foreach (var (container, checkIcon, _) in _optionItems) { checkIcon.Dispose(); } _optionItems.Clear(); } #endregion } }