369 lines
13 KiB
C#
369 lines
13 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace SHI.Modal
|
|
{
|
|
[UxmlElement]
|
|
public partial class TreeList : VisualElement
|
|
{
|
|
// 리소스 경로 상수
|
|
private const string UXML_PATH = "SHI/Modal/TreeList"; // Resources 폴더 기준 경로
|
|
private const string ITEM_UXML_PATH = "SHI/Modal/TreeListItem"; // Resources 폴더 기준 경로
|
|
|
|
|
|
// 내부 컴포넌트 참조
|
|
private TextField _searchField;
|
|
private TreeView _treeView;
|
|
|
|
private Button _closeButton;
|
|
private Button _clearButton;
|
|
|
|
// 아이템용 UXML 에셋을 미리 로드해두는 것이 성능상 좋음
|
|
private VisualTreeAsset _itemTemplate;
|
|
|
|
// 원본 루트 데이터(필터 복원용)
|
|
private List<TreeListItemData> _originalRoots = new();
|
|
|
|
// 데이터 소스
|
|
private List<TreeViewItemData<TreeListItemData>> _rootData;
|
|
|
|
// 고유 ID 생성용 시드
|
|
private int _idSeed = 1;
|
|
|
|
// 이전 선택 상태 추적용
|
|
private TreeListItemData? _previouslySelectedItem;
|
|
|
|
|
|
// [1] 외부에 노출할 이벤트 정의
|
|
// Action<T>: T 데이터를 함께 전달합니다.
|
|
public event Action<TreeListItemData> OnVisibilityChanged; // 눈 아이콘 클릭 시
|
|
public event Action<TreeListItemData> OnSelectionChanged; // 리스트 아이템 선택 시
|
|
public event Action OnClosed; // hide 시켰을 때
|
|
|
|
public TreeList()
|
|
{
|
|
// 1. UXML 로드 및 복제 (Instantiate)
|
|
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
if (visualTree == null)
|
|
{
|
|
Debug.LogError($"[TreeMenu] UXML not found at: {UXML_PATH}");
|
|
return;
|
|
}
|
|
|
|
// CloneTree(this)를 하면 UXML의 내용이 이 클래스(TreeMenu)의 자식으로 들어옴
|
|
visualTree.CloneTree(this);
|
|
|
|
_itemTemplate = Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
|
|
if (_itemTemplate == null)
|
|
{
|
|
Debug.LogError($"[TreeMenu] Item UXML not found at: {ITEM_UXML_PATH}");
|
|
return;
|
|
}
|
|
|
|
// 2. 요소 찾기 (Query) - 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();
|
|
|
|
}
|
|
|
|
private void InitializeLogic()
|
|
{
|
|
// 검색창 이벤트 연결
|
|
if (_searchField != null)
|
|
{
|
|
_searchField.RegisterValueChangedCallback(evt => OnSearch(evt.newValue));
|
|
}
|
|
|
|
// 트리뷰 설정
|
|
if (_treeView != null)
|
|
{
|
|
_treeView.makeItem = MakeTreeItem;
|
|
_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);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
this.style.display = DisplayStyle.Flex;
|
|
}
|
|
|
|
// 내부적인 선택 변경 핸들러 -> 외부 이벤트 호출
|
|
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;
|
|
}
|
|
|
|
|
|
// 외부에서 데이터를 주입하는 메서드
|
|
public void SetData(List<TreeListItemData> roots)
|
|
{
|
|
|
|
_originalRoots = roots ?? new List<TreeListItemData>();
|
|
|
|
_idSeed = 1;
|
|
|
|
var visited = new HashSet<TreeListItemData>();
|
|
|
|
_rootData = ConvertToTreeViewData(_originalRoots, visited, 0);
|
|
|
|
|
|
// TreeView에 루트 데이터 설정 (SetRootItems 사용)
|
|
_treeView.SetRootItems<TreeListItemData>(_rootData);
|
|
|
|
// 트리 뷰가 자식을 찾는 방법 정의
|
|
_treeView.Rebuild();
|
|
}
|
|
|
|
// 사이클 방지 + 고유 ID 부여
|
|
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);
|
|
|
|
var treeItem = new TreeViewItemData<TreeListItemData>(item.id, item, childData);
|
|
list.Add(treeItem);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
// --- [핵심] 각 행(Row)의 UI 생성 (MakeItem) ---
|
|
private VisualElement MakeTreeItem()
|
|
{
|
|
// UXML을 복제하여 새 아이템 생성
|
|
// Instantiate()는 TemplateContainer를 반환함
|
|
var templateContainer = _itemTemplate.Instantiate();
|
|
|
|
return templateContainer;
|
|
}
|
|
|
|
|
|
// --- [핵심] 데이터와 UI 연결 (BindItem) ---
|
|
private void BindTreeItem(VisualElement element, int index)
|
|
{
|
|
|
|
// TreeView 내부 로직으로 아이템 데이터 가져오기
|
|
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은 스크롤 시 재사용되므로 이벤트를 매번 새로 연결하는 방식은 주의해야 함.
|
|
// 여기서는 간편한 구현을 위해 clicked 대리자를 교체합니다.
|
|
if (toggleBtn.userData is Action oldAction) toggleBtn.clicked -= oldAction;
|
|
System.Action clickAction = () =>
|
|
{
|
|
item.IsVisible = !item.IsVisible;
|
|
UpdateVisibilityIcon(toggleBtn, item.IsVisible);
|
|
|
|
// 자식들에게 동일 상태 전파
|
|
SetChildrenVisibility(item, item.IsVisible);
|
|
|
|
// 화면에 그려진 자식 아이콘도 갱신되도록 목록 새로고침
|
|
// (바인딩 시 UpdateVisibilityIcon이 호출되어 반영됨)
|
|
_treeView.RefreshItems(); // Unity 2022+에서 제공
|
|
// 필요한 경우 아래로 대체 가능:
|
|
// _treeView.Rebuild();
|
|
|
|
// 3. [핵심] 외부로 이벤트 발송 (데이터 전달)
|
|
OnVisibilityChanged?.Invoke(item);
|
|
};
|
|
toggleBtn.userData = clickAction;
|
|
toggleBtn.clicked += clickAction;
|
|
}
|
|
|
|
// 자식들 재귀적으로 IsVisible 동기화
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
// 검색 기능
|
|
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.SetRootItems<TreeListItemData>(filteredWrappers);
|
|
_treeView.Rebuild();
|
|
|
|
// 매치 결과 펼치기
|
|
ExpandAll(filteredWrappers);
|
|
|
|
}
|
|
|
|
// 루트들에 대해 필터 수행
|
|
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;
|
|
}
|
|
|
|
// 단일 노드 필터링 (매치 또는 자식 매치 시 포함)
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 필터된 결과 전체 펼치기 (필요 시 조건 변경 가능)
|
|
private void ExpandAll(List<TreeViewItemData<TreeListItemData>> roots)
|
|
{
|
|
foreach (var r in roots)
|
|
ExpandRecursive(r);
|
|
}
|
|
|
|
private void ExpandRecursive(TreeViewItemData<TreeListItemData> wrapper)
|
|
{
|
|
_treeView.ExpandItem(wrapper.id);
|
|
if (wrapper.children != null)
|
|
{
|
|
foreach (var c in wrapper.children)
|
|
ExpandRecursive(c);
|
|
}
|
|
}
|
|
|
|
public void SelectByItemId(int itemId)
|
|
{
|
|
//itemId에 해당하는 아이템을 찾아 선택 상태로 만듭니다.
|
|
_treeView.SetSelection(new List<int> { itemId });
|
|
}
|
|
}
|
|
|
|
}
|