#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 모든 PropertyItem View의 기본 추상 클래스입니다. /// VisualElement를 상속하여 UXML/C#에서 단독 사용이 가능합니다. /// /// 주요 기능: /// /// 테마 자동 적용 (UTKThemeManager) /// 공통/개별 USS 자동 로드 /// Label + Value 레이아웃 제공 /// Data 클래스 바인딩 지원 /// 단독 사용 시 UXML 속성 지원 /// /// /// 관련 리소스: /// /// Resources/UIToolkit/Property/Views/{ViewName}.uxml - 각 뷰 UXML /// Resources/UIToolkit/Property/Views/{ViewName}Uss.uss - 각 뷰 USS /// Resources/UIToolkit/Property/Views/UTKPropertyItemViewCommonUss.uss - 공통 USS /// /// public abstract class UTKPropertyItemViewBase : VisualElement, IDisposable { #region Constants protected const string UXML_BASE_PATH = "UIToolkit/Property/Views/"; protected const string USS_BASE_PATH = "UIToolkit/Property/Views/"; protected const string USS_COMMON_PATH = "UIToolkit/Property/Views/UTKPropertyItemViewCommonUss"; protected const string USS_CLASS_READONLY = "utk-property-item-view--readonly"; protected const string USS_CLASS_HIDDEN = "utk-property-item-view--hidden"; protected const string USS_CLASS_NO_LABEL = "utk-property-item-view--no-label"; #endregion #region Static Cache private static readonly Dictionary _uxmlCache = new(); private static readonly Dictionary _ussCache = new(); private static StyleSheet? _commonUssCache; #endregion #region Fields protected bool _disposed; protected UTKLabel? _labelElement; protected VisualElement? _valueContainer; protected string _label = string.Empty; protected bool _isReadOnly = false; protected bool _isVisible = true; protected bool _showLabel = true; protected string? _tooltipText; #endregion #region Properties /// 라벨 텍스트 [UxmlAttribute("label")] public string Label { get => _label; set { _label = value; if (_labelElement != null) { _labelElement.Text = value; } } } /// 읽기 전용 여부 [UxmlAttribute("is-read-only")] public bool IsReadOnly { get => _isReadOnly; set { if (_isReadOnly != value) { _isReadOnly = value; UpdateReadOnlyState(); } } } /// 표시 여부 [UxmlAttribute("is-visible")] public bool IsVisible { get => _isVisible; set { if (_isVisible != value) { _isVisible = value; UpdateVisibility(); } } } /// 툴팁 텍스트 [UxmlAttribute("tooltip-text")] public string? TooltipText { get => _tooltipText; set { _tooltipText = value; UpdateTooltip(); } } /// 라벨 표시 여부 (false면 value가 전체 너비 사용) [UxmlAttribute("show-label")] public bool ShowLabel { get => _showLabel; set { if (_showLabel != value) { _showLabel = value; UpdateShowLabelState(); } } } #endregion #region Constructor protected UTKPropertyItemViewBase() { // 1. 테마 적용 UTKThemeManager.Instance.ApplyThemeToElement(this); // 2. 공통 USS 로드 ApplyCommonStyleSheet(); // 3. 테마 변경 구독 SubscribeToThemeChanges(); } #endregion #region Abstract Methods /// View 타입 이름 (UXML/USS 파일명에 사용) protected abstract string ViewTypeName { get; } /// 값 영역 UI 생성 (하위 클래스에서 구현) protected abstract void CreateValueUI(VisualElement container); /// UI 상태 갱신 (하위 클래스에서 구현) public abstract void RefreshUI(); /// 읽기 전용 상태 업데이트 (하위 클래스에서 오버라이드) protected abstract void OnReadOnlyStateChanged(bool isReadOnly); #endregion #region Protected Methods /// /// UXML 템플릿에서 UI를 생성합니다. /// /// 성공 여부 protected bool CreateUIFromUxml() { var asset = LoadUxmlAsset(ViewTypeName); if (asset == null) { return false; } var root = asset.Instantiate(); Add(root); // 개별 USS 로드 ApplyStyleSheet(ViewTypeName + "Uss"); // 공통 요소 참조 획득 _labelElement = this.Q("label"); _valueContainer = this.Q("value-container"); if (_labelElement != null) { _labelElement.Text = _label; } return true; } /// /// 코드로 기본 UI 레이아웃을 생성합니다 (Fallback). /// protected void CreateUIFallback() { AddToClassList("utk-property-item-view"); // 개별 USS 로드 ApplyStyleSheet(ViewTypeName + "Uss"); // Label _labelElement = new UTKLabel(_label, UTKLabel.LabelSize.Body2) { name = "label" }; _labelElement.AddToClassList("utk-property-item-view__label"); Add(_labelElement); // Value Container _valueContainer = new VisualElement { name = "value-container" }; _valueContainer.AddToClassList("utk-property-item-view__value"); Add(_valueContainer); // 하위 클래스에서 값 UI 생성 CreateValueUI(_valueContainer); } /// 읽기 전용 상태 업데이트 protected virtual void UpdateReadOnlyState() { if (_isReadOnly) { AddToClassList(USS_CLASS_READONLY); } else { RemoveFromClassList(USS_CLASS_READONLY); } OnReadOnlyStateChanged(_isReadOnly); } /// 표시 상태 업데이트 protected virtual void UpdateVisibility() { style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None; if (_isVisible) { RemoveFromClassList(USS_CLASS_HIDDEN); } else { AddToClassList(USS_CLASS_HIDDEN); } } /// 툴팁 업데이트 protected virtual void UpdateTooltip() { if (!string.IsNullOrEmpty(_tooltipText)) { this.SetTooltip(_tooltipText); } else { this.ClearTooltip(); } } /// 라벨 표시 상태 업데이트 protected virtual void UpdateShowLabelState() { if (_showLabel) { RemoveFromClassList(USS_CLASS_NO_LABEL); if (_labelElement != null) { _labelElement.style.display = DisplayStyle.Flex; } } else { AddToClassList(USS_CLASS_NO_LABEL); if (_labelElement != null) { _labelElement.style.display = DisplayStyle.None; } } } #endregion #region Theme private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(_ => { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; }); } private void OnThemeChanged(UTKTheme theme) { UTKThemeManager.Instance.ApplyThemeToElement(this); } #endregion #region StyleSheet Loading /// 공통 USS 스타일시트를 적용합니다. protected void ApplyCommonStyleSheet() { var commonUss = LoadCommonUssAsset(); if (commonUss != null) { styleSheets.Add(commonUss); } } /// 개별 USS 스타일시트를 적용합니다. protected void ApplyStyleSheet(string ussName) { var uss = LoadUssAsset(ussName); if (uss != null) { styleSheets.Add(uss); } } /// 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; } /// 공통 USS 에셋 로드 (캐시 사용) protected static StyleSheet? LoadCommonUssAsset() { if (_commonUssCache != null) { return _commonUssCache; } _commonUssCache = Resources.Load(USS_COMMON_PATH); return _commonUssCache; } /// USS 에셋 로드 (캐시 사용) protected static StyleSheet? LoadUssAsset(string ussName) { string path = USS_BASE_PATH + ussName; if (_ussCache.TryGetValue(path, out var cached)) { return cached; } var asset = Resources.Load(path); if (asset != null) { _ussCache[path] = asset; } return asset; } /// 모든 캐시 클리어 (에디터 용도) public static void ClearAllCache() { _uxmlCache.Clear(); _ussCache.Clear(); _commonUssCache = null; } #endregion #region Helper Methods /// /// PropertyItem의 공통 속성을 View에 적용합니다. /// 하위 클래스의 Bind 메서드에서 호출하세요. /// protected void ApplyCommonProperties(IUTKPropertyItem item) { Label = item.Name; IsReadOnly = item.IsReadOnly; IsVisible = item.IsVisible; TooltipText = item.Tooltip; ShowLabel = item.ShowLabel; } #endregion #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; _disposed = true; if (disposing) { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; this.ClearTooltip(); _labelElement = null; _valueContainer = null; } } #endregion } }