#nullable enable using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 모든 PropertyItem의 기본 추상 클래스 /// 공통 기능 및 UIToolkit 바인딩 로직 제공 /// /// 속성 값의 타입 public abstract class UTKPropertyItemBase : IUTKPropertyItem, IDisposable { #region Constants protected const int DEFAULT_DEBOUNCE_MS = 300; protected const string USS_CLASS_READONLY = "utk-property-item--readonly"; protected const string USS_CLASS_HIDDEN = "utk-property-item--hidden"; protected const string UXML_BASE_PATH = "UIToolkit/Property/Items/"; #endregion #region Static Cache private static readonly Dictionary _uxmlCache = new(); #endregion #region Fields private T _value; protected bool _isReadOnly; protected bool _isVisible = true; private string? _description; private string? _tooltip; private string? _groupId; private bool _disposed; protected VisualElement? _rootElement; protected CancellationTokenSource? _debounceCts; #endregion #region Properties public string Id { get; } public string Name { get; } public string DisplayName => Name; public bool IsGroup => false; public int TreeViewId { get; set; } public abstract UTKPropertyType PropertyType { get; } public T Value { get => _value; set { if (!Equals(_value, value)) { var oldValue = _value; _value = value; NotifyValueChanged(oldValue, value); RefreshUI(); } } } public string? Description { get => _description; set => _description = value; } public string? Tooltip { get => _tooltip; set { _tooltip = value; UpdateTooltip(); } } public bool IsReadOnly { get => _isReadOnly; set { if (_isReadOnly != value) { _isReadOnly = value; UpdateReadOnlyState(); } } } public bool IsVisible { get => _isVisible; set { if (_isVisible != value) { _isVisible = value; UpdateVisibility(); } } } public string? GroupId { get => _groupId; set => _groupId = value; } #endregion #region Events public event Action? OnValueChanged; public event Action, T, T>? OnTypedValueChanged; #endregion #region Constructor protected UTKPropertyItemBase(string id, string name, T initialValue) { Id = id ?? throw new ArgumentNullException(nameof(id)); Name = name ?? throw new ArgumentNullException(nameof(name)); _value = initialValue; } #endregion #region Public Methods public object? GetValue() => _value; public void SetValue(object? value) { if (value == null) { if (default(T) == null) { Value = default!; } } else if (value is T typedValue) { Value = typedValue; } else { try { Value = (T)Convert.ChangeType(value, typeof(T)); } catch (Exception ex) { Debug.LogWarning($"[UTKPropertyItem] Failed to convert value '{value}' to type {typeof(T)}: {ex.Message}"); } } } public abstract VisualElement CreateUI(); public virtual void BindUI(VisualElement element) { _rootElement = element; UpdateReadOnlyState(); UpdateVisibility(); UpdateTooltip(); } public virtual void UnbindUI(VisualElement element) { CancelDebounce(); if (_rootElement != null) { _rootElement.ClearTooltip(); } _rootElement = null; } public virtual void RefreshUI() { // 하위 클래스에서 오버라이드하여 UI 갱신 } #endregion #region Protected Methods protected void NotifyValueChanged(T oldValue, T newValue) { OnTypedValueChanged?.Invoke(this, oldValue, newValue); OnValueChanged?.Invoke(this, oldValue, newValue); } protected async UniTaskVoid DebounceValueChange(T newValue, int delayMs = DEFAULT_DEBOUNCE_MS) { CancelDebounce(); _debounceCts = new CancellationTokenSource(); try { await UniTask.Delay(delayMs, cancellationToken: _debounceCts.Token); Value = newValue; } catch (OperationCanceledException) { // 디바운스 취소됨 - 정상 동작 } } protected void CancelDebounce() { _debounceCts?.Cancel(); _debounceCts?.Dispose(); _debounceCts = null; } protected virtual void UpdateReadOnlyState() { if (_rootElement == null) return; if (_isReadOnly) { _rootElement.AddToClassList(USS_CLASS_READONLY); } else { _rootElement.RemoveFromClassList(USS_CLASS_READONLY); } } protected virtual void UpdateVisibility() { if (_rootElement == null) return; _rootElement.style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None; if (_isVisible) { _rootElement.RemoveFromClassList(USS_CLASS_HIDDEN); } else { _rootElement.AddToClassList(USS_CLASS_HIDDEN); } } protected virtual void UpdateTooltip() { if (_rootElement == null || string.IsNullOrEmpty(_tooltip)) return; _rootElement.SetTooltip(_tooltip); } protected UTKLabel CreateNameLabel() { var label = new UTKLabel(Name, UTKLabel.LabelSize.Body2); label.AddToClassList("utk-property-item__label"); return label; } protected VisualElement CreateContainer() { var container = new VisualElement(); container.AddToClassList("utk-property-item"); container.AddToClassList($"utk-property-item--{PropertyType.ToString().ToLower()}"); if (!string.IsNullOrEmpty(Description)) { container.AddToClassList("utk-property-item--has-description"); } return container; } /// /// UXML 템플릿에서 UI 생성 /// /// UXML 파일명 (확장자 제외) /// 생성된 루트 VisualElement protected VisualElement? CreateUIFromUxml(string uxmlName) { var asset = LoadUxmlAsset(uxmlName); if (asset == null) { Debug.LogWarning($"[UTKPropertyItem] UXML not found: {uxmlName}"); return null; } var root = asset.Instantiate(); var itemRoot = root.Q("item-root") ?? root; // name-label에 Name 설정 var nameLabel = itemRoot.Q("name-label"); if (nameLabel != null) { nameLabel.Text = Name; } return itemRoot; } /// /// UXML 에셋 로드 (캐시 사용) /// protected static VisualTreeAsset? LoadUxmlAsset(string uxmlName) { string path = UXML_BASE_PATH + uxmlName; if (_uxmlCache.TryGetValue(path, out var cached)) { return cached; } var asset = Resources.Load(path); if (asset != null) { _uxmlCache[path] = asset; } return asset; } /// /// UXML 캐시 클리어 (에디터 용도) /// public static void ClearUxmlCache() { _uxmlCache.Clear(); } #endregion #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; _disposed = true; if (disposing) { CancelDebounce(); if (_rootElement != null) { UnbindUI(_rootElement); } OnValueChanged = null; OnTypedValueChanged = null; } } #endregion } }