2026-01-08 20:15:57 +09:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
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>
|
2026-02-04 20:31:52 +09:00
|
|
|
/// UTKPropertyListWindow의 핵심 리스트 컴포넌트입니다.
|
|
|
|
|
/// TreeView 기반으로 그룹화된 속성들을 표시합니다.
|
|
|
|
|
///
|
|
|
|
|
/// <para><b>개요:</b></para>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// UTKPropertyList는 UXML/USS 기반의 속성 리스트 컴포넌트입니다.
|
|
|
|
|
/// TreeView를 사용하여 가상화(Virtualization)를 지원하며, 대량의 속성을 효율적으로 렌더링합니다.
|
|
|
|
|
/// 그룹별 펼침/접힘, 검색 필터링 기능을 제공합니다.
|
|
|
|
|
/// </para>
|
|
|
|
|
///
|
|
|
|
|
/// <para><b>주요 기능:</b></para>
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
/// <item>TreeView 기반 가상화 (Virtualization) - 대량 속성 성능 최적화</item>
|
|
|
|
|
/// <item>그룹별 펼침/접힘 기능</item>
|
|
|
|
|
/// <item>실시간 검색 필터링</item>
|
|
|
|
|
/// <item>다양한 속성 타입 지원 (Text, Number, Boolean, Dropdown 등)</item>
|
|
|
|
|
/// </list>
|
|
|
|
|
///
|
|
|
|
|
/// <para><b>관련 리소스:</b></para>
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
/// <item>Resources/UIToolkit/List/UTKPropertyList.uxml - 메인 레이아웃</item>
|
|
|
|
|
/// <item>Resources/UIToolkit/List/UTKPropertyListUss.uss - 스타일</item>
|
|
|
|
|
/// </list>
|
2026-01-08 20:15:57 +09:00
|
|
|
/// </summary>
|
|
|
|
|
[UxmlElement]
|
|
|
|
|
public partial class UTKPropertyList : VisualElement, IDisposable
|
|
|
|
|
{
|
|
|
|
|
#region Constants
|
2026-02-04 20:31:52 +09:00
|
|
|
private const string UXML_PATH = "UIToolkit/List/UTKPropertyList";
|
|
|
|
|
private const string USS_PATH = "UIToolkit/List/UTKPropertyListUss";
|
2026-02-10 20:48:49 +09:00
|
|
|
private const string GROUP_HEADER_UXML_PATH = "UIToolkit/List/UTKPropertyGroupHeader";
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Fields
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
private TreeView? _treeView;
|
2026-02-02 19:33:27 +09:00
|
|
|
private UTKInputField? _searchField;
|
2026-02-04 20:31:52 +09:00
|
|
|
private UTKButton? _clearButton;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
private readonly List<IUTKPropertyEntry> _entries = new();
|
|
|
|
|
private readonly Dictionary<string, IUTKPropertyGroup> _groupIndex = new();
|
|
|
|
|
private readonly Dictionary<string, IUTKPropertyItem> _itemIndex = new();
|
|
|
|
|
private readonly Dictionary<int, IUTKPropertyEntry> _treeViewIdToEntry = new();
|
|
|
|
|
|
|
|
|
|
private int _nextTreeViewId = 1;
|
|
|
|
|
private string _searchText = string.Empty;
|
2026-02-10 20:48:49 +09:00
|
|
|
|
|
|
|
|
private static VisualTreeAsset? _groupHeaderUxmlCache;
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
|
|
|
|
/// <summary>속성 값 변경 이벤트</summary>
|
|
|
|
|
public event Action<UTKPropertyValueChangedEventArgs>? OnPropertyValueChanged;
|
|
|
|
|
|
|
|
|
|
/// <summary>그룹 펼침/접힘 변경 이벤트</summary>
|
|
|
|
|
public event Action<IUTKPropertyGroup, bool>? OnGroupExpandedChanged;
|
|
|
|
|
|
|
|
|
|
/// <summary>속성 클릭 이벤트</summary>
|
|
|
|
|
public event Action<IUTKPropertyItem>? OnPropertyClicked;
|
2026-02-09 20:28:09 +09:00
|
|
|
|
|
|
|
|
/// <summary>버튼 클릭 이벤트 (액션 이름 전달)</summary>
|
|
|
|
|
public event Action<string, string>? OnPropertyButtonClicked;
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
|
|
|
|
public UTKPropertyList()
|
|
|
|
|
{
|
2026-02-04 20:31:52 +09:00
|
|
|
// 메인 UXML 로드
|
|
|
|
|
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
|
|
|
if (visualTree != null)
|
|
|
|
|
{
|
|
|
|
|
visualTree.CloneTree(this);
|
|
|
|
|
CreateUIFromUxml();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"[UTKPropertyList] UXML not found at: {UXML_PATH}, using fallback");
|
|
|
|
|
CreateUIFallback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 테마 적용 및 변경 구독
|
2026-01-08 20:15:57 +09:00
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
2026-02-04 20:31:52 +09:00
|
|
|
SubscribeToThemeChanges();
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
// USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨)
|
|
|
|
|
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
|
|
|
|
if (uss != null)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-04 20:31:52 +09:00
|
|
|
styleSheets.Add(uss);
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
// TreeView 초기화
|
|
|
|
|
InitializeTreeView();
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region UI Creation
|
2026-02-04 20:31:52 +09:00
|
|
|
/// <summary>UXML에서 UI 요소 참조를 획득합니다.</summary>
|
|
|
|
|
private void CreateUIFromUxml()
|
|
|
|
|
{
|
|
|
|
|
_searchField = this.Q<UTKInputField>("search-field");
|
|
|
|
|
_treeView = this.Q<TreeView>("property-tree-view");
|
|
|
|
|
_clearButton = this.Q<UTKButton>("clear-btn");
|
|
|
|
|
// 검색 필드 이벤트 연결
|
|
|
|
|
if (_searchField != null)
|
|
|
|
|
{
|
|
|
|
|
_searchField.OnSubmit += OnSearch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 검색어 지우기 버튼
|
|
|
|
|
if (_clearButton != null)
|
|
|
|
|
{
|
|
|
|
|
_clearButton.OnClicked += OnClearButtonClicked;
|
|
|
|
|
// 초기에는 숨김
|
|
|
|
|
_clearButton.style.display = DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>UXML 로드 실패 시 코드로 UI를 생성합니다.</summary>
|
|
|
|
|
private void CreateUIFallback()
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
|
|
|
|
// USS 클래스 기반 스타일링 - 인라인 스타일 최소화
|
|
|
|
|
AddToClassList("utk-property-list");
|
|
|
|
|
|
|
|
|
|
// 검색 필드
|
|
|
|
|
var searchContainer = new VisualElement { name = "search-container" };
|
|
|
|
|
searchContainer.AddToClassList("utk-property-list__search-container");
|
|
|
|
|
|
2026-02-02 19:33:27 +09:00
|
|
|
_searchField = new UTKInputField { name = "search-field" };
|
|
|
|
|
_searchField.Placeholder = "Search...";
|
2026-01-08 20:15:57 +09:00
|
|
|
_searchField.AddToClassList("utk-property-list__search-field");
|
2026-02-04 20:31:52 +09:00
|
|
|
_searchField.OnSubmit += OnSearch;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
searchContainer.Add(_searchField);
|
|
|
|
|
Add(searchContainer);
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
_clearButton = new UTKButton { name = "clear-btn", Variant = UTKButton.ButtonVariant.Text, Icon = "Close", IconSize = 12, IconOnly = true };
|
|
|
|
|
_clearButton.AddToClassList("utk-property-list__search-field-clear-button");
|
|
|
|
|
_clearButton.OnClicked += OnClearButtonClicked;
|
|
|
|
|
searchContainer.Add(_clearButton);
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
// TreeView
|
|
|
|
|
_treeView = new TreeView { name = "property-tree-view" };
|
|
|
|
|
_treeView.AddToClassList("utk-property-list__tree-view");
|
|
|
|
|
_treeView.selectionType = SelectionType.None;
|
|
|
|
|
_treeView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
|
|
|
|
_treeView.showAlternatingRowBackgrounds = AlternatingRowBackground.None;
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
Add(_treeView);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>TreeView 콜백을 초기화합니다.</summary>
|
|
|
|
|
private void InitializeTreeView()
|
|
|
|
|
{
|
|
|
|
|
if (_treeView == null) return;
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
_treeView.makeItem = MakeItem;
|
|
|
|
|
_treeView.bindItem = BindItem;
|
|
|
|
|
_treeView.unbindItem = UnbindItem;
|
2026-02-04 20:31:52 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
#region Theme
|
|
|
|
|
private void SubscribeToThemeChanges()
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
2026-02-10 20:48:49 +09:00
|
|
|
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;
|
2026-02-04 20:31:52 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnThemeChanged(UTKTheme theme)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Data Loading
|
|
|
|
|
/// <summary>평면 속성 목록 로드 (그룹 없이)</summary>
|
|
|
|
|
public void LoadProperties(List<IUTKPropertyItem> items)
|
|
|
|
|
{
|
|
|
|
|
ClearInternal();
|
|
|
|
|
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
AddPropertyInternal(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>그룹화된 속성 목록 로드</summary>
|
|
|
|
|
public void LoadGroupedProperties(List<IUTKPropertyGroup> groups)
|
|
|
|
|
{
|
|
|
|
|
ClearInternal();
|
|
|
|
|
|
|
|
|
|
foreach (var group in groups)
|
|
|
|
|
{
|
|
|
|
|
AddGroupInternal(group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>혼합 엔트리 목록 로드 (그룹 + 속성)</summary>
|
|
|
|
|
public void LoadMixedProperties(List<IUTKPropertyEntry> entries)
|
|
|
|
|
{
|
|
|
|
|
ClearInternal();
|
|
|
|
|
|
|
|
|
|
foreach (var entry in entries)
|
|
|
|
|
{
|
|
|
|
|
switch (entry)
|
|
|
|
|
{
|
|
|
|
|
case IUTKPropertyGroup group:
|
|
|
|
|
AddGroupInternal(group);
|
|
|
|
|
break;
|
|
|
|
|
case IUTKPropertyItem item:
|
|
|
|
|
AddPropertyInternal(item);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Group Management
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 그룹을 추가하고 TreeView를 갱신합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="group">추가할 속성 그룹.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void AddGroup(IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
AddGroupInternal(group);
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 지정한 ID의 그룹과 그룹 내 모든 아이템을 제거합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">제거할 그룹 ID.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void RemoveGroup(string groupId)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
// 그룹 내 아이템 제거
|
|
|
|
|
foreach (var item in group.Items.ToList())
|
|
|
|
|
{
|
|
|
|
|
UnsubscribeItem(item);
|
|
|
|
|
_itemIndex.Remove(item.Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UnsubscribeGroup(group);
|
|
|
|
|
_groupIndex.Remove(groupId);
|
|
|
|
|
_entries.Remove(group);
|
|
|
|
|
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 지정한 ID의 그룹을 반환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">조회할 그룹 ID.</param>
|
|
|
|
|
/// <returns>그룹 인스턴스 또는 null.</returns>
|
2026-01-08 20:15:57 +09:00
|
|
|
public IUTKPropertyGroup? GetGroup(string groupId)
|
|
|
|
|
{
|
|
|
|
|
return _groupIndex.TryGetValue(groupId, out var group) ? group : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 그룹의 펼침/접힘 상태를 설정합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">대상 그룹 ID.</param>
|
|
|
|
|
/// <param name="expanded">true면 펼침, false면 접힘.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void SetGroupExpanded(string groupId, bool expanded)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
group.IsExpanded = expanded;
|
|
|
|
|
|
|
|
|
|
if (_treeView != null)
|
|
|
|
|
{
|
|
|
|
|
if (expanded)
|
|
|
|
|
_treeView.ExpandItem(group.TreeViewId);
|
|
|
|
|
else
|
|
|
|
|
_treeView.CollapseItem(group.TreeViewId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 그룹의 펼침/접힘 상태를 토글합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">대상 그룹 ID.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void ToggleGroupExpanded(string groupId)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
SetGroupExpanded(groupId, !group.IsExpanded);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Property Management
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 최상위 속성 아이템을 추가하고 TreeView를 갱신합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="item">추가할 속성 아이템.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void AddProperty(IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
AddPropertyInternal(item);
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 지정한 그룹에 속성 아이템을 추가하고 TreeView를 갱신합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">대상 그룹 ID.</param>
|
|
|
|
|
/// <param name="item">추가할 속성 아이템.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void AddPropertyToGroup(string groupId, IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
group.AddItem(item);
|
|
|
|
|
_itemIndex[item.Id] = item;
|
|
|
|
|
SubscribeItem(item);
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 지정한 ID의 속성 아이템을 제거하고 TreeView를 갱신합니다.
|
|
|
|
|
/// 그룹에 속한 아이템이면 그룹에서도 제거됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="itemId">제거할 아이템 ID.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void RemoveProperty(string itemId)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(itemId, out var item))
|
|
|
|
|
{
|
|
|
|
|
UnsubscribeItem(item);
|
|
|
|
|
_itemIndex.Remove(itemId);
|
|
|
|
|
|
|
|
|
|
// 그룹에서 제거
|
|
|
|
|
if (!string.IsNullOrEmpty(item.GroupId) && _groupIndex.TryGetValue(item.GroupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
group.RemoveItem(itemId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_entries.Remove(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 지정한 ID의 속성 아이템을 반환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="itemId">조회할 아이템 ID.</param>
|
|
|
|
|
/// <returns>아이템 인스턴스 또는 null.</returns>
|
2026-01-08 20:15:57 +09:00
|
|
|
public IUTKPropertyItem? GetProperty(string itemId)
|
|
|
|
|
{
|
|
|
|
|
return _itemIndex.TryGetValue(itemId, out var item) ? item : null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Value Management
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 속성 값을 변경합니다. 바인딩된 View에 OnTypedValueChanged 이벤트로 자동 반영됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="propertyId">대상 속성 ID.</param>
|
|
|
|
|
/// <param name="newValue">새 값 (타입 변환 자동 시도).</param>
|
|
|
|
|
/// <param name="notify">true면 값 변경 알림 이벤트 발생.</param>
|
|
|
|
|
public void UpdatePropertyValue(string propertyId, object newValue, bool notify = false)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
item.SetValue(newValue, notify);
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 속성 값을 변경합니다. <see cref="UpdatePropertyValue"/>의 별칭입니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="propertyId">대상 속성 ID.</param>
|
|
|
|
|
/// <param name="value">새 값.</param>
|
|
|
|
|
/// <param name="notify">true면 값 변경 알림 이벤트 발생.</param>
|
|
|
|
|
public void SetPropertyValue(string propertyId, object value, bool notify = false)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
UpdatePropertyValue(propertyId, value, notify);
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Visibility & ReadOnly
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 속성 아이템의 가시성을 변경합니다. TreeView 항목이 추가/제거되므로 Rebuild가 발생합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="propertyId">대상 속성 ID.</param>
|
|
|
|
|
/// <param name="visible">true면 표시, false면 숨김.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void SetPropertyVisibility(string propertyId, bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
if (item.IsVisible == visible) return;
|
2026-01-08 20:15:57 +09:00
|
|
|
item.IsVisible = visible;
|
2026-02-10 20:48:49 +09:00
|
|
|
RefreshTreeViewLight();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>여러 속성의 가시성을 일괄 변경합니다. TreeView는 마지막에 한 번만 갱신됩니다.</summary>
|
|
|
|
|
public void SetPropertyVisibilityBatch(IEnumerable<(string propertyId, bool visible)> changes)
|
|
|
|
|
{
|
|
|
|
|
bool changed = false;
|
|
|
|
|
foreach (var (propertyId, visible) in changes)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(propertyId, out var item) && item.IsVisible != visible)
|
|
|
|
|
{
|
|
|
|
|
item.IsVisible = visible;
|
|
|
|
|
changed = true;
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
2026-02-10 20:48:49 +09:00
|
|
|
|
|
|
|
|
if (changed) RefreshTreeViewLight();
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 그룹의 가시성을 변경합니다. TreeView 항목이 추가/제거되므로 Rebuild가 발생합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">대상 그룹 ID.</param>
|
|
|
|
|
/// <param name="visible">true면 표시, false면 숨김.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void SetGroupVisibility(string groupId, bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
if (group.IsVisible == visible) return;
|
2026-01-08 20:15:57 +09:00
|
|
|
group.IsVisible = visible;
|
2026-02-10 20:48:49 +09:00
|
|
|
RefreshTreeViewLight();
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 속성 아이템의 읽기 전용 상태를 변경합니다.
|
|
|
|
|
/// OnStateChanged 이벤트를 통해 바인딩된 View에 자동 반영됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="propertyId">대상 속성 ID.</param>
|
|
|
|
|
/// <param name="isReadOnly">true면 읽기 전용.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void SetPropertyReadOnly(string propertyId, bool isReadOnly)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
|
|
|
{
|
|
|
|
|
item.IsReadOnly = isReadOnly;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 그룹 내 모든 아이템의 읽기 전용 상태를 일괄 변경합니다.
|
|
|
|
|
/// OnStateChanged 이벤트를 통해 바인딩된 View에 자동 반영됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="groupId">대상 그룹 ID.</param>
|
|
|
|
|
/// <param name="isReadOnly">true면 읽기 전용.</param>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void SetGroupReadOnly(string groupId, bool isReadOnly)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
group.SetAllItemsReadOnly(isReadOnly);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:48:49 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 속성 아이템의 라벨 표시 여부를 변경합니다.
|
|
|
|
|
/// OnStateChanged 이벤트를 통해 바인딩된 View에 자동 반영됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="propertyId">대상 속성 ID.</param>
|
|
|
|
|
/// <param name="showLabel">true면 라벨 표시, false면 값이 전체 너비 사용.</param>
|
|
|
|
|
public void SetPropertyShowLabel(string propertyId, bool showLabel)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
|
|
|
{
|
|
|
|
|
item.ShowLabel = showLabel;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Utilities
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 엔트리(그룹 + 아이템)를 제거하고 TreeView를 초기화합니다.
|
|
|
|
|
/// VisualElement.Clear()를 숨깁니다(new).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public new void Clear()
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
|
|
|
|
ClearInternal();
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 현재 데이터를 기반으로 TreeView를 다시 빌드합니다.
|
|
|
|
|
/// </summary>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void Refresh()
|
|
|
|
|
{
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>리스트를 표시합니다.</summary>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void Show()
|
|
|
|
|
{
|
|
|
|
|
style.display = DisplayStyle.Flex;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>리스트를 숨깁니다.</summary>
|
2026-01-08 20:15:57 +09:00
|
|
|
public void Hide()
|
|
|
|
|
{
|
|
|
|
|
style.display = DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Private Methods - Internal
|
|
|
|
|
private void AddGroupInternal(IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
if (_groupIndex.ContainsKey(group.GroupId)) return;
|
|
|
|
|
|
|
|
|
|
group.TreeViewId = _nextTreeViewId++;
|
|
|
|
|
_groupIndex[group.GroupId] = group;
|
|
|
|
|
_entries.Add(group);
|
|
|
|
|
|
|
|
|
|
SubscribeGroup(group);
|
|
|
|
|
|
|
|
|
|
// 그룹 내 아이템 등록
|
|
|
|
|
foreach (var item in group.Items)
|
|
|
|
|
{
|
|
|
|
|
item.TreeViewId = _nextTreeViewId++;
|
|
|
|
|
_itemIndex[item.Id] = item;
|
|
|
|
|
SubscribeItem(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddPropertyInternal(IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
if (_itemIndex.ContainsKey(item.Id)) return;
|
|
|
|
|
|
|
|
|
|
item.TreeViewId = _nextTreeViewId++;
|
|
|
|
|
_itemIndex[item.Id] = item;
|
|
|
|
|
|
|
|
|
|
// 그룹에 속하는지 확인
|
|
|
|
|
if (!string.IsNullOrEmpty(item.GroupId) && _groupIndex.TryGetValue(item.GroupId, out var group))
|
|
|
|
|
{
|
|
|
|
|
group.AddItem(item);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_entries.Add(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SubscribeItem(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ClearInternal()
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
// TreeView 내 바인딩된 View의 이벤트 콜백 정리 및 Dispose
|
|
|
|
|
CleanupAllTreeViewContainers();
|
|
|
|
|
|
|
|
|
|
// 데이터 이벤트 구독 해제
|
2026-01-08 20:15:57 +09:00
|
|
|
foreach (var group in _groupIndex.Values)
|
|
|
|
|
{
|
|
|
|
|
UnsubscribeGroup(group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var item in _itemIndex.Values)
|
|
|
|
|
{
|
|
|
|
|
UnsubscribeItem(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_entries.Clear();
|
|
|
|
|
_groupIndex.Clear();
|
|
|
|
|
_itemIndex.Clear();
|
|
|
|
|
_treeViewIdToEntry.Clear();
|
|
|
|
|
_nextTreeViewId = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
/// <summary>TreeView 내 모든 컨테이너의 콜백과 View를 정리합니다.</summary>
|
|
|
|
|
private void CleanupAllTreeViewContainers()
|
|
|
|
|
{
|
|
|
|
|
if (_treeView == null) return;
|
|
|
|
|
|
|
|
|
|
_treeView.Query<VisualElement>(className: "utk-property-item-container")
|
|
|
|
|
.ForEach(container =>
|
|
|
|
|
{
|
|
|
|
|
CleanupContainerCallbacks(container);
|
|
|
|
|
DisposeChildren(container);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
private void RefreshTreeView()
|
|
|
|
|
{
|
|
|
|
|
if (_treeView == null) return;
|
|
|
|
|
|
|
|
|
|
var treeItems = BuildTreeItems();
|
|
|
|
|
_treeView.SetRootItems(treeItems);
|
|
|
|
|
_treeView.Rebuild();
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
RestoreExpandedStates();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 경량 TreeView 갱신. 아이템 구조(Visibility 변경 등)만 바뀔 때 사용합니다.
|
|
|
|
|
/// Rebuild() 대신 RefreshItems()를 사용하여 기존 컨테이너를 재활용합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void RefreshTreeViewLight()
|
|
|
|
|
{
|
|
|
|
|
if (_treeView == null) return;
|
|
|
|
|
|
|
|
|
|
var treeItems = BuildTreeItems();
|
|
|
|
|
_treeView.SetRootItems(treeItems);
|
|
|
|
|
_treeView.RefreshItems();
|
|
|
|
|
|
|
|
|
|
RestoreExpandedStates();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>그룹 펼침 상태를 복원합니다.</summary>
|
|
|
|
|
private void RestoreExpandedStates()
|
|
|
|
|
{
|
|
|
|
|
if (_treeView == null) return;
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
foreach (var group in _groupIndex.Values)
|
|
|
|
|
{
|
|
|
|
|
if (group.IsExpanded)
|
|
|
|
|
{
|
|
|
|
|
_treeView.ExpandItem(group.TreeViewId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IList<TreeViewItemData<IUTKPropertyEntry>> BuildTreeItems()
|
|
|
|
|
{
|
|
|
|
|
var result = new List<TreeViewItemData<IUTKPropertyEntry>>();
|
|
|
|
|
_treeViewIdToEntry.Clear();
|
|
|
|
|
|
|
|
|
|
foreach (var entry in _entries)
|
|
|
|
|
{
|
|
|
|
|
if (!MatchesSearch(entry)) continue;
|
|
|
|
|
|
|
|
|
|
if (entry is IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
var children = new List<TreeViewItemData<IUTKPropertyEntry>>();
|
|
|
|
|
|
|
|
|
|
foreach (var item in group.Items)
|
|
|
|
|
{
|
|
|
|
|
if (item.IsVisible && MatchesSearch(item))
|
|
|
|
|
{
|
|
|
|
|
_treeViewIdToEntry[item.TreeViewId] = item;
|
|
|
|
|
children.Add(new TreeViewItemData<IUTKPropertyEntry>(item.TreeViewId, item));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (children.Count > 0 || string.IsNullOrEmpty(_searchText))
|
|
|
|
|
{
|
|
|
|
|
_treeViewIdToEntry[group.TreeViewId] = group;
|
|
|
|
|
result.Add(new TreeViewItemData<IUTKPropertyEntry>(group.TreeViewId, group, children));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (entry is IUTKPropertyItem item && item.IsVisible)
|
|
|
|
|
{
|
|
|
|
|
_treeViewIdToEntry[item.TreeViewId] = item;
|
|
|
|
|
result.Add(new TreeViewItemData<IUTKPropertyEntry>(item.TreeViewId, item));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool MatchesSearch(IUTKPropertyEntry entry)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(_searchText)) return true;
|
|
|
|
|
|
|
|
|
|
if (entry is IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
// 그룹 이름 검색 또는 자식 아이템 검색
|
|
|
|
|
if (group.GroupName.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
return group.Items.Any(item => MatchesSearch(item));
|
|
|
|
|
}
|
|
|
|
|
else if (entry is IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
return item.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
(item.Description?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region TreeView Callbacks
|
2026-02-10 20:48:49 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 컨테이너에 등록된 이벤트 콜백 정보를 보관합니다.
|
|
|
|
|
/// View 재사용 시 이전 이벤트를 정리하기 위해 사용합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private sealed class ContainerCallbackInfo
|
|
|
|
|
{
|
|
|
|
|
public VisualElement? ItemView;
|
|
|
|
|
public EventCallback<ClickEvent>? ClickCallback;
|
|
|
|
|
public Action<string>? ButtonClickHandler;
|
|
|
|
|
public Action<string>? ActionButtonClickHandler;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
private VisualElement MakeItem()
|
|
|
|
|
{
|
|
|
|
|
var container = new VisualElement();
|
|
|
|
|
container.AddToClassList("utk-property-item-container");
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BindItem(VisualElement element, int index)
|
|
|
|
|
{
|
|
|
|
|
var itemData = _treeView?.GetItemDataForIndex<IUTKPropertyEntry>(index);
|
|
|
|
|
if (itemData == null) return;
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 기존 자식 View 재사용 시도
|
|
|
|
|
if (itemData is IUTKPropertyItem item && TryRebindExistingView(element, item))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 재사용 불가 시 기존 View 정리 후 새로 생성
|
|
|
|
|
CleanupContainerCallbacks(element);
|
|
|
|
|
DisposeChildren(element);
|
2026-01-08 20:15:57 +09:00
|
|
|
element.Clear();
|
|
|
|
|
|
|
|
|
|
switch (itemData)
|
|
|
|
|
{
|
|
|
|
|
case IUTKPropertyGroup group:
|
|
|
|
|
BindGroupItem(element, group);
|
|
|
|
|
break;
|
2026-02-10 20:48:49 +09:00
|
|
|
case IUTKPropertyItem newItem:
|
|
|
|
|
BindPropertyItem(element, newItem);
|
2026-01-08 20:15:57 +09:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UnbindItem(VisualElement element, int index)
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
// Unbind만 수행 (View 인스턴스는 유지하여 재사용 가능)
|
2026-02-04 20:31:52 +09:00
|
|
|
foreach (var child in element.Children())
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-04 20:31:52 +09:00
|
|
|
if (child is IUTKPropertyItemView view)
|
|
|
|
|
{
|
|
|
|
|
view.Unbind();
|
|
|
|
|
}
|
2026-02-10 20:48:49 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 기존 자식 View가 동일 PropertyType이면 Unbind → Bind로 재사용합니다.
|
|
|
|
|
/// 이전 이벤트 콜백을 정리하고 새 item으로 재등록합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>재사용 성공 시 true</returns>
|
|
|
|
|
private bool TryRebindExistingView(VisualElement container, IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
if (container.childCount != 1) return false;
|
|
|
|
|
|
|
|
|
|
var existingChild = container[0];
|
|
|
|
|
if (existingChild is not IUTKPropertyItemView existingView) return false;
|
|
|
|
|
|
|
|
|
|
// 같은 PropertyType인지 확인
|
|
|
|
|
var expectedViewType = UTKPropertyItemViewFactory.GetViewType(item);
|
|
|
|
|
if (existingChild.GetType() != expectedViewType) return false;
|
|
|
|
|
|
|
|
|
|
// 이전 이벤트 콜백 정리
|
|
|
|
|
CleanupContainerCallbacks(container);
|
2026-02-09 20:28:09 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// Unbind → Bind (View 인스턴스 재사용)
|
|
|
|
|
existingView.Unbind();
|
|
|
|
|
existingView.Bind(item);
|
|
|
|
|
|
|
|
|
|
// 새 item에 대한 이벤트 콜백 재등록
|
|
|
|
|
RegisterPropertyCallbacks(container, existingChild, item);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 컨테이너에 저장된 이벤트 콜백을 해제합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static void CleanupContainerCallbacks(VisualElement container)
|
|
|
|
|
{
|
|
|
|
|
if (container.userData is not ContainerCallbackInfo info) return;
|
|
|
|
|
|
|
|
|
|
if (info.ItemView != null && info.ClickCallback != null)
|
|
|
|
|
{
|
|
|
|
|
info.ItemView.UnregisterCallback(info.ClickCallback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.ButtonClickHandler != null && info.ItemView is UTKButtonItemView btnView)
|
|
|
|
|
{
|
|
|
|
|
btnView.OnButtonClicked -= info.ButtonClickHandler;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.ActionButtonClickHandler != null && info.ItemView is UTKStringPropertyItemView strView)
|
|
|
|
|
{
|
|
|
|
|
strView.OnActionButtonClicked -= info.ActionButtonClickHandler;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.userData = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>자식 View를 Dispose합니다.</summary>
|
|
|
|
|
private static void DisposeChildren(VisualElement element)
|
|
|
|
|
{
|
|
|
|
|
foreach (var child in element.Children())
|
|
|
|
|
{
|
|
|
|
|
if (child is IUTKPropertyItemView view)
|
|
|
|
|
{
|
|
|
|
|
view.Unbind();
|
|
|
|
|
}
|
2026-02-09 20:28:09 +09:00
|
|
|
if (child is IDisposable disposable)
|
|
|
|
|
{
|
|
|
|
|
disposable.Dispose();
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BindGroupItem(VisualElement container, IUTKPropertyGroup group)
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
// UXML 로드 (캐시)
|
|
|
|
|
if (_groupHeaderUxmlCache == null)
|
|
|
|
|
{
|
|
|
|
|
_groupHeaderUxmlCache = Resources.Load<VisualTreeAsset>(GROUP_HEADER_UXML_PATH);
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
VisualElement groupElement;
|
|
|
|
|
UTKLabel expandIcon;
|
|
|
|
|
UTKLabel title;
|
|
|
|
|
// UTKLabel count;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
if (_groupHeaderUxmlCache != null)
|
|
|
|
|
{
|
|
|
|
|
var root = _groupHeaderUxmlCache.Instantiate();
|
|
|
|
|
groupElement = root.Q<VisualElement>("group-header");
|
|
|
|
|
expandIcon = root.Q<UTKLabel>("expand-icon");
|
|
|
|
|
title = root.Q<UTKLabel>("group-title");
|
|
|
|
|
// count = root.Q<UTKLabel>("group-count");
|
|
|
|
|
container.Add(root);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Fallback: UXML 로드 실패 시 코드로 생성
|
|
|
|
|
groupElement = new VisualElement();
|
|
|
|
|
groupElement.AddToClassList("utk-property-group__header");
|
|
|
|
|
|
|
|
|
|
expandIcon = new UTKLabel();
|
|
|
|
|
expandIcon.AddToClassList("utk-property-group__expand-icon");
|
|
|
|
|
|
|
|
|
|
title = new UTKLabel();
|
|
|
|
|
title.AddToClassList("utk-property-group__title");
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// count = new UTKLabel();
|
|
|
|
|
// count.AddToClassList("utk-property-group__count");
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
groupElement.Add(expandIcon);
|
|
|
|
|
groupElement.Add(title);
|
|
|
|
|
// groupElement.Add(count);
|
|
|
|
|
container.Add(groupElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 데이터 바인딩
|
|
|
|
|
expandIcon.SetMaterialIcon(group.IsExpanded ? UTKMaterialIcons.ExpandMore : UTKMaterialIcons.ChevronRight, 16);
|
|
|
|
|
title.Text = group.GroupName;
|
|
|
|
|
title.Size = UTKLabel.LabelSize.Label1;
|
|
|
|
|
title.IsBold = true;
|
|
|
|
|
// count.Text = $"({group.ItemCount})";
|
|
|
|
|
// count.Variant = UTKLabel.LabelVariant.Secondary;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 그룹 클릭 이벤트 - ContainerCallbackInfo로 관리
|
|
|
|
|
EventCallback<ClickEvent> clickCallback = _ =>
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
|
|
|
|
ToggleGroupExpanded(group.GroupId);
|
2026-02-02 19:33:27 +09:00
|
|
|
expandIcon.SetMaterialIcon(group.IsExpanded ? UTKMaterialIcons.ExpandMore : UTKMaterialIcons.ChevronRight, 16);
|
2026-01-08 20:15:57 +09:00
|
|
|
OnGroupExpandedChanged?.Invoke(group, group.IsExpanded);
|
2026-02-09 20:28:09 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
groupElement.RegisterCallback(clickCallback);
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 컨테이너에 콜백 정보 저장 (CleanupContainerCallbacks에서 정리)
|
|
|
|
|
container.userData = new ContainerCallbackInfo
|
2026-02-09 20:28:09 +09:00
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
ItemView = groupElement,
|
|
|
|
|
ClickCallback = clickCallback
|
|
|
|
|
};
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BindPropertyItem(VisualElement container, IUTKPropertyItem item)
|
|
|
|
|
{
|
2026-02-04 20:31:52 +09:00
|
|
|
// View Factory를 사용하여 View 생성 및 바인딩
|
|
|
|
|
var itemView = UTKPropertyItemViewFactory.CreateView(item);
|
2026-02-10 20:48:49 +09:00
|
|
|
container.Add(itemView);
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 이벤트 콜백 등록 및 컨테이너에 저장
|
|
|
|
|
RegisterPropertyCallbacks(container, itemView, item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// PropertyItem View에 이벤트 콜백을 등록하고 컨테이너에 정보를 저장합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void RegisterPropertyCallbacks(VisualElement container, VisualElement itemView, IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
var info = new ContainerCallbackInfo { ItemView = itemView };
|
|
|
|
|
|
|
|
|
|
// 클릭 이벤트 등록
|
|
|
|
|
info.ClickCallback = _ => OnPropertyClicked?.Invoke(item);
|
|
|
|
|
itemView.RegisterCallback(info.ClickCallback);
|
2026-02-09 20:28:09 +09:00
|
|
|
|
|
|
|
|
// 버튼 아이템인 경우 버튼 클릭 이벤트 구독
|
|
|
|
|
if (itemView is UTKButtonItemView buttonView)
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
info.ButtonClickHandler = actionName => OnPropertyButtonClicked?.Invoke(item.Id, actionName);
|
|
|
|
|
buttonView.OnButtonClicked += info.ButtonClickHandler;
|
2026-02-09 20:28:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String 아이템에 ActionButton이 있는 경우 이벤트 구독
|
|
|
|
|
if (itemView is UTKStringPropertyItemView stringView)
|
|
|
|
|
{
|
2026-02-10 20:48:49 +09:00
|
|
|
info.ActionButtonClickHandler = actionName => OnPropertyButtonClicked?.Invoke(item.Id, actionName);
|
|
|
|
|
stringView.OnActionButtonClicked += info.ActionButtonClickHandler;
|
2026-02-09 20:28:09 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 컨테이너에 콜백 정보 저장 (재사용/정리 시 참조)
|
|
|
|
|
container.userData = info;
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Event Subscriptions
|
|
|
|
|
private void SubscribeGroup(IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
group.OnExpandedChanged += OnGroupExpandedChangedInternal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UnsubscribeGroup(IUTKPropertyGroup group)
|
|
|
|
|
{
|
|
|
|
|
group.OnExpandedChanged -= OnGroupExpandedChangedInternal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SubscribeItem(IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
item.OnValueChanged += OnItemValueChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UnsubscribeItem(IUTKPropertyItem item)
|
|
|
|
|
{
|
|
|
|
|
item.OnValueChanged -= OnItemValueChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnGroupExpandedChangedInternal(bool expanded)
|
|
|
|
|
{
|
|
|
|
|
// TreeView 갱신은 ToggleGroupExpanded에서 처리
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 18:04:38 +09:00
|
|
|
private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue, bool notify)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-12 18:04:38 +09:00
|
|
|
if (notify)
|
|
|
|
|
{
|
|
|
|
|
var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
|
|
|
|
|
OnPropertyValueChanged?.Invoke(args);
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
private void OnSearch(string newValue)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-02 19:33:27 +09:00
|
|
|
_searchText = newValue ?? string.Empty;
|
2026-01-08 20:15:57 +09:00
|
|
|
RefreshTreeView();
|
2026-02-04 20:31:52 +09:00
|
|
|
// 클리어 버튼 가시성 업데이트
|
|
|
|
|
if (_clearButton != null)
|
|
|
|
|
{
|
|
|
|
|
_clearButton.style.display = string.IsNullOrEmpty(_searchText) ? DisplayStyle.None : DisplayStyle.Flex;
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
2026-02-04 20:31:52 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
private void OnClearButtonClicked()
|
|
|
|
|
{
|
|
|
|
|
if (_searchField != null && !string.IsNullOrEmpty(_searchField.value))
|
|
|
|
|
{
|
|
|
|
|
_searchField.value = string.Empty;
|
|
|
|
|
_searchText = string.Empty;
|
|
|
|
|
RefreshTreeView();
|
|
|
|
|
|
|
|
|
|
// 클리어 버튼 숨김
|
|
|
|
|
if (_clearButton != null)
|
|
|
|
|
{
|
|
|
|
|
_clearButton.style.display = DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region IDisposable
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (_disposed) return;
|
|
|
|
|
_disposed = true;
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
// 테마 변경 이벤트 해제
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
2026-02-10 20:48:49 +09:00
|
|
|
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
|
|
|
|
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
|
2026-02-04 20:31:52 +09:00
|
|
|
|
2026-02-10 20:48:49 +09:00
|
|
|
// 데이터 이벤트 해제 + TreeView View 콜백/Dispose 정리
|
|
|
|
|
ClearInternal();
|
|
|
|
|
|
|
|
|
|
// TreeView 콜백 해제
|
2026-01-08 20:15:57 +09:00
|
|
|
if (_treeView != null)
|
|
|
|
|
{
|
|
|
|
|
_treeView.makeItem = null;
|
|
|
|
|
_treeView.bindItem = null;
|
|
|
|
|
_treeView.unbindItem = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 20:31:52 +09:00
|
|
|
// 검색 필드 이벤트 정리
|
|
|
|
|
if (_searchField != null)
|
|
|
|
|
{
|
|
|
|
|
_searchField.OnSubmit -= OnSearch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_clearButton != null)
|
|
|
|
|
{
|
|
|
|
|
_clearButton.OnClicked -= OnClearButtonClicked;
|
|
|
|
|
_clearButton.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
// 이벤트 핸들러 정리
|
|
|
|
|
OnPropertyValueChanged = null;
|
|
|
|
|
OnGroupExpandedChanged = null;
|
|
|
|
|
OnPropertyClicked = null;
|
2026-02-09 20:28:09 +09:00
|
|
|
OnPropertyButtonClicked = null;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
// UI 참조 정리
|
|
|
|
|
_treeView = null;
|
|
|
|
|
_searchField = null;
|
2026-02-04 20:31:52 +09:00
|
|
|
_clearButton = null;
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|