3 Commits

Author SHA1 Message Date
logonkhi
9d02afd8e8 UTKPropertyTabListWindow 개발 중. UTKTabView Tab 버튼에 아이콘 설정 할 수 있게 기능 추가해야 함 2026-02-20 19:56:23 +09:00
logonkhi
b64c3e10bc UTKToolBar 완료 2026-02-20 19:17:36 +09:00
logonkhi
ad10e24d13 UTKReorableList 개발 중 2026-02-19 20:08:57 +09:00
55 changed files with 3557 additions and 182 deletions

View File

@@ -33,7 +33,9 @@
"Bash(cmd /c \"mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Data\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Items\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\ToolBar\"\"\")",
"Bash(/bin/ls:*)",
"Bash(/bin/mkdir -p:*)",
"Bash(/bin/rm:*)"
"Bash(/bin/rm:*)",
"WebFetch(domain:docs.unity3d.com)",
"Bash(ls:*)"
],
"deny": [],
"ask": []

View File

@@ -12,7 +12,6 @@
.utk-checkbox {
flex-direction: row;
align-items: center;
align-self: flex-start;
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
}

View File

@@ -20,7 +20,7 @@
-->
<ui:VisualElement name="group-header" class="utk-property-group__header">
<utk:UTKLabel name="expand-icon" class="utk-property-group__expand-icon" />
<utk:UTKLabel name="group-title" class="utk-property-group__title" />
<utk:UTKLabel name="group-title" class="utk-property-group__title" size="Label2" is-bold="true" />
<!-- <utk:UTKLabel name="group-count" class="utk-property-group__count" /> -->
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,17 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ui="UnityEngine.UIElements"
xmlns:utk="UVC.UIToolkit"
noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd"
editor-extension-mode="False">
<!-- USS는 테마 적용을 위해 C# 코드에서 로드합니다 (UXML에서 지정하지 않음) -->
<ui:VisualElement name="item-container" class="reordable-list-item">
<!-- 드래그 핸들 아이콘 (Material Icon: DragIndicator) -->
<utk:UTKLabel name="drag-handle" class="reordable-list-item__drag-handle" />
<!-- 사용 유무 체크박스 -->
<utk:UTKCheckBox name="active-checkbox" class="reordable-list-item__checkbox" />
<!-- 내용 표시/수정 입력 필드 -->
<utk:UTKInputField name="display-text-field" class="reordable-list-item__input" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 83ba2c4f240fbfb44a4ed4acd25aa5ce
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,57 @@
/*
* ===================================
* UTKReordableListItemUss.uss
* 재정렬 가능 리스트 아이템 스타일
* ===================================
*/
/* ===================================
Item Container
=================================== */
.reordable-list-item {
flex-direction: row;
align-items: center;
padding: var(--space-s) var(--space-m);
min-height: 36px;
flex-grow: 1;
}
/* ===================================
Drag Handle
=================================== */
.reordable-list-item__drag-handle {
width: 24px;
height: 24px;
-unity-font-definition: resource('Fonts/Icons/MaterialSymbolsOutlined');
font-size: 18px;
color: var(--color-text-secondary);
-unity-text-align: middle-center;
flex-shrink: 0;
margin-right: var(--space-xs);
cursor: move;
padding: 0;
}
.reordable-list-item__drag-handle:hover {
color: var(--color-text-primary);
}
/* ===================================
Checkbox
=================================== */
.reordable-list-item__checkbox {
flex-shrink: 0;
margin-right: var(--space-s);
}
/* ===================================
Input Field
=================================== */
.reordable-list-item__input {
flex-grow: 1;
min-width: 0;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 18f971ae90e8e1c4bad0afcc31efe1d1
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,61 @@
/*
* ===================================
* UTKReordableListUss.uss
* 재정렬 가능 리스트 컨테이너 스타일
* ===================================
*/
/* ===================================
Base Container
=================================== */
.reordable-list {
background-color: var(--color-bg-secondary);
border-radius: var(--radius-s);
border-width: var(--border-width);
border-color: var(--color-border);
padding-top: var(--space-s);
padding-bottom: var(--space-s);
}
/* ===================================
ListView 내부 여백 조정
=================================== */
.reordable-list .utk-listview {
flex-grow: 1;
border-width: 0;
border-radius: 0;
background-color: transparent;
}
/* ===================================
드래그 중 아이템 스타일 (Unity 내장 클래스)
=================================== */
.reordable-list .unity-list-view__reorderable-item {
justify-content: center;
}
.reordable-list .unity-list-view__reorderable-item__container {
flex-direction: row;
align-items: center;
padding-left: 0;
padding-right: 0;
}
/* ===================================
드래그 핸들 바 (Unity 내장 reorder handle)
커스텀 드래그 핸들 사용하므로 기본 숨김
=================================== */
.reordable-list .unity-list-view__reorderable-handle {
display: none;
width: 0;
min-width: 0;
max-width: 0;
}
.reordable-list .unity-list-view__reorderable-handle-bar {
display: none;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebeda1896b5d02d489342ed0c4e45698
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 62b1c6b38fe308748b808fb2b576e6e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
/*
* ===================================
* UTKSettingDisplayInfoTabViewUss.uss
* 설정 표시 정보 탭 뷰 컨테이너 스타일
* ===================================
*/
/* ===================================
Base Container
=================================== */
.setting-display-tab-view {
flex-grow: 1;
background-color: var(--color-bg-secondary);
border-radius: var(--radius-s);
border-width: var(--border-width);
border-color: var(--color-border);
}
/* ===================================
ListView 내부 여백 조정
=================================== */
.setting-display-tab-view .utk-listview {
flex-grow: 1;
border-width: 0;
border-radius: 0;
background-color: transparent;
}
/* ===================================
드래그 중 아이템 스타일 (Unity 내장 클래스)
=================================== */
.setting-display-tab-view .unity-list-view__reorderable-item__container {
flex-direction: row;
align-items: center;
}
/* ===================================
드래그 핸들 바 (Unity 내장 reorder handle)
커스텀 드래그 핸들 사용하므로 기본 숨김
=================================== */
.setting-display-tab-view .unity-list-view__reorderable-handle-bar {
display: none;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 360ccda715f15bc479dec409d0ed87cf
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -11,6 +11,11 @@
<VisualElement name="property-list-window-container" class="utk-window-sample-container" style="width: 380px;" />
</VisualElement>
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="UTKPropertyTabListWindow" />
<VisualElement name="property-tab-list-window-container" class="utk-window-sample-container" style="width: 380px;" />
</VisualElement>
<!-- Code Sample -->
<VisualElement class="utk-code-sample-container">
<utk:UTKCodeBlock name="code-csharp" title="C#" />

View File

@@ -159,6 +159,7 @@ ListView/TreeView 항목 텍스트 스타일
.unity-list-view__item .unity-text-element {
color: var(--color-text-primary) ;
font-size: var(--font-size-body2) ;
margin: 0;
}
.unity-collection-view__item--selected,

View File

@@ -11,7 +11,7 @@
.utk-tabview {
flex-direction: column;
flex-grow: 1;
flex-grow: 0;
color: var(--color-text-secondary);
}

View File

@@ -1,6 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="button-root" class="utk-toolbar-btn">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
<utk:UTKLabel name="icon" class="utk-toolbar-btn__icon" />
<utk:UTKLabel name="label" class="utk-toolbar-btn__label" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -51,7 +51,6 @@
font-size: var(--font-size-label4);
color: var(--color-text-secondary);
-unity-text-align: upper-center;
margin-top: 1px;
display: none;
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
}

View File

@@ -1,7 +1,7 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="button-root" class="utk-toolbar-btn utk-toolbar-expandable">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
<utk:UTKLabel name="icon" class="utk-toolbar-btn__icon" />
<utk:UTKLabel name="label" class="utk-toolbar-btn__label" />
<ui:VisualElement name="arrow" class="utk-toolbar-expandable__arrow" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -10,25 +10,31 @@
border-color: var(--color-border);
border-radius: var(--radius-m);
padding: var(--space-xs);
min-width: 120px;
min-width: 40px;
}
.utk-toolbar-submenu__container {
flex-direction: column;
}
.utk-toolbar-submenu TemplateContainer {
flex-grow: 1;
align-items: stretch;
}
/* 서브 메뉴 내 버튼은 가로로 펼침 */
.utk-toolbar-submenu .utk-toolbar-btn {
flex-direction: row;
min-width: 100px;
min-width: 28px;
min-height: 28px;
justify-content: flex-start;
padding: var(--space-xs) var(--space-m);
padding: var(--space-xs) 0;
margin: 1px 0;
flex-grow: 1;
}
.utk-toolbar-submenu .utk-toolbar-btn__icon {
margin-right: var(--space-s);
flex-grow: 1;
}
.utk-toolbar-submenu .utk-toolbar-btn__label {
@@ -36,4 +42,7 @@
font-size: var(--font-size-body2);
color: var(--color-text-primary);
-unity-text-align: middle-left;
margin-right: var(--space-m);
flex-grow: 100;
justify-content: flex-start;
}

View File

@@ -1,6 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="button-root" class="utk-toolbar-btn utk-toolbar-toggle">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
<utk:UTKLabel name="icon" class="utk-toolbar-btn__icon" />
<utk:UTKLabel name="label" class="utk-toolbar-btn__label" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
<VisualElement name="window-root" class="utk-property-tab-window">
<VisualElement name="header" class="utk-property-tab-window__header">
<utk:UTKLabel name="title" class="utk-property-tab-window__title" />
<utk:UTKButton name="close-btn" class="utk-property-tab-window__close-btn" variant="Text" icon-only="true" />
</VisualElement>
<utk:UTKTabView name="tab-view" class="utk-property-tab-window__tab-view" />
<utk:UTKPropertyList name="content" class="utk-property-tab-window__content" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 22934f67f61d09a419d467bdcc086cfb
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -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; /* 스크롤바 여유 공간 */
}

