#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
using static UVC.UIToolkit.UTKStyleGuide;
namespace UVC.UIToolkit
{
///
/// 계층적 트리 구조를 표시하는 커스텀 UI Toolkit 컴포넌트입니다.
///
/// 개요:
///
/// UTKComponentList는 Unity UI Toolkit의 TreeView를 래핑하여 검색, 가시성 토글,
/// 닫기 기능 등을 제공하는 재사용 가능한 컴포넌트입니다.
/// UXML 파일(UTKComponentList.uxml, UTKComponentListItem.uxml)과 함께 사용됩니다.
///
///
/// 주요 기능:
///
/// 계층적 트리 구조 표시 (펼치기/접기 지원)
/// 실시간 검색 필터링
/// 항목별 가시성(눈 아이콘) 토글
/// 선택 이벤트 처리
///
///
/// UXML 사용 예시:
///
///
///
///
/// ]]>
///
/// C# 사용 예시:
/// ("component-list");
///
/// // 2. 데이터 구성 - 카테고리(그룹)와 일반 아이템
/// var data = new List
/// {
/// // 카테고리(그룹) 생성
/// new UTKComponentListCategoryData
/// {
/// name = "모델 그룹 A",
/// isExpanded = true,
/// children = new List
/// {
/// // 일반 아이템
/// new UTKComponentListItemData
/// {
/// name = "의자 모델",
/// ExternalKey = "chair_001",
/// IsVisible = true
/// },
/// new UTKComponentListItemData
/// {
/// name = "책상 모델",
/// ExternalKey = "desk_001",
/// IsVisible = true
/// }
/// }
/// }
/// };
///
/// // 3. 데이터 설정
/// componentList.SetData(data);
///
/// // 4. 선택 이벤트 구독 - 아이템 선택 시 호출
/// componentList.OnItemSelected += (selectedItems) =>
/// {
/// foreach (var item in selectedItems)
/// {
/// Debug.Log($"선택됨: {item.name}");
/// }
/// };
///
/// // 5. 선택 해제 이벤트 구독
/// componentList.OnItemDeselected += (deselectedItems) =>
/// {
/// foreach (var item in deselectedItems)
/// {
/// Debug.Log($"선택 해제: {item.name}");
/// }
/// };
///
/// // 6. 가시성 변경 이벤트 구독 - 눈 아이콘 클릭 시 호출
/// componentList.OnItemVisibilityChanged += (item, isVisible) =>
/// {
/// // 3D 모델의 GameObject 활성화/비활성화
/// var gameObject = FindGameObjectByKey(item.ExternalKey);
/// if (gameObject != null)
/// {
/// gameObject.SetActive(isVisible);
/// }
/// };
///
/// // 7. 삭제 이벤트 구독 (Delete/Backspace 키)
/// componentList.EnabledDeleteItem = true; // 삭제 기능 활성화 필수
/// componentList.OnItemDeleted += (item) =>
/// {
/// Debug.Log($"삭제 요청: {item.name}");
/// componentList.DeleteItem(item); // 리스트에서 제거
/// };
///
/// // 8. 더블클릭 이벤트 구독
/// componentList.OnItemDoubleClicked += (item) =>
/// {
/// Debug.Log($"더블클릭: {item.name}");
/// // 카메라 포커스 등의 동작 수행
/// };
///
/// // 9. 아이콘 클릭 이벤트 (setting-btn, search-btn)
/// componentList.OnItemIconClicked += (iconName, item) =>
/// {
/// if (iconName == "setting-btn")
/// {
/// ShowCategorySettings(item);
/// }
/// };
///
/// // 10. 검색 실행
/// componentList.ApplySearch("의자");
///
/// // 11. 프로그래밍 방식 선택
/// componentList.SelectItem("의자 모델", notify: true);
/// componentList.DeselectItem("의자 모델", notify: false);
/// componentList.ClearSelection();
///
/// // 12. 아이템 추가/삭제
/// var newItem = new UTKComponentListItemData { name = "새 아이템" };
/// componentList.AddItem(newItem); // 루트에 추가
/// componentList.AddItem(categoryData, newItem); // 특정 카테고리에 추가
/// componentList.DeleteItem(newItem); // 삭제
///
/// // 13. 리소스 해제 (OnDestroy에서 호출)
/// componentList.Dispose();
/// ]]>
///
/// 관련 리소스:
///
/// Resources/UIToolkit/List/UTKComponentList.uxml - 메인 레이아웃
/// Resources/UIToolkit/List/UTKComponentListItem.uxml - 개별 항목 템플릿
/// Resources/UIToolkit/List/UTKComponentListGroupItem.uxml - 그룹 항목 템플릿
///
///
/// 선택 해제 방지:
///
/// 빈 영역 클릭 시 선택이 해제되지 않도록 하려면 EventSystem의 InputSystemUIInputModule 컴포넌트에서
/// Deselect On Background Click 옵션을 해제해야 합니다.
///
///
[UxmlElement]
public partial class UTKComponentList : VisualElement, IDisposable
{
#region IDisposable
private bool _disposed = false;
#endregion
#region 상수 (Constants)
/// 메인 UXML 파일 경로 (Resources 폴더 기준)
private const string UXML_PATH = "UIToolkit/List/UTKComponentList";
private const string USS_PATH = "UIToolkit/List/UTKComponentListUss";
/// 일반 항목 UXML 파일 경로 (Resources 폴더 기준)
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKComponentListItem";
/// 그룹 항목 UXML 파일 경로 (Resources 폴더 기준)
private const string GROUP_ITEM_UXML_PATH = "UIToolkit/List/UTKComponentListGroupItem";
#endregion
#region 캐싱된 리소스 (Cached Resources)
/// 일반 항목 UXML 템플릿
private VisualTreeAsset? _itemTemplate;
/// 그룹 항목 UXML 템플릿
private VisualTreeAsset? _groupItemTemplate;
#endregion
#region UI 컴포넌트 참조 (UI Component References)
/// 검색어 입력 필드
private TextField? _searchField;
/// Unity UI Toolkit의 TreeView 컴포넌트
private TreeView? _treeView;
/// 검색어 지우기 버튼 (UTKButton)
private UTKButton? _clearButton;
#endregion
#region 내부 데이터 (Internal Data)
///
/// 원본 루트 데이터입니다.
/// 검색 필터 해제 시 원래 데이터로 복원하는 데 사용됩니다.
///
private List _originalRoots = new();
///
/// TreeView에 바인딩되는 데이터 소스입니다.
/// TreeViewItemData는 Unity의 TreeView가 요구하는 래퍼 타입입니다.
///
private List>? _rootData;
///
/// 항목 ID 자동 생성을 위한 시드 값입니다.
/// SetData() 호출 시 id가 0인 항목에 순차적으로 ID를 할당합니다.
///
private int _idSeed = 1;
///
/// 이전에 선택된 항목들입니다.
/// 선택 해제 이벤트 발송에 사용됩니다.
///
private List _previouslySelectedItems = new();
///
/// 선택 이벤트 발송을 일시적으로 억제하는 플래그입니다.
/// 프로그래밍 방식으로 선택 시 이벤트를 발송하지 않으려면 true로 설정합니다.
///
private bool _suppressSelectionEvent = false;
///
/// 펼침/접힘 이벤트 처리를 일시적으로 억제하는 플래그입니다.
/// ExpandByData() 실행 중 이벤트로 인한 데이터 덮어쓰기를 방지합니다.
///
private bool _suppressExpandEvent = false;
#endregion
#region 공개 속성 (Public Properties)
///
/// 항목 삭제 기능 활성화 여부입니다.
/// true일 때만 Delete/Backspace 키로 항목 삭제 이벤트가 발생합니다.
/// 기본값은 false입니다.
///
public bool EnabledDeleteItem { get; set; } = false;
///
/// 현재 검색어를 가져오거나 설정합니다.
/// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다.
///
public string SearchQuery
{
get => _searchField?.value ?? string.Empty;
set { if (_searchField != null) _searchField.value = value; }
}
#endregion
#region 외부 이벤트 (Public Events)
///
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
///
public Action>? OnItemSelected;
///
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
///
public Action>? OnItemDeselected;
///
/// 항목의 가시성(눈 아이콘)이 변경될 때 발생합니다.
/// 3D 모델의 GameObject 활성화/비활성화에 연동합니다.
///
public event Action? OnItemVisibilityChanged;
///
/// 메인/검색 리스트에서 항목이 삭제될 때 발생합니다 (Delete 키).
///
public Action? OnItemDeleted;
///
/// 메인/검색 리스트에서 항목이 더블클릭될 때 발생합니다.
///
public Action? OnItemDoubleClicked;
///
/// 아이콘을 클릭할 때 발생합니다.
///
public Action? OnItemIconClicked;
#endregion
#region 생성자 (Constructor)
///
/// UTKComponentList 컴포넌트를 초기화합니다.
/// UXML 템플릿을 로드하고 내부 컴포넌트를 설정합니다.
///
public UTKComponentList()
{
// 1. 메인 UXML 로드 및 복제
// CloneTree(this)로 UXML 내용이 이 클래스의 자식으로 추가됨
var visualTree = Resources.Load(UXML_PATH);
if (visualTree == null)
{
Debug.LogError($"[TreeMenu] UXML not found at: {UXML_PATH}");
return;
}
visualTree!.CloneTree(this);
// 2. 항목 템플릿 로드
_itemTemplate = Resources.Load(ITEM_UXML_PATH);
_groupItemTemplate = Resources.Load(GROUP_ITEM_UXML_PATH);
if (_itemTemplate == null)
Debug.LogError($"[UTKComponentList] Item UXML not found at: {ITEM_UXML_PATH}");
if (_groupItemTemplate == null)
Debug.LogError($"[UTKComponentList] Group Item UXML not found at: {GROUP_ITEM_UXML_PATH}");
// 테마 적용 및 변경 구독
UTKThemeManager.Instance.ApplyThemeToElement(this);
SubscribeToThemeChanges();
// USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨)
var uss = Resources.Load(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
// 3. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
_searchField = this.Q("search-field");
_treeView = this.Q("main-tree-view");
_clearButton = this.Q("clear-btn");
// 4. Clear 버튼 아이콘 설정
if (_clearButton != null)
{
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
}
// 5. 이벤트 연결 및 로직 초기화
InitializeLogic();
}
#endregion
#region 초기화 (Initialization)
///
/// 내부 로직과 이벤트 핸들러를 초기화합니다.
///
private void InitializeLogic()
{
// 검색창 이벤트: Enter 키를 눌렀을 때 또는 포커스를 잃었을 때 필터링 실행
if (_searchField != null)
{
_searchField.RegisterCallback(OnSearchFieldKeyDown);
_searchField.RegisterCallback(OnSearchFieldFocusOut);
}
// TreeView 설정
// makeItem: 새 항목 UI 생성 시 호출
// bindItem: 데이터를 UI에 바인딩할 때 호출
// selectionChanged: 선택 변경 시 호출
if (_treeView != null)
{
_treeView.makeItem = MakeTreeItem;
_treeView.bindItem = BindTreeItem;
_treeView.selectionChanged += OnTreeViewSelectionChanged;
_treeView.itemsChosen += OnTreeViewItemsChosen;
_treeView.RegisterCallback(OnTreeViewKeyDown);
// 펼침/접힘 이벤트 처리
_treeView.itemExpandedChanged += OnTreeViewItemExpandedChanged;
}
// 검색어 지우기 버튼
if(_clearButton != null)
{
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
_clearButton.OnClicked += OnClearButtonClicked;
}
// 스크롤바 hover/active 색상 설정
SetupScrollerDraggerColors();
}
///
/// 스크롤바 드래거의 hover/active 색상을 설정합니다.
/// USS로는 드래거에 직접 hover/active 상태를 적용할 수 없어 코드로 구현합니다.
///
private void SetupScrollerDraggerColors()
{
if (_treeView == null) return;
// TreeView가 렌더링된 후에 스크롤러 설정
_treeView.RegisterCallback(OnTreeViewGeometryChanged);
}
// USS 변수 정의 (customStyle에서 읽기 위한 키)
private static readonly CustomStyleProperty s_DraggerNormalColor = new("--scroller-dragger-normal");
private static readonly CustomStyleProperty s_DraggerHoverColor = new("--scroller-dragger-hover");
private static readonly CustomStyleProperty s_DraggerActiveColor = new("--scroller-dragger-active");
private static readonly CustomStyleProperty s_TrackerNormalColor = new("--scroller-tracker-normal");
private static readonly CustomStyleProperty s_TrackerHoverColor = new("--scroller-tracker-hover");
///
/// TreeView 렌더링 완료 후 스크롤러 색상을 설정합니다.
///
private void OnTreeViewGeometryChanged(GeometryChangedEvent evt)
{
if (_treeView == null) return;
// 한 번만 실행
_treeView.UnregisterCallback(OnTreeViewGeometryChanged);
// USS 변수에서 색상 읽기 (실패 시 UTKStyleGuide 기본값 사용)
Color normalColor = ScrollbarDraggerNormal;
Color hoverColor = ScrollbarDraggerHover;
Color activeColor = ScrollbarDraggerActive;
Color trackerNormalColor = ScrollbarTrackerNormal;
Color trackerHoverColor = ScrollbarTrackerHover;
if (_treeView.customStyle.TryGetValue(s_DraggerNormalColor, out Color n)) normalColor = n;
if (_treeView.customStyle.TryGetValue(s_DraggerHoverColor, out Color h)) hoverColor = h;
if (_treeView.customStyle.TryGetValue(s_DraggerActiveColor, out Color a)) activeColor = a;
if (_treeView.customStyle.TryGetValue(s_TrackerNormalColor, out Color tn)) trackerNormalColor = tn;
if (_treeView.customStyle.TryGetValue(s_TrackerHoverColor, out Color th)) trackerHoverColor = th;
// TreeView 내부의 ScrollView 찾기
var scrollView = _treeView.Q();
if (scrollView == null) return;
// 수직 스크롤바
var verticalScroller = scrollView.verticalScroller;
if (verticalScroller != null)
{
SetupDraggerEvents(verticalScroller, normalColor, hoverColor, activeColor, trackerNormalColor, trackerHoverColor);
}
// 수평 스크롤바
var horizontalScroller = scrollView.horizontalScroller;
if (horizontalScroller != null)
{
SetupDraggerEvents(horizontalScroller, normalColor, hoverColor, activeColor, trackerNormalColor, trackerHoverColor);
}
}
///
/// 개별 스크롤러에 드래거 색상 이벤트를 설정합니다.
///
private void SetupDraggerEvents(Scroller scroller, Color normalColor, Color hoverColor, Color activeColor, Color trackerNormalColor, Color trackerHoverColor)
{
var dragger = scroller.Q(className: "unity-base-slider__dragger");
var tracker = scroller.Q(className: "unity-base-slider__tracker");
if (dragger == null) return;
// 드래거에 직접 이벤트 등록
dragger.RegisterCallback(evt =>
{
dragger.style.backgroundColor = hoverColor;
});
dragger.RegisterCallback(evt =>
{
dragger.style.backgroundColor = normalColor;
});
dragger.RegisterCallback(evt =>
{
dragger.style.backgroundColor = activeColor;
});
dragger.RegisterCallback(evt =>
{
dragger.style.backgroundColor = hoverColor;
});
// 트래커(배경) 마우스 오버 시 색상 변경
if (tracker != null)
{
tracker.RegisterCallback(evt =>
{
tracker.style.backgroundColor = trackerHoverColor;
});
tracker.RegisterCallback(evt =>
{
tracker.style.backgroundColor = trackerNormalColor;
});
}
}
#endregion
#region 공개 메서드 (Public Methods)
///
/// 트리 리스트를 화면에 표시합니다.
///
public void Show()
{
this.style.display = DisplayStyle.Flex;
}
///
/// 현재 검색 필드의 값으로 검색을 실행합니다.
///
public void ApplySearch()
{
OnSearch(_searchField?.value ?? string.Empty);
}
///
/// 지정된 검색어로 검색을 실행합니다.
/// 검색 필드의 값도 함께 업데이트됩니다.
///
/// 검색어
public void ApplySearch(string query)
{
if (_searchField != null)
{
_searchField.value = query;
}
OnSearch(query);
}
///
/// 트리 데이터를 설정합니다.
/// UTKComponentListItemDataBase 리스트를 받아 TreeView에 바인딩합니다.
///
/// 루트 항목들의 리스트 (계층 구조의 최상위 항목들)
public void SetData(List roots)
{
// 원본 데이터 저장 (필터 복원용)
_originalRoots = roots ?? new List();
// ID 시드 초기화
_idSeed = 1;
// 순환 참조 감지를 위한 방문 집합
var visited = new HashSet();
// TreeView 형식으로 데이터 변환
_rootData = ConvertToTreeViewData(_originalRoots, visited, 0);
// TreeView에 데이터 설정
_treeView!.SetRootItems(_rootData);
// UI 갱신 및 데이터의 isExpanded 상태에 따라 펼치기
_treeView!.Rebuild();
ExpandByData(_originalRoots);
}
///
/// 데이터의 isExpanded 값에 따라 항목을 펼치거나 접습니다.
///
/// 처리할 항목 리스트
private void ExpandByData(List items)
{
if (_treeView == null || items == null) return;
// 이벤트 억제 시작 (재귀 호출 고려하여 최상위에서만 설정)
bool wasSupressed = _suppressExpandEvent;
_suppressExpandEvent = true;
try
{
ExpandByDataInternal(items);
}
finally
{
_suppressExpandEvent = wasSupressed;
}
}
///
/// ExpandByData의 내부 재귀 구현입니다.
///
private void ExpandByDataInternal(List items)
{
foreach (var item in items)
{
if (item.children != null && item.children.Count > 0)
{
if (item.isExpanded)
{
_treeView!.ExpandItem(item.id);
}
else
{
_treeView!.CollapseItem(item.id);
}
// 자식 항목들도 재귀적으로 처리
ExpandByDataInternal(item.children);
}
}
}
///
/// 지정된 ID의 항목을 프로그래밍 방식으로 선택합니다.
///
/// 선택할 항목의 ID
public void SelectByItemId(int itemId)
{
_treeView?.SetSelection(new List { itemId });
}
///
/// 지정된 이름 목록에 해당하는 항목만 표시하고 나머지는 숨깁니다.
///
/// 표시할 항목들의 이름 목록
/// 검색 깊이 (1=1뎁스 자식만, 2=2뎁스까지, 0이하=전체)
public void ShowItems(List items, int depth = 1)
{
if (_originalRoots == null || _originalRoots.Count == 0) return;
var visibleNames = new HashSet(items ?? new List());
foreach (var root in _originalRoots)
{
SetVisibilityByNames(root, visibleNames, depth, 0);
}
// UI 갱신
_treeView?.RefreshItems();
}
///
/// 루트 레벨에 새 항목을 추가합니다.
/// ID가 0이면 자동으로 할당됩니다.
///
/// 추가할 항목 데이터
public void AddItem(UTKComponentListItemDataBase data)
{
if (data == null) return;
// ID가 없으면 자동 할당
if (data.id == 0) data.id = _idSeed++;
// 원본 데이터에 추가
_originalRoots.Add(data);
// _rootData가 null이면 초기화
_rootData ??= new List>();
// TreeViewItemData 래퍼 생성 및 추가
var childData = data.children != null && data.children.Count > 0
? ConvertToTreeViewData(data.children, new HashSet(), 1)
: null;
var treeItem = new TreeViewItemData(data.id, data, childData);
_rootData.Add(treeItem);
// TreeView 갱신
_treeView?.SetRootItems(_rootData);
_treeView?.Rebuild();
ExpandByData(_originalRoots);
}
///
/// 지정된 부모 카테고리의 자식으로 새 항목을 추가합니다.
///
/// 부모 카테고리 (null이면 루트에 추가)
/// 추가할 항목 데이터
public void AddItem(UTKComponentListCategoryData? parent, UTKComponentListItemDataBase data)
{
if (data == null) return;
// 부모가 없으면 루트에 추가
if (parent == null)
{
AddItem(data);
return;
}
// ID가 없으면 자동 할당
if (data.id == 0) data.id = _idSeed++;
// 부모의 children 리스트에 추가
parent.Add(data);
// TreeView 데이터 재구성 및 갱신
RefreshTreeViewData();
}
///
/// 트리에서 항목을 완전히 삭제합니다 (데이터 + 뷰 + 선택 상태).
/// 자식 항목이 있는 경우 함께 삭제됩니다.
///
/// 삭제할 항목 데이터
public void DeleteItem(UTKComponentListItemDataBase data)
{
if (data == null) return;
bool removed = false;
// 루트 레벨에서 삭제 시도
if (_originalRoots.Remove(data))
{
removed = true;
}
else
{
// 루트에 없으면 자식에서 재귀적으로 검색하여 삭제
foreach (var root in _originalRoots)
{
if (RemoveFromChildren(root, data))
{
removed = true;
break;
}
}
}
if (removed)
{
// 선택 상태에서 삭제된 항목 및 자식들 제거
RemoveFromSelection(data);
// TreeView 데이터 재구성 및 갱신
RefreshTreeViewData();
}
}
///
/// 항목의 이름을 변경하고 UI를 갱신합니다.
///
/// 변경할 항목 데이터
/// 새 이름
public void SetItemName(UTKComponentListItemDataBase data, string newName)
{
if (data == null || string.IsNullOrEmpty(newName)) return;
data.name = newName;
// UI 갱신 (해당 항목의 레이블 업데이트)
_treeView?.RefreshItems();
}
///
/// 이름으로 항목을 찾아 선택합니다.
/// 검색 모드 중이면 검색을 취소하고 원본 데이터에서 선택합니다.
/// SelectionType에 따라 단일 선택이면 기존 선택을 대체하고, 다중 선택이면 기존 선택에 추가합니다.
///
/// 선택할 항목의 이름
/// 선택 이벤트 발송 여부 (false면 OnItemSelected/OnItemDeselected 이벤트 억제)
public void SelectItem(string name, bool notify = true)
{
if (string.IsNullOrEmpty(name) || _treeView == null) return;
// SelectionType.None인 경우 선택 불가
if (_treeView.selectionType == SelectionType.None) return;
// 검색 중이면 검색 취소
if (_searchField != null && !string.IsNullOrEmpty(_searchField.value))
{
_searchField.value = string.Empty;
OnSearch(string.Empty);
}
// 이름으로 항목 찾기
var item = FindItemByName(name);
if (item == null) return;
// 이미 선택되어 있으면 무시
if (_previouslySelectedItems.Contains(item)) return;
List newSelection;
// SelectionType에 따라 선택 방식 결정
if (_treeView.selectionType == SelectionType.Multiple)
{
// 다중 선택: 기존 선택에 추가
newSelection = _previouslySelectedItems.Select(i => i.id).ToList();
newSelection.Add(item.id);
}
else
{
// 단일 선택: 기존 선택 대체
newSelection = new List { item.id };
}
// 이벤트 억제 플래그 설정
_suppressSelectionEvent = !notify;
try
{
_treeView.SetSelection(newSelection);
}
finally
{
_suppressSelectionEvent = false;
}
}
///
/// 이름으로 항목을 찾아 선택을 해제합니다.
/// 단일 선택 모드에서는 모든 선택이 해제되고, 다중 선택 모드에서는 해당 항목만 제외됩니다.
///
/// 선택 해제할 항목의 이름
/// 선택 해제 이벤트 발송 여부 (false면 OnItemDeselected 이벤트 억제)
public void DeselectItem(string name, bool notify = true)
{
if (string.IsNullOrEmpty(name) || _treeView == null) return;
// SelectionType.None인 경우 선택 해제 불필요
if (_treeView.selectionType == SelectionType.None) return;
var item = FindItemByName(name);
if (item == null) return;
// 현재 선택되어 있지 않으면 무시
if (!_previouslySelectedItems.Contains(item)) return;
List newSelection;
// SelectionType에 따라 선택 해제 방식 결정
if (_treeView.selectionType == SelectionType.Multiple)
{
// 다중 선택: 해당 항목만 제외
newSelection = _previouslySelectedItems
.Where(i => i.id != item.id)
.Select(i => i.id)
.ToList();
}
else
{
// 단일 선택: 전체 선택 해제
newSelection = new List();
}
// 이벤트 억제 플래그 설정
_suppressSelectionEvent = !notify;
try
{
_treeView.SetSelection(newSelection);
}
finally
{
_suppressSelectionEvent = false;
}
}
///
/// 모든 항목의 선택을 해제합니다.
///
public void ClearSelection()
{
_treeView?.ClearSelection();
}
///
/// 원본 데이터를 기반으로 TreeView 데이터를 재구성하고 UI를 갱신합니다.
///
private void RefreshTreeViewData()
{
var visited = new HashSet();
_rootData = ConvertToTreeViewData(_originalRoots, visited, 0);
_treeView?.SetRootItems(_rootData);
_treeView?.Rebuild();
ExpandByData(_originalRoots);
}
///
/// 부모 노드의 자식들에서 대상 항목을 재귀적으로 찾아 삭제합니다.
///
/// 부모 노드
/// 삭제할 대상 항목
/// 삭제 성공 여부
private bool RemoveFromChildren(UTKComponentListItemDataBase parent, UTKComponentListItemDataBase target)
{
if (parent.children == null || parent.children.Count == 0) return false;
// 카테고리인 경우에만 자식에서 삭제 가능
if (parent is UTKComponentListCategoryData category)
{
// 직접 자식에서 삭제 시도
if (category.Remove(target))
{
return true;
}
}
// 하위 자식들에서 재귀적으로 검색
foreach (var child in parent.children)
{
if (RemoveFromChildren(child, target))
{
return true;
}
}
return false;
}
///
/// 이름으로 항목을 재귀적으로 검색합니다.
///
/// 찾을 항목의 이름
/// 찾은 항목 또는 null
private UTKComponentListItemDataBase? FindItemByName(string name)
{
foreach (var root in _originalRoots)
{
var found = FindItemByNameRecursive(root, name);
if (found != null) return found;
}
return null;
}
///
/// 삭제된 항목과 그 자식들을 선택 상태에서 제거합니다.
/// _previouslySelectedItems와 TreeView의 선택 상태 모두에서 제거합니다.
///
/// 삭제된 항목
private void RemoveFromSelection(UTKComponentListItemDataBase data)
{
// 삭제할 항목과 모든 자식들의 ID 수집
var idsToRemove = new HashSet();
CollectItemIds(data, idsToRemove);
// _previouslySelectedItems에서 제거
_previouslySelectedItems.RemoveAll(item => idsToRemove.Contains(item.id));
// TreeView 선택 상태에서 제거
if (_treeView != null)
{
var currentSelection = _treeView.selectedIndices.ToList();
var newSelection = new List();
foreach (var index in currentSelection)
{
var selectedItem = _treeView.GetItemDataForIndex(index);
if (selectedItem != null && !idsToRemove.Contains(selectedItem.id))
{
newSelection.Add(selectedItem.id);
}
}
if (currentSelection.Count != newSelection.Count)
{
_treeView.SetSelection(newSelection);
}
}
}
///
/// 항목과 모든 자식들의 ID를 재귀적으로 수집합니다.
///
/// 수집 시작 노드
/// ID를 저장할 HashSet
private void CollectItemIds(UTKComponentListItemDataBase node, HashSet ids)
{
ids.Add(node.id);
if (node.children != null)
{
foreach (var child in node.children)
{
CollectItemIds(child, ids);
}
}
}
///
/// 노드와 그 자식들에서 이름으로 항목을 재귀적으로 검색합니다.
///
/// 검색 시작 노드
/// 찾을 항목의 이름
/// 찾은 항목 또는 null
private UTKComponentListItemDataBase? FindItemByNameRecursive(UTKComponentListItemDataBase node, string name)
{
if (node.name == name) return node;
if (node.children != null)
{
foreach (var child in node.children)
{
var found = FindItemByNameRecursive(child, name);
if (found != null) return found;
}
}
return null;
}
///
/// 재귀적으로 항목의 가시성을 이름 목록에 따라 설정합니다.
///
/// 현재 노드
/// 표시할 이름 목록
/// 최대 검색 깊이 (0이하=무제한)
/// 현재 깊이
private void SetVisibilityByNames(UTKComponentListItemDataBase node, HashSet 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);
// 가시성 변경 이벤트 발송
OnItemVisibilityChanged?.Invoke(node, node.IsVisible);
}
// 자식들도 재귀적으로 처리
if (node.children != null)
{
foreach (var child in node.children)
{
SetVisibilityByNames(child, visibleNames, maxDepth, currentDepth + 1);
}
}
}
#endregion
#region 선택 처리 (Selection Handling)
///
/// TreeView의 선택 변경 이벤트를 처리합니다.
/// SelectionType에 따라 TreeView가 자동으로 선택 항목 수를 제한합니다:
/// - None: 선택 불가 (selectedItems 항상 비어있음)
/// - Single: 최대 1개 선택
/// - Multiple: 여러 개 선택 가능
///
/// 선택된 항목들
private void OnTreeViewSelectionChanged(IEnumerable