Files
XRLib/Assets/Scripts/UVC/UIToolkit/Property/Items/Base/UTKPropertyItemBase.cs

358 lines
9.7 KiB
C#
Raw Normal View History

2026-01-08 20:15:57 +09:00
#nullable enable
using System;
2026-02-02 19:33:27 +09:00
using System.Collections.Generic;
2026-01-08 20:15:57 +09:00
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
2026-01-20 20:18:47 +09:00
namespace UVC.UIToolkit
2026-01-08 20:15:57 +09:00
{
/// <summary>
/// 모든 PropertyItem의 기본 추상 클래스
/// 공통 기능 및 UIToolkit 바인딩 로직 제공
/// </summary>
/// <typeparam name="T">속성 값의 타입</typeparam>
public abstract class UTKPropertyItemBase<T> : IUTKPropertyItem<T>, 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";
2026-02-02 19:33:27 +09:00
protected const string UXML_BASE_PATH = "UIToolkit/Property/Items/";
#endregion
#region Static Cache
private static readonly Dictionary<string, VisualTreeAsset> _uxmlCache = new();
2026-01-08 20:15:57 +09:00
#endregion
#region Fields
private T _value;
2026-02-03 20:43:36 +09:00
protected bool _isReadOnly;
protected bool _isVisible = true;
2026-01-08 20:15:57 +09:00
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<IUTKPropertyItem, object?, object?>? OnValueChanged;
public event Action<IUTKPropertyItem<T>, 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);
}
2026-02-02 19:33:27 +09:00
protected UTKLabel CreateNameLabel()
2026-01-08 20:15:57 +09:00
{
2026-02-02 19:33:27 +09:00
var label = new UTKLabel(Name, UTKLabel.LabelSize.Body2);
2026-01-08 20:15:57 +09:00
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;
}
2026-02-02 19:33:27 +09:00
/// <summary>
/// UXML 템플릿에서 UI 생성
/// </summary>
/// <param name="uxmlName">UXML 파일명 (확장자 제외)</param>
/// <returns>생성된 루트 VisualElement</returns>
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<VisualElement>("item-root") ?? root;
// name-label에 Name 설정
var nameLabel = itemRoot.Q<UTKLabel>("name-label");
if (nameLabel != null)
{
nameLabel.Text = Name;
}
return itemRoot;
}
/// <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>
/// UXML 캐시 클리어 (에디터 용도)
/// </summary>
public static void ClearUxmlCache()
{
_uxmlCache.Clear();
}
2026-01-08 20:15:57 +09:00
#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
}
}