612 lines
19 KiB
C#
612 lines
19 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace UVC.UIToolkit
|
|
{
|
|
/// <summary>
|
|
/// UTKPropertyWindow의 핵심 리스트 컴포넌트
|
|
/// TreeView 기반으로 그룹화된 속성들을 표시
|
|
/// </summary>
|
|
[UxmlElement]
|
|
public partial class UTKPropertyList : VisualElement, IDisposable
|
|
{
|
|
#region Constants
|
|
private const string USS_PATH = "UIToolkit/Property/UTKPropertyCommon";
|
|
#endregion
|
|
|
|
#region Fields
|
|
private bool _disposed;
|
|
private TreeView? _treeView;
|
|
private TextField? _searchField;
|
|
|
|
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;
|
|
#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;
|
|
#endregion
|
|
|
|
#region Constructor
|
|
public UTKPropertyList()
|
|
{
|
|
// 테마 스타일시트 등록
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
// 컴포넌트 스타일시트 로드
|
|
var styleSheet = Resources.Load<StyleSheet>(USS_PATH);
|
|
if (styleSheet != null)
|
|
{
|
|
styleSheets.Add(styleSheet);
|
|
}
|
|
|
|
CreateUI();
|
|
}
|
|
#endregion
|
|
|
|
#region UI Creation
|
|
private void CreateUI()
|
|
{
|
|
// USS 클래스 기반 스타일링 - 인라인 스타일 최소화
|
|
AddToClassList("utk-property-list");
|
|
|
|
// 검색 필드
|
|
var searchContainer = new VisualElement { name = "search-container" };
|
|
searchContainer.AddToClassList("utk-property-list__search-container");
|
|
|
|
_searchField = new TextField { name = "search-field" };
|
|
_searchField.AddToClassList("utk-property-list__search-field");
|
|
_searchField.RegisterValueChangedCallback(OnSearchChanged);
|
|
|
|
var searchPlaceholder = new Label("Search...") { name = "search-placeholder" };
|
|
searchPlaceholder.AddToClassList("utk-property-list__search-placeholder");
|
|
searchPlaceholder.pickingMode = PickingMode.Ignore;
|
|
|
|
_searchField.RegisterCallback<FocusInEvent>(_ => searchPlaceholder.style.display = DisplayStyle.None);
|
|
_searchField.RegisterCallback<FocusOutEvent>(_ =>
|
|
{
|
|
if (string.IsNullOrEmpty(_searchField.value))
|
|
searchPlaceholder.style.display = DisplayStyle.Flex;
|
|
});
|
|
|
|
searchContainer.Add(_searchField);
|
|
searchContainer.Add(searchPlaceholder);
|
|
Add(searchContainer);
|
|
|
|
// 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;
|
|
|
|
_treeView.makeItem = MakeItem;
|
|
_treeView.bindItem = BindItem;
|
|
_treeView.unbindItem = UnbindItem;
|
|
|
|
Add(_treeView);
|
|
}
|
|
#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
|
|
public void AddGroup(IUTKPropertyGroup group)
|
|
{
|
|
AddGroupInternal(group);
|
|
RefreshTreeView();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
public IUTKPropertyGroup? GetGroup(string groupId)
|
|
{
|
|
return _groupIndex.TryGetValue(groupId, out var group) ? group : null;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ToggleGroupExpanded(string groupId)
|
|
{
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
{
|
|
SetGroupExpanded(groupId, !group.IsExpanded);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods - Property Management
|
|
public void AddProperty(IUTKPropertyItem item)
|
|
{
|
|
AddPropertyInternal(item);
|
|
RefreshTreeView();
|
|
}
|
|
|
|
public void AddPropertyToGroup(string groupId, IUTKPropertyItem item)
|
|
{
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
{
|
|
group.AddItem(item);
|
|
_itemIndex[item.Id] = item;
|
|
SubscribeItem(item);
|
|
RefreshTreeView();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
public IUTKPropertyItem? GetProperty(string itemId)
|
|
{
|
|
return _itemIndex.TryGetValue(itemId, out var item) ? item : null;
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods - Value Management
|
|
public void UpdatePropertyValue(string propertyId, object newValue)
|
|
{
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
{
|
|
item.SetValue(newValue);
|
|
}
|
|
}
|
|
|
|
public void SetPropertyValue(string propertyId, object value)
|
|
{
|
|
UpdatePropertyValue(propertyId, value);
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods - Visibility & ReadOnly
|
|
public void SetPropertyVisibility(string propertyId, bool visible)
|
|
{
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
{
|
|
item.IsVisible = visible;
|
|
}
|
|
}
|
|
|
|
public void SetGroupVisibility(string groupId, bool visible)
|
|
{
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
{
|
|
group.IsVisible = visible;
|
|
}
|
|
}
|
|
|
|
public void SetPropertyReadOnly(string propertyId, bool isReadOnly)
|
|
{
|
|
if (_itemIndex.TryGetValue(propertyId, out var item))
|
|
{
|
|
item.IsReadOnly = isReadOnly;
|
|
}
|
|
}
|
|
|
|
public void SetGroupReadOnly(string groupId, bool isReadOnly)
|
|
{
|
|
if (_groupIndex.TryGetValue(groupId, out var group))
|
|
{
|
|
group.SetAllItemsReadOnly(isReadOnly);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods - Utilities
|
|
public void Clear()
|
|
{
|
|
ClearInternal();
|
|
RefreshTreeView();
|
|
}
|
|
|
|
public void Refresh()
|
|
{
|
|
RefreshTreeView();
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
style.display = DisplayStyle.Flex;
|
|
}
|
|
|
|
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()
|
|
{
|
|
// 이벤트 구독 해제
|
|
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;
|
|
}
|
|
|
|
private void RefreshTreeView()
|
|
{
|
|
if (_treeView == null) return;
|
|
|
|
var treeItems = BuildTreeItems();
|
|
_treeView.SetRootItems(treeItems);
|
|
_treeView.Rebuild();
|
|
|
|
// 펼침 상태 복원
|
|
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
|
|
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;
|
|
|
|
element.Clear();
|
|
|
|
switch (itemData)
|
|
{
|
|
case IUTKPropertyGroup group:
|
|
BindGroupItem(element, group);
|
|
break;
|
|
case IUTKPropertyItem item:
|
|
BindPropertyItem(element, item);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UnbindItem(VisualElement element, int index)
|
|
{
|
|
var itemData = _treeView?.GetItemDataForIndex<IUTKPropertyEntry>(index);
|
|
|
|
if (itemData is IUTKPropertyItem item)
|
|
{
|
|
item.UnbindUI(element);
|
|
}
|
|
|
|
element.Clear();
|
|
}
|
|
|
|
private void BindGroupItem(VisualElement container, IUTKPropertyGroup group)
|
|
{
|
|
var groupElement = new VisualElement();
|
|
groupElement.AddToClassList("utk-property-group__header");
|
|
|
|
var expandIcon = new Label(group.IsExpanded ? "▼" : "▶");
|
|
expandIcon.AddToClassList("utk-property-group__expand-icon");
|
|
|
|
var title = new Label(group.GroupName);
|
|
title.AddToClassList("utk-property-group__title");
|
|
|
|
var count = new Label($"({group.ItemCount})");
|
|
count.AddToClassList("utk-property-group__count");
|
|
|
|
groupElement.Add(expandIcon);
|
|
groupElement.Add(title);
|
|
groupElement.Add(count);
|
|
|
|
groupElement.RegisterCallback<ClickEvent>(_ =>
|
|
{
|
|
ToggleGroupExpanded(group.GroupId);
|
|
expandIcon.text = group.IsExpanded ? "▼" : "▶";
|
|
OnGroupExpandedChanged?.Invoke(group, group.IsExpanded);
|
|
});
|
|
|
|
container.Add(groupElement);
|
|
}
|
|
|
|
private void BindPropertyItem(VisualElement container, IUTKPropertyItem item)
|
|
{
|
|
var itemUI = item.CreateUI();
|
|
item.BindUI(itemUI);
|
|
|
|
itemUI.RegisterCallback<ClickEvent>(_ => OnPropertyClicked?.Invoke(item));
|
|
|
|
container.Add(itemUI);
|
|
}
|
|
#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에서 처리
|
|
}
|
|
|
|
private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue)
|
|
{
|
|
var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
|
|
OnPropertyValueChanged?.Invoke(args);
|
|
}
|
|
|
|
private void OnSearchChanged(ChangeEvent<string> evt)
|
|
{
|
|
_searchText = evt.newValue ?? string.Empty;
|
|
RefreshTreeView();
|
|
}
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
// TreeView 콜백 정리
|
|
if (_treeView != null)
|
|
{
|
|
_treeView.makeItem = null;
|
|
_treeView.bindItem = null;
|
|
_treeView.unbindItem = null;
|
|
}
|
|
|
|
// 이벤트 정리
|
|
ClearInternal();
|
|
|
|
// 이벤트 핸들러 정리
|
|
OnPropertyValueChanged = null;
|
|
OnGroupExpandedChanged = null;
|
|
OnPropertyClicked = null;
|
|
|
|
// UI 참조 정리
|
|
_treeView = null;
|
|
_searchField = null;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|