Files
XRLib/Assets/Scripts/UVC/UIToolkit/List/UTKReordableTabList.cs
2026-02-26 09:56:34 +09:00

394 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 탭 기반 재정렬 가능 리스트.
/// UTKTabView와 UTKReordableList를 조합하여 탭별로 독립적인 재정렬 리스트를 제공합니다.
/// </summary>
/// <remarks>
/// <para><b>구조:</b></para>
/// <code>
/// UTKReordableTabList
/// └─ UTKTabView
/// ├─ UTKTab "탭1" → UTKReordableList (인스턴스 1)
/// ├─ UTKTab "탭2" → UTKReordableList (인스턴스 2)
/// └─ UTKTab "탭N" → UTKReordableList (인스턴스 N)
/// </code>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// var tabList = new UTKReordableTabList();
/// tabList.SetData(new Dictionary<string, List<ReordableListItemData>>
/// {
/// { "일반", generalItems },
/// { "고급", advancedItems }
/// });
///
/// // 전체 데이터 가져오기
/// var allData = tabList.GetData();
///
/// // 특정 탭 데이터 가져오기
/// var generalData = tabList.GetData("일반");
/// var firstTabData = tabList.GetData(0);
///
/// // 이벤트 구독
/// tabList.OnOrderChanged += (tabName, tabIndex) =>
/// {
/// Debug.Log($"[{tabName}] (탭 {tabIndex}) 순서 변경됨");
/// };
/// tabList.OnDataChanged += (tabName, tabIndex) =>
/// {
/// Debug.Log($"[{tabName}] (탭 {tabIndex}) 데이터 변경됨");
/// };
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKReordableTabList />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKReordableTabList : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/List/UTKReordableTabListUss";
#endregion
#region Fields
private bool _disposed;
private UTKTabView? _tabView;
/// <summary>탭 이름 → UTKReordableList 매핑</summary>
private readonly Dictionary<string, UTKReordableList> _tabLists = new();
/// <summary>탭 이름 순서 보존용</summary>
private readonly List<string> _tabNames = new();
/// <summary>리스트별 이벤트 콜백 참조 (해제용)</summary>
private readonly Dictionary<string, TabListCallbackInfo> _callbackInfos = new();
#endregion
#region Events
/// <summary>순서 변경 시 발생 (탭 이름, 탭 인덱스)</summary>
public event Action<string, int>? OnOrderChanged;
/// <summary>데이터(체크/텍스트) 변경 시 발생 (탭 이름, 탭 인덱스)</summary>
public event Action<string, int>? OnDataChanged;
#endregion
#region Constructor
public UTKReordableTabList() : base()
{
// 1. 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 2. USS 로드
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
// 3. UI 생성
CreateUI();
// 4. 테마 변경 구독
SubscribeToThemeChanges();
}
#endregion
#region Setup
private void CreateUI()
{
AddToClassList("reordable-tab-list");
_tabView = new UTKTabView();
_tabView.AddToClassList("reordable-tab-list__tabview");
_tabView.Align = TabAlign.Top;
Add(_tabView);
// 외부 UTKTabView의 align-left 등 descendant selector가
// 내부 TabView에 침투하는 것을 인라인 스타일로 차단
_tabView.schedule.Execute(() =>
{
var headerContainer = _tabView.Q(className: "unity-tab-view__header-container");
if (headerContainer != null)
{
headerContainer.style.flexDirection = FlexDirection.Row;
headerContainer.style.borderRightWidth = 0;
headerContainer.style.borderTopWidth = 0;
headerContainer.style.borderLeftWidth = 0;
}
// underline도 top-align 방식(하단 가로선)으로 강제
_tabView.Query(className: "unity-tab__header-underline").ForEach(underline =>
{
underline.style.left = 0;
underline.style.right = 0;
underline.style.bottom = 0;
underline.style.top = StyleKeyword.Auto;
underline.style.width = StyleKeyword.Auto;
underline.style.height = 2;
});
// tab header margin도 top-align 방식으로 강제
_tabView.Query(className: "unity-tab__header").ForEach(header =>
{
header.style.marginRight = new StyleLength(StyleKeyword.Null);
header.style.marginBottom = 0;
});
});
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
}
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Public API - SetData
/// <summary>
/// 탭별 데이터를 설정합니다. 기존 구성을 모두 제거하고 재생성합니다.
/// </summary>
/// <param name="data">탭 이름 → 아이템 데이터 목록 매핑.</param>
public void SetData(Dictionary<string, List<ReordableListItemData>> data)
{
// 기존 구성 제거
ClearAll();
if (data == null || _tabView == null) return;
foreach (var kvp in data)
{
var tabName = kvp.Key;
var items = kvp.Value ?? new List<ReordableListItemData>();
_tabNames.Add(tabName);
// UTKReordableList 생성
var reordableList = new UTKReordableList();
reordableList.AddToClassList("reordable-tab-list__list");
reordableList.SetData(items);
// 이벤트 연결 (콜백 참조 보관하여 해제 가능하게)
var capturedName = tabName;
var capturedIndex = _tabNames.Count - 1;
Action onOrder = () => OnOrderChanged?.Invoke(capturedName, capturedIndex);
Action onData = () => OnDataChanged?.Invoke(capturedName, capturedIndex);
reordableList.OnOrderChanged += onOrder;
reordableList.OnDataChanged += onData;
_callbackInfos[tabName] = new TabListCallbackInfo(onOrder, onData);
_tabLists[tabName] = reordableList;
// 탭 추가
_tabView.AddUTKTab(tabName, reordableList);
}
}
/// <summary>
/// Dictionary 형식의 데이터를 변환하여 설정합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <param name="data">탭 이름 → Dictionary 목록 매핑.</param>
public void SetData(Dictionary<string, List<Dictionary<string, string>>> data)
{
if (data == null)
{
ClearAll();
return;
}
var converted = new Dictionary<string, List<ReordableListItemData>>(data.Count);
foreach (var kvp in data)
{
var items = new List<ReordableListItemData>();
if (kvp.Value != null)
{
for (int i = 0; i < kvp.Value.Count; i++)
{
var dict = kvp.Value[i];
var item = new ReordableListItemData();
item.Order = dict.TryGetValue("order", out var orderStr) && int.TryParse(orderStr, out var order)
? order
: i;
item.IsActive = dict.TryGetValue("active", out var activeStr) && bool.TryParse(activeStr, out var active)
? active
: true;
item.DisplayText = dict.TryGetValue("text", out var text)
? text ?? ""
: "";
items.Add(item);
}
}
converted[kvp.Key] = items;
}
SetData(converted);
}
#endregion
#region Public API - GetData
/// <summary>
/// 모든 탭의 데이터를 반환합니다.
/// </summary>
/// <returns>탭 이름 → 아이템 데이터 목록 매핑.</returns>
public Dictionary<string, List<ReordableListItemData>> GetData()
{
var result = new Dictionary<string, List<ReordableListItemData>>(_tabLists.Count);
foreach (var tabName in _tabNames)
{
if (_tabLists.TryGetValue(tabName, out var list))
{
result[tabName] = list.GetData();
}
}
return result;
}
/// <summary>
/// 특정 탭의 데이터를 이름으로 반환합니다.
/// </summary>
/// <param name="tabName">탭 이름.</param>
/// <returns>아이템 데이터 목록 또는 null.</returns>
public List<ReordableListItemData>? GetData(string tabName)
{
return _tabLists.TryGetValue(tabName, out var list) ? list.GetData() : null;
}
/// <summary>
/// 특정 탭의 데이터를 인덱스로 반환합니다.
/// </summary>
/// <param name="tabIndex">탭 인덱스.</param>
/// <returns>아이템 데이터 목록 또는 null.</returns>
public List<ReordableListItemData>? GetData(int tabIndex)
{
if (tabIndex < 0 || tabIndex >= _tabNames.Count) return null;
return GetData(_tabNames[tabIndex]);
}
/// <summary>
/// 모든 탭의 데이터를 Dictionary 형식으로 변환하여 반환합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <returns>탭 이름 → Dictionary 목록 매핑.</returns>
public Dictionary<string, List<Dictionary<string, string>>> ToDictionary()
{
var result = new Dictionary<string, List<Dictionary<string, string>>>(_tabLists.Count);
foreach (var tabName in _tabNames)
{
if (_tabLists.TryGetValue(tabName, out var list))
{
result[tabName] = list.ToDictionary();
}
}
return result;
}
#endregion
#region Internal
/// <summary>
/// 기존 탭과 리스트를 모두 제거합니다.
/// </summary>
private void ClearAll()
{
// 이벤트 콜백 해제 후 리스트 Dispose
foreach (var tabName in _tabNames)
{
if (_tabLists.TryGetValue(tabName, out var list))
{
if (_callbackInfos.TryGetValue(tabName, out var info))
{
list.OnOrderChanged -= info.OnOrderHandler;
list.OnDataChanged -= info.OnDataHandler;
}
list.Dispose();
}
}
_tabLists.Clear();
_tabNames.Clear();
_callbackInfos.Clear();
// 탭 제거
_tabView?.ClearTabs();
}
#endregion
#region Internal Types
/// <summary>리스트 이벤트 콜백 참조 추적</summary>
private class TabListCallbackInfo
{
public readonly Action OnOrderHandler;
public readonly Action OnDataHandler;
public TabListCallbackInfo(Action onOrderHandler, Action onDataHandler)
{
OnOrderHandler = onOrderHandler;
OnDataHandler = onDataHandler;
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
// 리스트 정리
ClearAll();
// TabView 정리
_tabView?.Dispose();
_tabView = null;
// 이벤트 정리
OnOrderChanged = null;
OnDataChanged = null;
}
#endregion
}
}