UTKPropertyTabListWindow 개발 중. UTKTabView Tab 버튼에 아이콘 설정 할 수 있게 기능 추가해야 함

This commit is contained in:
logonkhi
2026-02-20 19:56:23 +09:00
parent b64c3e10bc
commit 9d02afd8e8
13 changed files with 1290 additions and 33 deletions

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

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

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

@@ -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()
{
@@ -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,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

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