#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 } }