625 lines
24 KiB
C#
625 lines
24 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace SHI.Modal
|
|
{
|
|
/// <summary>
|
|
/// 계층적 트리 구조를 표시하는 커스텀 UI Toolkit 컴포넌트입니다.
|
|
///
|
|
/// <para><b>개요:</b></para>
|
|
/// <para>
|
|
/// TreeList는 Unity UI Toolkit의 TreeView를 래핑하여 검색, 가시성 토글,
|
|
/// 닫기 기능 등을 제공하는 재사용 가능한 컴포넌트입니다.
|
|
/// UXML 파일(TreeList.uxml, TreeListItem.uxml)과 함께 사용됩니다.
|
|
/// </para>
|
|
///
|
|
/// <para><b>주요 기능:</b></para>
|
|
/// <list type="bullet">
|
|
/// <item>계층적 트리 구조 표시 (펼치기/접기 지원)</item>
|
|
/// <item>실시간 검색 필터링</item>
|
|
/// <item>항목별 가시성(눈 아이콘) 토글</item>
|
|
/// <item>선택 이벤트 처리</item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>UXML에서 사용:</b></para>
|
|
/// <code>
|
|
/// <SHI.Modal.TreeList name="tree-list" />
|
|
/// </code>
|
|
///
|
|
/// <para><b>코드에서 사용:</b></para>
|
|
/// <code>
|
|
/// var treeList = root.Q<TreeList>();
|
|
/// treeList.OnSelectionChanged += (item) => Debug.Log($"선택: {item.name}");
|
|
/// treeList.OnVisibilityChanged += (item) => model.SetActive(item.id, item.IsVisible);
|
|
/// treeList.SetData(treeItems);
|
|
/// </code>
|
|
///
|
|
/// <para><b>관련 리소스:</b></para>
|
|
/// <list type="bullet">
|
|
/// <item>Resources/SHI/Modal/TreeList.uxml - 메인 레이아웃</item>
|
|
/// <item>Resources/SHI/Modal/TreeListItem.uxml - 개별 항목 템플릿</item>
|
|
/// </list>
|
|
/// </summary>
|
|
[UxmlElement]
|
|
public partial class TreeList : VisualElement, IDisposable
|
|
{
|
|
#region IDisposable
|
|
private bool _disposed = false;
|
|
#endregion
|
|
#region 상수 (Constants)
|
|
/// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary>
|
|
private const string UXML_PATH = "SHI/Modal/TreeList";
|
|
|
|
#endregion
|
|
|
|
#region UI 컴포넌트 참조 (UI Component References)
|
|
/// <summary>검색어 입력 필드</summary>
|
|
private TextField? _searchField;
|
|
|
|
/// <summary>Unity UI Toolkit의 TreeView 컴포넌트</summary>
|
|
private TreeView? _treeView;
|
|
|
|
/// <summary>트리 리스트 닫기 버튼</summary>
|
|
private Button? _closeButton;
|
|
|
|
/// <summary>검색어 지우기 버튼</summary>
|
|
private Button? _clearButton;
|
|
#endregion
|
|
|
|
#region 내부 데이터 (Internal Data)
|
|
|
|
|
|
/// <summary>
|
|
/// 원본 루트 데이터입니다.
|
|
/// 검색 필터 해제 시 원래 데이터로 복원하는 데 사용됩니다.
|
|
/// </summary>
|
|
private List<TreeListItemData> _originalRoots = new();
|
|
|
|
/// <summary>
|
|
/// TreeView에 바인딩되는 데이터 소스입니다.
|
|
/// TreeViewItemData는 Unity의 TreeView가 요구하는 래퍼 타입입니다.
|
|
/// </summary>
|
|
private List<TreeViewItemData<TreeListItemData>>? _rootData;
|
|
|
|
/// <summary>
|
|
/// 항목 ID 자동 생성을 위한 시드 값입니다.
|
|
/// SetData() 호출 시 id가 0인 항목에 순차적으로 ID를 할당합니다.
|
|
/// </summary>
|
|
private int _idSeed = 1;
|
|
|
|
/// <summary>
|
|
/// 이전에 선택된 항목입니다.
|
|
/// 선택 해제 이벤트 발송에 사용됩니다.
|
|
/// </summary>
|
|
private TreeListItemData? _previouslySelectedItem;
|
|
#endregion
|
|
|
|
#region 외부 이벤트 (Public Events)
|
|
/// <summary>
|
|
/// 항목의 가시성(눈 아이콘)이 변경될 때 발생합니다.
|
|
/// 3D 모델의 GameObject 활성화/비활성화에 연동합니다.
|
|
/// </summary>
|
|
public event Action<TreeListItemData>? OnVisibilityChanged;
|
|
|
|
/// <summary>
|
|
/// 항목 선택 상태가 변경될 때 발생합니다.
|
|
/// 선택 및 선택 해제 모두에서 발생하며, item.isSelected로 구분합니다.
|
|
/// </summary>
|
|
public event Action<TreeListItemData>? OnSelectionChanged;
|
|
|
|
/// <summary>
|
|
/// 트리 리스트가 닫힐 때(숨겨질 때) 발생합니다.
|
|
/// 닫기 버튼 클릭 시 트리거됩니다.
|
|
/// </summary>
|
|
public event Action? OnClosed;
|
|
#endregion
|
|
|
|
#region 생성자 (Constructor)
|
|
/// <summary>
|
|
/// TreeList 컴포넌트를 초기화합니다.
|
|
/// UXML 템플릿을 로드하고 내부 컴포넌트를 설정합니다.
|
|
/// </summary>
|
|
public TreeList()
|
|
{
|
|
// 1. 메인 UXML 로드 및 복제
|
|
// CloneTree(this)로 UXML 내용이 이 클래스의 자식으로 추가됨
|
|
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
if (visualTree == null)
|
|
{
|
|
Debug.LogError($"[TreeMenu] UXML not found at: {UXML_PATH}");
|
|
return;
|
|
}
|
|
visualTree!.CloneTree(this);
|
|
|
|
|
|
// 2. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
|
|
_searchField = this.Q<TextField>("search-field");
|
|
_treeView = this.Q<TreeView>("main-tree-view");
|
|
_closeButton = this.Q<Button>("hide-btn");
|
|
_clearButton = this.Q<Button>("clear-btn");
|
|
|
|
// 3. 이벤트 연결 및 로직 초기화
|
|
InitializeLogic();
|
|
}
|
|
#endregion
|
|
|
|
#region 초기화 (Initialization)
|
|
/// <summary>
|
|
/// 내부 로직과 이벤트 핸들러를 초기화합니다.
|
|
/// </summary>
|
|
private void InitializeLogic()
|
|
{
|
|
// 검색창 이벤트: 입력 값이 변경될 때마다 필터링 실행
|
|
if (_searchField != null)
|
|
{
|
|
_searchField.RegisterValueChangedCallback(OnSearchValueChanged);
|
|
}
|
|
|
|
// TreeView 설정
|
|
// makeItem: 새 항목 UI 생성 시 호출
|
|
// bindItem: 데이터를 UI에 바인딩할 때 호출
|
|
// selectionChanged: 선택 변경 시 호출
|
|
if (_treeView != null)
|
|
{
|
|
_treeView.bindItem = BindTreeItem;
|
|
_treeView.selectionChanged += OnTreeViewSelectionChanged;
|
|
}
|
|
|
|
// 닫기 버튼: 트리 리스트를 숨기고 이벤트 발생
|
|
if (_closeButton != null)
|
|
{
|
|
_closeButton.clicked += () =>
|
|
{
|
|
this.style.display = DisplayStyle.None;
|
|
OnClosed?.Invoke();
|
|
};
|
|
}
|
|
|
|
// 검색어 지우기 버튼
|
|
if(_clearButton != null)
|
|
{
|
|
_clearButton.clicked += () =>
|
|
{
|
|
if (_searchField.value.Length > 0)
|
|
{
|
|
_searchField.value = string.Empty;
|
|
OnSearch(string.Empty);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 공개 메서드 (Public Methods)
|
|
/// <summary>
|
|
/// 트리 리스트를 화면에 표시합니다.
|
|
/// </summary>
|
|
public void Show()
|
|
{
|
|
this.style.display = DisplayStyle.Flex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 트리 데이터를 설정합니다.
|
|
/// TreeListItemData 리스트를 받아 TreeView에 바인딩합니다.
|
|
/// </summary>
|
|
/// <param name="roots">루트 항목들의 리스트 (계층 구조의 최상위 항목들)</param>
|
|
public void SetData(List<TreeListItemData> roots)
|
|
{
|
|
// 원본 데이터 저장 (필터 복원용)
|
|
_originalRoots = roots ?? new List<TreeListItemData>();
|
|
|
|
// ID 시드 초기화
|
|
_idSeed = 1;
|
|
|
|
// 순환 참조 감지를 위한 방문 집합
|
|
var visited = new HashSet<TreeListItemData>();
|
|
|
|
// TreeView 형식으로 데이터 변환
|
|
_rootData = ConvertToTreeViewData(_originalRoots, visited, 0);
|
|
|
|
// TreeView에 데이터 설정
|
|
_treeView!.SetRootItems<TreeListItemData>(_rootData);
|
|
|
|
// UI 갱신 및 모든 항목 펼치기
|
|
_treeView!.Rebuild();
|
|
_treeView!.ExpandAll();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 ID의 항목을 프로그래밍 방식으로 선택합니다.
|
|
/// </summary>
|
|
/// <param name="itemId">선택할 항목의 ID</param>
|
|
public void SelectByItemId(int itemId)
|
|
{
|
|
_treeView.SetSelection(new List<int> { itemId });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 이름 목록에 해당하는 항목만 표시하고 나머지는 숨깁니다.
|
|
/// </summary>
|
|
/// <param name="items">표시할 항목들의 이름 목록</param>
|
|
/// <param name="depth">검색 깊이 (1=1뎁스 자식만, 2=2뎁스까지, 0이하=전체)</param>
|
|
public void ShowItems(List<string> items, int depth = 1)
|
|
{
|
|
if (_originalRoots == null || _originalRoots.Count == 0) return;
|
|
|
|
var visibleNames = new HashSet<string>(items ?? new List<string>());
|
|
|
|
foreach (var root in _originalRoots)
|
|
{
|
|
SetVisibilityByNames(root, visibleNames, depth, 0);
|
|
}
|
|
|
|
// UI 갱신
|
|
_treeView?.RefreshItems();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재귀적으로 항목의 가시성을 이름 목록에 따라 설정합니다.
|
|
/// </summary>
|
|
/// <param name="node">현재 노드</param>
|
|
/// <param name="visibleNames">표시할 이름 목록</param>
|
|
/// <param name="maxDepth">최대 검색 깊이 (0이하=무제한)</param>
|
|
/// <param name="currentDepth">현재 깊이</param>
|
|
private void SetVisibilityByNames(TreeListItemData node, HashSet<string> visibleNames, int maxDepth, int currentDepth)
|
|
{
|
|
if (node == null) return;
|
|
|
|
// maxDepth <= 0 이면 전체 검색, 아니면 깊이 제한 체크
|
|
bool isWithinDepth = maxDepth <= 0 || currentDepth < maxDepth;
|
|
|
|
if (isWithinDepth)
|
|
{
|
|
// 이름이 목록에 있으면 visible, 없으면 hidden
|
|
node.IsVisible = visibleNames.Contains(node.name);
|
|
|
|
// 가시성 변경 이벤트 발송
|
|
OnVisibilityChanged?.Invoke(node);
|
|
}
|
|
|
|
// 자식들도 재귀적으로 처리
|
|
if (node.children != null)
|
|
{
|
|
foreach (var child in node.children)
|
|
{
|
|
SetVisibilityByNames(child, visibleNames, maxDepth, currentDepth + 1);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 선택 처리 (Selection Handling)
|
|
/// <summary>
|
|
/// TreeView의 선택 변경 이벤트를 처리합니다.
|
|
/// 외부에 OnSelectionChanged 이벤트를 발송합니다.
|
|
/// </summary>
|
|
/// <param name="selectedItems">선택된 항목들</param>
|
|
private void OnTreeViewSelectionChanged(System.Collections.Generic.IEnumerable<object> selectedItems)
|
|
{
|
|
// 단일 선택 기준 (첫 번째 항목만 처리)
|
|
var currentItem = selectedItems.FirstOrDefault() as TreeListItemData;
|
|
|
|
// 이전 선택 항목이 있고 현재와 다르면 선택 해제 이벤트 발송
|
|
if (_previouslySelectedItem != null && _previouslySelectedItem != currentItem)
|
|
{
|
|
_previouslySelectedItem.isSelected = false;
|
|
OnSelectionChanged?.Invoke(_previouslySelectedItem);
|
|
}
|
|
|
|
// 현재 선택 항목이 있으면 선택 이벤트 발송
|
|
if (currentItem != null)
|
|
{
|
|
currentItem.isSelected = true;
|
|
OnSelectionChanged?.Invoke(currentItem);
|
|
}
|
|
|
|
// 현재 선택 상태 저장
|
|
_previouslySelectedItem = currentItem;
|
|
}
|
|
#endregion
|
|
|
|
#region 데이터 변환 (Data Conversion)
|
|
/// <summary>
|
|
/// TreeListItemData 리스트를 TreeView가 요구하는 TreeViewItemData 형식으로 변환합니다.
|
|
/// 재귀적으로 자식 항목도 변환하며, 순환 참조를 감지합니다.
|
|
/// </summary>
|
|
/// <param name="items">변환할 항목 리스트</param>
|
|
/// <param name="visited">순환 참조 감지용 방문 집합</param>
|
|
/// <param name="depth">현재 깊이 (디버깅용)</param>
|
|
/// <returns>TreeViewItemData 래퍼 리스트</returns>
|
|
private List<TreeViewItemData<TreeListItemData>> ConvertToTreeViewData(List<TreeListItemData> items, HashSet<TreeListItemData> visited, int depth)
|
|
{
|
|
var list = new List<TreeViewItemData<TreeListItemData>>(items.Count);
|
|
foreach (var item in items)
|
|
{
|
|
if (item == null) continue;
|
|
|
|
// 순환 참조 체크: 이미 방문한 항목이면 자식 무시
|
|
if (!visited.Add(item))
|
|
{
|
|
Debug.LogWarning($"[TreeList] Cycle detected at item '{item.name}' → children 무시");
|
|
item.children = null;
|
|
}
|
|
|
|
// ID가 없으면 자동 할당
|
|
if (item.id == 0) item.id = _idSeed++;
|
|
|
|
// 자식 항목 재귀 변환
|
|
List<TreeViewItemData<TreeListItemData>>? childData = null;
|
|
if (item.children != null && item.children.Count > 0)
|
|
childData = ConvertToTreeViewData(item.children, visited, depth + 1);
|
|
|
|
// TreeViewItemData 래퍼 생성
|
|
var treeItem = new TreeViewItemData<TreeListItemData>(item.id, item, childData);
|
|
list.Add(treeItem);
|
|
}
|
|
return list;
|
|
}
|
|
#endregion
|
|
|
|
#region TreeView 항목 바인딩 (TreeView Item Creation/Binding)
|
|
|
|
/// <summary>
|
|
/// 데이터를 UI 요소에 바인딩합니다.
|
|
/// TreeView의 bindItem 콜백으로 사용됩니다.
|
|
/// 스크롤 시 재사용되는 항목에 새 데이터를 연결합니다.
|
|
/// </summary>
|
|
/// <param name="element">바인딩할 UI 요소</param>
|
|
/// <param name="index">TreeView 내부 인덱스</param>
|
|
private void BindTreeItem(VisualElement element, int index)
|
|
{
|
|
// 인덱스로 데이터 획득
|
|
var item = _treeView.GetItemDataForIndex<TreeListItemData>(index);
|
|
if (item == null) return;
|
|
|
|
// 1. 항목 이름 레이블 설정
|
|
var label = element.Q<Label>("item-label");
|
|
if (label != null) label.text = item.name;
|
|
|
|
// 2. 가시성 아이콘 버튼 설정
|
|
var toggleBtn = element.Q<Button>("visibility-btn");
|
|
if (toggleBtn != null) UpdateVisibilityIcon(toggleBtn, item.IsVisible);
|
|
|
|
// 3. 가시성 버튼 클릭 이벤트 연결
|
|
// 주의: bindItem은 스크롤 시 재호출되므로 기존 이벤트 제거 후 재등록
|
|
if (toggleBtn.userData is Action oldAction) toggleBtn.clicked -= oldAction;
|
|
System.Action clickAction = () =>
|
|
{
|
|
// 가시성 상태 토글
|
|
item.IsVisible = !item.IsVisible;
|
|
UpdateVisibilityIcon(toggleBtn, item.IsVisible);
|
|
|
|
// 자식들에게 동일 상태 전파
|
|
SetChildrenVisibility(item, item.IsVisible);
|
|
|
|
// 화면에 보이는 자식 아이콘도 갱신
|
|
_treeView.RefreshItems();
|
|
|
|
// 외부에 이벤트 발송 (3D 모델 가시성 동기화)
|
|
OnVisibilityChanged?.Invoke(item);
|
|
};
|
|
toggleBtn.userData = clickAction;
|
|
toggleBtn.clicked += clickAction;
|
|
}
|
|
#endregion
|
|
|
|
#region 가시성 처리 (Visibility Handling)
|
|
/// <summary>
|
|
/// 자식 항목들의 가시성을 재귀적으로 동기화합니다.
|
|
/// 부모의 가시성이 변경되면 모든 하위 항목에 동일하게 적용됩니다.
|
|
/// </summary>
|
|
/// <param name="node">현재 노드</param>
|
|
/// <param name="isVisible">설정할 가시성 상태</param>
|
|
private void SetChildrenVisibility(TreeListItemData node, bool isVisible)
|
|
{
|
|
if (node.children == null || node.children.Count == 0) return;
|
|
foreach (var child in node.children)
|
|
{
|
|
if (child == null) continue;
|
|
child.IsVisible = isVisible;
|
|
// 재귀적으로 하위 자식에도 적용
|
|
SetChildrenVisibility(child, isVisible);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 가시성 버튼의 아이콘 스타일을 업데이트합니다.
|
|
/// USS 클래스를 토글하여 아이콘 변경을 처리합니다.
|
|
/// </summary>
|
|
/// <param name="btn">가시성 토글 버튼</param>
|
|
/// <param name="isVisible">현재 가시성 상태</param>
|
|
private void UpdateVisibilityIcon(Button btn, bool isVisible)
|
|
{
|
|
if (isVisible)
|
|
{
|
|
btn.RemoveFromClassList("visibility-off");
|
|
btn.AddToClassList("visibility-on");
|
|
}
|
|
else
|
|
{
|
|
btn.RemoveFromClassList("visibility-on");
|
|
btn.AddToClassList("visibility-off");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 검색 기능 (Search Functionality)
|
|
/// <summary>
|
|
/// 검색 필드 값 변경 콜백입니다.
|
|
/// </summary>
|
|
/// <param name="evt">값 변경 이벤트</param>
|
|
private void OnSearchValueChanged(ChangeEvent<string> evt)
|
|
{
|
|
OnSearch(evt.newValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 검색어에 따라 트리를 필터링합니다.
|
|
/// 검색어가 비어있으면 원본 데이터로 복원됩니다.
|
|
/// </summary>
|
|
/// <param name="query">검색어</param>
|
|
private void OnSearch(string query)
|
|
{
|
|
// 검색어가 없으면 원본 데이터 복원
|
|
if (string.IsNullOrEmpty(query))
|
|
{
|
|
_treeView.SetRootItems<TreeListItemData>(_rootData);
|
|
_treeView.Rebuild();
|
|
return;
|
|
}
|
|
|
|
// 대소문자 무시 검색
|
|
string qLower = query.Trim().ToLowerInvariant();
|
|
var filteredWrappers = FilterTree(qLower);
|
|
|
|
// 필터링된 결과로 TreeView 갱신
|
|
_treeView.SetRootItems<TreeListItemData>(filteredWrappers);
|
|
_treeView.Rebuild();
|
|
|
|
// 검색 결과 모두 펼치기
|
|
ExpandAll(filteredWrappers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 루트 항목에 대해 필터링을 수행합니다.
|
|
/// </summary>
|
|
/// <param name="qLower">소문자로 변환된 검색어</param>
|
|
/// <returns>필터링된 TreeViewItemData 리스트</returns>
|
|
private List<TreeViewItemData<TreeListItemData>> FilterTree(string qLower)
|
|
{
|
|
var result = new List<TreeViewItemData<TreeListItemData>>();
|
|
foreach (var root in _originalRoots)
|
|
{
|
|
TreeViewItemData<TreeListItemData>? filtered = FilterNode(root, qLower);
|
|
if (filtered != null) result.Add(filtered.Value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 단일 노드를 필터링합니다.
|
|
/// 자신 또는 자식이 검색어와 매치되면 결과에 포함됩니다.
|
|
/// </summary>
|
|
/// <param name="node">필터링할 노드</param>
|
|
/// <param name="qLower">소문자로 변환된 검색어</param>
|
|
/// <returns>매치된 경우 TreeViewItemData, 아니면 null</returns>
|
|
private TreeViewItemData<TreeListItemData>? FilterNode(TreeListItemData node, string qLower)
|
|
{
|
|
// 자기 자신이 매치되는지 확인
|
|
bool selfMatch = NodeMatches(node, qLower);
|
|
List<TreeViewItemData<TreeListItemData>>? childFiltered = null;
|
|
|
|
// 자식들도 재귀적으로 필터링
|
|
if (node.children != null && node.children.Count > 0)
|
|
{
|
|
foreach (var child in node.children)
|
|
{
|
|
TreeViewItemData<TreeListItemData>? f = FilterNode(child, qLower);
|
|
if (f != null)
|
|
{
|
|
childFiltered ??= new List<TreeViewItemData<TreeListItemData>>();
|
|
childFiltered.Add(f.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 자신이 매치되거나 매치된 자식이 있으면 포함
|
|
if (selfMatch || (childFiltered != null && childFiltered.Count > 0))
|
|
{
|
|
return new TreeViewItemData<TreeListItemData>(node.id, node, childFiltered);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드가 검색어와 매치되는지 확인합니다.
|
|
/// name, option, ExternalKey 필드를 검색 대상으로 합니다.
|
|
/// </summary>
|
|
/// <param name="item">검사할 항목</param>
|
|
/// <param name="qLower">소문자로 변환된 검색어</param>
|
|
/// <returns>매치 여부</returns>
|
|
private bool NodeMatches(TreeListItemData item, string qLower)
|
|
{
|
|
if (item.name != null && item.name.ToLowerInvariant().Contains(qLower)) return true;
|
|
if (!string.IsNullOrEmpty(item.option) && item.option.ToLowerInvariant().Contains(qLower)) return true;
|
|
if (!string.IsNullOrEmpty(item.ExternalKey) && item.ExternalKey.ToLowerInvariant().Contains(qLower)) return true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 필터링된 결과의 모든 항목을 펼칩니다.
|
|
/// </summary>
|
|
/// <param name="roots">펼칠 루트 항목들</param>
|
|
private void ExpandAll(List<TreeViewItemData<TreeListItemData>> roots)
|
|
{
|
|
foreach (var r in roots)
|
|
ExpandRecursive(r);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드와 그 자식들을 재귀적으로 펼칩니다.
|
|
/// </summary>
|
|
/// <param name="wrapper">펼칠 TreeViewItemData</param>
|
|
private void ExpandRecursive(TreeViewItemData<TreeListItemData> wrapper)
|
|
{
|
|
_treeView.ExpandItem(wrapper.id);
|
|
if (wrapper.children != null)
|
|
{
|
|
foreach (var c in wrapper.children)
|
|
ExpandRecursive(c);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
/// <summary>
|
|
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
// 검색 필드 이벤트 해제
|
|
if (_searchField != null)
|
|
{
|
|
_searchField.UnregisterValueChangedCallback(OnSearchValueChanged);
|
|
}
|
|
|
|
// TreeView 이벤트 핸들러 해제
|
|
if (_treeView != null)
|
|
{
|
|
_treeView.selectionChanged -= OnTreeViewSelectionChanged;
|
|
_treeView.bindItem = null;
|
|
_treeView.makeItem = null;
|
|
}
|
|
|
|
// 외부 이벤트 구독자 정리
|
|
OnVisibilityChanged = null;
|
|
OnSelectionChanged = null;
|
|
OnClosed = null;
|
|
|
|
// 데이터 정리
|
|
_originalRoots.Clear();
|
|
_rootData?.Clear();
|
|
_rootData = null;
|
|
_previouslySelectedItem = null;
|
|
|
|
// ID 시드 초기화
|
|
_idSeed = 1;
|
|
|
|
// UI 참조 정리
|
|
_searchField = null;
|
|
_treeView = null;
|
|
_closeButton = null;
|
|
_clearButton = null;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|