394 lines
14 KiB
C#
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
|
|
}
|
|
}
|