Files
XRLib/Assets/Scripts/UVC/UIToolkit/List/UTKReordableList.cs

384 lines
13 KiB
C#
Raw Normal View History

2026-02-19 20:08:57 +09:00
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 설정 표시 정보 아이템 데이터.
/// </summary>
public class ReordableListItemData
{
/// <summary>표시 순서 (ListView 인덱스 기준)</summary>
public int Order { get; set; }
/// <summary>사용 유무</summary>
public bool IsActive { get; set; }
/// <summary>표시 내용</summary>
public string DisplayText { get; set; } = "";
}
/// <summary>
/// 설정 표시 정보 탭 뷰.
/// UTKListView를 활용하여 마우스 드래그로 항목 순서를 변경할 수 있는 설정 목록 뷰입니다.
/// 각 항목은 드래그 핸들, 체크박스(사용 유무), 입력 필드(내용 수정)로 구성됩니다.
/// </summary>
[UxmlElement]
public partial class UTKReordableList : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/List/UTKReordableListUss";
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKReordableListItem";
private const string ITEM_USS_PATH = "UIToolkit/List/UTKReordableListItemUss";
private const float ITEM_HEIGHT = 36f;
#endregion
#region Fields
private bool _disposed;
private UTKListView? _listView;
private List<ReordableListItemData> _items = new();
private VisualTreeAsset? _itemTemplate;
private StyleSheet? _itemStyleSheet;
#endregion
#region Events
/// <summary>순서 변경 시 발생</summary>
public event Action? OnOrderChanged;
/// <summary>데이터(체크/텍스트) 변경 시 발생</summary>
public event Action? OnDataChanged;
#endregion
#region Constructor
public UTKReordableList() : 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-list");
SetupListView();
}
private void SetupListView()
{
_listView = new UTKListView();
_listView.makeItem = MakeItem;
_listView.bindItem = BindItem;
_listView.unbindItem = UnbindItem;
_listView.fixedItemHeight = ITEM_HEIGHT;
_listView.selectionType = SelectionType.Single;
_listView.reorderable = true;
_listView.reorderMode = ListViewReorderMode.Animated;
_listView.itemIndexChanged += OnItemIndexChanged;
Add(_listView);
}
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 ListView Callbacks
private VisualElement MakeItem()
{
// UXML 캐싱
_itemTemplate ??= Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
// USS 캐싱 (코드에서 로드 - UXML에서 지정하지 않음)
_itemStyleSheet ??= Resources.Load<StyleSheet>(ITEM_USS_PATH);
if (_itemTemplate != null)
{
var root = _itemTemplate.Instantiate();
// USS 적용 (코드에서 적용)
if (_itemStyleSheet != null)
root.styleSheets.Add(_itemStyleSheet);
// 드래그 핸들 아이콘 설정
var handle = root.Q<Label>("drag-handle");
if (handle != null)
handle.text = UTKMaterialIcons.DragIndicator;
return root;
}
return CreateItemFallback();
}
private VisualElement CreateItemFallback()
{
var container = new VisualElement();
container.name = "item-container";
container.AddToClassList("reordable-list-item");
container.style.flexDirection = FlexDirection.Row;
container.style.alignItems = Align.Center;
container.style.minHeight = ITEM_HEIGHT;
var handle = new Label(UTKMaterialIcons.DragIndicator);
handle.name = "drag-handle";
handle.AddToClassList("reordable-list-item__drag-handle");
container.Add(handle);
var checkbox = new UTKCheckBox();
checkbox.name = "active-checkbox";
checkbox.AddToClassList("reordable-list-item__checkbox");
container.Add(checkbox);
var inputField = new UTKInputField();
inputField.name = "display-text-field";
inputField.AddToClassList("reordable-list-item__input");
container.Add(inputField);
return container;
}
private void BindItem(VisualElement element, int index)
{
if (index < 0 || index >= _items.Count) return;
var data = _items[index];
// 요소 참조
var container = element.Q<VisualElement>("item-container");
var checkbox = container?.Q<UTKCheckBox>("active-checkbox");
var inputField = container?.Q<UTKInputField>("display-text-field");
if (checkbox == null || inputField == null) return;
// 이전 콜백 해제 (재바인딩 시 중복 방지)
CleanupItemCallbacks(element);
// 값 설정 (notify: false로 이벤트 발생 방지)
checkbox.SetChecked(data.IsActive, notify: false);
inputField.SetValue(data.DisplayText, notify: false);
// 새 콜백 등록
Action<bool> onChecked = (value) =>
{
data.IsActive = value;
OnDataChanged?.Invoke();
};
Action<string> onText = (value) =>
{
data.DisplayText = value;
OnDataChanged?.Invoke();
};
checkbox.OnValueChanged += onChecked;
inputField.OnValueChanged += onText;
// 콜백 정보 저장 (해제용)
element.userData = new ItemCallbackInfo(checkbox, inputField, onChecked, onText);
}
private void UnbindItem(VisualElement element, int index)
{
CleanupItemCallbacks(element);
}
private void CleanupItemCallbacks(VisualElement element)
{
if (element.userData is ItemCallbackInfo info)
{
info.Checkbox.OnValueChanged -= info.OnCheckedHandler;
info.InputField.OnValueChanged -= info.OnTextHandler;
element.userData = null;
}
}
private void OnItemIndexChanged(int oldIndex, int newIndex)
{
// ListView가 내부적으로 itemsSource 순서를 변경함
// Order 값만 현재 인덱스에 맞게 재계산
SyncOrderValues();
OnOrderChanged?.Invoke();
}
private void SyncOrderValues()
{
for (int i = 0; i < _items.Count; i++)
{
_items[i].Order = i;
}
}
#endregion
#region Public API
/// <summary>
/// 데이터를 설정하고 ListView를 갱신합니다.
/// </summary>
/// <param name="items">표시할 아이템 데이터 목록.</param>
public void SetData(List<ReordableListItemData> items)
{
_items = items ?? new List<ReordableListItemData>();
SyncOrderValues();
if (_listView != null)
{
_listView.itemsSource = _items;
_listView.RefreshItems();
}
}
/// <summary>
/// List&lt;Dictionary&gt;로부터 데이터를 변환하여 설정합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <param name="listDict">변환할 Dictionary 목록.</param>
public void SetData(List<Dictionary<string, string>> listDict)
{
var items = new List<ReordableListItemData>();
if (listDict != null)
{
for (int i = 0; i < listDict.Count; i++)
{
var dict = listDict[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);
}
}
SetData(items);
}
/// <summary>
/// Order 값이 동기화된 데이터 목록을 반환합니다.
/// </summary>
/// <returns>현재 ListView 순서가 반영된 데이터 목록.</returns>
public List<ReordableListItemData> GetData()
{
SyncOrderValues();
return new List<ReordableListItemData>(_items);
}
/// <summary>
/// 전체 아이템을 List&lt;Dictionary&lt;string, string&gt;&gt;로 변환하여 반환합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <returns>각 아이템을 Dictionary로 변환한 목록.</returns>
public List<Dictionary<string, string>> ToDictionary()
{
SyncOrderValues();
var result = new List<Dictionary<string, string>>(_items.Count);
foreach (var item in _items)
{
result.Add(new Dictionary<string, string>
{
["order"] = item.Order.ToString(),
["active"] = item.IsActive.ToString(),
["text"] = item.DisplayText
});
}
return result;
}
#endregion
#region Internal Types
/// <summary>바인딩 콜백 참조 추적</summary>
private class ItemCallbackInfo
{
public readonly UTKCheckBox Checkbox;
public readonly UTKInputField InputField;
public readonly Action<bool> OnCheckedHandler;
public readonly Action<string> OnTextHandler;
public ItemCallbackInfo(
UTKCheckBox checkbox,
UTKInputField inputField,
Action<bool> onCheckedHandler,
Action<string> onTextHandler)
{
Checkbox = checkbox;
InputField = inputField;
OnCheckedHandler = onCheckedHandler;
OnTextHandler = onTextHandler;
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
// ListView 이벤트 해제
if (_listView != null)
{
_listView.itemIndexChanged -= OnItemIndexChanged;
_listView.Dispose();
}
// 이벤트 정리
OnOrderChanged = null;
OnDataChanged = null;
// 캐시 정리
_itemTemplate = null;
_itemStyleSheet = null;
}
#endregion
}
}