#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
///
/// UTKPropertyList를 감싸는 윈도우 래퍼입니다.
/// 헤더(타이틀 + 닫기 버튼), 드래그 이동, 그리고 내부 UTKPropertyList를 통한
/// 속성 목록 관리 기능을 제공합니다.
///
/// 주요 기능:
///
/// - 윈도우 프레임 (헤더, 타이틀, 닫기 버튼)
/// - 헤더 드래그로 위치 이동
/// - UTKPropertyList 위임 메서드 (데이터 로드, 속성 관리, 값 변경 등)
/// - 속성 값 변경 / 클릭 / 버튼 클릭 이벤트
///
///
/// 관련 리소스:
///
/// - Resources/UIToolkit/Window/UTKPropertyListWindow.uxml
/// - Resources/UIToolkit/Window/UTKPropertyListWindowUss.uss
///
///
/// 사용 예 (C#):
///
/// // 1. 윈도우 생성 및 설정
/// var window = new UTKPropertyListWindow("속성 편집기");
/// window.ShowCloseButton = true;
/// window.SetSize(300, 600);
///
/// // 2. 이벤트 구독
/// window.OnCloseClicked += () => window.Hide();
/// window.OnPropertyValueChanged += args =>
/// {
/// Debug.Log($"{args.PropertyId} = {args.NewValue}");
///
/// // 조건부 가시성 제어 예시
/// if (args.PropertyId == "type" && args.NewValue is string type)
/// {
/// window.SetPropertyVisibilityBatch(new (string, bool)[]
/// {
/// ("option_a", type == "A"),
/// ("option_b", type == "B"),
/// });
/// }
/// };
///
/// // 3. 데이터 로드 (그룹 + 개별 아이템 혼합)
/// var entries = new List();
/// entries.Add(new UTKStringPropertyItem("name", "이름", "기본값"));
///
/// var group = new UTKPropertyGroup("transform", "Transform");
/// group.AddItem(new UTKVector3PropertyItem("pos", "Position", Vector3.zero));
/// entries.Add(group);
///
/// window.LoadMixedProperties(entries);
///
/// // 4. 런타임 속성 제어
/// window.SetPropertyReadOnly("name", true); // 읽기 전용 전환
/// window.SetPropertyVisibility("pos", false); // 숨김
/// window.UpdatePropertyValue("name", "새 이름"); // 값 변경
///
/// root.Add(window);
///
///
/// 사용 예 (UXML):
///
///
///
///
///
///
///
[UxmlElement]
public partial class UTKPropertyListWindow : VisualElement, IDisposable
{
#region Constants
private const string UXML_PATH = "UIToolkit/Window/UTKPropertyListWindow";
private const string USS_PATH = "UIToolkit/Window/UTKPropertyListWindowUss";
#endregion
#region Fields
private bool _disposed;
private UTKPropertyList? _propertyList;
private VisualElement? _header;
private UTKLabel? _titleLabel;
private UTKButton? _closeButton;
private string _title = "Properties";
private bool _showCloseButton = false;
private bool _isDragging;
private Vector2 _dragStartPosition;
private Vector2 _dragStartMousePosition;
#endregion
#region Properties
/// 윈도우 타이틀
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null)
{
_titleLabel.Text = value;
}
}
}
/// 닫기 버튼 표시 여부
public bool ShowCloseButton
{
get => _showCloseButton;
set
{
_showCloseButton = value;
if (_closeButton != null)
{
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// 내부 PropertyList 접근
public UTKPropertyList PropertyList => _propertyList ??= new UTKPropertyList();
#endregion
#region Events
/// 닫기 버튼 클릭 이벤트
public event Action? OnCloseClicked;
/// 속성 값 변경 이벤트 (PropertyList 위임)
public event Action? OnPropertyValueChanged
{
add => PropertyList.OnPropertyValueChanged += value;
remove => PropertyList.OnPropertyValueChanged -= value;
}
/// 그룹 펼침/접힘 이벤트 (PropertyList 위임)
public event Action? OnGroupExpandedChanged
{
add => PropertyList.OnGroupExpandedChanged += value;
remove => PropertyList.OnGroupExpandedChanged -= value;
}
/// 속성 클릭 이벤트 (PropertyList 위임)
public event Action? OnPropertyClicked
{
add => PropertyList.OnPropertyClicked += value;
remove => PropertyList.OnPropertyClicked -= value;
}
/// 버튼 클릭 이벤트 (PropertyList 위임)
public event Action? OnPropertyButtonClicked
{
add => PropertyList.OnPropertyButtonClicked += value;
remove => PropertyList.OnPropertyButtonClicked -= value;
}
#endregion
#region Constructor
public UTKPropertyListWindow()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
SubscribeToThemeChanges();
var styleSheet = Resources.Load(USS_PATH);
if (styleSheet != null)
{
styleSheets.Add(styleSheet);
}
CreateUI();
}
public UTKPropertyListWindow(string title) : this()
{
Title = title;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-property-window");
var asset = Resources.Load(UXML_PATH);
if (asset != null)
{
CreateUIFromUxml(asset);
}
else
{
CreateUIFallback();
}
// 드래그 이벤트
if (_header != null)
{
_header.RegisterCallback(OnHeaderPointerDown);
_header.RegisterCallback(OnHeaderPointerMove);
_header.RegisterCallback(OnHeaderPointerUp);
}
}
private void CreateUIFromUxml(VisualTreeAsset asset)
{
var root = asset.Instantiate();
var windowRoot = root.Q("window-root");
if (windowRoot != null)
{
// windowRoot의 자식들을 현재 요소로 이동
foreach (var child in windowRoot.Children().ToArray())
{
Add(child);
}
}
else
{
// window-root가 없으면 전체 추가
Add(root);
}
// 요소 참조 가져오기
_header = this.Q("header");
_titleLabel = this.Q("title");
_closeButton = this.Q("close-btn");
_propertyList = this.Q("content");
// 타이틀 설정
if (_titleLabel != null)
{
_titleLabel.Text = _title;
_titleLabel.Size = UTKLabel.LabelSize.Label3;
}
// 닫기 버튼 설정
if (_closeButton != null)
{
_closeButton.SetMaterialIcon(UTKMaterialIcons.Close, 14);
_closeButton.IconOnly = true;
_closeButton.OnClicked += () => OnCloseClicked?.Invoke();
_closeButton.style.display = _showCloseButton ? DisplayStyle.Flex : DisplayStyle.None;
}
// PropertyList가 없으면 생성
if (_propertyList == null)
{
_propertyList = new UTKPropertyList();
_propertyList.name = "content";
_propertyList.AddToClassList("utk-property-window__content");
Add(_propertyList);
}
}
private void CreateUIFallback()
{
// 헤더
_header = new VisualElement();
_header.name = "header";
_header.AddToClassList("utk-property-window__header");
_titleLabel = new UTKLabel(_title, UTKLabel.LabelSize.Label3);
_titleLabel.name = "title";
_titleLabel.AddToClassList("utk-property-window__title");
_header.Add(_titleLabel);
_closeButton = new UTKButton("", UTKMaterialIcons.Close, UTKButton.ButtonVariant.Text, 14);
_closeButton.name = "close-btn";
_closeButton.IconOnly = true;
_closeButton.AddToClassList("utk-property-window__close-btn");
_closeButton.OnClicked += () => OnCloseClicked?.Invoke();
_header.Add(_closeButton);
Add(_header);
// PropertyList
_propertyList = new UTKPropertyList();
_propertyList.AddToClassList("utk-property-window__content");
Add(_propertyList);
}
#endregion
#region Public Methods - Data Loading (PropertyList 위임)
/// 평면 속성 목록을 로드합니다 (그룹 없이).
/// 로드할 속성 아이템 목록.
public void LoadProperties(List items) => PropertyList.LoadProperties(items);
/// 그룹화된 속성 목록을 로드합니다.
/// 로드할 속성 그룹 목록.
public void LoadGroupedProperties(List groups) => PropertyList.LoadGroupedProperties(groups);
/// 그룹과 개별 아이템이 혼합된 엔트리 목록을 로드합니다.
/// 로드할 엔트리 목록 (IUTKPropertyGroup 또는 IUTKPropertyItem).
public void LoadMixedProperties(List entries) => PropertyList.LoadMixedProperties(entries);
#endregion
#region Public Methods - Group Management (PropertyList 위임)
/// 그룹을 추가합니다.
/// 추가할 속성 그룹.
public void AddGroup(IUTKPropertyGroup group) => PropertyList.AddGroup(group);
/// 지정한 ID의 그룹과 내부 아이템을 모두 제거합니다.
/// 제거할 그룹 ID.
public void RemoveGroup(string groupId) => PropertyList.RemoveGroup(groupId);
/// 지정한 ID의 그룹을 반환합니다.
/// 조회할 그룹 ID.
/// 그룹 인스턴스 또는 null.
public IUTKPropertyGroup? GetGroup(string groupId) => PropertyList.GetGroup(groupId);
/// 그룹의 펼침/접힘 상태를 설정합니다.
/// 대상 그룹 ID.
/// true면 펼침, false면 접힘.
public void SetGroupExpanded(string groupId, bool expanded) => PropertyList.SetGroupExpanded(groupId, expanded);
/// 그룹의 펼침/접힘 상태를 토글합니다.
/// 대상 그룹 ID.
public void ToggleGroupExpanded(string groupId) => PropertyList.ToggleGroupExpanded(groupId);
#endregion
#region Public Methods - Property Management (PropertyList 위임)
/// 최상위 속성 아이템을 추가합니다.
/// 추가할 속성 아이템.
public void AddProperty(IUTKPropertyItem item) => PropertyList.AddProperty(item);
/// 지정한 그룹에 속성 아이템을 추가합니다.
/// 대상 그룹 ID.
/// 추가할 속성 아이템.
public void AddPropertyToGroup(string groupId, IUTKPropertyItem item) => PropertyList.AddPropertyToGroup(groupId, item);
/// 지정한 ID의 속성 아이템을 제거합니다.
/// 제거할 아이템 ID.
public void RemoveProperty(string itemId) => PropertyList.RemoveProperty(itemId);
/// 지정한 ID의 속성 아이템을 반환합니다.
/// 조회할 아이템 ID.
/// 아이템 인스턴스 또는 null.
public IUTKPropertyItem? GetProperty(string itemId) => PropertyList.GetProperty(itemId);
#endregion
#region Public Methods - Value Management (PropertyList 위임)
/// 속성 값을 변경합니다. 바인딩된 View에 자동 반영됩니다.
/// 대상 속성 ID.
/// 새 값 (타입 변환 자동 시도).
/// true면 값 변경 알림 이벤트 발생.
public void UpdatePropertyValue(string propertyId, object newValue, bool notify = false) => PropertyList.UpdatePropertyValue(propertyId, newValue, notify);
/// 속성 값을 변경합니다. 의 별칭입니다.
/// 대상 속성 ID.
/// 새 값.
/// true면 값 변경 알림 이벤트 발생.
public void SetPropertyValue(string propertyId, object value, bool notify = false) => PropertyList.SetPropertyValue(propertyId, value, notify);
#endregion
#region Public Methods - Visibility & ReadOnly (PropertyList 위임)
/// 속성 아이템의 가시성을 변경합니다. TreeView Rebuild가 발생합니다.
/// 대상 속성 ID.
/// true면 표시, false면 숨김.
public void SetPropertyVisibility(string propertyId, bool visible) => PropertyList.SetPropertyVisibility(propertyId, visible);
/// 여러 속성의 가시성을 일괄 변경합니다. TreeView는 마지막에 한 번만 갱신됩니다.
/// 변경할 (속성 ID, 가시성) 튜플 목록.
public void SetPropertyVisibilityBatch(IEnumerable<(string propertyId, bool visible)> changes) => PropertyList.SetPropertyVisibilityBatch(changes);
/// 그룹의 가시성을 변경합니다. TreeView Rebuild가 발생합니다.
/// 대상 그룹 ID.
/// true면 표시, false면 숨김.
public void SetGroupVisibility(string groupId, bool visible) => PropertyList.SetGroupVisibility(groupId, visible);
/// 속성 아이템의 읽기 전용 상태를 변경합니다. OnStateChanged로 View에 자동 반영됩니다.
/// 대상 속성 ID.
/// true면 읽기 전용.
public void SetPropertyReadOnly(string propertyId, bool isReadOnly) => PropertyList.SetPropertyReadOnly(propertyId, isReadOnly);
/// 그룹 내 모든 아이템의 읽기 전용 상태를 일괄 변경합니다.
/// 대상 그룹 ID.
/// true면 읽기 전용.
public void SetGroupReadOnly(string groupId, bool isReadOnly) => PropertyList.SetGroupReadOnly(groupId, isReadOnly);
#endregion
#region Public Methods - Utilities (PropertyList 위임)
/// 모든 엔트리(그룹 + 아이템)를 제거하고 초기화합니다.
public void Clear() => PropertyList.Clear();
/// 현재 데이터를 기반으로 TreeView를 다시 빌드합니다.
public void Refresh() => PropertyList.Refresh();
#endregion
#region Public Methods - Window
/// 윈도우를 표시합니다.
public void Show()
{
style.display = DisplayStyle.Flex;
}
/// 윈도우를 숨깁니다.
public void Hide()
{
style.display = DisplayStyle.None;
}
///
/// 윈도우의 위치를 설정합니다 (absolute 포지셔닝).
///
/// 왼쪽 오프셋 (px).
/// 상단 오프셋 (px).
public void SetPosition(float x, float y)
{
style.left = x;
style.top = y;
}
///
/// 윈도우의 크기를 설정합니다.
///
/// 너비 (px).
/// 높이 (px).
public void SetSize(float width, float height)
{
style.width = width;
style.height = height;
}
///
/// 부모 요소 기준으로 윈도우를 중앙에 배치합니다.
/// schedule.Execute로 다음 프레임에 실행됩니다.
///
public void CenterOnScreen()
{
schedule.Execute(() =>
{
var parent = this.parent;
if (parent == null) return;
float parentWidth = parent.resolvedStyle.width;
float parentHeight = parent.resolvedStyle.height;
float selfWidth = resolvedStyle.width;
float selfHeight = resolvedStyle.height;
style.left = (parentWidth - selfWidth) / 2;
style.top = (parentHeight - selfHeight) / 2;
});
}
#endregion
#region Dragging
private void OnHeaderPointerDown(PointerDownEvent evt)
{
if (evt.button != 0) return;
_isDragging = true;
_dragStartPosition = new Vector2(resolvedStyle.left, resolvedStyle.top);
_dragStartMousePosition = evt.position;
_header?.CapturePointer(evt.pointerId);
}
private void OnHeaderPointerMove(PointerMoveEvent evt)
{
if (!_isDragging) return;
Vector2 delta = (Vector2)evt.position - _dragStartMousePosition;
style.left = _dragStartPosition.x + delta.x;
style.top = _dragStartPosition.y + delta.y;
}
private void OnHeaderPointerUp(PointerUpEvent evt)
{
if (!_isDragging) return;
_isDragging = false;
_header?.ReleasePointer(evt.pointerId);
}
#endregion
#region 테마 (Theme)
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback(OnAttachToPanelForTheme);
RegisterCallback(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 IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 테마 변경 이벤트 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
// 드래그 이벤트 해제
if (_header != null)
{
_header.UnregisterCallback(OnHeaderPointerDown);
_header.UnregisterCallback(OnHeaderPointerMove);
_header.UnregisterCallback(OnHeaderPointerUp);
}
// PropertyList 정리
_propertyList?.Dispose();
_propertyList = null;
// 이벤트 정리
OnCloseClicked = null;
// UI 참조 정리
_header = null;
_titleLabel = null;
_closeButton = null;
}
#endregion
}
}