Files
EnglewoodLAB/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKPropertyItemViewBase.cs

461 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 모든 PropertyItem View의 기본 추상 클래스입니다.
/// VisualElement를 상속하여 UXML/C#에서 단독 사용이 가능합니다.
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>테마 자동 적용 (UTKThemeManager)</item>
/// <item>공통/개별 USS 자동 로드</item>
/// <item>Label + Value 레이아웃 제공</item>
/// <item>Data 클래스 바인딩 지원</item>
/// <item>단독 사용 시 UXML 속성 지원</item>
/// </list>
///
/// <para><b>관련 리소스:</b></para>
/// <list type="bullet">
/// <item>Resources/UIToolkit/Property/Views/{ViewName}.uxml - 각 뷰 UXML</item>
/// <item>Resources/UIToolkit/Property/Views/{ViewName}Uss.uss - 각 뷰 USS</item>
/// <item>Resources/UIToolkit/Property/Views/UTKPropertyItemViewCommonUss.uss - 공통 USS</item>
/// </list>
/// </summary>
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<string, VisualTreeAsset> _uxmlCache = new();
private static readonly Dictionary<string, StyleSheet> _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;
private IUTKPropertyItem? _boundItemBase;
#endregion
#region Properties
/// <summary>라벨 텍스트</summary>
[UxmlAttribute("label")]
public string Label
{
get => _label;
set
{
_label = value;
if (_labelElement != null)
{
_labelElement.Text = value;
}
}
}
/// <summary>읽기 전용 여부</summary>
[UxmlAttribute("is-read-only")]
public bool IsReadOnly
{
get => _isReadOnly;
set
{
if (_isReadOnly != value)
{
_isReadOnly = value;
UpdateReadOnlyState();
}
}
}
/// <summary>표시 여부</summary>
[UxmlAttribute("is-visible")]
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
UpdateVisibility();
}
}
}
/// <summary>툴팁 텍스트</summary>
[UxmlAttribute("tooltip-text")]
public string? TooltipText
{
get => _tooltipText;
set
{
_tooltipText = value;
UpdateTooltip();
}
}
/// <summary>라벨 표시 여부 (false면 value가 전체 너비 사용)</summary>
[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
/// <summary>View 타입 이름 (UXML/USS 파일명에 사용)</summary>
protected abstract string ViewTypeName { get; }
/// <summary>값 영역 UI 생성 (하위 클래스에서 구현)</summary>
protected abstract void CreateValueUI(VisualElement container);
/// <summary>UI 상태 갱신 (하위 클래스에서 구현)</summary>
public abstract void RefreshUI();
/// <summary>읽기 전용 상태 업데이트 (하위 클래스에서 오버라이드)</summary>
protected abstract void OnReadOnlyStateChanged(bool isReadOnly);
#endregion
#region Protected Methods
/// <summary>
/// UXML 템플릿에서 UI를 생성합니다.
/// </summary>
/// <returns>성공 여부</returns>
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<UTKLabel>("label");
_valueContainer = this.Q<VisualElement>("value-container");
if (_labelElement != null)
{
_labelElement.Text = _label;
}
return true;
}
/// <summary>
/// 코드로 기본 UI 레이아웃을 생성합니다 (Fallback).
/// </summary>
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);
}
/// <summary>읽기 전용 상태 업데이트</summary>
protected virtual void UpdateReadOnlyState()
{
if (_isReadOnly)
{
AddToClassList(USS_CLASS_READONLY);
}
else
{
RemoveFromClassList(USS_CLASS_READONLY);
}
OnReadOnlyStateChanged(_isReadOnly);
}
/// <summary>표시 상태 업데이트</summary>
protected virtual void UpdateVisibility()
{
style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None;
if (_isVisible)
{
RemoveFromClassList(USS_CLASS_HIDDEN);
}
else
{
AddToClassList(USS_CLASS_HIDDEN);
}
}
/// <summary>툴팁 업데이트</summary>
protected virtual void UpdateTooltip()
{
if (!string.IsNullOrEmpty(_tooltipText))
{
this.SetTooltip(_tooltipText);
}
else
{
this.ClearTooltip();
}
}
/// <summary>라벨 표시 상태 업데이트</summary>
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<AttachToPanelEvent>(OnAttachToPanel);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region StyleSheet Loading
/// <summary>공통 USS 스타일시트를 적용합니다.</summary>
protected void ApplyCommonStyleSheet()
{
var commonUss = LoadCommonUssAsset();
if (commonUss != null)
{
styleSheets.Add(commonUss);
}
}
/// <summary>개별 USS 스타일시트를 적용합니다.</summary>
protected void ApplyStyleSheet(string ussName)
{
var uss = LoadUssAsset(ussName);
if (uss != null)
{
styleSheets.Add(uss);
}
}
/// <summary>UXML 에셋 로드 (캐시 사용)</summary>
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<VisualTreeAsset>(path);
if (asset != null)
{
_uxmlCache[path] = asset;
}
return asset;
}
/// <summary>공통 USS 에셋 로드 (캐시 사용)</summary>
protected static StyleSheet? LoadCommonUssAsset()
{
if (_commonUssCache != null)
{
return _commonUssCache;
}
_commonUssCache = Resources.Load<StyleSheet>(USS_COMMON_PATH);
return _commonUssCache;
}
/// <summary>USS 에셋 로드 (캐시 사용)</summary>
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<StyleSheet>(path);
if (asset != null)
{
_ussCache[path] = asset;
}
return asset;
}
/// <summary>모든 캐시 클리어 (에디터 용도)</summary>
public static void ClearAllCache()
{
_uxmlCache.Clear();
_ussCache.Clear();
_commonUssCache = null;
}
#endregion
#region Helper Methods
/// <summary>
/// PropertyItem의 공통 속성을 View에 적용합니다.
/// 하위 클래스의 Bind 메서드에서 호출하세요.
/// </summary>
protected void ApplyCommonProperties(IUTKPropertyItem item)
{
Label = item.Name;
IsReadOnly = item.IsReadOnly;
IsVisible = item.IsVisible;
TooltipText = item.Tooltip;
ShowLabel = item.ShowLabel;
}
/// <summary>
/// 상태 변경 이벤트를 구독합니다. 하위 View의 Bind에서 호출하세요.
/// </summary>
protected void BindBase(IUTKPropertyItem item)
{
UnbindBase();
_boundItemBase = item;
_boundItemBase.OnStateChanged += OnItemStateChanged;
}
/// <summary>
/// 상태 변경 이벤트 구독을 해제합니다. 하위 View의 Unbind에서 호출하세요.
/// </summary>
protected void UnbindBase()
{
if (_boundItemBase != null)
{
_boundItemBase.OnStateChanged -= OnItemStateChanged;
_boundItemBase = null;
}
}
private void OnItemStateChanged(IUTKPropertyItem item)
{
IsReadOnly = item.IsReadOnly;
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)
{
UnbindBase();
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
this.ClearTooltip();
_labelElement = null;
_valueContainer = null;
}
}
#endregion
}
}