diff --git a/Assets/Resources/UIToolkit/Sample/Window/UTKPropertyListWindowSample.uxml b/Assets/Resources/UIToolkit/Sample/Window/UTKPropertyListWindowSample.uxml
index 31b66433..6cf80f86 100644
--- a/Assets/Resources/UIToolkit/Sample/Window/UTKPropertyListWindowSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Window/UTKPropertyListWindowSample.uxml
@@ -11,6 +11,11 @@
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Tab/UTKTabView.uss b/Assets/Resources/UIToolkit/Tab/UTKTabView.uss
index ae730fd7..e64e7ba6 100644
--- a/Assets/Resources/UIToolkit/Tab/UTKTabView.uss
+++ b/Assets/Resources/UIToolkit/Tab/UTKTabView.uss
@@ -11,7 +11,7 @@
.utk-tabview {
flex-direction: column;
- flex-grow: 1;
+ flex-grow: 0;
color: var(--color-text-secondary);
}
diff --git a/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml
new file mode 100644
index 00000000..07aec303
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml.meta b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml.meta
new file mode 100644
index 00000000..b78ab3c1
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindow.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 22934f67f61d09a419d467bdcc086cfb
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss
new file mode 100644
index 00000000..d93dfcc3
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss
@@ -0,0 +1,96 @@
+/*
+ * UTKPropertyTabListWindowUss.uss
+ *
+ * UTKPropertyTabListWindow 컴포넌트의 스타일 정의입니다.
+ * 테마 지원: var(--color-*) 변수 사용
+ *
+ * UTKPropertyListWindow 스타일을 기반으로 탭 영역이 추가되었습니다.
+ */
+
+/* ============================================
+ 윈도우 루트 (Window Root)
+ ============================================ */
+
+.utk-property-tab-window {
+ background-color: var(--color-bg-panel);
+ flex-grow: 1;
+ height: 100%;
+ min-width: 390px;
+ width: 390px;
+ padding: 10px 20px 25px 20px;
+}
+
+/* ============================================
+ 헤더 (Header)
+ ============================================ */
+
+.utk-property-tab-window__header {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ height: 24px;
+ flex-shrink: 0;
+}
+
+/* UTKLabel 타이틀 스타일 */
+.utk-property-tab-window__title {
+ flex-grow: 1;
+}
+
+.utk-property-tab-window__title .utk-label__text {
+ color: var(--color-text-primary);
+ font-size: var(--font-size-label3);
+ -unity-font-definition: resource('Fonts/Pretendard/Pretendard-Medium');
+ -unity-font-style: normal;
+ margin: 0;
+ padding: 0;
+ -unity-text-align: middle-left;
+}
+
+/* ============================================
+ 닫기 버튼 (Close Button)
+ ============================================ */
+
+.utk-property-tab-window__close-btn {
+ width: 22px;
+ height: 22px;
+ min-width: 22px;
+ min-height: 22px;
+ border-width: 0;
+ padding: 0;
+ margin: 0;
+ align-self: center;
+ display: none; /* 기본 숨김, 필요시 flex로 변경 */
+}
+
+/* ============================================
+ 탭 뷰 (Tab View)
+ ============================================ */
+
+.utk-property-tab-window__tab-view {
+ flex-grow: 0;
+ flex-shrink: 0;
+ margin-bottom: 8px;
+}
+
+/* 탭 콘텐츠 영역 숨기기 (실제 콘텐츠는 외부 UTKPropertyList에 표시) */
+.utk-property-tab-window__tab-view > .unity-tab-view__content-container {
+ display: none;
+ flex-grow: 0;
+ height: 0;
+ padding: 0;
+ margin: 0;
+}
+
+/* ============================================
+ 콘텐츠 (Content - UTKPropertyList)
+ ============================================ */
+
+.utk-property-tab-window__content {
+ flex-grow: 1;
+}
+
+#unity-content-viewport {
+ padding-right: 4px; /* 스크롤바 여유 공간 */
+}
diff --git a/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss.meta b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss.meta
new file mode 100644
index 00000000..bdfe6a08
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Window/UTKPropertyTabListWindowUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 487dd3e0868e89645b0b5cfae0108370
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs b/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
index 2c406589..9cae383d 100644
--- a/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
+++ b/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
@@ -19,9 +19,10 @@ namespace UVC.Sample.UIToolkit
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
- private UTKToggle _themeToggle;
+ private UTKToggle? _themeToggle;
- private UTKPropertyListWindow _propertyWindow;
+ private UTKPropertyListWindow? _propertyWindow;
+ private UTKPropertyTabListWindow? _propertyTabWindow;
private void Start()
{
@@ -34,43 +35,48 @@ namespace UVC.Sample.UIToolkit
}
_uiDocument = doc;
- var toggle = _uiDocument.rootVisualElement.Q("toggle");
- if (toggle == null)
- {
- Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다.");
- return;
- }
- _themeToggle = toggle;
-
- var window = _uiDocument.rootVisualElement.Q("window");
- if (window == null)
- {
- Debug.LogError("UXML에서 UTKPropertyListWindow를 찾을 수 없습니다.");
- return;
- }
- _propertyWindow = window;
-
UTKThemeManager.Instance.RegisterRoot(_uiDocument.rootVisualElement);
UTKThemeManager.Instance.SetTheme(initialTheme);
-
- _themeToggle.OnValueChanged += (isOn) =>
- {
- UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
- };
var root = _uiDocument.rootVisualElement;
- CreateSamplePropertyWindow(root);
+
+ // PropertyListWindow 샘플
+ var window = root.Q("window");
+ if (window != null)
+ {
+ _propertyWindow = window;
+ _propertyWindow.style.position = Position.Absolute;
+ _propertyWindow.style.top = 50;
+ _propertyWindow.style.left = 0;
+ _propertyWindow.style.bottom = 0;
+ _propertyWindow.style.width = 300;
+ CreateSamplePropertyWindow();
+ }
+
+ // PropertyTabListWindow 샘플
+ var tabWindow = root.Q("tabWindow");
+ if (tabWindow != null)
+ {
+ _propertyTabWindow = tabWindow;
+ _propertyTabWindow.style.position = Position.Absolute;
+ _propertyTabWindow.style.top = 50;
+ _propertyTabWindow.style.right = 0;
+ _propertyTabWindow.style.bottom = 0;
+ _propertyTabWindow.style.width = 300;
+ CreateSamplePropertyTabWindow();
+ }
+
+ UTKThemeManager.Instance.OnThemeChanged += theme =>
+ {
+ UTKThemeManager.Instance.ApplyThemeToElement(_uiDocument.rootVisualElement);
+ };
}
- private void CreateSamplePropertyWindow(VisualElement root)
+ private void CreateSamplePropertyWindow()
{
+ if (_propertyWindow == null) return;
- // 세로 높이를 부모에 맞게 꽉 채우기
- _propertyWindow.style.position = Position.Absolute;
- _propertyWindow.style.top = 0;
- _propertyWindow.style.bottom = 0;
- _propertyWindow.style.right = 0;
- _propertyWindow.style.width = 300;
+ _propertyWindow.ShowCloseButton = true;
_propertyWindow.OnCloseClicked += () =>
{
Debug.Log("Property Window Close clicked");
@@ -105,8 +111,111 @@ namespace UVC.Sample.UIToolkit
// 샘플 데이터 생성
var entries = CreateSampleEntries();
_propertyWindow.LoadMixedProperties(entries);
+ }
- root.Add(_propertyWindow);
+ ///
+ /// UTKPropertyTabListWindow 샘플 데이터 설정
+ /// 탭별로 서로 다른 데이터 타입(Flat/Grouped/Mixed)을 보여줍니다.
+ ///
+ private void CreateSamplePropertyTabWindow()
+ {
+ if (_propertyTabWindow == null) return;
+
+ _propertyTabWindow.ShowCloseButton = true;
+ _propertyTabWindow.OnCloseClicked += () =>
+ {
+ Debug.Log("Tab Property Window Close clicked");
+ _propertyTabWindow?.Hide();
+ };
+
+ _propertyTabWindow.OnTabChanged += (index, data) =>
+ {
+ Debug.Log($"Tab Changed: index={index}, name={data?.Name ?? "All"}");
+ };
+
+ _propertyTabWindow.OnPropertyValueChanged += args =>
+ {
+ Debug.Log($"[Tab] Property Changed: {args.PropertyId} {args.PropertyName} ({args.PropertyType}) = {args.NewValue}");
+ };
+
+ // === 탭 1: 기본 속성 (Grouped) ===
+ var basicTab = new TabPropertyData("기본", UTKMaterialIcons.Settings);
+ var basicGroups = new List();
+
+ var infoGroup = new UTKPropertyGroup("tab_info", "기본 정보");
+ infoGroup.AddItem(new UTKStringPropertyItem("tab_name", "이름", "오브젝트 A"));
+ infoGroup.AddItem(new UTKBoolPropertyItem("tab_active", "활성화", true));
+ infoGroup.AddItem(new UTKEnumPropertyItem("tab_layer", "레이어", SampleLayer.Default));
+ infoGroup.AddItem(new UTKDropdownPropertyItem("tab_tag", "태그",
+ new List { "Untagged", "Player", "Enemy" }, "Player"));
+ basicGroups.Add(infoGroup);
+
+ var transformGroup = new UTKPropertyGroup("tab_transform", "Transform");
+ transformGroup.AddItem(new UTKVector3PropertyItem("tab_pos", "Position", new Vector3(0, 1, 0)));
+ transformGroup.AddItem(new UTKVector3PropertyItem("tab_rot", "Rotation", Vector3.zero));
+ transformGroup.AddItem(new UTKVector3PropertyItem("tab_scale", "Scale", Vector3.one));
+ basicGroups.Add(transformGroup);
+
+ basicTab.SetGroupedData(basicGroups);
+
+ // === 탭 2: 외관 (Grouped) ===
+ var appearanceTab = new TabPropertyData("외관", UTKMaterialIcons.Palette);
+ var appearanceGroups = new List();
+
+ var colorGroup = new UTKPropertyGroup("tab_colors", "색상");
+ colorGroup.AddItem(new UTKColorPropertyItem("tab_main_color", "메인 색상", Color.blue));
+ colorGroup.AddItem(new UTKColorPropertyItem("tab_emission", "발광 색상", Color.yellow, true));
+ colorGroup.AddItem(new UTKFloatPropertyItem("tab_alpha", "투명도", 1f, 0f, 1f, useSlider: true));
+ appearanceGroups.Add(colorGroup);
+
+ var materialGroup = new UTKPropertyGroup("tab_material", "머티리얼");
+ materialGroup.AddItem(new UTKDropdownPropertyItem("tab_shader", "셰이더",
+ new List { "Standard", "Unlit", "URP/Lit", "URP/Simple Lit" }, "URP/Lit"));
+ materialGroup.AddItem(new UTKFloatPropertyItem("tab_metallic", "메탈릭", 0.5f, 0f, 1f, useSlider: true));
+ materialGroup.AddItem(new UTKFloatPropertyItem("tab_smoothness", "부드러움", 0.5f, 0f, 1f, useSlider: true));
+ appearanceGroups.Add(materialGroup);
+
+ appearanceTab.SetGroupedData(appearanceGroups);
+
+ // === 탭 3: 고급 설정 (Flat) ===
+ var advancedTab = new TabPropertyData("고급", UTKMaterialIcons.Tune);
+ var advancedItems = new List
+ {
+ new UTKBoolPropertyItem("tab_debug", "디버그 모드", false),
+ new UTKIntPropertyItem("tab_priority", "우선순위", 0, -10, 10, useStepper: true),
+ new UTKFloatPropertyItem("tab_lod_bias", "LOD Bias", 1.0f, 0.1f, 5.0f, useSlider: true, useStepper: true),
+ new UTKMultiSelectDropdownPropertyItem("tab_flags", "플래그",
+ new List { "Static", "Batching", "Navigation", "Occluder", "Occludee" },
+ new List { "Static", "Batching" }),
+ new UTKRadioPropertyItem("tab_quality", "품질",
+ new List { "Low", "Medium", "High", "Ultra" }, 2),
+ };
+ advancedTab.SetFlatData(advancedItems);
+
+ // === 탭 4: 일정 (Mixed) ===
+ var scheduleTab = new TabPropertyData("일정", UTKMaterialIcons.CalendarMonth);
+ var scheduleEntries = new List();
+
+ scheduleEntries.Add(new UTKDatePropertyItem("tab_created", "생성일", DateTime.Today.AddDays(-30)));
+ scheduleEntries.Add(new UTKDateTimePropertyItem("tab_modified", "수정일", DateTime.Now));
+
+ var periodGroup = new UTKPropertyGroup("tab_period", "유효 기간");
+ periodGroup.AddItem(new UTKDateRangePropertyItem("tab_valid", "유효 기간",
+ DateTime.Today, DateTime.Today.AddMonths(1)));
+ periodGroup.AddItem(new UTKDateTimeRangePropertyItem("tab_session", "세션 기간",
+ DateTime.Now, DateTime.Now.AddHours(2)));
+ scheduleEntries.Add(periodGroup);
+
+ scheduleTab.SetMixedData(scheduleEntries);
+
+ // 탭 데이터 설정
+ _propertyTabWindow.SetTabData(new List
+ {
+ basicTab,
+ appearanceTab,
+ advancedTab,
+ scheduleTab
+ });
}
private List CreateSampleEntries()
@@ -621,6 +730,8 @@ namespace UVC.Sample.UIToolkit
{
_propertyWindow?.Dispose();
_propertyWindow = null;
+ _propertyTabWindow?.Dispose();
+ _propertyTabWindow = null;
}
// 샘플 열거형
diff --git a/Assets/Sample/UIToolkit/UTKPropertyListWindowUXML.uxml b/Assets/Sample/UIToolkit/UTKPropertyListWindowUXML.uxml
index 93adc798..173a829f 100644
--- a/Assets/Sample/UIToolkit/UTKPropertyListWindowUXML.uxml
+++ b/Assets/Sample/UIToolkit/UTKPropertyListWindowUXML.uxml
@@ -1,6 +1,7 @@
+
diff --git a/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs b/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
index 383ed74c..f1407d90 100644
--- a/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
+++ b/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
@@ -72,6 +72,18 @@ namespace UVC.UIToolkit
public event Action? OnPropertyButtonClicked;
#endregion
+ #region Properties
+ ///
+ /// 현재 검색어를 가져오거나 설정합니다.
+ /// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다.
+ ///
+ public string SearchQuery
+ {
+ get => _searchField?.value ?? string.Empty;
+ set { if (_searchField != null) _searchField.value = value; }
+ }
+ #endregion
+
#region Constructor
public UTKPropertyList()
{
@@ -985,6 +997,28 @@ namespace UVC.UIToolkit
}
}
+ ///
+ /// 현재 검색 필드의 값으로 검색을 실행합니다.
+ ///
+ public void ApplySearch()
+ {
+ OnSearch(_searchField?.value ?? string.Empty);
+ }
+
+ ///
+ /// 지정된 검색어로 검색을 실행합니다.
+ /// 검색 필드의 값도 함께 업데이트됩니다.
+ ///
+ /// 검색어
+ public void ApplySearch(string query)
+ {
+ if (_searchField != null)
+ {
+ _searchField.value = query;
+ }
+ OnSearch(query);
+ }
+
private void OnSearch(string newValue)
{
_searchText = newValue ?? string.Empty;
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs b/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs
new file mode 100644
index 00000000..95ed27f9
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs
@@ -0,0 +1,103 @@
+#nullable enable
+using System.Collections.Generic;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 탭의 프로퍼티 데이터 로드 유형
+ ///
+ public enum TabPropertyDataType
+ {
+ /// 평면 속성 목록 (그룹 없이)
+ Flat,
+ /// 그룹화된 속성 목록
+ Grouped,
+ /// 그룹과 개별 아이템이 혼합된 목록
+ Mixed
+ }
+
+ ///
+ /// 탭별 프로퍼티 설정 데이터 클래스입니다.
+ /// 탭의 메타데이터(이름, 아이콘, 활성화 상태)와
+ /// 해당 탭에 표시할 프로퍼티 데이터를 보유합니다.
+ ///
+ public class TabPropertyData
+ {
+ #region Properties
+ /// 탭 이름 (표시 텍스트)
+ public string Name { get; set; } = string.Empty;
+
+ /// 탭 아이콘 (Material Icon 유니코드, null이면 아이콘 없음)
+ public string? Icon { get; set; }
+
+ /// 탭 활성화 상태
+ public bool IsEnabled { get; set; } = true;
+
+ /// 탭 툴팁
+ public string? Tooltip { get; set; }
+
+ /// 데이터 로드 유형
+ public TabPropertyDataType DataType { get; private set; }
+ #endregion
+
+ #region Data Fields
+ private List? _flatItems;
+ private List? _groupedItems;
+ private List? _mixedItems;
+ #endregion
+
+ #region Constructor
+ /// 기본 생성자
+ public TabPropertyData(string name)
+ {
+ Name = name;
+ }
+
+ /// 아이콘 포함 생성자
+ public TabPropertyData(string name, string? icon) : this(name)
+ {
+ Icon = icon;
+ }
+ #endregion
+
+ #region Data Setters
+ /// 평면 속성 목록 설정 (Flat 타입)
+ public void SetFlatData(List items)
+ {
+ DataType = TabPropertyDataType.Flat;
+ _flatItems = items;
+ _groupedItems = null;
+ _mixedItems = null;
+ }
+
+ /// 그룹화된 속성 목록 설정 (Grouped 타입)
+ public void SetGroupedData(List groups)
+ {
+ DataType = TabPropertyDataType.Grouped;
+ _flatItems = null;
+ _groupedItems = groups;
+ _mixedItems = null;
+ }
+
+ /// 혼합 속성 목록 설정 (Mixed 타입)
+ public void SetMixedData(List entries)
+ {
+ DataType = TabPropertyDataType.Mixed;
+ _flatItems = null;
+ _groupedItems = null;
+ _mixedItems = entries;
+ }
+ #endregion
+
+ #region Data Getters
+ /// 평면 데이터 반환 (Flat 타입이 아니면 null)
+ public List? GetFlatData() => _flatItems;
+
+ /// 그룹화된 데이터 반환 (Grouped 타입이 아니면 null)
+ public List? GetGroupedData() => _groupedItems;
+
+ /// 혼합 데이터 반환 (Mixed 타입이 아니면 null)
+ public List? GetMixedData() => _mixedItems;
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs.meta b/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs.meta
new file mode 100644
index 00000000..28cf6ee4
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Property/Core/TabPropertyData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e3dae65c9584b6444a75fc9835a6333c
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs
new file mode 100644
index 00000000..09fde449
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs
@@ -0,0 +1,871 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// UTKPropertyList와 UTKTabView를 결합한 탭 기반 프로퍼티 윈도우입니다.
+ /// 헤더(타이틀 + 닫기 버튼), 탭 뷰, 프로퍼티 리스트로 구성되며,
+ /// 탭별로 서로 다른 프로퍼티 데이터를 설정할 수 있습니다.
+ ///
+ /// 주요 기능:
+ ///
+ /// - 윈도우 프레임 (헤더, 타이틀, 닫기 버튼)
+ /// - UTKTabView 기반 탭 전환
+ /// - 탭별 프로퍼티 데이터 관리 (Flat/Grouped/Mixed 지원)
+ /// - 탭별 검색어 저장/복원
+ /// - 선택적 "전체(All)" 탭
+ /// - 헤더 드래그로 위치 이동
+ ///
+ ///
+ /// 사용 예 (C#):
+ ///
+ /// var window = new UTKPropertyTabListWindow("속성 편집기");
+ /// window.ShowCloseButton = true;
+ /// window.ShowAllTab = false;
+ ///
+ /// // 탭 데이터 설정
+ /// var generalTab = new TabPropertyData("일반");
+ /// generalTab.SetGroupedData(new List<IUTKPropertyGroup> { transformGroup, renderGroup });
+ ///
+ /// var advancedTab = new TabPropertyData("고급", "\ue8b8");
+ /// advancedTab.SetFlatData(new List<IUTKPropertyItem> { debugItem, logItem });
+ ///
+ /// window.SetTabData(new List<TabPropertyData> { generalTab, advancedTab });
+ ///
+ /// // 이벤트 구독
+ /// window.OnTabChanged += (index, data) => Debug.Log($"탭 변경: {data?.Name}");
+ /// window.OnPropertyValueChanged += args => Debug.Log($"{args.PropertyId} = {args.NewValue}");
+ ///
+ /// root.Add(window);
+ ///
+ ///
+ [UxmlElement]
+ public partial class UTKPropertyTabListWindow : VisualElement, IDisposable
+ {
+ #region Constants
+ private const string UXML_PATH = "UIToolkit/Window/UTKPropertyTabListWindow";
+ private const string USS_PATH = "UIToolkit/Window/UTKPropertyTabListWindowUss";
+ private const int ALL_TAB_INDEX = -1;
+ #endregion
+
+ #region Fields
+ private bool _disposed;
+
+ // UI 요소 참조
+ private VisualElement? _header;
+ private UTKLabel? _titleLabel;
+ private UTKButton? _closeButton;
+ private UTKTabView? _tabView;
+ private UTKPropertyList? _propertyList;
+
+ // 탭 데이터
+ private readonly List _tabDataList = new();
+ private int _selectedTabIndex = ALL_TAB_INDEX;
+ private bool _showAllTab = true;
+
+ // 탭별 검색어 저장
+ private readonly Dictionary _tabSearchQueries = new();
+
+ // 드래그 상태
+ private bool _isDragging;
+ private Vector2 _dragStartPosition;
+ private Vector2 _dragStartMousePosition;
+
+ // 윈도우 속성
+ private string _title = "Properties";
+ private bool _showCloseButton = false;
+ #endregion
+
+ #region Properties
+ /// 윈도우 타이틀
+ [UxmlAttribute("title")]
+ public string Title
+ {
+ get => _title;
+ set
+ {
+ _title = value;
+ if (_titleLabel != null)
+ {
+ _titleLabel.Text = value;
+ }
+ }
+ }
+
+ /// 닫기 버튼 표시 여부
+ [UxmlAttribute("show-close-button")]
+ public bool ShowCloseButton
+ {
+ get => _showCloseButton;
+ set
+ {
+ _showCloseButton = value;
+ if (_closeButton != null)
+ {
+ _closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+ }
+ }
+
+ /// "전체(All)" 탭 표시 여부
+ [UxmlAttribute("show-all-tab")]
+ public bool ShowAllTab
+ {
+ get => _showAllTab;
+ set
+ {
+ if (_showAllTab == value) return;
+ _showAllTab = value;
+ if (_tabDataList.Count > 0)
+ {
+ RebuildTabs();
+ SelectTab(_showAllTab ? ALL_TAB_INDEX : 0);
+ }
+ }
+ }
+
+ /// 현재 선택된 탭 인덱스 (-1: All 탭)
+ public int SelectedTabIndex => _selectedTabIndex;
+
+ /// 내부 UTKPropertyList 접근
+ public UTKPropertyList PropertyList => _propertyList ??= new UTKPropertyList();
+
+ /// 내부 UTKTabView 접근
+ public UTKTabView? TabView => _tabView;
+ #endregion
+
+ #region Events
+ /// 닫기 버튼 클릭 이벤트
+ public event Action? OnCloseClicked;
+
+ /// 탭 변경 이벤트 (탭 인덱스, TabPropertyData)
+ public event Action? OnTabChanged;
+
+ /// 속성 값 변경 이벤트 (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 UTKPropertyTabListWindow()
+ {
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ SubscribeToThemeChanges();
+
+ var styleSheet = Resources.Load(USS_PATH);
+ if (styleSheet != null)
+ {
+ styleSheets.Add(styleSheet);
+ }
+
+ CreateUI();
+ }
+
+ public UTKPropertyTabListWindow(string title) : this()
+ {
+ Title = title;
+ }
+
+ public UTKPropertyTabListWindow(string title, bool showAllTab) : this(title)
+ {
+ _showAllTab = showAllTab;
+ }
+ #endregion
+
+ #region UI Creation
+ private void CreateUI()
+ {
+ AddToClassList("utk-property-tab-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);
+ }
+
+ // UTKTabView 탭 변경 이벤트 구독
+ if (_tabView != null)
+ {
+ _tabView.OnTabChanged += OnTabViewTabChanged;
+ }
+ }
+
+ private void CreateUIFromUxml(VisualTreeAsset asset)
+ {
+ var root = asset.Instantiate();
+ var windowRoot = root.Q("window-root");
+
+ if (windowRoot != null)
+ {
+ foreach (var child in windowRoot.Children().ToArray())
+ {
+ Add(child);
+ }
+ }
+ else
+ {
+ Add(root);
+ }
+
+ // 요소 참조 가져오기
+ _header = this.Q("header");
+ _titleLabel = this.Q("title");
+ _closeButton = this.Q("close-btn");
+ _tabView = this.Q("tab-view");
+ _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;
+ }
+
+ // TabView가 없으면 생성
+ if (_tabView == null)
+ {
+ _tabView = new UTKTabView();
+ _tabView.name = "tab-view";
+ _tabView.AddToClassList("utk-property-tab-window__tab-view");
+ // PropertyList 앞에 삽입
+ if (_propertyList != null)
+ {
+ int index = IndexOf(_propertyList);
+ Insert(index, _tabView);
+ }
+ else
+ {
+ Add(_tabView);
+ }
+ }
+
+ // PropertyList가 없으면 생성
+ if (_propertyList == null)
+ {
+ _propertyList = new UTKPropertyList();
+ _propertyList.name = "content";
+ _propertyList.AddToClassList("utk-property-tab-window__content");
+ Add(_propertyList);
+ }
+ }
+
+ private void CreateUIFallback()
+ {
+ // 헤더
+ _header = new VisualElement();
+ _header.name = "header";
+ _header.AddToClassList("utk-property-tab-window__header");
+
+ _titleLabel = new UTKLabel(_title, UTKLabel.LabelSize.Label3);
+ _titleLabel.name = "title";
+ _titleLabel.AddToClassList("utk-property-tab-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-tab-window__close-btn");
+ _closeButton.OnClicked += () => OnCloseClicked?.Invoke();
+ _closeButton.style.display = _showCloseButton ? DisplayStyle.Flex : DisplayStyle.None;
+ _header.Add(_closeButton);
+
+ Add(_header);
+
+ // 탭 뷰
+ _tabView = new UTKTabView();
+ _tabView.name = "tab-view";
+ _tabView.AddToClassList("utk-property-tab-window__tab-view");
+ Add(_tabView);
+
+ // PropertyList
+ _propertyList = new UTKPropertyList();
+ _propertyList.name = "content";
+ _propertyList.AddToClassList("utk-property-tab-window__content");
+ Add(_propertyList);
+ }
+ #endregion
+
+ #region Public Methods - Tab Data Management
+
+ ///
+ /// 탭 데이터 목록을 설정합니다.
+ /// 기존 탭을 모두 제거하고 새로운 탭을 생성합니다.
+ ///
+ /// 탭 설정 데이터 목록
+ public void SetTabData(List tabDataList)
+ {
+ _tabDataList.Clear();
+ _tabDataList.AddRange(tabDataList);
+ _tabSearchQueries.Clear();
+
+ RebuildTabs();
+
+ // 기본 탭 선택
+ if (_showAllTab)
+ SelectTab(ALL_TAB_INDEX);
+ else if (_tabDataList.Count > 0)
+ SelectTab(0);
+ }
+
+ ///
+ /// 탭 데이터를 추가합니다.
+ ///
+ /// 추가할 탭 데이터
+ public void AddTabData(TabPropertyData tabData)
+ {
+ _tabDataList.Add(tabData);
+ RebuildTabs();
+ }
+
+ ///
+ /// 특정 인덱스의 탭 데이터를 제거합니다.
+ ///
+ /// 제거할 탭 인덱스 (0-based)
+ public void RemoveTabData(int index)
+ {
+ if (index < 0 || index >= _tabDataList.Count) return;
+
+ _tabDataList.RemoveAt(index);
+ _tabSearchQueries.Remove(index);
+ RebuildTabs();
+
+ // 현재 선택된 탭이 제거된 경우 기본 탭 선택
+ if (_selectedTabIndex == index)
+ {
+ SelectTab(_showAllTab ? ALL_TAB_INDEX : 0);
+ }
+ else if (_selectedTabIndex > index)
+ {
+ // 인덱스 보정
+ _selectedTabIndex--;
+ }
+ }
+
+ ///
+ /// 특정 인덱스의 탭 데이터를 반환합니다.
+ ///
+ /// 탭 인덱스 (0-based)
+ /// 탭 데이터 또는 null
+ public TabPropertyData? GetTabData(int index)
+ {
+ if (index >= 0 && index < _tabDataList.Count)
+ return _tabDataList[index];
+ return null;
+ }
+
+ ///
+ /// 탭을 선택하고 해당 데이터를 PropertyList에 로드합니다.
+ ///
+ /// 탭 인덱스 (-1: All 탭, 0+: 개별 탭)
+ public void SelectTab(int tabIndex)
+ {
+ // 유효성 검사
+ if (tabIndex != ALL_TAB_INDEX && (tabIndex < 0 || tabIndex >= _tabDataList.Count))
+ return;
+ if (tabIndex == ALL_TAB_INDEX && !_showAllTab)
+ return;
+
+ // 1. 이전 탭의 검색어 저장
+ SaveCurrentSearchQuery();
+
+ // 2. 탭 인덱스 변경
+ _selectedTabIndex = tabIndex;
+
+ // 3. UTKTabView 선택 동기화
+ SyncTabViewSelection(tabIndex);
+
+ // 4. 데이터 로드
+ LoadDataForTab(tabIndex);
+
+ // 5. 검색어 복원
+ RestoreSearchQuery(tabIndex);
+
+ // 6. 탭 변경 이벤트 발생
+ var tabData = tabIndex >= 0 && tabIndex < _tabDataList.Count
+ ? _tabDataList[tabIndex]
+ : null;
+ OnTabChanged?.Invoke(tabIndex, tabData);
+ }
+
+ ///
+ /// 현재 선택된 탭의 데이터를 다시 로드합니다.
+ ///
+ public void RefreshCurrentTab()
+ {
+ LoadDataForTab(_selectedTabIndex);
+ }
+ #endregion
+
+ #region Public Methods - Group Management (PropertyList 위임)
+ /// 그룹을 추가합니다.
+ public void AddGroup(IUTKPropertyGroup group) => PropertyList.AddGroup(group);
+
+ /// 지정한 ID의 그룹과 내부 아이템을 모두 제거합니다.
+ public void RemoveGroup(string groupId) => PropertyList.RemoveGroup(groupId);
+
+ /// 지정한 ID의 그룹을 반환합니다.
+ public IUTKPropertyGroup? GetGroup(string groupId) => PropertyList.GetGroup(groupId);
+
+ /// 그룹의 펼침/접힘 상태를 설정합니다.
+ public void SetGroupExpanded(string groupId, bool expanded) => PropertyList.SetGroupExpanded(groupId, expanded);
+
+ /// 그룹의 펼침/접힘 상태를 토글합니다.
+ public void ToggleGroupExpanded(string groupId) => PropertyList.ToggleGroupExpanded(groupId);
+ #endregion
+
+ #region Public Methods - Property Management (PropertyList 위임)
+ /// 최상위 속성 아이템을 추가합니다.
+ public void AddProperty(IUTKPropertyItem item) => PropertyList.AddProperty(item);
+
+ /// 지정한 그룹에 속성 아이템을 추가합니다.
+ public void AddPropertyToGroup(string groupId, IUTKPropertyItem item) => PropertyList.AddPropertyToGroup(groupId, item);
+
+ /// 지정한 ID의 속성 아이템을 제거합니다.
+ public void RemoveProperty(string itemId) => PropertyList.RemoveProperty(itemId);
+
+ /// 지정한 ID의 속성 아이템을 반환합니다.
+ public IUTKPropertyItem? GetProperty(string itemId) => PropertyList.GetProperty(itemId);
+ #endregion
+
+ #region Public Methods - Value Management (PropertyList 위임)
+ /// 속성 값을 변경합니다.
+ public void UpdatePropertyValue(string propertyId, object newValue, bool notify = false) => PropertyList.UpdatePropertyValue(propertyId, newValue, notify);
+
+ /// 속성 값을 변경합니다. UpdatePropertyValue의 별칭입니다.
+ public void SetPropertyValue(string propertyId, object value, bool notify = false) => PropertyList.SetPropertyValue(propertyId, value, notify);
+ #endregion
+
+ #region Public Methods - Visibility & ReadOnly (PropertyList 위임)
+ /// 속성 아이템의 가시성을 변경합니다.
+ public void SetPropertyVisibility(string propertyId, bool visible) => PropertyList.SetPropertyVisibility(propertyId, visible);
+
+ /// 여러 속성의 가시성을 일괄 변경합니다.
+ public void SetPropertyVisibilityBatch(IEnumerable<(string propertyId, bool visible)> changes) => PropertyList.SetPropertyVisibilityBatch(changes);
+
+ /// 그룹의 가시성을 변경합니다.
+ public void SetGroupVisibility(string groupId, bool visible) => PropertyList.SetGroupVisibility(groupId, visible);
+
+ /// 속성 아이템의 읽기 전용 상태를 변경합니다.
+ public void SetPropertyReadOnly(string propertyId, bool isReadOnly) => PropertyList.SetPropertyReadOnly(propertyId, isReadOnly);
+
+ /// 그룹 내 모든 아이템의 읽기 전용 상태를 일괄 변경합니다.
+ public void SetGroupReadOnly(string groupId, bool isReadOnly) => PropertyList.SetGroupReadOnly(groupId, isReadOnly);
+ #endregion
+
+ #region Public Methods - Utilities (PropertyList 위임)
+ /// 모든 엔트리(그룹 + 아이템)를 제거하고 초기화합니다.
+ public new 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 포지셔닝).
+ public void SetPosition(float x, float y)
+ {
+ style.left = x;
+ style.top = y;
+ }
+
+ /// 윈도우의 크기를 설정합니다.
+ public void SetSize(float width, float height)
+ {
+ style.width = width;
+ style.height = height;
+ }
+
+ /// 부모 요소 기준으로 윈도우를 중앙에 배치합니다.
+ 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 Private Methods - Tab Management
+
+ ///
+ /// UTKTabView에 탭을 재구성합니다.
+ ///
+ private void RebuildTabs()
+ {
+ if (_tabView == null) return;
+
+ // 이벤트 일시 해제 (재구성 중 탭 변경 이벤트 방지)
+ _tabView.OnTabChanged -= OnTabViewTabChanged;
+ _tabView.ClearTabs();
+
+ // "All" 탭 생성 (옵션)
+ if (_showAllTab)
+ {
+ _tabView.AddUTKTab("All");
+ }
+
+ // 개별 탭 생성
+ for (int i = 0; i < _tabDataList.Count; i++)
+ {
+ var data = _tabDataList[i];
+ var tabName = data.Name;
+
+ // 아이콘이 있으면 탭 이름 앞에 추가
+ if (!string.IsNullOrEmpty(data.Icon))
+ {
+ tabName = $"{data.Icon} {data.Name}";
+ }
+
+ var tab = _tabView.AddUTKTab(tabName);
+ tab.IsEnabled = data.IsEnabled;
+
+ if (!string.IsNullOrEmpty(data.Tooltip))
+ {
+ tab.tooltip = data.Tooltip;
+ }
+ }
+
+ // 이벤트 재구독
+ _tabView.OnTabChanged += OnTabViewTabChanged;
+ }
+
+ ///
+ /// UTKTabView의 선택 상태를 동기화합니다.
+ ///
+ private void SyncTabViewSelection(int tabIndex)
+ {
+ if (_tabView == null) return;
+
+ int viewIndex;
+ if (_showAllTab)
+ viewIndex = tabIndex == ALL_TAB_INDEX ? 0 : tabIndex + 1;
+ else
+ viewIndex = tabIndex;
+
+ if (viewIndex >= 0 && viewIndex < _tabView.UTKTabs.Count)
+ _tabView.SelectedIndex = viewIndex;
+ }
+
+ ///
+ /// UTKTabView에서 탭 변경 이벤트 발생 시 처리
+ ///
+ private void OnTabViewTabChanged(int viewIndex, Tab? tab)
+ {
+ int dataIndex;
+ if (_showAllTab)
+ dataIndex = viewIndex == 0 ? ALL_TAB_INDEX : viewIndex - 1;
+ else
+ dataIndex = viewIndex;
+
+ // 이미 선택된 탭이면 무시
+ if (dataIndex == _selectedTabIndex) return;
+
+ SelectTab(dataIndex);
+ }
+
+ ///
+ /// 탭에 해당하는 데이터를 UTKPropertyList에 로드합니다.
+ ///
+ private void LoadDataForTab(int tabIndex)
+ {
+ if (_propertyList == null) return;
+
+ if (tabIndex == ALL_TAB_INDEX)
+ {
+ LoadAllTabData();
+ }
+ else if (tabIndex >= 0 && tabIndex < _tabDataList.Count)
+ {
+ LoadSingleTabData(_tabDataList[tabIndex]);
+ }
+ }
+
+ ///
+ /// 단일 탭의 데이터를 UTKPropertyList에 로드합니다.
+ ///
+ private void LoadSingleTabData(TabPropertyData tabData)
+ {
+ if (_propertyList == null) return;
+
+ switch (tabData.DataType)
+ {
+ case TabPropertyDataType.Flat:
+ var flatItems = tabData.GetFlatData();
+ if (flatItems != null)
+ _propertyList.LoadProperties(flatItems);
+ else
+ _propertyList.Clear();
+ break;
+
+ case TabPropertyDataType.Grouped:
+ var groupedItems = tabData.GetGroupedData();
+ if (groupedItems != null)
+ _propertyList.LoadGroupedProperties(groupedItems);
+ else
+ _propertyList.Clear();
+ break;
+
+ case TabPropertyDataType.Mixed:
+ var mixedItems = tabData.GetMixedData();
+ if (mixedItems != null)
+ _propertyList.LoadMixedProperties(mixedItems);
+ else
+ _propertyList.Clear();
+ break;
+ }
+ }
+
+ ///
+ /// 모든 탭의 데이터를 병합하여 UTKPropertyList에 로드합니다.
+ /// 병합 전략: Mixed 방식으로 통합 (IUTKPropertyEntry 리스트로 변환)
+ ///
+ private void LoadAllTabData()
+ {
+ if (_propertyList == null) return;
+
+ var allEntries = new List();
+
+ foreach (var tabData in _tabDataList)
+ {
+ switch (tabData.DataType)
+ {
+ case TabPropertyDataType.Flat:
+ var flatItems = tabData.GetFlatData();
+ if (flatItems != null)
+ {
+ foreach (var item in flatItems)
+ allEntries.Add(item);
+ }
+ break;
+
+ case TabPropertyDataType.Grouped:
+ var groupedItems = tabData.GetGroupedData();
+ if (groupedItems != null)
+ {
+ foreach (var group in groupedItems)
+ allEntries.Add(group);
+ }
+ break;
+
+ case TabPropertyDataType.Mixed:
+ var mixedItems = tabData.GetMixedData();
+ if (mixedItems != null)
+ {
+ allEntries.AddRange(mixedItems);
+ }
+ break;
+ }
+ }
+
+ if (allEntries.Count > 0)
+ _propertyList.LoadMixedProperties(allEntries);
+ else
+ _propertyList.Clear();
+ }
+ #endregion
+
+ #region Private Methods - Search Persistence
+
+ ///
+ /// 현재 탭의 검색어를 저장합니다.
+ ///
+ private void SaveCurrentSearchQuery()
+ {
+ if (_propertyList == null) return;
+
+ var currentQuery = _propertyList.SearchQuery;
+ if (!string.IsNullOrEmpty(currentQuery))
+ _tabSearchQueries[_selectedTabIndex] = currentQuery;
+ else
+ _tabSearchQueries.Remove(_selectedTabIndex);
+ }
+
+ ///
+ /// 지정된 탭의 저장된 검색어를 복원합니다.
+ ///
+ private void RestoreSearchQuery(int tabIndex)
+ {
+ if (_propertyList == null) return;
+
+ if (_tabSearchQueries.TryGetValue(tabIndex, out var savedQuery) && !string.IsNullOrEmpty(savedQuery))
+ _propertyList.ApplySearch(savedQuery);
+ else
+ _propertyList.ApplySearch(string.Empty);
+ }
+ #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);
+ }
+
+ // TabView 이벤트 해제 및 정리
+ if (_tabView != null)
+ {
+ _tabView.OnTabChanged -= OnTabViewTabChanged;
+ _tabView.Dispose();
+ }
+
+ // PropertyList 정리
+ _propertyList?.Dispose();
+ _propertyList = null;
+
+ // 이벤트 정리
+ OnCloseClicked = null;
+ OnTabChanged = null;
+
+ // 데이터 정리
+ _tabDataList.Clear();
+ _tabSearchQueries.Clear();
+
+ // UI 참조 정리
+ _header = null;
+ _titleLabel = null;
+ _closeButton = null;
+ _tabView = null;
+ }
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs.meta b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs.meta
new file mode 100644
index 00000000..da83c5fb
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyTabListWindow.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7d88092ac39914443bea15b2fd68853d
\ No newline at end of file