View File

@@ -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

View File

@@ -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<UTKToggle>("toggle");
if (toggle == null)
{
Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다.");
return;
}
_themeToggle = toggle;
var window = _uiDocument.rootVisualElement.Q<UTKPropertyListWindow>("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<UTKPropertyListWindow>("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<UTKPropertyTabListWindow>("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);
/// <summary>
/// UTKPropertyTabListWindow 샘플 데이터 설정
/// 탭별로 서로 다른 데이터 타입(Flat/Grouped/Mixed)을 보여줍니다.
/// </summary>
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<IUTKPropertyGroup>();
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<string> { "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<IUTKPropertyGroup>();
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<string> { "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<IUTKPropertyItem>
{
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<string> { "Static", "Batching", "Navigation", "Occluder", "Occludee" },
new List<string> { "Static", "Batching" }),
new UTKRadioPropertyItem("tab_quality", "품질",
new List<string> { "Low", "Medium", "High", "Ultra" }, 2),
};
advancedTab.SetFlatData(advancedItems);
// === 탭 4: 일정 (Mixed) ===
var scheduleTab = new TabPropertyData("일정", UTKMaterialIcons.CalendarMonth);
var scheduleEntries = new List<IUTKPropertyEntry>();
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<TabPropertyData>
{
basicTab,
appearanceTab,
advancedTab,
scheduleTab
});
}
private List<IUTKPropertyEntry> CreateSampleEntries()
@@ -621,6 +730,8 @@ namespace UVC.Sample.UIToolkit
{
_propertyWindow?.Dispose();
_propertyWindow = null;
_propertyTabWindow?.Dispose();
_propertyTabWindow = null;
}
// 샘플 열거형

View File

@@ -1,6 +1,7 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<VisualElement style="width: 100%; height: 100%;">
<utk:UTKPropertyListWindow name="window" />
<utk:UTKPropertyTabListWindow name="tabWindow" />
<utk:UTKToggle name="toggle" label="테마 변경" style="position: absolute; top: 10px; left: 10px;" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,497 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &1097328750
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1097328752}
- component: {fileID: 1097328754}
- component: {fileID: 1097328755}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1097328752
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1097328754
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 4bb0d1734d5c1b647ae0ffdb7879a92a, type: 3}
m_SortingOrder: 0
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!114 &1097328755
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4274ed098fc4bf048bb92836e8982c8f, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UTKReordableListSample
uiDocument: {fileID: 1097328754}
initialTheme: 0
--- !u!1 &1331954412
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1331954415}
- component: {fileID: 1331954414}
- component: {fileID: 1331954413}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1331954413
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_MoveRepeatDelay: 0.5
m_MoveRepeatRate: 0.1
m_XRTrackingOrigin: {fileID: 0}
m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_DeselectOnBackgroundClick: 0
m_PointerBehavior: 0
m_CursorLockBehavior: 0
m_ScrollDeltaPerTick: 6
--- !u!114 &1331954414
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1331954415
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1414861612
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1414861614}
- component: {fileID: 1414861613}
- component: {fileID: 1414861615}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &1414861613
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
serializedVersion: 11
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &1414861614
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!114 &1414861615
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &2136621999
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2136622002}
- component: {fileID: 2136622001}
- component: {fileID: 2136622000}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &2136622000
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
--- !u!20 &2136622001
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &2136622002
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 2136622002}
- {fileID: 1414861614}
- {fileID: 1331954415}
- {fileID: 1097328752}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8b1b2ce854a8a3d47ac1fc46dfaf615f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
/// <summary>
/// UTKReordableList의 기능을 테스트하기 위한 샘플 MonoBehaviour입니다.
/// Dictionary 기반 SetData/ToDictionary, 이벤트 핸들러, 데이터 CRUD를 확인합니다.
/// </summary>
public class UTKReordableListSample : MonoBehaviour
{
[SerializeField]
public UIDocument uiDocument;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle _themeToggle;
private UTKReordableList _reordableList;
void Start()
{
// UIDocument 참조 확인
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("UIDocument가 할당되지 않았습니다.");
return;
}
uiDocument = doc;
var root = uiDocument.rootVisualElement;
// 테마 토글
_themeToggle = root.Q<UTKToggle>("toggle");
if (_themeToggle == null)
{
Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다.");
return;
}
// ReordableList
_reordableList = root.Q<UTKReordableList>("window");
if (_reordableList == null)
{
Debug.LogError("UXML에서 UTKReordableList를 찾을 수 없습니다.");
return;
}
// 테마 초기화
UTKThemeManager.Instance.RegisterRoot(root);
UTKThemeManager.Instance.SetTheme(initialTheme);
_themeToggle.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
};
// 이벤트 핸들러 등록
_reordableList.OnOrderChanged += () => Debug.Log("[Sample] 순서 변경됨");
_reordableList.OnDataChanged += () => Debug.Log("[Sample] 데이터 변경됨");
// 샘플 데이터 설정 (Dictionary 방식)
SetSampleData();
// 하단 버튼 영역 생성
CreateButtons(root);
}
/// <summary>
/// Dictionary 기반으로 샘플 데이터를 설정합니다.
/// </summary>
private void SetSampleData()
{
var listDict = new List<Dictionary<string, string>>
{
new() { ["order"] = "0", ["active"] = "True", ["text"] = "온도" },
new() { ["order"] = "1", ["active"] = "False", ["text"] = "습도" },
new() { ["order"] = "2", ["active"] = "True", ["text"] = "압력" },
new() { ["order"] = "3", ["active"] = "True", ["text"] = "풍속" },
new() { ["order"] = "4", ["active"] = "False", ["text"] = "조도" },
};
_reordableList.SetData(listDict);
}
/// <summary>
/// 테스트 버튼들을 생성합니다.
/// </summary>
private void CreateButtons(VisualElement root)
{
var buttonContainer = new VisualElement();
buttonContainer.style.flexDirection = FlexDirection.Row;
buttonContainer.style.justifyContent = Justify.Center;
buttonContainer.style.paddingTop = 8;
buttonContainer.style.paddingBottom = 8;
// ToDictionary 버튼
var toDictBtn = new UTKButton("ToDictionary", variant: UTKButton.ButtonVariant.Primary);
toDictBtn.OnClicked += OnToDictionaryClicked;
toDictBtn.style.marginRight = 4;
buttonContainer.Add(toDictBtn);
// 데이터 리셋 버튼
var resetBtn = new UTKButton("리셋", variant: UTKButton.ButtonVariant.Normal);
resetBtn.OnClicked += () => SetSampleData();
resetBtn.style.marginRight = 4;
buttonContainer.Add(resetBtn);
// 아이템 추가 버튼
var addBtn = new UTKButton("추가", variant: UTKButton.ButtonVariant.OutlinePrimary);
addBtn.OnClicked += OnAddItemClicked;
buttonContainer.Add(addBtn);
root.Add(buttonContainer);
}
/// <summary>
/// ToDictionary를 호출하여 현재 데이터를 콘솔에 출력합니다.
/// </summary>
private void OnToDictionaryClicked()
{
var result = _reordableList.ToDictionary();
Debug.Log($"[Sample] ToDictionary 결과 ({result.Count}건):");
foreach (var dict in result)
{
Debug.Log($" order={dict["order"]}, active={dict["active"]}, text={dict["text"]}");
}
}
/// <summary>
/// 새 아이템을 추가합니다.
/// </summary>
private void OnAddItemClicked()
{
var currentData = _reordableList.ToDictionary();
var newIndex = currentData.Count;
currentData.Add(new Dictionary<string, string>
{
["order"] = newIndex.ToString(),
["active"] = "True",
["text"] = $"항목 {newIndex}"
});
_reordableList.SetData(currentData);
Debug.Log($"[Sample] 아이템 추가됨 (총 {currentData.Count}건)");
}
private void OnDestroy()
{
_reordableList?.Dispose();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4274ed098fc4bf048bb92836e8982c8f

View File

@@ -0,0 +1,6 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<VisualElement style="width: 100%; height: 100%;">
<utk:UTKReordableList name="window" style="width: 300px; height: 200px;" />
<utk:UTKToggle name="toggle" label="테마 변경" style="position: absolute; top: 10px; right: 10px;" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4bb0d1734d5c1b647ae0ffdb7879a92a
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,497 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &1097328750
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1097328752}
- component: {fileID: 1097328754}
- component: {fileID: 1097328755}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1097328752
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1097328754
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 54e4f33c8b08cb54f97dbdb5edd79e1e, type: 3}
m_SortingOrder: 1
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!114 &1097328755
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 39265a781c40bdb4a90aa56b0fbf44a6, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.Sample.UIToolkit.UTKToolBarSample
_uiDocument: {fileID: 1097328754}
initialTheme: 0
--- !u!1 &1331954412
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1331954415}
- component: {fileID: 1331954414}
- component: {fileID: 1331954413}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1331954413
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_MoveRepeatDelay: 0.5
m_MoveRepeatRate: 0.1
m_XRTrackingOrigin: {fileID: 0}
m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_DeselectOnBackgroundClick: 0
m_PointerBehavior: 0
m_CursorLockBehavior: 0
m_ScrollDeltaPerTick: 6
--- !u!114 &1331954414
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1331954415
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1414861612
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1414861614}
- component: {fileID: 1414861613}
- component: {fileID: 1414861615}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &1414861613
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
serializedVersion: 11
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &1414861614
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!114 &1414861615
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &2136621999
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2136622002}
- component: {fileID: 2136622001}
- component: {fileID: 2136622000}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &2136622000
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
--- !u!20 &2136622001
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &2136622002
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 2136622002}
- {fileID: 1414861614}
- {fileID: 1331954415}
- {fileID: 1097328752}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7cce97e0d24c7794da00973b5ead99be
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
using UVC.Log;
namespace UVC.Sample.UIToolkit
{
/// <summary>
/// UTKSettingModalSample 독립 실행 샘플 코드입니다.
/// </summary>
public class UTKSettingModalSample : MonoBehaviour
{
[SerializeField] private UIDocument? _uiDocument;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle? _themeToggle;
private VisualElement? _root;
private void Start()
{
// UIDocument 참조 확인
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("UIDocument가 할당되지 않았습니다.");
return;
}
_uiDocument = doc;
_root = _uiDocument.rootVisualElement;
UTKThemeManager.Instance.RegisterRoot(_root);
UTKThemeManager.Instance.SetTheme(initialTheme);
// 테마 토글
_themeToggle = _root.Q<UTKToggle>("toggle");
if (_themeToggle != null)
{
_themeToggle.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
};
}
}
private void OnDestroy()
{
ULog.Debug("UTKSettingModalSample 정리 완료");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e88ad13f58976fb4a837242a7e1c8282

View File

@@ -0,0 +1,6 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<VisualElement style="width: 100%; height: 100%; flex-direction: column;">
<VisualElement name="toolbar-area" style="flex-grow: 1; flex-direction: column; padding: 8px;" />
<utk:UTKToggle name="toggle" label="테마 변경" style="position: absolute; top: 8px; right: 10px; z-index: 10;" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6c8eae7ee21b96245b325f08111b214b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -371,14 +371,11 @@ public async UniTask SaveDataAsync()
}
SetCodeSamples(root,
csharpCode: @"// 1. 초기화 (앱 시작 시 한 번)
UTKTooltipManager.Instance.Initialize(rootVisualElement);
// 2. 버튼에 툴팁 연결
csharpCode: @"// 1. 버튼에 툴팁 연결
var saveButton = new UTKButton("""", UTKMaterialIcons.Save);
UTKTooltipManager.Instance.AttachTooltip(saveButton, ""저장 (Ctrl+S)"");
// 3. 다국어 키로 툴팁 연결
// 2. 다국어 키로 툴팁 연결
UTKTooltipManager.Instance.AttachTooltip(settingsButton, ""tooltip_settings"");
// 4. 아이콘 버튼에 툴팁

View File

@@ -205,7 +205,6 @@ public partial class UTKStyleGuideSample : MonoBehaviour
}
UTKThemeManager.Instance.SetTheme(initialTheme);
UTKTooltipManager.Instance.Initialize(_root);
CreateUI();
}

View File

@@ -84,7 +84,7 @@ namespace UVC.Sample.UIToolkit
/// </summary>
private void CreateHorizontalToolBar(VisualElement parent)
{
var label = new Label("Horizontal Toolbar");
var label = new Label("Horizontal Toolbar");
label.style.fontSize = 14;
label.style.marginTop = 8;
label.style.marginBottom = 4;
@@ -93,30 +93,30 @@ namespace UVC.Sample.UIToolkit
_horizontalModel = new UTKToolBarModel();
// Standard
_horizontalModel.AddStandardButton("저장", UTKMaterialIcons.Save, new DebugLogCommand("저장"), "파일 저장");
_horizontalModel.AddStandardButton("실행 취소", UTKMaterialIcons.Undo, new DebugLogCommand("실행 취소"));
_horizontalModel.AddStandardButton("다시 실행", UTKMaterialIcons.Redo, new DebugLogCommand("다시 실행"));
_horizontalModel.AddStandardButton("", UTKMaterialIcons.Save, new DebugLogCommand("저장"), "파일 저장");
_horizontalModel.AddStandardButton("", UTKMaterialIcons.Undo, new DebugLogCommand("실행 취소"));
_horizontalModel.AddStandardButton("", UTKMaterialIcons.Redo, new DebugLogCommand("다시 실행"));
_horizontalModel.AddSeparator();
// Toggle
_horizontalModel.AddToggleButton("그리드", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff, tooltip: "그리드 표시/숨김");
_horizontalModel.AddToggleButton("스냅", false, UTKMaterialIcons.FilterCenterFocus, UTKMaterialIcons.CenterFocusWeak, tooltip: "스냅 활성화");
_horizontalModel.AddToggleButton("", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff, tooltip: "그리드 표시/숨김");
_horizontalModel.AddToggleButton("", false, UTKMaterialIcons.FilterCenterFocus, UTKMaterialIcons.CenterFocusWeak, tooltip: "스냅 활성화");
_horizontalModel.AddSeparator();
// Radio
_horizontalModel.AddRadioButton("tool", "선택", true, UTKMaterialIcons.NearMe, tooltip: "선택 도구");
_horizontalModel.AddRadioButton("tool", "이동", false, UTKMaterialIcons.OpenWith, tooltip: "이동 도구");
_horizontalModel.AddRadioButton("tool", "회전", false, UTKMaterialIcons.Refresh, tooltip: "회전 도구");
_horizontalModel.AddRadioButton("tool", "", true, UTKMaterialIcons.NearMe, tooltip: "선택 도구");
_horizontalModel.AddRadioButton("tool", "", false, UTKMaterialIcons.OpenWith, tooltip: "이동 도구");
_horizontalModel.AddRadioButton("tool", "", false, UTKMaterialIcons.Refresh, tooltip: "회전 도구");
_horizontalModel.AddSeparator();
// Expandable
var shapeBtn = _horizontalModel.AddExpandableButton("도형", UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "삼각형", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true });
var shapeBtn = _horizontalModel.AddExpandableButton("", UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true, Tooltip = "사각형 추가" });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true, Tooltip = "원형 추가" });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true, Tooltip = "삼각형 추가" });
_horizontalToolBar = new UTKToolBar();
_horizontalToolBar.Orientation = UTKToolBarOrientation.Horizontal;

View File

@@ -10,17 +10,13 @@ namespace UVC.UIToolkit
{
/// <summary>
/// VisualElement에 툴팁을 설정합니다.
/// UTKTooltipManager가 초기화되어 있어야 합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">툴팁 텍스트 또는 다국어 키</param>
/// <returns>체이닝을 위한 원본 요소</returns>
public static T SetTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.AttachTooltip(element, tooltip);
}
UTKTooltipManager.Instance.AttachTooltip(element, tooltip);
return element;
}
@@ -31,10 +27,7 @@ namespace UVC.UIToolkit
/// <returns>체이닝을 위한 원본 요소</returns>
public static T ClearTooltip<T>(this T element) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.DetachTooltip(element);
}
UTKTooltipManager.Instance.DetachTooltip(element);
return element;
}
@@ -46,10 +39,7 @@ namespace UVC.UIToolkit
/// <returns>체이닝을 위한 원본 요소</returns>
public static T UpdateTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.UpdateTooltip(element, tooltip);
}
UTKTooltipManager.Instance.UpdateTooltip(element, tooltip);
return element;
}
}

View File

@@ -12,6 +12,7 @@ namespace UVC.UIToolkit
/// <summary>
/// UIToolkit 기반 툴팁 매니저.
/// VisualElement에 마우스 오버 시 툴팁을 표시하는 싱글톤 관리자입니다.
/// panel.visualTree를 사용하여 모든 UI 위에 툴팁을 표시합니다.
/// </summary>
/// <remarks>
/// <para><b>Tooltip(툴팁)이란?</b></para>
@@ -25,7 +26,7 @@ namespace UVC.UIToolkit
/// <para>
/// UTKTooltipManager는 싱글톤으로 구현되어 있습니다.
/// <c>UTKTooltipManager.Instance</c>로 접근하며, 앱 전체에서 하나의 툴팁 UI를 공유합니다.
/// 사용 전에 반드시 <c>Initialize(root)</c>를 호출해야 합니다.
/// panel.visualTree를 사용하므로 별도 Initialize 호출이 필요 없습니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
@@ -38,7 +39,6 @@ namespace UVC.UIToolkit
///
/// <para><b>주요 메서드:</b></para>
/// <list type="bullet">
/// <item><description><c>Initialize(root)</c> - 초기화 (루트 요소 지정)</description></item>
/// <item><description><c>AttachTooltip(element, text)</c> - 요소에 툴팁 연결</description></item>
/// <item><description><c>DetachTooltip(element)</c> - 툴팁 제거</description></item>
/// <item><description><c>Show(text, position)</c> - 즉시 표시</description></item>
@@ -56,20 +56,17 @@ namespace UVC.UIToolkit
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 1. 초기화 (앱 시작 시 한 번)
/// UTKTooltipManager.Instance.Initialize(rootVisualElement);
///
/// // 2. 버튼에 툴팁 연결
/// // 1. 버튼에 툴팁 연결 (Initialize 불필요)
/// var saveButton = new UTKButton("", UTKMaterialIcons.Save);
/// UTKTooltipManager.Instance.AttachTooltip(saveButton, "저장 (Ctrl+S)");
///
/// // 3. 다국어 키로 툴팁 연결
/// // 2. 다국어 키로 툴팁 연결
/// UTKTooltipManager.Instance.AttachTooltip(settingsButton, "tooltip_settings");
///
/// // 4. 툴팁 업데이트
/// // 3. 툴팁 업데이트
/// UTKTooltipManager.Instance.UpdateTooltip(button, "새로운 설명");
///
/// // 5. 툴팁 제거
/// // 4. 툴팁 제거
/// UTKTooltipManager.Instance.DetachTooltip(button);
/// </code>
/// </example>
@@ -90,12 +87,11 @@ namespace UVC.UIToolkit
#endregion
#region Fields
private VisualElement? _root;
private VisualElement? _tooltipContainer;
private Label? _tooltipLabel;
private bool _isInitialized;
private bool _isVisible;
private bool _disposed;
private StyleSheet? _loadedUss;
private CancellationTokenSource? _showDelayCts;
private readonly Dictionary<VisualElement, string> _tooltipRegistry = new();
@@ -105,24 +101,19 @@ namespace UVC.UIToolkit
#endregion
#region Properties
public bool IsInitialized => _isInitialized;
public bool IsVisible => _isVisible;
#endregion
#region Initialization
/// <summary>
/// 툴팁 매니저를 초기화합니다.
/// 툴팁 UI를 생성합니다 (아직 visual tree에 추가하지 않음).
/// </summary>
/// <param name="root">VisualElement 트리의 루트</param>
public void Initialize(VisualElement root)
private void EnsureTooltipUI()
{
if (_isInitialized)
{
Debug.LogWarning("[UTKTooltipManager] Already initialized.");
return;
}
if (_tooltipContainer != null) return;
_root = root;
// USS 로드
_loadedUss = Resources.Load<StyleSheet>(USS_PATH);
// UXML 로드 시도
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
@@ -143,13 +134,11 @@ namespace UVC.UIToolkit
_tooltipContainer.style.position = Position.Absolute;
_tooltipContainer.style.display = DisplayStyle.None;
_tooltipContainer.pickingMode = PickingMode.Ignore;
_root.Add(_tooltipContainer);
}
// 테마 변경 이벤트 구독
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
_isInitialized = true;
}
/// <summary>
@@ -174,16 +163,6 @@ namespace UVC.UIToolkit
pickingMode = PickingMode.Ignore
};
// 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
// USS 스타일시트 로드
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
_tooltipContainer.styleSheets.Add(uss);
}
// USS 클래스로 스타일 적용
_tooltipContainer.AddToClassList("utk-tooltip-container");
@@ -196,6 +175,33 @@ namespace UVC.UIToolkit
_tooltipContainer.Add(_tooltipLabel);
}
/// <summary>
/// 툴팁 컨테이너를 대상 요소의 panel.visualTree에 추가합니다.
/// </summary>
/// <param name="element">대상 요소 (panel 접근용)</param>
private void AttachToPanel(VisualElement element)
{
if (_tooltipContainer == null || element.panel == null) return;
var visualTree = element.panel.visualTree;
// 이미 해당 visualTree에 추가되어 있으면 스킵
if (_tooltipContainer.parent == visualTree) return;
// 다른 곳에 붙어 있으면 제거
_tooltipContainer.RemoveFromHierarchy();
// panel.visualTree에 추가
visualTree.Add(_tooltipContainer);
// 테마/USS 재적용
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
if (_loadedUss != null)
{
_tooltipContainer.styleSheets.Add(_loadedUss);
}
}
#endregion
#region Public Methods
@@ -203,10 +209,10 @@ namespace UVC.UIToolkit
/// 툴팁을 즉시 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
/// <param name="position">월드 좌표</param>
public void Show(string text, Vector2 position)
{
if (!_isInitialized || _tooltipContainer == null || _tooltipLabel == null)
if (_tooltipContainer == null || _tooltipLabel == null)
return;
// 다국어 처리
@@ -230,7 +236,7 @@ namespace UVC.UIToolkit
/// 지연 후 툴팁을 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
/// <param name="position">월드 좌표</param>
/// <param name="delayMs">지연 시간 (밀리초)</param>
public async UniTaskVoid ShowDelayed(string text, Vector2 position, int delayMs = SHOW_DELAY_MS)
{
@@ -275,21 +281,22 @@ namespace UVC.UIToolkit
// 기존 등록 제거
DetachTooltip(element);
// 툴팁 UI 생성 보장
EnsureTooltipUI();
_tooltipRegistry[element] = tooltip;
// 이벤트 콜백 생성 및 등록
// 참고: evt.position은 로컬 좌표이므로, 패널 기준 좌표로 변환 필요
EventCallback<PointerEnterEvent> enterCallback = evt =>
{
if (_tooltipRegistry.TryGetValue(element, out var text))
{
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
ShowDelayed(text, rootPosition).Forget();
// panel.visualTree에 툴팁 컨테이너 추가
AttachToPanel(element);
// worldBound 기준 좌표 사용
var worldPos = element.LocalToWorld(evt.localPosition);
ShowDelayed(text, worldPos).Forget();
}
};
@@ -299,13 +306,9 @@ namespace UVC.UIToolkit
{
if (_isVisible)
{
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
AdjustPosition(rootPosition);
// worldBound 기준 좌표 사용
var worldPos = element.LocalToWorld(evt.localPosition);
AdjustPosition(worldPos);
}
};
@@ -406,37 +409,39 @@ namespace UVC.UIToolkit
}
/// <summary>
/// 화면 경계 내에서 위치 조정
/// 화면 경계 내에서 위치 조정 (월드 좌표 기준)
/// </summary>
private void AdjustPosition(Vector2 mousePosition)
private void AdjustPosition(Vector2 worldPosition)
{
if (_tooltipContainer == null || _root == null)
if (_tooltipContainer == null || _tooltipContainer.panel == null)
return;
var panelRoot = _tooltipContainer.panel.visualTree;
var tooltipSize = new Vector2(
_tooltipContainer.resolvedStyle.width,
_tooltipContainer.resolvedStyle.height
);
var rootSize = new Vector2(
_root.resolvedStyle.width,
_root.resolvedStyle.height
var panelSize = new Vector2(
panelRoot.resolvedStyle.width,
panelRoot.resolvedStyle.height
);
// 기본 위치: 마우스 오른쪽 아래
float x = mousePosition.x + POSITION_OFFSET;
float y = mousePosition.y + POSITION_OFFSET;
float x = worldPosition.x + POSITION_OFFSET;
float y = worldPosition.y + POSITION_OFFSET;
// 오른쪽 경계 체크
if (x + tooltipSize.x > rootSize.x)
if (x + tooltipSize.x > panelSize.x)
{
x = mousePosition.x - tooltipSize.x - POSITION_OFFSET;
x = worldPosition.x - tooltipSize.x - POSITION_OFFSET;
}
// 아래쪽 경계 체크
if (y + tooltipSize.y > rootSize.y)
if (y + tooltipSize.y > panelSize.y)
{
y = mousePosition.y - tooltipSize.y - POSITION_OFFSET;
y = worldPosition.y - tooltipSize.y - POSITION_OFFSET;
}
// 왼쪽/위쪽 경계 체크
@@ -476,9 +481,7 @@ namespace UVC.UIToolkit
_tooltipContainer?.RemoveFromHierarchy();
_tooltipContainer = null;
_tooltipLabel = null;
_root = null;
_isInitialized = false;
_isVisible = false;
_instance = null;
}

View File

@@ -4,6 +4,7 @@ using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Extention;
namespace UVC.UIToolkit
{
@@ -233,6 +234,7 @@ namespace UVC.UIToolkit
if (_label != null)
{
_label.selection.isSelectable = value;
_label.pickingMode = value ? PickingMode.Position : PickingMode.Ignore;
}
}
}
@@ -456,7 +458,7 @@ namespace UVC.UIToolkit
{
AddToClassList("utk-label");
_label = new Label { name = "label" };
_label = new Label { name = "label", pickingMode = PickingMode.Ignore };
_label.AddToClassList("utk-label__text");
Add(_label);
@@ -621,14 +623,14 @@ namespace UVC.UIToolkit
// 아이콘과 텍스트 사이의 간격을 적용
if (_iconLabel != null)
{
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
}
if (_imageIcon != null)
{
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
}
}
@@ -669,7 +671,7 @@ namespace UVC.UIToolkit
_iconLabel.style.display = DisplayStyle.Flex;
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetEffectiveIconSize());
}
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
@@ -693,6 +695,7 @@ namespace UVC.UIToolkit
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetEffectiveIconSize());
}
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
@@ -816,6 +819,7 @@ namespace UVC.UIToolkit
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();

View File

@@ -72,6 +72,18 @@ namespace UVC.UIToolkit
public event Action<string, string>? OnPropertyButtonClicked;
#endregion
#region Properties
/// <summary>
/// 현재 검색어를 가져오거나 설정합니다.
/// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다.
/// </summary>
public string SearchQuery
{
get => _searchField?.value ?? string.Empty;
set { if (_searchField != null) _searchField.value = value; }
}
#endregion
#region Constructor
public UTKPropertyList()
{
@@ -874,7 +886,8 @@ namespace UVC.UIToolkit
title = new UTKLabel();
title.AddToClassList("utk-property-group__title");
title.Size = UTKLabel.LabelSize.Label2;
title.IsBold = true;
// count = new UTKLabel();
// count.AddToClassList("utk-property-group__count");
@@ -887,8 +900,7 @@ namespace UVC.UIToolkit
// 데이터 바인딩
expandIcon.SetMaterialIcon(group.IsExpanded ? UTKMaterialIcons.ExpandMore : UTKMaterialIcons.ChevronRight, 16);
title.Text = group.GroupName;
title.Size = UTKLabel.LabelSize.Label1;
title.IsBold = true;
// count.Text = $"({group.ItemCount})";
// count.Variant = UTKLabel.LabelVariant.Secondary;
@@ -985,6 +997,28 @@ namespace UVC.UIToolkit
}
}
/// <summary>
/// 현재 검색 필드의 값으로 검색을 실행합니다.
/// </summary>
public void ApplySearch()
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// 지정된 검색어로 검색을 실행합니다.
/// 검색 필드의 값도 함께 업데이트됩니다.
/// </summary>
/// <param name="query">검색어</param>
public void ApplySearch(string query)
{
if (_searchField != null)
{
_searchField.value = query;
}
OnSearch(query);
}
private void OnSearch(string newValue)
{
_searchText = newValue ?? string.Empty;

View File

@@ -0,0 +1,407 @@
#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.selectionType = SelectionType.None; // 선택 비활성화 (체크박스 사용)
_listView.itemIndexChanged += OnItemIndexChanged;
// Unity 내장 reorderable-handle 숨김 (CSS 선택자가 매칭되지 않는 경우 대비)
_listView.RegisterCallback<AttachToPanelEvent>(_ =>
{
_listView.schedule.Execute(() => HideBuiltInHandles()).ExecuteLater(50);
});
Add(_listView);
}
/// <summary>
/// Unity ListView 내장 드래그 핸들을 숨깁니다.
/// 커스텀 드래그 핸들(DragIndicator 아이콘)을 사용하므로 내장 핸들은 불필요합니다.
/// </summary>
private void HideBuiltInHandles()
{
if (_listView == null) return;
_listView.Query(className: "unity-list-view__reorderable-handle").ForEach(el =>
{
el.style.display = DisplayStyle.None;
el.style.width = 0;
});
}
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<UTKLabel>("drag-handle");
if (handle != null)
handle.SetMaterialIcon(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 UTKLabel("", 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;
// ListView가 makeItem 반환 후 inline flex-grow:0을 강제하므로 bindItem에서 덮어씀
element.style.flexGrow = 1;
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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fed060569bd98e748b45857977376b63

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9210d67d228d2814eb32cc841227db5b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 설정 표시 정보 탭 뷰.
/// UTKReordableList를 사용하여 표시 항목의 순서, 사용 유무, 내용을 관리합니다.
/// </summary>
[UxmlElement]
public partial class UTKSettingDisplayInfoTabView : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/Setting/UTKSettingDisplayInfoTabViewUss";
#endregion
#region Fields
private bool _disposed;
private UTKReordableList? _reordableList;
#endregion
#region Events
/// <summary>순서 변경 시 발생</summary>
public event Action? OnOrderChanged;
/// <summary>데이터(체크/텍스트) 변경 시 발생</summary>
public event Action? OnDataChanged;
#endregion
#region Constructor
public UTKSettingDisplayInfoTabView() : 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();
SampleSetAndGetWithDictionary();
}
#endregion
#region Setup
private void CreateUI()
{
AddToClassList("setting-display-tab-view");
_reordableList = new UTKReordableList();
_reordableList.style.flexGrow = 1;
_reordableList.OnOrderChanged += () => OnOrderChanged?.Invoke();
_reordableList.OnDataChanged += () => OnDataChanged?.Invoke();
Add(_reordableList);
// 하단 버튼 영역
var buttonContainer = new VisualElement();
buttonContainer.style.flexDirection = FlexDirection.Row;
buttonContainer.style.justifyContent = Justify.FlexEnd;
buttonContainer.style.paddingTop = 8;
var saveBtn = new UTKButton("저장", variant: UTKButton.ButtonVariant.Primary);
saveBtn.OnClicked += OnSaveButtonClicked;
buttonContainer.Add(saveBtn);
Add(buttonContainer);
}
private void OnSaveButtonClicked()
{
List<Dictionary<string, string>> result = ToDictionary();
foreach (var dict in result)
{
Debug.Log($"order={dict["order"]}, active={dict["active"]}, text={dict["text"]}");
}
}
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 Public API
/// <summary>
/// 데이터를 설정하고 리스트를 갱신합니다.
/// </summary>
/// <param name="items">표시할 아이템 데이터 목록.</param>
public void SetData(List<ReordableListItemData> items)
{
_reordableList?.SetData(items);
}
/// <summary>
/// List&lt;Dictionary&gt;로부터 데이터를 변환하여 설정합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <param name="listDict">변환할 Dictionary 목록.</param>
public void SetData(List<Dictionary<string, string>> listDict)
{
_reordableList?.SetData(listDict);
}
/// <summary>
/// Order 값이 동기화된 데이터 목록을 반환합니다.
/// </summary>
/// <returns>현재 리스트 순서가 반영된 데이터 목록.</returns>
public List<ReordableListItemData> GetData()
{
return _reordableList?.GetData() ?? new List<ReordableListItemData>();
}
/// <summary>
/// 전체 아이템을 List&lt;Dictionary&lt;string, string&gt;&gt;로 변환하여 반환합니다.
/// Dictionary 키: "order" (순서), "active" (사용 유무), "text" (표시 내용)
/// </summary>
/// <returns>각 아이템을 Dictionary로 변환한 목록.</returns>
public List<Dictionary<string, string>> ToDictionary()
{
return _reordableList?.ToDictionary() ?? new List<Dictionary<string, string>>();
}
#endregion
#region Sample
/// <summary>
/// Dictionary 기반 데이터 설정 및 조회 샘플.
/// </summary>
public void SampleSetAndGetWithDictionary()
{
// 1. Dictionary 데이터로 설정
var listDict = new List<Dictionary<string, string>>
{
new() { ["order"] = "0", ["active"] = "True", ["text"] = "온도" },
new() { ["order"] = "1", ["active"] = "False", ["text"] = "습도" },
new() { ["order"] = "2", ["active"] = "True", ["text"] = "압력" },
};
SetData(listDict);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
// ReordableList 정리
_reordableList?.Dispose();
_reordableList = null;
// 이벤트 정리
OnOrderChanged = null;
OnDataChanged = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 69a4b7a0f627e79418a0ff355aab75d4

View File

@@ -0,0 +1,103 @@
#nullable enable
using System.Collections.Generic;
namespace UVC.UIToolkit
{
/// <summary>
/// 탭의 프로퍼티 데이터 로드 유형
/// </summary>
public enum TabPropertyDataType
{
/// <summary>평면 속성 목록 (그룹 없이)</summary>
Flat,
/// <summary>그룹화된 속성 목록</summary>
Grouped,
/// <summary>그룹과 개별 아이템이 혼합된 목록</summary>
Mixed
}
/// <summary>
/// 탭별 프로퍼티 설정 데이터 클래스입니다.
/// 탭의 메타데이터(이름, 아이콘, 활성화 상태)와
/// 해당 탭에 표시할 프로퍼티 데이터를 보유합니다.
/// </summary>
public class TabPropertyData
{
#region Properties
/// <summary>탭 이름 (표시 텍스트)</summary>
public string Name { get; set; } = string.Empty;
/// <summary>탭 아이콘 (Material Icon 유니코드, null이면 아이콘 없음)</summary>
public string? Icon { get; set; }
/// <summary>탭 활성화 상태</summary>
public bool IsEnabled { get; set; } = true;
/// <summary>탭 툴팁</summary>
public string? Tooltip { get; set; }
/// <summary>데이터 로드 유형</summary>
public TabPropertyDataType DataType { get; private set; }
#endregion
#region Data Fields
private List<IUTKPropertyItem>? _flatItems;
private List<IUTKPropertyGroup>? _groupedItems;
private List<IUTKPropertyEntry>? _mixedItems;
#endregion
#region Constructor
/// <summary>기본 생성자</summary>
public TabPropertyData(string name)
{
Name = name;
}
/// <summary>아이콘 포함 생성자</summary>
public TabPropertyData(string name, string? icon) : this(name)
{
Icon = icon;
}
#endregion
#region Data Setters
/// <summary>평면 속성 목록 설정 (Flat 타입)</summary>
public void SetFlatData(List<IUTKPropertyItem> items)
{
DataType = TabPropertyDataType.Flat;
_flatItems = items;
_groupedItems = null;
_mixedItems = null;
}
/// <summary>그룹화된 속성 목록 설정 (Grouped 타입)</summary>
public void SetGroupedData(List<IUTKPropertyGroup> groups)
{
DataType = TabPropertyDataType.Grouped;
_flatItems = null;
_groupedItems = groups;
_mixedItems = null;
}
/// <summary>혼합 속성 목록 설정 (Mixed 타입)</summary>
public void SetMixedData(List<IUTKPropertyEntry> entries)
{
DataType = TabPropertyDataType.Mixed;
_flatItems = null;
_groupedItems = null;
_mixedItems = entries;
}
#endregion
#region Data Getters
/// <summary>평면 데이터 반환 (Flat 타입이 아니면 null)</summary>
public List<IUTKPropertyItem>? GetFlatData() => _flatItems;
/// <summary>그룹화된 데이터 반환 (Grouped 타입이 아니면 null)</summary>
public List<IUTKPropertyGroup>? GetGroupedData() => _groupedItems;
/// <summary>혼합 데이터 반환 (Mixed 타입이 아니면 null)</summary>
public List<IUTKPropertyEntry>? GetMixedData() => _mixedItems;
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3dae65c9584b6444a75fc9835a6333c

View File

@@ -15,10 +15,10 @@ namespace UVC.UIToolkit
#region Fields
/// <summary>아이콘 요소 (Material Icon Label 또는 Image)</summary>
protected Label? _iconLabel;
protected UTKLabel? _iconLabel;
/// <summary>텍스트 라벨</summary>
protected Label? _textLabel;
protected UTKLabel? _textLabel;
/// <summary>루트 버튼 요소</summary>
protected VisualElement? _rootButton;
@@ -103,12 +103,13 @@ namespace UVC.UIToolkit
{
var root = asset.Instantiate();
_rootButton = root.Q<VisualElement>("button-root");
_iconLabel = root.Q<Label>("icon");
_textLabel = root.Q<Label>("label");
_iconLabel = root.Q<UTKLabel>("icon");
_textLabel = root.Q<UTKLabel>("label");
_textLabel.Size = UTKLabel.LabelSize.Caption; // UXML에서 기본 크기를 설정하므로 코드에서 다시 지정
// TemplateContainer가 아이콘 정렬을 방해하지 않도록 설정
root.style.flexGrow = 1;
root.style.alignItems = Align.Center;
root.style.alignItems = Align.Stretch;
root.style.justifyContent = Justify.Center;
Add(root);
@@ -129,12 +130,13 @@ namespace UVC.UIToolkit
_rootButton = new VisualElement();
_rootButton.AddToClassList("utk-toolbar-btn");
_iconLabel = new Label();
_iconLabel = new UTKLabel();
_iconLabel.AddToClassList("utk-toolbar-btn__icon");
_rootButton.Add(_iconLabel);
_textLabel = new Label();
_textLabel = new UTKLabel();
_textLabel.AddToClassList("utk-toolbar-btn__label");
_textLabel.Size = UTKLabel.LabelSize.Caption;
_rootButton.Add(_textLabel);
Add(_rootButton);
@@ -164,6 +166,7 @@ namespace UVC.UIToolkit
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
UpdateTooltip(_data.Tooltip);
}
/// <summary>
@@ -216,29 +219,15 @@ namespace UVC.UIToolkit
if (useMaterialIcon)
{
// Material Icon (폰트 기반)
// Material Icon (폰트 기반) - UTKLabel의 SetMaterialIcon 사용
_iconLabel.RemoveFromClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = iconPath;
_iconLabel.style.backgroundImage = StyleKeyword.None;
_iconLabel.SetMaterialIcon(iconPath);
}
else
{
// 이미지 아이콘
// 이미지 아이콘 - UTKLabel의 SetImageIcon 사용
_iconLabel.AddToClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = "";
var sprite = Resources.Load<Sprite>(iconPath);
if (sprite != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(sprite);
}
else
{
var texture = Resources.Load<Texture2D>(iconPath);
if (texture != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(texture);
}
}
_iconLabel.SetImageIcon(iconPath);
}
}
@@ -250,7 +239,26 @@ namespace UVC.UIToolkit
{
if (_textLabel != null)
{
_textLabel.text = text;
_textLabel.Text = text;
_textLabel.style.display = string.IsNullOrEmpty(text) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
/// <summary>
/// 툴팁을 업데이트합니다.
/// </summary>
/// <param name="tooltipText">툴팁 텍스트</param>
protected void UpdateTooltip(string? tooltipText)
{
if (_rootButton == null) return;
if (string.IsNullOrEmpty(tooltipText))
{
UTKTooltipManager.Instance.DetachTooltip(_rootButton);
}
else
{
UTKTooltipManager.Instance.UpdateTooltip(_rootButton, tooltipText);
}
}
@@ -282,6 +290,7 @@ namespace UVC.UIToolkit
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
UpdateTooltip(_data.Tooltip);
}
#endregion
@@ -354,6 +363,12 @@ namespace UVC.UIToolkit
// 데이터 바인딩 해제
UnbindData();
// 툴팁 해제
if (_rootButton != null)
{
UTKTooltipManager.Instance.DetachTooltip(_rootButton);
}
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);

View File

@@ -0,0 +1,871 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// UTKPropertyList와 UTKTabView를 결합한 탭 기반 프로퍼티 윈도우입니다.
/// 헤더(타이틀 + 닫기 버튼), 탭 뷰, 프로퍼티 리스트로 구성되며,
/// 탭별로 서로 다른 프로퍼티 데이터를 설정할 수 있습니다.
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>윈도우 프레임 (헤더, 타이틀, 닫기 버튼)</item>
/// <item>UTKTabView 기반 탭 전환</item>
/// <item>탭별 프로퍼티 데이터 관리 (Flat/Grouped/Mixed 지원)</item>
/// <item>탭별 검색어 저장/복원</item>
/// <item>선택적 "전체(All)" 탭</item>
/// <item>헤더 드래그로 위치 이동</item>
/// </list>
///
/// <para><b>사용 예 (C#):</b></para>
/// <code>
/// var window = new UTKPropertyTabListWindow("속성 편집기");
/// window.ShowCloseButton = true;
/// window.ShowAllTab = false;
///
/// // 탭 데이터 설정
/// var generalTab = new TabPropertyData("일반");
/// generalTab.SetGroupedData(new List&lt;IUTKPropertyGroup&gt; { transformGroup, renderGroup });
///
/// var advancedTab = new TabPropertyData("고급", "\ue8b8");
/// advancedTab.SetFlatData(new List&lt;IUTKPropertyItem&gt; { debugItem, logItem });
///
/// window.SetTabData(new List&lt;TabPropertyData&gt; { generalTab, advancedTab });
///
/// // 이벤트 구독
/// window.OnTabChanged += (index, data) =&gt; Debug.Log($"탭 변경: {data?.Name}");
/// window.OnPropertyValueChanged += args =&gt; Debug.Log($"{args.PropertyId} = {args.NewValue}");
///
/// root.Add(window);
/// </code>
/// </summary>
[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<TabPropertyData> _tabDataList = new();
private int _selectedTabIndex = ALL_TAB_INDEX;
private bool _showAllTab = true;
// 탭별 검색어 저장
private readonly Dictionary<int, string> _tabSearchQueries = new();
// 드래그 상태
private bool _isDragging;
private Vector2 _dragStartPosition;
private Vector2 _dragStartMousePosition;
// 윈도우 속성
private string _title = "Properties";
private bool _showCloseButton = false;
#endregion
#region Properties
/// <summary>윈도우 타이틀</summary>
[UxmlAttribute("title")]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null)
{
_titleLabel.Text = value;
}
}
}
/// <summary>닫기 버튼 표시 여부</summary>
[UxmlAttribute("show-close-button")]
public bool ShowCloseButton
{
get => _showCloseButton;
set
{
_showCloseButton = value;
if (_closeButton != null)
{
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>"전체(All)" 탭 표시 여부</summary>
[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);
}
}
}
/// <summary>현재 선택된 탭 인덱스 (-1: All 탭)</summary>
public int SelectedTabIndex => _selectedTabIndex;
/// <summary>내부 UTKPropertyList 접근</summary>
public UTKPropertyList PropertyList => _propertyList ??= new UTKPropertyList();
/// <summary>내부 UTKTabView 접근</summary>
public UTKTabView? TabView => _tabView;
#endregion
#region Events
/// <summary>닫기 버튼 클릭 이벤트</summary>
public event Action? OnCloseClicked;
/// <summary>탭 변경 이벤트 (탭 인덱스, TabPropertyData)</summary>
public event Action<int, TabPropertyData?>? OnTabChanged;
/// <summary>속성 값 변경 이벤트 (PropertyList 위임)</summary>
public event Action<UTKPropertyValueChangedEventArgs>? OnPropertyValueChanged
{
add => PropertyList.OnPropertyValueChanged += value;
remove => PropertyList.OnPropertyValueChanged -= value;
}
/// <summary>그룹 펼침/접힘 이벤트 (PropertyList 위임)</summary>
public event Action<IUTKPropertyGroup, bool>? OnGroupExpandedChanged
{
add => PropertyList.OnGroupExpandedChanged += value;
remove => PropertyList.OnGroupExpandedChanged -= value;
}
/// <summary>속성 클릭 이벤트 (PropertyList 위임)</summary>
public event Action<IUTKPropertyItem>? OnPropertyClicked
{
add => PropertyList.OnPropertyClicked += value;
remove => PropertyList.OnPropertyClicked -= value;
}
/// <summary>버튼 클릭 이벤트 (PropertyList 위임)</summary>
public event Action<string, string>? OnPropertyButtonClicked
{
add => PropertyList.OnPropertyButtonClicked += value;
remove => PropertyList.OnPropertyButtonClicked -= value;
}
#endregion
#region Constructor
public UTKPropertyTabListWindow()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
SubscribeToThemeChanges();
var styleSheet = Resources.Load<StyleSheet>(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<VisualTreeAsset>(UXML_PATH);
if (asset != null)
{
CreateUIFromUxml(asset);
}
else
{
CreateUIFallback();
}
// 드래그 이벤트
if (_header != null)
{
_header.RegisterCallback<PointerDownEvent>(OnHeaderPointerDown);
_header.RegisterCallback<PointerMoveEvent>(OnHeaderPointerMove);
_header.RegisterCallback<PointerUpEvent>(OnHeaderPointerUp);
}
// UTKTabView 탭 변경 이벤트 구독
if (_tabView != null)
{
_tabView.OnTabChanged += OnTabViewTabChanged;
}
}
private void CreateUIFromUxml(VisualTreeAsset asset)
{
var root = asset.Instantiate();
var windowRoot = root.Q<VisualElement>("window-root");
if (windowRoot != null)
{
foreach (var child in windowRoot.Children().ToArray())
{
Add(child);
}
}
else
{
Add(root);
}
// 요소 참조 가져오기
_header = this.Q<VisualElement>("header");
_titleLabel = this.Q<UTKLabel>("title");
_closeButton = this.Q<UTKButton>("close-btn");
_tabView = this.Q<UTKTabView>("tab-view");
_propertyList = this.Q<UTKPropertyList>("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
/// <summary>
/// 탭 데이터 목록을 설정합니다.
/// 기존 탭을 모두 제거하고 새로운 탭을 생성합니다.
/// </summary>
/// <param name="tabDataList">탭 설정 데이터 목록</param>
public void SetTabData(List<TabPropertyData> tabDataList)
{
_tabDataList.Clear();
_tabDataList.AddRange(tabDataList);
_tabSearchQueries.Clear();
RebuildTabs();
// 기본 탭 선택
if (_showAllTab)
SelectTab(ALL_TAB_INDEX);
else if (_tabDataList.Count > 0)
SelectTab(0);
}
/// <summary>
/// 탭 데이터를 추가합니다.
/// </summary>
/// <param name="tabData">추가할 탭 데이터</param>
public void AddTabData(TabPropertyData tabData)
{
_tabDataList.Add(tabData);
RebuildTabs();
}
/// <summary>
/// 특정 인덱스의 탭 데이터를 제거합니다.
/// </summary>
/// <param name="index">제거할 탭 인덱스 (0-based)</param>
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--;
}
}
/// <summary>
/// 특정 인덱스의 탭 데이터를 반환합니다.
/// </summary>
/// <param name="index">탭 인덱스 (0-based)</param>
/// <returns>탭 데이터 또는 null</returns>
public TabPropertyData? GetTabData(int index)
{
if (index >= 0 && index < _tabDataList.Count)
return _tabDataList[index];
return null;
}
/// <summary>
/// 탭을 선택하고 해당 데이터를 PropertyList에 로드합니다.
/// </summary>
/// <param name="tabIndex">탭 인덱스 (-1: All 탭, 0+: 개별 탭)</param>
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);
}
/// <summary>
/// 현재 선택된 탭의 데이터를 다시 로드합니다.
/// </summary>
public void RefreshCurrentTab()
{
LoadDataForTab(_selectedTabIndex);
}
#endregion
#region Public Methods - Group Management (PropertyList )
/// <summary>그룹을 추가합니다.</summary>
public void AddGroup(IUTKPropertyGroup group) => PropertyList.AddGroup(group);
/// <summary>지정한 ID의 그룹과 내부 아이템을 모두 제거합니다.</summary>
public void RemoveGroup(string groupId) => PropertyList.RemoveGroup(groupId);
/// <summary>지정한 ID의 그룹을 반환합니다.</summary>
public IUTKPropertyGroup? GetGroup(string groupId) => PropertyList.GetGroup(groupId);
/// <summary>그룹의 펼침/접힘 상태를 설정합니다.</summary>
public void SetGroupExpanded(string groupId, bool expanded) => PropertyList.SetGroupExpanded(groupId, expanded);
/// <summary>그룹의 펼침/접힘 상태를 토글합니다.</summary>
public void ToggleGroupExpanded(string groupId) => PropertyList.ToggleGroupExpanded(groupId);
#endregion
#region Public Methods - Property Management (PropertyList )
/// <summary>최상위 속성 아이템을 추가합니다.</summary>
public void AddProperty(IUTKPropertyItem item) => PropertyList.AddProperty(item);
/// <summary>지정한 그룹에 속성 아이템을 추가합니다.</summary>
public void AddPropertyToGroup(string groupId, IUTKPropertyItem item) => PropertyList.AddPropertyToGroup(groupId, item);
/// <summary>지정한 ID의 속성 아이템을 제거합니다.</summary>
public void RemoveProperty(string itemId) => PropertyList.RemoveProperty(itemId);
/// <summary>지정한 ID의 속성 아이템을 반환합니다.</summary>
public IUTKPropertyItem? GetProperty(string itemId) => PropertyList.GetProperty(itemId);
#endregion
#region Public Methods - Value Management (PropertyList )
/// <summary>속성 값을 변경합니다.</summary>
public void UpdatePropertyValue(string propertyId, object newValue, bool notify = false) => PropertyList.UpdatePropertyValue(propertyId, newValue, notify);
/// <summary>속성 값을 변경합니다. UpdatePropertyValue의 별칭입니다.</summary>
public void SetPropertyValue(string propertyId, object value, bool notify = false) => PropertyList.SetPropertyValue(propertyId, value, notify);
#endregion
#region Public Methods - Visibility & ReadOnly (PropertyList )
/// <summary>속성 아이템의 가시성을 변경합니다.</summary>
public void SetPropertyVisibility(string propertyId, bool visible) => PropertyList.SetPropertyVisibility(propertyId, visible);
/// <summary>여러 속성의 가시성을 일괄 변경합니다.</summary>
public void SetPropertyVisibilityBatch(IEnumerable<(string propertyId, bool visible)> changes) => PropertyList.SetPropertyVisibilityBatch(changes);
/// <summary>그룹의 가시성을 변경합니다.</summary>
public void SetGroupVisibility(string groupId, bool visible) => PropertyList.SetGroupVisibility(groupId, visible);
/// <summary>속성 아이템의 읽기 전용 상태를 변경합니다.</summary>
public void SetPropertyReadOnly(string propertyId, bool isReadOnly) => PropertyList.SetPropertyReadOnly(propertyId, isReadOnly);
/// <summary>그룹 내 모든 아이템의 읽기 전용 상태를 일괄 변경합니다.</summary>
public void SetGroupReadOnly(string groupId, bool isReadOnly) => PropertyList.SetGroupReadOnly(groupId, isReadOnly);
#endregion
#region Public Methods - Utilities (PropertyList )
/// <summary>모든 엔트리(그룹 + 아이템)를 제거하고 초기화합니다.</summary>
public new void Clear() => PropertyList.Clear();
/// <summary>현재 데이터를 기반으로 TreeView를 다시 빌드합니다.</summary>
public void Refresh() => PropertyList.Refresh();
#endregion
#region Public Methods - Window
/// <summary>윈도우를 표시합니다.</summary>
public void Show()
{
style.display = DisplayStyle.Flex;
}
/// <summary>윈도우를 숨깁니다.</summary>
public void Hide()
{
style.display = DisplayStyle.None;
}
/// <summary>윈도우의 위치를 설정합니다 (absolute 포지셔닝).</summary>
public void SetPosition(float x, float y)
{
style.left = x;
style.top = y;
}
/// <summary>윈도우의 크기를 설정합니다.</summary>
public void SetSize(float width, float height)
{
style.width = width;
style.height = height;
}
/// <summary>부모 요소 기준으로 윈도우를 중앙에 배치합니다.</summary>
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
/// <summary>
/// UTKTabView에 탭을 재구성합니다.
/// </summary>
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;
}
/// <summary>
/// UTKTabView의 선택 상태를 동기화합니다.
/// </summary>
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;
}
/// <summary>
/// UTKTabView에서 탭 변경 이벤트 발생 시 처리
/// </summary>
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);
}
/// <summary>
/// 탭에 해당하는 데이터를 UTKPropertyList에 로드합니다.
/// </summary>
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]);
}
}
/// <summary>
/// 단일 탭의 데이터를 UTKPropertyList에 로드합니다.
/// </summary>
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;
}
}
/// <summary>
/// 모든 탭의 데이터를 병합하여 UTKPropertyList에 로드합니다.
/// 병합 전략: Mixed 방식으로 통합 (IUTKPropertyEntry 리스트로 변환)
/// </summary>
private void LoadAllTabData()
{
if (_propertyList == null) return;
var allEntries = new List<IUTKPropertyEntry>();
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
/// <summary>
/// 현재 탭의 검색어를 저장합니다.
/// </summary>
private void SaveCurrentSearchQuery()
{
if (_propertyList == null) return;
var currentQuery = _propertyList.SearchQuery;
if (!string.IsNullOrEmpty(currentQuery))
_tabSearchQueries[_selectedTabIndex] = currentQuery;
else
_tabSearchQueries.Remove(_selectedTabIndex);
}
/// <summary>
/// 지정된 탭의 저장된 검색어를 복원합니다.
/// </summary>
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<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 IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 테마 변경 이벤트 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
// 드래그 이벤트 해제
if (_header != null)
{
_header.UnregisterCallback<PointerDownEvent>(OnHeaderPointerDown);
_header.UnregisterCallback<PointerMoveEvent>(OnHeaderPointerMove);
_header.UnregisterCallback<PointerUpEvent>(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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7d88092ac39914443bea15b2fd68853d