384 lines
13 KiB
C#
384 lines
13 KiB
C#
#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<Dictionary>로부터 데이터를 변환하여 설정합니다.
|
|
/// 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<Dictionary<string, string>>로 변환하여 반환합니다.
|
|
/// 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
|
|
}
|
|
}
|