UTKPropertyItem 개선

This commit is contained in:
logonkhi
2026-02-04 20:31:52 +09:00
parent 8181eae4c6
commit c9af0d2d6f
202 changed files with 8337 additions and 3878 deletions

View File

@@ -466,7 +466,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ClickEvent>(OnClick);
RegisterCallback<KeyDownEvent>(OnKeyDown);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()

View File

@@ -160,7 +160,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ClickEvent>(OnClick);
RegisterCallback<KeyDownEvent>(OnKeyDown);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()

View File

@@ -94,7 +94,7 @@ Assets/Scripts/UVC/UIToolkit/
│ │ ├── UTKVector2PropertyItem.cs
│ │ └── UTKVector3PropertyItem.cs
│ ├── UTKPropertyList.cs # 프로퍼티 리스트 컨테이너
│ └── UTKPropertyWindow.cs # 프로퍼티 윈도우
│ └── UTKPropertyListWindow.cs # 프로퍼티 윈도우
├── Slider/
│ ├── UTKMinMaxSlider.cs # 범위 슬라이더
│ ├── UTKProgressBar.cs # 프로그레스 바
@@ -324,7 +324,7 @@ IUTKPropertyEntry (기본 인터페이스)
```csharp
// 프로퍼티 윈도우 생성
var propertyWindow = new UTKPropertyWindow("설정");
var propertyWindow = new UTKPropertyListWindow("설정");
// 그룹 생성
var generalGroup = new UTKPropertyGroup("일반 설정");
@@ -528,7 +528,7 @@ public void Dispose()
| **버튼** | UTKButton | `SetMaterialIcon`, `SetImageIcon`, `OnClicked` |
| **리스트** | UTKAccordionList | `SetData`, `Search`, `ExpandAll`, `OnItemClick` |
| **트리** | UTKTreeView | `SetData`, `ExpandAll`, `CollapseAll` |
| **프로퍼티** | UTKPropertyWindow | `LoadProperties`, `AddGroup`, `UpdatePropertyValue` |
| **프로퍼티** | UTKPropertyListWindow | `LoadProperties`, `AddGroup`, `UpdatePropertyValue` |
| **모달** | UTKModal | `Show`, `Close`, `AddContent`, `AddToFooter` |
| **이미지** | UTKImage | `LoadAsync`, `SetTexture`, `ClearImage` |
| **입력** | UTK*Field | `Value`, `OnValueChanged` |

View File

@@ -271,7 +271,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ClickEvent>(OnClick);
RegisterCallback<KeyDownEvent>(OnKeyDown);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()

View File

@@ -218,7 +218,7 @@ namespace UVC.UIToolkit
{
OnSubmit?.Invoke(value);
}
});
}, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()

View File

@@ -336,8 +336,7 @@ namespace UVC.UIToolkit
_downButton?.RegisterCallback<ClickEvent>(OnDownButtonClick);
_textField?.RegisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldKeyDown);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldTabKeyDown, TrickleDown.TrickleDown);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
RegisterCallback<MouseEnterEvent>(OnMouseEnter);
RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
@@ -376,11 +375,7 @@ namespace UVC.UIToolkit
Decrement();
evt.StopPropagation();
}
}
private void OnTextFieldTabKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Tab)
else if (evt.keyCode == KeyCode.Tab)
{
if (evt.shiftKey && OnShiftTabPressed != null)
{
@@ -466,8 +461,7 @@ namespace UVC.UIToolkit
_downButton?.UnregisterCallback<ClickEvent>(OnDownButtonClick);
_textField?.UnregisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldKeyDown);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldTabKeyDown);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
UnregisterCallback<MouseEnterEvent>(OnMouseEnter);
UnregisterCallback<MouseLeaveEvent>(OnMouseLeave);

View File

@@ -149,7 +149,7 @@ namespace UVC.UIToolkit
#region UI (UI Component References)
private TextField? _searchField;
private UTKInputField? _searchField;
private UTKButton? _clearButton;
private Label? _searchResultLabel;
private TreeView? _treeView;
@@ -286,15 +286,9 @@ namespace UVC.UIToolkit
}
// UI 요소 참조 획득
_searchField = this.Q<TextField>("search-field");
_searchField = this.Q<UTKInputField>("search-field");
_clearButton = this.Q<UTKButton>("clear-btn");
_searchResultLabel = this.Q<Label>("search-result-label");
// Clear 버튼 아이콘 설정
if (_clearButton != null)
{
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
}
_treeView = this.Q<TreeView>("accordion-tree-view");
// 초기화
@@ -324,8 +318,7 @@ namespace UVC.UIToolkit
// 검색 필드 이벤트 (Enter 키 또는 포커스 잃을 때 검색)
if (_searchField != null)
{
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit += OnSearch;
}
// 검색어 지우기 버튼
@@ -1447,26 +1440,6 @@ namespace UVC.UIToolkit
#region (Search Functionality)
/// <summary>
/// 검색 필드 키 입력 이벤트를 처리합니다.
/// </summary>
private void OnSearchFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
OnSearch(_searchField?.value ?? string.Empty);
evt.StopPropagation();
}
}
/// <summary>
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
/// </summary>
private void OnSearchFieldFocusOut(FocusOutEvent evt)
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// 검색을 실행합니다.
/// </summary>
@@ -1474,13 +1447,15 @@ namespace UVC.UIToolkit
{
if (!EnableSearch) return;
var searchText = query ?? string.Empty;
// 클리어 버튼 가시성 업데이트
if (_clearButton != null)
{
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
_clearButton.style.display = string.IsNullOrEmpty(searchText) ? DisplayStyle.None : DisplayStyle.Flex;
}
PerformSearch(query);
PerformSearch(searchText);
}
private void PerformSearch(string query)
@@ -1729,8 +1704,7 @@ namespace UVC.UIToolkit
// 검색 필드 이벤트 해제
if (_searchField != null)
{
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit -= OnSearch;
}
if (_clearButton != null)

View File

@@ -183,7 +183,7 @@ namespace UVC.UIToolkit
#region UI (UI Component References)
/// <summary>검색어 입력 필드</summary>
private TextField? _searchField;
private UTKInputField? _searchField;
/// <summary>Unity UI Toolkit의 TreeView 컴포넌트</summary>
private TreeView? _treeView;
@@ -324,16 +324,10 @@ namespace UVC.UIToolkit
}
// 3. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
_searchField = this.Q<TextField>("search-field");
_searchField = this.Q<UTKInputField>("search-field");
_treeView = this.Q<TreeView>("main-tree-view");
_clearButton = this.Q<UTKButton>("clear-btn");
// 4. Clear 버튼 아이콘 설정
if (_clearButton != null)
{
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
}
// 5. 이벤트 연결 및 로직 초기화
InitializeLogic();
}
@@ -348,8 +342,7 @@ namespace UVC.UIToolkit
// 검색창 이벤트: Enter 키를 눌렀을 때 또는 포커스를 잃었을 때 필터링 실행
if (_searchField != null)
{
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit += OnSearch;
}
// TreeView 설정
@@ -362,7 +355,7 @@ namespace UVC.UIToolkit
_treeView.bindItem = BindTreeItem;
_treeView.selectionChanged += OnTreeViewSelectionChanged;
_treeView.itemsChosen += OnTreeViewItemsChosen;
_treeView.RegisterCallback<KeyDownEvent>(OnTreeViewKeyDown);
_treeView.RegisterCallback<KeyDownEvent>(OnTreeViewKeyDown, TrickleDown.TrickleDown);
// 펼침/접힘 이벤트 처리
_treeView.itemExpandedChanged += OnTreeViewItemExpandedChanged;
@@ -1470,28 +1463,7 @@ namespace UVC.UIToolkit
#endregion
#region (Search Functionality)
/// <summary>
/// 검색 필드에서 Enter 키를 눌렀을 때 검색을 실행합니다.
/// </summary>
/// <param name="evt">키 입력 이벤트</param>
private void OnSearchFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
OnSearch(_searchField?.value ?? string.Empty);
evt.StopPropagation();
}
}
/// <summary>
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
/// </summary>
/// <param name="evt">포커스 아웃 이벤트</param>
private void OnSearchFieldFocusOut(FocusOutEvent evt)
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// Clear 버튼 클릭 이벤트를 처리합니다.
/// </summary>
@@ -1731,8 +1703,7 @@ namespace UVC.UIToolkit
// 검색 필드 이벤트 해제
if (_searchField != null)
{
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit -= OnSearch;
}
// TreeView 이벤트 핸들러 해제
@@ -1741,7 +1712,7 @@ namespace UVC.UIToolkit
_treeView.selectionChanged -= OnTreeViewSelectionChanged;
_treeView.itemsChosen -= OnTreeViewItemsChosen;
_treeView.itemExpandedChanged -= OnTreeViewItemExpandedChanged;
_treeView.UnregisterCallback<KeyDownEvent>(OnTreeViewKeyDown);
_treeView.UnregisterCallback<KeyDownEvent>(OnTreeViewKeyDown, TrickleDown.TrickleDown);
_treeView.bindItem = null;
_treeView.makeItem = null;
}

View File

@@ -183,7 +183,7 @@ namespace UVC.UIToolkit
#region UI (UI Component References)
/// <summary>검색어 입력 필드</summary>
private TextField? _searchField;
private UTKInputField? _searchField;
/// <summary>Unity UI Toolkit의 ListView 컴포넌트</summary>
private ListView? _listView;
@@ -364,17 +364,11 @@ namespace UVC.UIToolkit
}
// 4. UI 요소 참조 획득
_searchField = this.Q<TextField>("search-field");
_searchField = this.Q<UTKInputField>("search-field");
_listView = this.Q<ListView>("main-list-view");
_clearButton = this.Q<UTKButton>("clear-btn");
_searchResultLabel = this.Q<Label>("search-result-label");
// 5. Clear 버튼 아이콘 설정
if (_clearButton != null)
{
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
}
// 6. 이벤트 연결 및 로직 초기화
InitializeLogic();
}
@@ -391,8 +385,7 @@ namespace UVC.UIToolkit
// 검색 필드 이벤트 등록 (Enter 키 또는 포커스 잃을 때 검색)
if (_searchField != null)
{
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit += OnSearch;
}
// 검색어 지우기 버튼 이벤트 등록
@@ -1140,26 +1133,6 @@ namespace UVC.UIToolkit
#region (Search Functionality)
/// <summary>
/// 검색 필드 키 입력 이벤트를 처리합니다.
/// </summary>
private void OnSearchFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
OnSearch(_searchField?.value ?? string.Empty);
evt.StopPropagation();
}
}
/// <summary>
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
/// </summary>
private void OnSearchFieldFocusOut(FocusOutEvent evt)
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// 검색어 지우기 버튼 클릭 이벤트를 처리합니다.
/// </summary>
@@ -1312,8 +1285,7 @@ namespace UVC.UIToolkit
// 이벤트 핸들러 해제
if (_searchField != null)
{
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit -= OnSearch;
}
if (_clearButton != null)

View File

@@ -8,20 +8,43 @@ using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// UTKPropertyWindow의 핵심 리스트 컴포넌트
/// TreeView 기반으로 그룹화된 속성들을 표시
/// UTKPropertyListWindow의 핵심 리스트 컴포넌트입니다.
/// TreeView 기반으로 그룹화된 속성들을 표시합니다.
///
/// <para><b>개요:</b></para>
/// <para>
/// UTKPropertyList는 UXML/USS 기반의 속성 리스트 컴포넌트입니다.
/// TreeView를 사용하여 가상화(Virtualization)를 지원하며, 대량의 속성을 효율적으로 렌더링합니다.
/// 그룹별 펼침/접힘, 검색 필터링 기능을 제공합니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>TreeView 기반 가상화 (Virtualization) - 대량 속성 성능 최적화</item>
/// <item>그룹별 펼침/접힘 기능</item>
/// <item>실시간 검색 필터링</item>
/// <item>다양한 속성 타입 지원 (Text, Number, Boolean, Dropdown 등)</item>
/// </list>
///
/// <para><b>관련 리소스:</b></para>
/// <list type="bullet">
/// <item>Resources/UIToolkit/List/UTKPropertyList.uxml - 메인 레이아웃</item>
/// <item>Resources/UIToolkit/List/UTKPropertyListUss.uss - 스타일</item>
/// </list>
/// </summary>
[UxmlElement]
public partial class UTKPropertyList : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Property/UTKPropertyCommon";
private const string UXML_PATH = "UIToolkit/List/UTKPropertyList";
private const string USS_PATH = "UIToolkit/List/UTKPropertyListUss";
#endregion
#region Fields
private bool _disposed;
private TreeView? _treeView;
private UTKInputField? _searchField;
private UTKButton? _clearButton;
private readonly List<IUTKPropertyEntry> _entries = new();
private readonly Dictionary<string, IUTKPropertyGroup> _groupIndex = new();
@@ -46,22 +69,60 @@ namespace UVC.UIToolkit
#region Constructor
public UTKPropertyList()
{
// 테마 스타일시트 등록
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 컴포넌트 스타일시트 로드
var styleSheet = Resources.Load<StyleSheet>(USS_PATH);
if (styleSheet != null)
// 메인 UXML 로드
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (visualTree != null)
{
styleSheets.Add(styleSheet);
visualTree.CloneTree(this);
CreateUIFromUxml();
}
else
{
Debug.LogWarning($"[UTKPropertyList] UXML not found at: {UXML_PATH}, using fallback");
CreateUIFallback();
}
CreateUI();
// 테마 적용 및 변경 구독
UTKThemeManager.Instance.ApplyThemeToElement(this);
SubscribeToThemeChanges();
// USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨)
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
// TreeView 초기화
InitializeTreeView();
}
#endregion
#region UI Creation
private void CreateUI()
/// <summary>UXML에서 UI 요소 참조를 획득합니다.</summary>
private void CreateUIFromUxml()
{
_searchField = this.Q<UTKInputField>("search-field");
_treeView = this.Q<TreeView>("property-tree-view");
_clearButton = this.Q<UTKButton>("clear-btn");
// 검색 필드 이벤트 연결
if (_searchField != null)
{
_searchField.OnSubmit += OnSearch;
}
// 검색어 지우기 버튼
if (_clearButton != null)
{
_clearButton.OnClicked += OnClearButtonClicked;
// 초기에는 숨김
_clearButton.style.display = DisplayStyle.None;
}
}
/// <summary>UXML 로드 실패 시 코드로 UI를 생성합니다.</summary>
private void CreateUIFallback()
{
// USS 클래스 기반 스타일링 - 인라인 스타일 최소화
AddToClassList("utk-property-list");
@@ -73,11 +134,16 @@ namespace UVC.UIToolkit
_searchField = new UTKInputField { name = "search-field" };
_searchField.Placeholder = "Search...";
_searchField.AddToClassList("utk-property-list__search-field");
_searchField.OnValueChanged += OnSearchChanged;
_searchField.OnSubmit += OnSearch;
searchContainer.Add(_searchField);
Add(searchContainer);
_clearButton = new UTKButton { name = "clear-btn", Variant = UTKButton.ButtonVariant.Text, Icon = "Close", IconSize = 12, IconOnly = true };
_clearButton.AddToClassList("utk-property-list__search-field-clear-button");
_clearButton.OnClicked += OnClearButtonClicked;
searchContainer.Add(_clearButton);
// TreeView
_treeView = new TreeView { name = "property-tree-view" };
_treeView.AddToClassList("utk-property-list__tree-view");
@@ -85,11 +151,33 @@ namespace UVC.UIToolkit
_treeView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_treeView.showAlternatingRowBackgrounds = AlternatingRowBackground.None;
Add(_treeView);
}
/// <summary>TreeView 콜백을 초기화합니다.</summary>
private void InitializeTreeView()
{
if (_treeView == null) return;
_treeView.makeItem = MakeItem;
_treeView.bindItem = BindItem;
_treeView.unbindItem = UnbindItem;
}
#endregion
Add(_treeView);
#region Theme
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
@@ -481,11 +569,13 @@ namespace UVC.UIToolkit
private void UnbindItem(VisualElement element, int index)
{
var itemData = _treeView?.GetItemDataForIndex<IUTKPropertyEntry>(index);
if (itemData is IUTKPropertyItem item)
// View에서 직접 Unbind 처리 (IUTKPropertyItemView 구현체인 경우)
foreach (var child in element.Children())
{
item.UnbindUI(element);
if (child is IUTKPropertyItemView view)
{
view.Unbind();
}
}
element.Clear();
@@ -523,12 +613,12 @@ namespace UVC.UIToolkit
private void BindPropertyItem(VisualElement container, IUTKPropertyItem item)
{
var itemUI = item.CreateUI();
item.BindUI(itemUI);
// View Factory를 사용하여 View 생성 및 바인딩
var itemView = UTKPropertyItemViewFactory.CreateView(item);
itemUI.RegisterCallback<ClickEvent>(_ => OnPropertyClicked?.Invoke(item));
itemView.RegisterCallback<ClickEvent>(_ => OnPropertyClicked?.Invoke(item));
container.Add(itemUI);
container.Add(itemView);
}
#endregion
@@ -564,11 +654,34 @@ namespace UVC.UIToolkit
OnPropertyValueChanged?.Invoke(args);
}
private void OnSearchChanged(string newValue)
private void OnSearch(string newValue)
{
_searchText = newValue ?? string.Empty;
RefreshTreeView();
// 클리어 버튼 가시성 업데이트
if (_clearButton != null)
{
_clearButton.style.display = string.IsNullOrEmpty(_searchText) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
private void OnClearButtonClicked()
{
if (_searchField != null && !string.IsNullOrEmpty(_searchField.value))
{
_searchField.value = string.Empty;
_searchText = string.Empty;
RefreshTreeView();
// 클리어 버튼 숨김
if (_clearButton != null)
{
_clearButton.style.display = DisplayStyle.None;
}
}
}
#endregion
#region IDisposable
@@ -577,6 +690,9 @@ namespace UVC.UIToolkit
if (_disposed) return;
_disposed = true;
// 테마 변경 이벤트 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
// TreeView 콜백 정리
if (_treeView != null)
{
@@ -585,6 +701,18 @@ namespace UVC.UIToolkit
_treeView.unbindItem = null;
}
// 검색 필드 이벤트 정리
if (_searchField != null)
{
_searchField.OnSubmit -= OnSearch;
}
if (_clearButton != null)
{
_clearButton.OnClicked -= OnClearButtonClicked;
_clearButton.Dispose();
}
// 이벤트 정리
ClearInternal();
@@ -596,6 +724,7 @@ namespace UVC.UIToolkit
// UI 참조 정리
_treeView = null;
_searchField = null;
_clearButton = null;
}
#endregion
}

View File

@@ -220,7 +220,7 @@ namespace UVC.UIToolkit
Close();
evt.StopPropagation();
}
});
}, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()

View File

@@ -1,12 +1,18 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 개별 속성 아이템의 인터페이스
/// 각 PropertyType별 구현에서 이 인터페이스를 상속
/// 개별 속성 아이템의 데이터 인터페이스입니다.
/// UI는 별도의 View 클래스(IUTKPropertyItemView)에서 담당합니다.
///
/// <para><b>View-Data 분리 아키텍처:</b></para>
/// <list type="bullet">
/// <item>Data (IUTKPropertyItem): 속성 값과 메타데이터 관리</item>
/// <item>View (IUTKPropertyItemView): UI 표시 및 사용자 상호작용</item>
/// <item>Factory (UTKPropertyItemViewFactory): Data에 맞는 View 생성</item>
/// </list>
/// </summary>
public interface IUTKPropertyItem : IUTKPropertyEntry
{
@@ -36,18 +42,6 @@ namespace UVC.UIToolkit
/// <summary>값을 설정 (타입 변환 포함)</summary>
void SetValue(object? value);
/// <summary>UI 요소 생성</summary>
VisualElement CreateUI();
/// <summary>UI 요소에 데이터 바인딩</summary>
void BindUI(VisualElement element);
/// <summary>UI 요소 바인딩 해제</summary>
void UnbindUI(VisualElement element);
/// <summary>UI 상태 갱신 (값, ReadOnly, Visible 등)</summary>
void RefreshUI();
}
/// <summary>

View File

@@ -3,7 +3,7 @@
namespace UVC.UIToolkit
{
/// <summary>
/// UTKPropertyWindow에서 지원하는 속성 타입
/// UTKPropertyListWindow에서 지원하는 속성 타입
/// 기존 PropertyType과 동일한 열거형
/// </summary>
public enum UTKPropertyType

View File

@@ -1,52 +1,60 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 모든 PropertyItem의 기본 추상 클래스
/// 공통 기능 및 UIToolkit 바인딩 로직 제공
/// 모든 PropertyItem의 기본 추상 클래스입니다.
/// 순수 데이터 클래스로, UI는 별도의 View 클래스에서 담당합니다.
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>속성 값 관리 (Value, IsReadOnly, IsVisible)</item>
/// <item>값 변경 이벤트 발생</item>
/// <item>메타데이터 관리 (Description, Tooltip, GroupId)</item>
/// </list>
///
/// <para><b>View-Data 분리:</b></para>
/// <list type="bullet">
/// <item>Data (UTKPropertyItemBase): 이 클래스 - 속성 값과 메타데이터</item>
/// <item>View (UTKPropertyItemViewBase): UI 표시 및 사용자 상호작용</item>
/// <item>Factory (UTKPropertyItemViewFactory): Data에 맞는 View 생성</item>
/// </list>
/// </summary>
/// <typeparam name="T">속성 값의 타입</typeparam>
public abstract class UTKPropertyItemBase<T> : IUTKPropertyItem<T>, IDisposable
{
#region Constants
protected const int DEFAULT_DEBOUNCE_MS = 300;
protected const string USS_CLASS_READONLY = "utk-property-item--readonly";
protected const string USS_CLASS_HIDDEN = "utk-property-item--hidden";
protected const string UXML_BASE_PATH = "UIToolkit/Property/Items/";
#endregion
#region Static Cache
private static readonly Dictionary<string, VisualTreeAsset> _uxmlCache = new();
#endregion
#region Fields
private T _value;
protected bool _isReadOnly;
protected bool _isVisible = true;
private bool _isReadOnly;
private bool _isVisible = true;
private string? _description;
private string? _tooltip;
private string? _groupId;
private bool _disposed;
protected VisualElement? _rootElement;
protected CancellationTokenSource? _debounceCts;
#endregion
#region Properties
/// <summary>고유 ID</summary>
public string Id { get; }
/// <summary>표시 이름</summary>
public string Name { get; }
/// <summary>표시 이름 (Name과 동일)</summary>
public string DisplayName => Name;
/// <summary>그룹 여부 (항상 false)</summary>
public bool IsGroup => false;
/// <summary>TreeView 내부 ID</summary>
public int TreeViewId { get; set; }
/// <summary>속성 타입</summary>
public abstract UTKPropertyType PropertyType { get; }
/// <summary>현재 값</summary>
public T Value
{
get => _value;
@@ -57,53 +65,39 @@ namespace UVC.UIToolkit
var oldValue = _value;
_value = value;
NotifyValueChanged(oldValue, value);
RefreshUI();
}
}
}
/// <summary>설명 (부가 정보)</summary>
public string? Description
{
get => _description;
set => _description = value;
}
/// <summary>툴팁 텍스트</summary>
public string? Tooltip
{
get => _tooltip;
set
{
_tooltip = value;
UpdateTooltip();
}
set => _tooltip = value;
}
/// <summary>읽기 전용 여부</summary>
public bool IsReadOnly
{
get => _isReadOnly;
set
{
if (_isReadOnly != value)
{
_isReadOnly = value;
UpdateReadOnlyState();
}
}
set => _isReadOnly = value;
}
/// <summary>표시 여부</summary>
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
UpdateVisibility();
}
}
set => _isVisible = value;
}
/// <summary>소속 그룹 ID</summary>
public string? GroupId
{
get => _groupId;
@@ -112,11 +106,20 @@ namespace UVC.UIToolkit
#endregion
#region Events
/// <summary>값 변경 이벤트 (object 타입)</summary>
public event Action<IUTKPropertyItem, object?, object?>? OnValueChanged;
/// <summary>값 변경 이벤트 (제네릭 타입)</summary>
public event Action<IUTKPropertyItem<T>, T, T>? OnTypedValueChanged;
#endregion
#region Constructor
/// <summary>
/// PropertyItem을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
protected UTKPropertyItemBase(string id, string name, T initialValue)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
@@ -126,8 +129,10 @@ namespace UVC.UIToolkit
#endregion
#region Public Methods
/// <summary>현재 값을 object로 반환합니다.</summary>
public object? GetValue() => _value;
/// <summary>값을 설정합니다 (타입 변환 포함).</summary>
public void SetValue(object? value)
{
if (value == null)
@@ -153,187 +158,26 @@ namespace UVC.UIToolkit
}
}
}
public abstract VisualElement CreateUI();
public virtual void BindUI(VisualElement element)
{
_rootElement = element;
UpdateReadOnlyState();
UpdateVisibility();
UpdateTooltip();
}
public virtual void UnbindUI(VisualElement element)
{
CancelDebounce();
if (_rootElement != null)
{
_rootElement.ClearTooltip();
}
_rootElement = null;
}
public virtual void RefreshUI()
{
// 하위 클래스에서 오버라이드하여 UI 갱신
}
#endregion
#region Protected Methods
/// <summary>값 변경을 알립니다.</summary>
protected void NotifyValueChanged(T oldValue, T newValue)
{
OnTypedValueChanged?.Invoke(this, oldValue, newValue);
OnValueChanged?.Invoke(this, oldValue, newValue);
}
protected async UniTaskVoid DebounceValueChange(T newValue, int delayMs = DEFAULT_DEBOUNCE_MS)
{
CancelDebounce();
_debounceCts = new CancellationTokenSource();
try
{
await UniTask.Delay(delayMs, cancellationToken: _debounceCts.Token);
Value = newValue;
}
catch (OperationCanceledException)
{
// 디바운스 취소됨 - 정상 동작
}
}
protected void CancelDebounce()
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = null;
}
protected virtual void UpdateReadOnlyState()
{
if (_rootElement == null) return;
if (_isReadOnly)
{
_rootElement.AddToClassList(USS_CLASS_READONLY);
}
else
{
_rootElement.RemoveFromClassList(USS_CLASS_READONLY);
}
}
protected virtual void UpdateVisibility()
{
if (_rootElement == null) return;
_rootElement.style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None;
if (_isVisible)
{
_rootElement.RemoveFromClassList(USS_CLASS_HIDDEN);
}
else
{
_rootElement.AddToClassList(USS_CLASS_HIDDEN);
}
}
protected virtual void UpdateTooltip()
{
if (_rootElement == null || string.IsNullOrEmpty(_tooltip)) return;
_rootElement.SetTooltip(_tooltip);
}
protected UTKLabel CreateNameLabel()
{
var label = new UTKLabel(Name, UTKLabel.LabelSize.Body2);
label.AddToClassList("utk-property-item__label");
return label;
}
protected VisualElement CreateContainer()
{
var container = new VisualElement();
container.AddToClassList("utk-property-item");
container.AddToClassList($"utk-property-item--{PropertyType.ToString().ToLower()}");
if (!string.IsNullOrEmpty(Description))
{
container.AddToClassList("utk-property-item--has-description");
}
return container;
}
/// <summary>
/// UXML 템플릿에서 UI 생성
/// </summary>
/// <param name="uxmlName">UXML 파일명 (확장자 제외)</param>
/// <returns>생성된 루트 VisualElement</returns>
protected VisualElement? CreateUIFromUxml(string uxmlName)
{
var asset = LoadUxmlAsset(uxmlName);
if (asset == null)
{
Debug.LogWarning($"[UTKPropertyItem] UXML not found: {uxmlName}");
return null;
}
var root = asset.Instantiate();
var itemRoot = root.Q<VisualElement>("item-root") ?? root;
// name-label에 Name 설정
var nameLabel = itemRoot.Q<UTKLabel>("name-label");
if (nameLabel != null)
{
nameLabel.Text = Name;
}
return itemRoot;
}
/// <summary>
/// UXML 에셋 로드 (캐시 사용)
/// </summary>
protected static VisualTreeAsset? LoadUxmlAsset(string uxmlName)
{
string path = UXML_BASE_PATH + uxmlName;
if (_uxmlCache.TryGetValue(path, out var cached))
{
return cached;
}
var asset = Resources.Load<VisualTreeAsset>(path);
if (asset != null)
{
_uxmlCache[path] = asset;
}
return asset;
}
/// <summary>
/// UXML 캐시 클리어 (에디터 용도)
/// </summary>
public static void ClearUxmlCache()
{
_uxmlCache.Clear();
}
#endregion
#region IDisposable
/// <summary>리소스를 해제합니다.</summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>리소스를 해제합니다.</summary>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
@@ -341,13 +185,6 @@ namespace UVC.UIToolkit
if (disposing)
{
CancelDebounce();
if (_rootElement != null)
{
UnbindUI(_rootElement);
}
OnValueChanged = null;
OnTypedValueChanged = null;
}

View File

@@ -1,118 +1,30 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 불리언 속성 아이템
/// Toggle을 사용한 체크박스 형식
/// 불리언 속성 데이터 클래스입니다.
/// UI는 UTKBoolPropertyItemView에서 담당합니다.
/// </summary>
public class UTKBoolPropertyItem : UTKPropertyItemBase<bool>
{
#region Fields
private UTKToggle? _toggle;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Bool;
#endregion
#region Constructor
/// <summary>
/// 불리언 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKBoolPropertyItem(string id, string name, bool initialValue = false, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKBoolPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_toggle = container.Q<UTKToggle>("value-field");
if (_toggle != null)
{
_toggle.IsOn = Value;
_toggle.IsInteractive = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_toggle = new UTKToggle();
_toggle.name = "value-field";
_toggle.IsOn = Value;
_toggle.IsInteractive = !IsReadOnly;
valueContainer.Add(_toggle);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_toggle = element.Q<UTKToggle>("value-field");
if (_toggle != null)
{
_toggle.IsOn = Value;
_toggle.IsInteractive = !IsReadOnly;
_toggle.OnValueChanged += OnToggleChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_toggle != null)
{
_toggle.OnValueChanged -= OnToggleChanged;
_toggle = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_toggle != null && _toggle.IsOn != Value)
{
_toggle.SetOn(Value, false);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_toggle != null)
{
_toggle.IsInteractive = !IsReadOnly;
}
}
#endregion
#region Private Methods
private void OnToggleChanged(bool newValue)
{
// Toggle은 디바운스 없이 즉시 적용
Value = newValue;
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,24 +1,20 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 색상 속성 아이템
/// 색상 미리보기 + UTKColorPicker 연동
/// 색상 속성 데이터 클래스입니다.
/// UI는 UTKColorPropertyItemView에서 담당합니다.
/// </summary>
public class UTKColorPropertyItem : UTKPropertyItemBase<Color>
{
#region Fields
private VisualElement? _colorPreview;
private UTKButton? _pickerButton;
private UTKInputField? _hexField;
private UTKColorPicker? _currentPicker;
private bool _useAlpha;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Color;
/// <summary>알파 채널 사용 여부</summary>
@@ -30,279 +26,19 @@ namespace UVC.UIToolkit
#endregion
#region Constructor
/// <summary>
/// 색상 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="useAlpha">알파 채널 사용 여부</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKColorPropertyItem(string id, string name, Color initialValue = default, bool useAlpha = false, bool isReadOnly = false)
: base(id, name, initialValue)
{
_useAlpha = useAlpha;
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKColorPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_colorPreview = container.Q<VisualElement>("color-preview");
_hexField = container.Q<UTKInputField>("hex-field");
_pickerButton = container.Q<UTKButton>("picker-btn");
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value;
_colorPreview.SetEnabled(!IsReadOnly);
}
if (_hexField != null)
{
_hexField.Value = ColorToHex(Value);
_hexField.isReadOnly = IsReadOnly;
}
if(_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
// 색상 미리보기
_colorPreview = new VisualElement();
_colorPreview.name = "color-preview";
_colorPreview.AddToClassList("utk-property-item__color-preview");
_colorPreview.style.backgroundColor = Value;
_colorPreview.SetEnabled(!IsReadOnly);
valueContainer.Add(_colorPreview);
// Hex 입력
_hexField = new UTKInputField();
_hexField.name = "hex-field";
_hexField.Value = ColorToHex(Value);
_hexField.style.width = 80;
_hexField.style.marginLeft = 5;
_hexField.isReadOnly = IsReadOnly;
valueContainer.Add(_hexField);
// 피커 버튼
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_pickerButton.name = "picker-btn";
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_pickerButton);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_colorPreview = element.Q<VisualElement>("color-preview");
_hexField = element.Q<UTKInputField>("hex-field");
_pickerButton = element.Q<UTKButton>("picker-btn");
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value;
_colorPreview.SetEnabled(!IsReadOnly);
_colorPreview.RegisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_hexField != null)
{
_hexField.Value = ColorToHex(Value);
_hexField.isReadOnly = IsReadOnly;
_hexField.OnValueChanged += OnHexChanged;
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_colorPreview != null)
{
_colorPreview.UnregisterCallback<ClickEvent>(OnPreviewClicked);
_colorPreview = null;
}
if (_hexField != null)
{
_hexField.OnValueChanged -= OnHexChanged;
_hexField = null;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
_pickerButton = null;
}
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value;
}
if (_hexField != null)
{
var hex = ColorToHex(Value);
if (_hexField.Value != hex)
{
_hexField.SetValue(hex, false);
}
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
_colorPreview?.SetEnabled(!IsReadOnly);
if(_hexField != null) _hexField.isReadOnly = IsReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnPreviewClicked(ClickEvent evt)
{
if (!IsReadOnly)
{
OpenPicker();
}
}
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OpenPicker()
{
if (_currentPicker != null || _rootElement == null) return;
// 최상위 루트 찾기
var root = _rootElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKColorPicker.Show(root, Value, Name, _useAlpha);
_currentPicker.OnColorChanged += OnPickerColorChanged;
_currentPicker.OnColorSelected += OnPickerColorSelected;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnColorChanged -= OnPickerColorChanged;
_currentPicker.OnColorSelected -= OnPickerColorSelected;
_currentPicker.Close(); // Close()를 호출하여 블로커도 함께 정리
_currentPicker = null;
}
}
private void OnPickerColorChanged(Color color)
{
// 실시간 미리보기 업데이트 (값 변경 이벤트는 발생시키지 않음)
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = color;
}
if (_hexField != null)
{
_hexField.SetValue(ColorToHex(color), false);
}
}
private void OnPickerColorSelected(Color color)
{
Value = color;
ClosePicker();
}
private void OnHexChanged(string newValue)
{
if (TryParseHex(newValue, out Color color))
{
DebounceValueChange(color, 300).Forget();
}
}
private string ColorToHex(Color color)
{
if (_useAlpha)
{
return "#" + ColorUtility.ToHtmlStringRGBA(color);
}
else
{
return "#" + ColorUtility.ToHtmlStringRGB(color);
}
}
private bool TryParseHex(string hex, out Color color)
{
if (!hex.StartsWith("#"))
{
hex = "#" + hex;
}
if (ColorUtility.TryParseHtmlString(hex, out color))
{
if (!_useAlpha)
{
color.a = 1f;
}
return true;
}
color = default;
return false;
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing)
{
ClosePicker();
}
base.Dispose(disposing);
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,167 +1,45 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 상태 + 색상 복합 속성 아이템
/// 상태 라벨 + 색상 피커
/// 상태 + 색상 복합 속성 데이터 클래스입니다.
/// UI는 UTKColorStatePropertyItemView에서 담당합니다.
/// </summary>
public class UTKColorStatePropertyItem : UTKPropertyItemBase<UTKColorState>
{
#region Fields
private UTKLabel? _stateLabel;
private VisualElement? _colorPreview;
private UTKButton? _pickerButton;
private UTKColorPicker? _currentPicker;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.ColorState;
#endregion
#region Constructor
/// <summary>
/// 상태 + 색상 복합 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKColorStatePropertyItem(string id, string name, UTKColorState initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 상태 + 색상 복합 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="state">상태 텍스트</param>
/// <param name="color">색상</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKColorStatePropertyItem(string id, string name, string state, Color color, bool isReadOnly = false)
: base(id, name, new UTKColorState(state, color))
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKColorStatePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_stateLabel = container.Q<UTKLabel>("state-label");
_colorPreview = container.Q<VisualElement>("color-preview");
_pickerButton = container.Q<UTKButton>("picker-btn");
if (_stateLabel != null)
{
_stateLabel.Text = Value.State;
}
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value.Color;
_colorPreview.SetEnabled(!IsReadOnly);
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
valueContainer.style.flexDirection = FlexDirection.Row;
// 상태 라벨
_stateLabel = new UTKLabel(Value.State, UTKLabel.LabelSize.Body2);
_stateLabel.name = "state-label";
_stateLabel.AddToClassList("utk-property-item__state-label");
valueContainer.Add(_stateLabel);
// 색상 미리보기
_colorPreview = new VisualElement();
_colorPreview.name = "color-preview";
_colorPreview.AddToClassList("utk-property-item__color-preview");
_colorPreview.style.backgroundColor = Value.Color;
_colorPreview.SetEnabled(!IsReadOnly);
valueContainer.Add(_colorPreview);
// 피커 버튼
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_pickerButton.name = "picker-btn";
_pickerButton.AddToClassList("utk-property-item__picker-btn");
_pickerButton.IsEnabled = !IsReadOnly;
valueContainer.Add(_pickerButton);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_stateLabel = element.Q<UTKLabel>("state-label");
_colorPreview = element.Q<VisualElement>("color-preview");
_pickerButton = element.Q<UTKButton>("picker-btn");
if (_stateLabel != null)
{
_stateLabel.Text = Value.State;
}
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value.Color;
_colorPreview.SetEnabled(!IsReadOnly);
_colorPreview.RegisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_colorPreview != null)
{
_colorPreview.UnregisterCallback<ClickEvent>(OnPreviewClicked);
_colorPreview = null;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
_pickerButton = null;
}
_stateLabel = null;
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_stateLabel != null && _stateLabel.Text != Value.State)
{
_stateLabel.Text = Value.State;
}
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = Value.Color;
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_pickerButton != null) _pickerButton.IsEnabled = !IsReadOnly;
if (_colorPreview != null) _colorPreview.SetEnabled(!IsReadOnly);
IsReadOnly = isReadOnly;
}
#endregion
@@ -178,59 +56,5 @@ namespace UVC.UIToolkit
Value = new UTKColorState(Value.State, color);
}
#endregion
#region Private Methods
private void OnPreviewClicked(ClickEvent evt)
{
if (!IsReadOnly) OpenPicker();
}
private void OnPickerButtonClicked() => OpenPicker();
private void OpenPicker()
{
if (_currentPicker != null || _rootElement == null) return;
var root = _rootElement;
while (root.parent != null) root = root.parent;
_currentPicker = UTKColorPicker.Show(root, Value.Color, $"{Name} - {Value.State}", false);
_currentPicker.OnColorChanged += OnPickerColorChanged;
_currentPicker.OnColorSelected += OnPickerColorSelected;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnColorChanged -= OnPickerColorChanged;
_currentPicker.OnColorSelected -= OnPickerColorSelected;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerColorChanged(Color color)
{
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = color;
}
}
private void OnPickerColorSelected(Color color)
{
Value = new UTKColorState(Value.State, color);
ClosePicker();
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing) ClosePicker();
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -1,221 +1,42 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 날짜 속성 아이템
/// TextField + UTKDatePicker 연동
/// 날짜 속성 데이터 클래스입니다.
/// UI는 UTKDatePropertyItemView에서 담당합니다.
/// </summary>
public class UTKDatePropertyItem : UTKPropertyItemBase<DateTime>
{
#region Fields
private UTKInputField? _dateField;
private UTKButton? _pickerButton;
private UTKDatePicker? _currentPicker;
private string _dateFormat = "yyyy-MM-dd";
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Date;
/// <summary>날짜 표시 형식</summary>
public string DateFormat
{
get => _dateFormat;
set
{
_dateFormat = value ?? "yyyy-MM-dd";
RefreshUI();
}
set => _dateFormat = value ?? "yyyy-MM-dd";
}
#endregion
#region Constructor
/// <summary>
/// 날짜 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값 (default이면 오늘 날짜)</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDatePropertyItem(string id, string name, DateTime initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue == default ? DateTime.Today : initialValue)
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKDatePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_dateField = container.Q<UTKInputField>("date-field");
_pickerButton = container.Q<UTKButton>("picker-btn");
if (_dateField != null)
{
_dateField.Value = Value.ToString(_dateFormat);
_dateField.isReadOnly = IsReadOnly;
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_dateField = new UTKInputField();
_dateField.name = "date-field";
_dateField.Value = Value.ToString(_dateFormat);
_dateField.style.flexGrow = 1;
_dateField.isReadOnly = IsReadOnly;
valueContainer.Add(_dateField);
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_pickerButton.name = "picker-btn";
_pickerButton.AddToClassList("utk-property-item__picker-btn");
_pickerButton.IsEnabled = !IsReadOnly;
valueContainer.Add(_pickerButton);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_dateField = element.Q<UTKInputField>("date-field");
_pickerButton = element.Q<UTKButton>("picker-btn");
if (_dateField != null)
{
_dateField.Value = Value.ToString(_dateFormat);
_dateField.isReadOnly = IsReadOnly;
_dateField.OnValueChanged += OnDateTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_dateField != null)
{
_dateField.OnValueChanged -= OnDateTextChanged;
_dateField = null;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
_pickerButton = null;
}
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_dateField != null)
{
var formatted = Value.ToString(_dateFormat);
if (_dateField.Value != formatted)
{
_dateField.SetValue(formatted, false);
}
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_dateField != null) _dateField.isReadOnly = IsReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OpenPicker()
{
if (_currentPicker != null || _rootElement == null) return;
var root = _rootElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKDatePicker.Show(root, Value, UTKDatePicker.PickerMode.DateOnly, Name);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime date)
{
Value = date.Date;
ClosePicker();
}
private void OnPickerClosed()
{
_currentPicker = null;
}
private void OnDateTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
DebounceValueChange(date.Date, 500).Forget();
}
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing)
{
ClosePicker();
}
base.Dispose(disposing);
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,313 +1,56 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 날짜 범위 속성 아이템
/// 시작일, 종료일 두 개의 DatePicker
/// 날짜 범위 속성 데이터 클래스입니다.
/// UI는 UTKDateRangePropertyItemView에서 담당합니다.
/// </summary>
public class UTKDateRangePropertyItem : UTKPropertyItemBase<UTKDateRange>
{
#region Fields
private UTKInputField? _startField;
private UTKInputField? _endField;
private UTKButton? _startPickerBtn;
private UTKButton? _endPickerBtn;
private UTKDatePicker? _currentPicker;
private bool _isEditingStart;
private string _dateFormat = "yyyy-MM-dd";
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.DateRange;
/// <summary>날짜 표시 형식</summary>
public string DateFormat
{
get => _dateFormat;
set
{
_dateFormat = value ?? "yyyy-MM-dd";
RefreshUI();
}
set => _dateFormat = value ?? "yyyy-MM-dd";
}
#endregion
#region Constructor
/// <summary>
/// 날짜 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 범위 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDateRangePropertyItem(string id, string name, UTKDateRange initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue.Start == default ? new UTKDateRange(DateTime.Today, DateTime.Today) : initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 날짜 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="start">시작 날짜</param>
/// <param name="end">종료 날짜</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDateRangePropertyItem(string id, string name, DateTime start, DateTime end, bool isReadOnly = false)
: base(id, name, new UTKDateRange(start, end))
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKDateRangePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_startField = container.Q<UTKInputField>("start-field");
_endField = container.Q<UTKInputField>("end-field");
_startPickerBtn = container.Q<UTKButton>("start-picker-btn");
_endPickerBtn = container.Q<UTKButton>("end-picker-btn");
if (_startField != null)
{
_startField.Value = Value.Start.ToString(_dateFormat);
_startField.isReadOnly = IsReadOnly;
}
if (_endField != null)
{
_endField.Value = Value.End.ToString(_dateFormat);
_endField.isReadOnly = IsReadOnly;
}
if(_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
}
if(_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
valueContainer.style.flexDirection = FlexDirection.Row;
// Start field
_startField = new UTKInputField();
_startField.name = "start-field";
_startField.Value = Value.Start.ToString(_dateFormat);
_startField.style.flexGrow = 1;
_startField.isReadOnly = IsReadOnly;
valueContainer.Add(_startField);
_startPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_startPickerBtn.name = "start-picker-btn";
_startPickerBtn.IsEnabled = !IsReadOnly;
_startPickerBtn.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_startPickerBtn);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item__range-separator");
valueContainer.Add(separator);
// End field
_endField = new UTKInputField();
_endField.name = "end-field";
_endField.Value = Value.End.ToString(_dateFormat);
_endField.style.flexGrow = 1;
_endField.isReadOnly = IsReadOnly;
valueContainer.Add(_endField);
_endPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_endPickerBtn.name = "end-picker-btn";
_endPickerBtn.IsEnabled = !IsReadOnly;
_endPickerBtn.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_endPickerBtn);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_startField = element.Q<UTKInputField>("start-field");
_endField = element.Q<UTKInputField>("end-field");
_startPickerBtn = element.Q<UTKButton>("start-picker-btn");
_endPickerBtn = element.Q<UTKButton>("end-picker-btn");
if (_startField != null)
{
_startField.Value = Value.Start.ToString(_dateFormat);
_startField.isReadOnly = IsReadOnly;
_startField.OnValueChanged += OnStartTextChanged;
}
if (_endField != null)
{
_endField.Value = Value.End.ToString(_dateFormat);
_endField.isReadOnly = IsReadOnly;
_endField.OnValueChanged += OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
_startPickerBtn.OnClicked += OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
_endPickerBtn.OnClicked += OnEndPickerClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_startField != null)
{
_startField.OnValueChanged -= OnStartTextChanged;
_startField = null;
}
if (_endField != null)
{
_endField.OnValueChanged -= OnEndTextChanged;
_endField = null;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked -= OnStartPickerClicked;
_startPickerBtn = null;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked -= OnEndPickerClicked;
_endPickerBtn = null;
}
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_startField != null)
{
var formatted = Value.Start.ToString(_dateFormat);
if (_startField.Value != formatted)
{
_startField.SetValue(formatted, false);
}
}
if (_endField != null)
{
var formatted = Value.End.ToString(_dateFormat);
if (_endField.Value != formatted)
{
_endField.SetValue(formatted, false);
}
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_startField != null) _startField.isReadOnly = IsReadOnly;
if (_endField != null) _endField.isReadOnly = IsReadOnly;
if (_startPickerBtn != null) _startPickerBtn.IsEnabled = !IsReadOnly;
if (_endPickerBtn != null) _endPickerBtn.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnStartPickerClicked()
{
_isEditingStart = true;
OpenPicker(Value.Start);
}
private void OnEndPickerClicked()
{
_isEditingStart = false;
OpenPicker(Value.End);
}
private void OpenPicker(DateTime initialDate)
{
if (_currentPicker != null || _rootElement == null) return;
var root = _rootElement;
while (root.parent != null) root = root.parent;
string title = _isEditingStart ? $"{Name} - 시작일" : $"{Name} - 종료일";
_currentPicker = UTKDatePicker.Show(root, initialDate, UTKDatePicker.PickerMode.DateOnly, title);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime date)
{
if (_isEditingStart)
{
Value = new UTKDateRange(date.Date, Value.End);
}
else
{
Value = new UTKDateRange(Value.Start, date.Date);
}
ClosePicker();
}
private void OnPickerClosed() => _currentPicker = null;
private void OnStartTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
var range = new UTKDateRange(date.Date, Value.End);
DebounceValueChange(range, 500).Forget();
}
}
private void OnEndTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
var range = new UTKDateRange(Value.Start, date.Date);
DebounceValueChange(range, 500).Forget();
}
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing) ClosePicker();
base.Dispose(disposing);
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,221 +1,42 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 날짜+시간 속성 아이템
/// TextField + UTKDatePicker(DateAndTime 모드) 연동
/// 날짜+시간 속성 데이터 클래스입니다.
/// UI는 UTKDateTimePropertyItemView에서 담당합니다.
/// </summary>
public class UTKDateTimePropertyItem : UTKPropertyItemBase<DateTime>
{
#region Fields
private UTKInputField? _dateTimeField;
private UTKButton? _pickerButton;
private UTKDatePicker? _currentPicker;
private string _dateTimeFormat = "yyyy-MM-dd HH:mm";
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.DateTime;
/// <summary>날짜시간 표시 형식</summary>
public string DateTimeFormat
{
get => _dateTimeFormat;
set
{
_dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
RefreshUI();
}
set => _dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
}
#endregion
#region Constructor
/// <summary>
/// 날짜+시간 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값 (default이면 현재 시간)</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDateTimePropertyItem(string id, string name, DateTime initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue == default ? DateTime.Now : initialValue)
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKDateTimePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_dateTimeField = container.Q<UTKInputField>("datetime-field");
_pickerButton = container.Q<UTKButton>("picker-btn");
if (_dateTimeField != null)
{
_dateTimeField.Value = Value.ToString(_dateTimeFormat);
_dateTimeField.isReadOnly = IsReadOnly;
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_dateTimeField = new UTKInputField();
_dateTimeField.name = "datetime-field";
_dateTimeField.Value = Value.ToString(_dateTimeFormat);
_dateTimeField.style.flexGrow = 1;
_dateTimeField.isReadOnly = IsReadOnly;
valueContainer.Add(_dateTimeField);
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_pickerButton.name = "picker-btn";
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_pickerButton);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_dateTimeField = element.Q<UTKInputField>("datetime-field");
_pickerButton = element.Q<UTKButton>("picker-btn");
if (_dateTimeField != null)
{
_dateTimeField.Value = Value.ToString(_dateTimeFormat);
_dateTimeField.isReadOnly = IsReadOnly;
_dateTimeField.OnValueChanged += OnDateTimeTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_dateTimeField != null)
{
_dateTimeField.OnValueChanged -= OnDateTimeTextChanged;
_dateTimeField = null;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
_pickerButton = null;
}
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_dateTimeField != null)
{
var formatted = Value.ToString(_dateTimeFormat);
if (_dateTimeField.Value != formatted)
{
_dateTimeField.SetValue(formatted, false);
}
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_dateTimeField != null) _dateTimeField.isReadOnly = IsReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OpenPicker()
{
if (_currentPicker != null || _rootElement == null) return;
var root = _rootElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKDatePicker.Show(root, Value, UTKDatePicker.PickerMode.DateAndTime, Name);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime dateTime)
{
Value = dateTime;
ClosePicker();
}
private void OnPickerClosed()
{
_currentPicker = null;
}
private void OnDateTimeTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
DebounceValueChange(dateTime, 500).Forget();
}
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing)
{
ClosePicker();
}
base.Dispose(disposing);
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,314 +1,56 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 날짜시간 범위 속성 아이템
/// 시작, 종료 두 개의 DateTimePicker
/// 날짜시간 범위 속성 데이터 클래스입니다.
/// UI는 UTKDateTimeRangePropertyItemView에서 담당합니다.
/// </summary>
public class UTKDateTimeRangePropertyItem : UTKPropertyItemBase<UTKDateTimeRange>
{
#region Fields
private UTKInputField? _startField;
private UTKInputField? _endField;
private UTKButton? _startPickerBtn;
private UTKButton? _endPickerBtn;
private UTKDatePicker? _currentPicker;
private bool _isEditingStart;
private string _dateTimeFormat = "yyyy-MM-dd HH:mm";
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.DateTimeRange;
/// <summary>날짜시간 표시 형식</summary>
public string DateTimeFormat
{
get => _dateTimeFormat;
set
{
_dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
RefreshUI();
}
set => _dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
}
#endregion
#region Constructor
/// <summary>
/// 날짜시간 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 범위 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDateTimeRangePropertyItem(string id, string name, UTKDateTimeRange initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue.Start == default ? new UTKDateTimeRange(DateTime.Now, DateTime.Now) : initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 날짜시간 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="start">시작 날짜시간</param>
/// <param name="end">종료 날짜시간</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDateTimeRangePropertyItem(string id, string name, DateTime start, DateTime end, bool isReadOnly = false)
: base(id, name, new UTKDateTimeRange(start, end))
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKDateTimeRangePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_startField = container.Q<UTKInputField>("start-field");
_endField = container.Q<UTKInputField>("end-field");
_startPickerBtn = container.Q<UTKButton>("start-picker-btn");
_endPickerBtn = container.Q<UTKButton>("end-picker-btn");
if (_startField != null)
{
_startField.Value = Value.Start.ToString(_dateTimeFormat);
_startField.isReadOnly = IsReadOnly;
}
if (_endField != null)
{
_endField.Value = Value.End.ToString(_dateTimeFormat);
_endField.isReadOnly = IsReadOnly;
}
if (_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
}
if (_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
valueContainer.style.flexDirection = FlexDirection.Row;
// Start field
_startField = new UTKInputField();
_startField.name = "start-field";
_startField.Value = Value.Start.ToString(_dateTimeFormat);
_startField.style.flexGrow = 1;
_startField.isReadOnly = IsReadOnly;
valueContainer.Add(_startField);
_startPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_startPickerBtn.name = "start-picker-btn";
_startPickerBtn.IsEnabled = !IsReadOnly;
_startPickerBtn.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_startPickerBtn);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item__range-separator");
valueContainer.Add(separator);
// End field
_endField = new UTKInputField();
_endField.name = "end-field";
_endField.Value = Value.End.ToString(_dateTimeFormat);
_endField.style.flexGrow = 1;
_endField.isReadOnly = IsReadOnly;
valueContainer.Add(_endField);
_endPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary);
_endPickerBtn.name = "end-picker-btn";
_endPickerBtn.IsEnabled = !IsReadOnly;
_endPickerBtn.AddToClassList("utk-property-item__picker-btn");
valueContainer.Add(_endPickerBtn);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_startField = element.Q<UTKInputField>("start-field");
_endField = element.Q<UTKInputField>("end-field");
_startPickerBtn = element.Q<UTKButton>("start-picker-btn");
_endPickerBtn = element.Q<UTKButton>("end-picker-btn");
if (_startField != null)
{
_startField.Value = Value.Start.ToString(_dateTimeFormat);
_startField.isReadOnly = IsReadOnly;
_startField.OnValueChanged += OnStartTextChanged;
}
if (_endField != null)
{
_endField.Value = Value.End.ToString(_dateTimeFormat);
_endField.isReadOnly = IsReadOnly;
_endField.OnValueChanged += OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
_startPickerBtn.OnClicked += OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
_endPickerBtn.OnClicked += OnEndPickerClicked;
}
}
public override void UnbindUI(VisualElement element)
{
if (_startField != null)
{
_startField.OnValueChanged -= OnStartTextChanged;
_startField = null;
}
if (_endField != null)
{
_endField.OnValueChanged -= OnEndTextChanged;
_endField = null;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked -= OnStartPickerClicked;
_startPickerBtn = null;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked -= OnEndPickerClicked;
_endPickerBtn = null;
}
ClosePicker();
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_startField != null)
{
var formatted = Value.Start.ToString(_dateTimeFormat);
if (_startField.Value != formatted)
{
_startField.SetValue(formatted, false);
}
}
if (_endField != null)
{
var formatted = Value.End.ToString(_dateTimeFormat);
if (_endField.Value != formatted)
{
_endField.SetValue(formatted, false);
}
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_startField != null) _startField.isReadOnly = IsReadOnly;
if (_endField != null) _endField.isReadOnly = IsReadOnly;
if (_startPickerBtn != null) _startPickerBtn.IsEnabled = !IsReadOnly;
if (_endPickerBtn != null) _endPickerBtn.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnStartPickerClicked()
{
_isEditingStart = true;
OpenPicker(Value.Start);
}
private void OnEndPickerClicked()
{
_isEditingStart = false;
OpenPicker(Value.End);
}
private void OpenPicker(DateTime initialDateTime)
{
if (_currentPicker != null || _rootElement == null) return;
var root = _rootElement;
while (root.parent != null) root = root.parent;
string title = _isEditingStart ? $"{Name} - 시작" : $"{Name} - 종료";
_currentPicker = UTKDatePicker.Show(root, initialDateTime, UTKDatePicker.PickerMode.DateAndTime, title);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime dateTime)
{
if (_isEditingStart)
{
Value = new UTKDateTimeRange(dateTime, Value.End);
}
else
{
Value = new UTKDateTimeRange(Value.Start, dateTime);
}
ClosePicker();
}
private void OnPickerClosed() => _currentPicker = null;
private void OnStartTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
var range = new UTKDateTimeRange(dateTime, Value.End);
DebounceValueChange(range, 500).Forget();
}
}
private void OnEndTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
var range = new UTKDateTimeRange(Value.Start, dateTime);
DebounceValueChange(range, 500).Forget();
}
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
if (disposing) ClosePicker();
base.Dispose(disposing);
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,42 +1,41 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 드롭다운 목록 속성 아이템
/// 문자열 리스트에서 선택
/// 드롭다운 목록 속성 데이터 클래스입니다.
/// UI는 UTKDropdownPropertyItemView에서 담당합니다.
/// </summary>
public class UTKDropdownPropertyItem : UTKPropertyItemBase<string>
{
#region Fields
private UTKDropdown? _dropdown;
private List<string> _choices;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.DropdownList;
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set
{
_choices = value ?? new List<string>();
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
}
}
set => _choices = value ?? new List<string>();
}
#endregion
#region Constructor
public UTKDropdownPropertyItem(string id, string name, List<string> choices, string initialValue = "", bool isReadOnly = false)
/// <summary>
/// 드롭다운 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="initialValue">초기 선택 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDropdownPropertyItem(string id, string name, List<string> choices, string initialValue = "", bool isReadOnly = false)
: base(id, name, initialValue)
{
_choices = choices ?? new List<string>();
@@ -46,10 +45,18 @@ namespace UVC.UIToolkit
{
Value = _choices[0];
}
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
public UTKDropdownPropertyItem(string id, string name, IEnumerable<string> choices, int selectedIndex = 0, bool isReadOnly = false)
/// <summary>
/// 드롭다운 속성을 생성합니다 (인덱스 기반).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="selectedIndex">초기 선택 인덱스</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKDropdownPropertyItem(string id, string name, IEnumerable<string> choices, int selectedIndex = 0, bool isReadOnly = false)
: base(id, name, string.Empty)
{
_choices = choices?.ToList() ?? new List<string>();
@@ -62,97 +69,7 @@ namespace UVC.UIToolkit
{
Value = _choices[0];
}
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKDropdownPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_dropdown = container.Q<UTKDropdown>("dropdown-field");
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
int selectedIndex = _choices.IndexOf(Value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_dropdown = new UTKDropdown();
_dropdown.name = "dropdown-field";
_dropdown.SetOptions(_choices);
int selectedIndex = _choices.IndexOf(Value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = !IsReadOnly;
valueContainer.Add(_dropdown);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_dropdown = element.Q<UTKDropdown>("dropdown-field");
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
int selectedIndex = _choices.IndexOf(Value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = !IsReadOnly;
_dropdown.OnSelectionChanged += OnDropdownChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_dropdown != null)
{
_dropdown.OnSelectionChanged -= OnDropdownChanged;
_dropdown = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_dropdown != null && _dropdown.SelectedValue != Value)
{
_dropdown.SetSelectedValue(Value, notify: false);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_dropdown != null)
{
_dropdown.IsEnabled = !IsReadOnly;
}
IsReadOnly = isReadOnly;
}
#endregion
@@ -163,10 +80,6 @@ namespace UVC.UIToolkit
if (!_choices.Contains(choice))
{
_choices.Add(choice);
if (_dropdown != null)
{
_dropdown.AddOption(choice);
}
}
}
@@ -174,25 +87,12 @@ namespace UVC.UIToolkit
public bool RemoveChoice(string choice)
{
bool removed = _choices.Remove(choice);
if (removed && _dropdown != null)
if (removed && Value == choice && _choices.Count > 0)
{
_dropdown.SetOptions(_choices);
// 현재 선택 값이 제거되면 첫 번째 항목 선택
if (Value == choice && _choices.Count > 0)
{
Value = _choices[0];
}
Value = _choices[0];
}
return removed;
}
#endregion
#region Private Methods
private void OnDropdownChanged(int index, string newValue)
{
Value = newValue;
}
#endregion
}
}

View File

@@ -1,21 +1,20 @@
#nullable enable
using System;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 열거형 속성 아이템
/// UTKEnumDropDown을 사용한 열거형 선택
/// 열거형 속성 데이터 클래스입니다.
/// UI는 UTKEnumPropertyItemView에서 담당합니다.
/// </summary>
public class UTKEnumPropertyItem : UTKPropertyItemBase<Enum>
{
#region Fields
private UTKEnumDropDown? _enumDropdown;
private Type _enumType;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Enum;
/// <summary>열거형 타입</summary>
@@ -23,105 +22,18 @@ namespace UVC.UIToolkit
#endregion
#region Constructor
/// <summary>
/// 열거형 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값 (열거형 타입 추론에 사용)</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKEnumPropertyItem(string id, string name, Enum initialValue, bool isReadOnly = false)
: base(id, name, initialValue ?? throw new ArgumentNullException(nameof(initialValue)))
{
_enumType = initialValue.GetType();
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKEnumPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_enumDropdown = container.Q<UTKEnumDropDown>("enum-dropdown");
if (_enumDropdown != null)
{
_enumDropdown.Init(Value);
_enumDropdown.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_enumDropdown = new UTKEnumDropDown();
_enumDropdown.name = "enum-dropdown";
_enumDropdown.Init(Value);
_enumDropdown.IsEnabled = !IsReadOnly;
valueContainer.Add(_enumDropdown);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_enumDropdown = element.Q<UTKEnumDropDown>("enum-dropdown");
if (_enumDropdown != null)
{
_enumDropdown.Value = Value;
_enumDropdown.IsEnabled = !IsReadOnly;
_enumDropdown.OnValueChanged += OnEnumDropdownChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_enumDropdown != null)
{
_enumDropdown.OnValueChanged -= OnEnumDropdownChanged;
_enumDropdown = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_enumDropdown != null && _enumDropdown.Value != Value)
{
_enumDropdown.Value = Value;
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_enumDropdown != null)
{
_enumDropdown.IsEnabled = !IsReadOnly;
}
}
#endregion
#region Private Methods
private void OnEnumDropdownChanged(Enum? newValue)
{
if (newValue != null)
{
Value = newValue;
}
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,24 +1,21 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 실수 속성 아이템
/// FloatField 또는 Slider 사용
/// 실수 속성 데이터 클래스입니다.
/// UI는 UTKFloatPropertyItemView에서 담당합니다.
/// </summary>
public class UTKFloatPropertyItem : UTKPropertyItemBase<float>
{
#region Fields
private UTKFloatField? _floatField;
private UTKSlider? _slider;
private bool _useSlider;
private float _minValue;
private float _maxValue = 1f;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Float;
/// <summary>슬라이더 사용 여부</summary>
@@ -32,217 +29,48 @@ namespace UVC.UIToolkit
public float MinValue
{
get => _minValue;
set
{
_minValue = value;
if (_slider != null)
{
_slider.lowValue = value;
}
}
set => _minValue = value;
}
/// <summary>최대값 (슬라이더 모드)</summary>
public float MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
if (_slider != null)
{
_slider.highValue = value;
}
}
set => _maxValue = value;
}
#endregion
#region Constructor
/// <summary>
/// 실수 속성을 생성합니다 (필드 모드).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKFloatPropertyItem(string id, string name, float initialValue = 0f, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 실수 속성을 생성합니다 (슬라이더 모드).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="minValue">최소값</param>
/// <param name="maxValue">최대값</param>
/// <param name="useSlider">슬라이더 사용 여부</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKFloatPropertyItem(string id, string name, float initialValue, float minValue, float maxValue, bool useSlider = true, bool isReadOnly = false)
: base(id, name, initialValue)
{
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
string uxmlName = _useSlider ? "UTKFloatPropertyItemSlider" : "UTKFloatPropertyItem";
var container = CreateUIFromUxml(uxmlName);
if (container == null)
{
return CreateUIFallback();
}
_floatField = container.Q<UTKFloatField>("value-field");
_slider = container.Q<UTKSlider>("slider-field");
if (_floatField != null)
{
_floatField.Value = Value;
_floatField.isReadOnly = IsReadOnly;
}
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.Value = Value;
_slider.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
if (_useSlider)
{
container.AddToClassList("utk-property-item--slider");
}
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
if (_useSlider)
{
_slider = new UTKSlider("", _minValue, _maxValue, Value);
_slider.name = "slider-field";
_slider.IsEnabled = !IsReadOnly;
_slider.AddToClassList("utk-property-item__slider");
valueContainer.Add(_slider);
_floatField = new UTKFloatField();
_floatField.name = "value-field";
_floatField.Value = Value;
_floatField.isReadOnly = IsReadOnly;
_floatField.AddToClassList("utk-property-item__number-field");
valueContainer.Add(_floatField);
}
else
{
_floatField = new UTKFloatField();
_floatField.name = "value-field";
_floatField.Value = Value;
_floatField.isReadOnly = IsReadOnly;
valueContainer.Add(_floatField);
}
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_floatField = element.Q<UTKFloatField>("value-field");
_slider = element.Q<UTKSlider>("slider-field");
if (_floatField != null)
{
_floatField.Value = Value;
_floatField.isReadOnly = IsReadOnly;
_floatField.OnValueChanged += OnFloatFieldChanged;
}
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.Value = Value;
_slider.IsEnabled = !IsReadOnly;
_slider.OnValueChanged += OnSliderChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_floatField != null)
{
_floatField.OnValueChanged -= OnFloatFieldChanged;
_floatField = null;
}
if (_slider != null)
{
_slider.OnValueChanged -= OnSliderChanged;
_slider = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, Value))
{
_floatField.SetValueWithoutNotify(Value);
}
if (_slider != null && !Mathf.Approximately(_slider.Value, Value))
{
_slider.SetValueWithoutNotify(Value);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_floatField != null)
{
_floatField.isReadOnly = IsReadOnly;
}
if (_slider != null) _slider.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnFloatFieldChanged(float newValue)
{
Debug.Log($"OnFloatFieldChanged: {newValue}");
float clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue;
if (_slider != null && !Mathf.Approximately(_slider.Value, clampedValue))
{
_slider.SetValueWithoutNotify(clampedValue);
}
if (_floatField != null && !Mathf.Approximately(_floatField.Value, clampedValue))
{
_floatField.SetValueWithoutNotify(clampedValue);
}
DebounceValueChange(clampedValue, 100).Forget();
}
private void OnSliderChanged(float newValue)
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, newValue))
{
_floatField.SetValueWithoutNotify(newValue);
}
// 슬라이더는 빠른 반응이 필요하므로 짧은 디바운스
DebounceValueChange(newValue, 50).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,171 +1,44 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 실수 범위 속성 아이템
/// Min, Max 두 개의 FloatField
/// 실수 범위 속성 데이터 클래스입니다.
/// UI는 UTKFloatRangePropertyItemView에서 담당합니다.
/// </summary>
public class UTKFloatRangePropertyItem : UTKPropertyItemBase<UTKFloatRange>
{
#region Fields
private UTKFloatField? _minField;
private UTKFloatField? _maxField;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.FloatRange;
#endregion
#region Constructor
public UTKFloatRangePropertyItem(string id, string name, UTKFloatRange initialValue = default, bool isReadOnly = false)
/// <summary>
/// 실수 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 범위 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKFloatRangePropertyItem(string id, string name, UTKFloatRange initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 실수 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="min">최소값</param>
/// <param name="max">최대값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKFloatRangePropertyItem(string id, string name, float min, float max, bool isReadOnly = false)
: base(id, name, new UTKFloatRange(min, max))
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKFloatRangePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_minField = container.Q<UTKFloatField>("min-field");
_maxField = container.Q<UTKFloatField>("max-field");
if (_minField != null)
{
_minField.Value = Value.Min;
_minField.isReadOnly = IsReadOnly;
}
if (_maxField != null)
{
_maxField.Value = Value.Max;
_maxField.isReadOnly = IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
valueContainer.style.flexDirection = FlexDirection.Row;
_minField = new UTKFloatField();
_minField.name = "min-field";
_minField.Value = Value.Min;
_minField.style.flexGrow = 1;
_minField.isReadOnly = IsReadOnly;
valueContainer.Add(_minField);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item__range-separator");
valueContainer.Add(separator);
_maxField = new UTKFloatField();
_maxField.name = "max-field";
_maxField.Value = Value.Max;
_maxField.style.flexGrow = 1;
_maxField.isReadOnly = IsReadOnly;
valueContainer.Add(_maxField);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_minField = element.Q<UTKFloatField>("min-field");
_maxField = element.Q<UTKFloatField>("max-field");
if (_minField != null)
{
_minField.Value = Value.Min;
_minField.isReadOnly = IsReadOnly;
_minField.OnValueChanged += OnMinChanged;
}
if (_maxField != null)
{
_maxField.Value = Value.Max;
_maxField.isReadOnly = IsReadOnly;
_maxField.OnValueChanged += OnMaxChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_minField != null)
{
_minField.OnValueChanged -= OnMinChanged;
_minField = null;
}
if (_maxField != null)
{
_maxField.OnValueChanged -= OnMaxChanged;
_maxField = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_minField != null && !Mathf.Approximately(_minField.Value, Value.Min))
{
_minField.SetValueWithoutNotify(Value.Min);
}
if (_maxField != null && !Mathf.Approximately(_maxField.Value, Value.Max))
{
_maxField.SetValueWithoutNotify(Value.Max);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_minField != null) _minField.isReadOnly = IsReadOnly;
if (_maxField != null) _maxField.isReadOnly = IsReadOnly;
}
#endregion
#region Private Methods
private void OnMinChanged(float newMin)
{
var newValue = new UTKFloatRange(newMin, Value.Max);
DebounceValueChange(newValue, 100).Forget();
}
private void OnMaxChanged(float newMax)
{
var newValue = new UTKFloatRange(Value.Min, newMax);
DebounceValueChange(newValue, 100).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,24 +1,21 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 정수 속성 아이템
/// IntegerField 또는 SliderInt 사용
/// 정수 속성 데이터 클래스입니다.
/// UI는 UTKIntPropertyItemView에서 담당합니다.
/// </summary>
public class UTKIntPropertyItem : UTKPropertyItemBase<int>
{
#region Fields
private UTKIntegerField? _intField;
private UTKSliderInt? _slider;
private bool _useSlider;
private int _minValue;
private int _maxValue = 100;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Int;
/// <summary>슬라이더 사용 여부</summary>
@@ -32,209 +29,46 @@ namespace UVC.UIToolkit
public int MinValue
{
get => _minValue;
set
{
_minValue = value;
if (_slider != null)
{
_slider.lowValue = value;
}
}
set => _minValue = value;
}
/// <summary>최대값 (슬라이더 모드)</summary>
public int MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
if (_slider != null)
{
_slider.highValue = value;
}
}
set => _maxValue = value;
}
#endregion
#region Constructor
/// <summary>
/// 정수 속성을 생성합니다 (필드 모드).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
public UTKIntPropertyItem(string id, string name, int initialValue = 0)
: base(id, name, initialValue)
{
}
/// <summary>
/// 정수 속성을 생성합니다 (슬라이더 모드).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="minValue">최소값</param>
/// <param name="maxValue">최대값</param>
/// <param name="useSlider">슬라이더 사용 여부</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKIntPropertyItem(string id, string name, int initialValue, int minValue, int maxValue, bool useSlider = true, bool isReadOnly = false)
: base(id, name, initialValue)
{
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
_isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
string uxmlName = _useSlider ? "UTKIntPropertyItemSlider" : "UTKIntPropertyItem";
var container = CreateUIFromUxml(uxmlName);
if (container == null)
{
return CreateUIFallback();
}
_intField = container.Q<UTKIntegerField>("value-field");
_slider = container.Q<UTKSliderInt>("slider-field");
if (_intField != null)
{
_intField.Value = Value;
_intField.isReadOnly = IsReadOnly;
}
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.Value = Value;
_slider.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
if (_useSlider)
{
container.AddToClassList("utk-property-item--slider");
}
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
if (_useSlider)
{
_slider = new UTKSliderInt("", _minValue, _maxValue, Value);
_slider.name = "slider-field";
_slider.AddToClassList("utk-property-item__slider");
_slider.IsEnabled = !IsReadOnly;
valueContainer.Add(_slider);
_intField = new UTKIntegerField();
_intField.name = "value-field";
_intField.Value = Value;
_intField.isReadOnly = IsReadOnly;
_intField.AddToClassList("utk-property-item__number-field");
valueContainer.Add(_intField);
}
else
{
_intField = new UTKIntegerField();
_intField.name = "value-field";
_intField.Value = Value;
_intField.isReadOnly = IsReadOnly;
valueContainer.Add(_intField);
}
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_intField = element.Q<UTKIntegerField>("value-field");
_slider = element.Q<UTKSliderInt>("slider-field");
if (_intField != null)
{
_intField.Value = Value;
_intField.isReadOnly = IsReadOnly;
_intField.OnValueChanged += OnIntFieldChanged;
}
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.Value = Value;
_slider.IsEnabled = !IsReadOnly;
_slider.OnValueChanged += OnSliderChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_intField != null)
{
_intField.OnValueChanged -= OnIntFieldChanged;
_intField = null;
}
if (_slider != null)
{
_slider.OnValueChanged -= OnSliderChanged;
_slider = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_intField != null && _intField.Value != Value)
{
_intField.SetValueWithoutNotify(Value);
}
if (_slider != null && _slider.Value != Value)
{
_slider.SetValueWithoutNotify(Value);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_intField != null)
{
_intField.isReadOnly = IsReadOnly;
}
if (_slider != null) _slider.IsEnabled = !IsReadOnly;
}
#endregion
#region Private Methods
private void OnIntFieldChanged(int newValue)
{
int clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue;
if (_slider != null && _slider.Value != clampedValue)
{
_slider.SetValueWithoutNotify(clampedValue);
}
DebounceValueChange(clampedValue, 100).Forget();
}
private void OnSliderChanged(int newValue)
{
if (_intField != null && _intField.Value != newValue)
{
_intField.SetValueWithoutNotify(newValue);
}
// 슬라이더는 빠른 반응이 필요하므로 짧은 디바운스
DebounceValueChange(newValue, 50).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,170 +1,44 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 정수 범위 속성 아이템
/// Min, Max 두 개의 IntegerField
/// 정수 범위 속성 데이터 클래스입니다.
/// UI는 UTKIntRangePropertyItemView에서 담당합니다.
/// </summary>
public class UTKIntRangePropertyItem : UTKPropertyItemBase<UTKIntRange>
{
#region Fields
private UTKIntegerField? _minField;
private UTKIntegerField? _maxField;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.IntRange;
#endregion
#region Constructor
/// <summary>
/// 정수 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 범위 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKIntRangePropertyItem(string id, string name, UTKIntRange initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
}
/// <summary>
/// 정수 범위 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="min">최소값</param>
/// <param name="max">최대값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKIntRangePropertyItem(string id, string name, int min, int max, bool isReadOnly = false)
: base(id, name, new UTKIntRange(min, max))
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKIntRangePropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_minField = container.Q<UTKIntegerField>("min-field");
_maxField = container.Q<UTKIntegerField>("max-field");
if (_minField != null)
{
_minField.Value = Value.Min;
_minField.isReadOnly = IsReadOnly;
}
if (_maxField != null)
{
_maxField.Value = Value.Max;
_maxField.isReadOnly = IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
valueContainer.style.flexDirection = FlexDirection.Row;
_minField = new UTKIntegerField();
_minField.name = "min-field";
_minField.Value = Value.Min;
_minField.style.flexGrow = 1;
_minField.isReadOnly = IsReadOnly;
valueContainer.Add(_minField);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item__range-separator");
valueContainer.Add(separator);
_maxField = new UTKIntegerField();
_maxField.name = "max-field";
_maxField.Value = Value.Max;
_maxField.style.flexGrow = 1;
_maxField.isReadOnly = IsReadOnly;
valueContainer.Add(_maxField);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_minField = element.Q<UTKIntegerField>("min-field");
_maxField = element.Q<UTKIntegerField>("max-field");
if (_minField != null)
{
_minField.Value = Value.Min;
_minField.isReadOnly = IsReadOnly;
_minField.OnValueChanged += OnMinChanged;
}
if (_maxField != null)
{
_maxField.Value = Value.Max;
_maxField.isReadOnly = IsReadOnly;
_maxField.OnValueChanged += OnMaxChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_minField != null)
{
_minField.OnValueChanged -= OnMinChanged;
_minField = null;
}
if (_maxField != null)
{
_maxField.OnValueChanged -= OnMaxChanged;
_maxField = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_minField != null && _minField.Value != Value.Min)
{
_minField.SetValueWithoutNotify(Value.Min);
}
if (_maxField != null && _maxField.Value != Value.Max)
{
_maxField.SetValueWithoutNotify(Value.Max);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_minField != null) _minField.isReadOnly = IsReadOnly;
if (_maxField != null) _maxField.isReadOnly = IsReadOnly;
}
#endregion
#region Private Methods
private void OnMinChanged(int newMin)
{
var newValue = new UTKIntRange(newMin, Value.Max);
DebounceValueChange(newValue, 100).Forget();
}
private void OnMaxChanged(int newMax)
{
var newValue = new UTKIntRange(Value.Min, newMax);
DebounceValueChange(newValue, 100).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -2,34 +2,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 라디오 그룹 속성 아이템
/// UTKRadioButton 그룹으로 단일 선택
/// 라디오 그룹 속성 데이터 클래스입니다.
/// UI는 UTKRadioPropertyItemView에서 담당합니다.
/// </summary>
public class UTKRadioPropertyItem : UTKPropertyItemBase<int>
{
#region Fields
private VisualElement? _radioContainer;
private List<UTKRadioButton> _radioButtons = new();
private List<string> _choices;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.RadioGroup;
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set
{
_choices = value ?? new List<string>();
RebuildRadioButtons();
}
set => _choices = value ?? new List<string>();
}
/// <summary>현재 선택된 항목의 텍스트</summary>
@@ -47,118 +41,35 @@ namespace UVC.UIToolkit
#endregion
#region Constructor
/// <summary>
/// 라디오 그룹 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="selectedIndex">초기 선택 인덱스</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKRadioPropertyItem(string id, string name, List<string> choices, int selectedIndex = 0, bool isReadOnly = false)
: base(id, name, Math.Max(0, Math.Min(selectedIndex, (choices?.Count ?? 1) - 1)))
{
base._isReadOnly = isReadOnly;
_choices = choices ?? new List<string>();
IsReadOnly = isReadOnly;
}
/// <summary>
/// 라디오 그룹 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="selectedIndex">초기 선택 인덱스</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKRadioPropertyItem(string id, string name, IEnumerable<string> choices, int selectedIndex = 0, bool isReadOnly = false)
: this(id, name, choices?.ToList() ?? new List<string>(), selectedIndex, isReadOnly)
{
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKRadioPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_radioContainer = container.Q<VisualElement>("radio-container");
if (_radioContainer != null)
{
CreateRadioButtons();
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_radioContainer = new VisualElement();
_radioContainer.name = "radio-container";
_radioContainer.AddToClassList("utk-property-item__radio-container");
valueContainer.Add(_radioContainer);
CreateRadioButtons();
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_radioContainer = element.Q<VisualElement>("radio-container");
if (_radioContainer != null)
{
// 기존 라디오 버튼 찾기 또는 새로 생성
_radioButtons.Clear();
var existingButtons = _radioContainer.Query<UTKRadioButton>().ToList();
if (existingButtons.Count == _choices.Count)
{
// 기존 버튼 재사용
for (int i = 0; i < existingButtons.Count; i++)
{
var radio = existingButtons[i];
_radioButtons.Add(radio);
radio.IsEnabled = !IsReadOnly;
int index = i;
radio.OnValueChanged += (isChecked) => OnRadioChanged(index, isChecked);
}
}
else
{
// 새로 생성
CreateRadioButtons();
}
UpdateSelection();
}
}
public override void UnbindUI(VisualElement element)
{
_radioButtons.Clear();
_radioContainer = null;
base.UnbindUI(element);
}
public override void RefreshUI()
{
UpdateSelection();
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
foreach (var radio in _radioButtons)
{
radio.IsEnabled = !IsReadOnly;
}
}
#endregion
#region Public Methods
/// <summary>텍스트로 선택</summary>
public void SelectByText(string text)
@@ -170,64 +81,5 @@ namespace UVC.UIToolkit
}
}
#endregion
#region Private Methods
private void CreateRadioButtons()
{
if (_radioContainer == null) return;
_radioContainer.Clear();
_radioButtons.Clear();
for (int i = 0; i < _choices.Count; i++)
{
var radio = new UTKRadioButton(_choices[i]);
radio.name = $"radio-{i}";
radio.AddToClassList("utk-property-item__radio");
int index = i;
radio.OnValueChanged += (isChecked) => OnRadioChanged(index, isChecked);
radio.IsEnabled = !IsReadOnly;
_radioButtons.Add(radio);
_radioContainer.Add(radio);
}
UpdateSelection();
}
private void RebuildRadioButtons()
{
if (_radioContainer != null)
{
CreateRadioButtons();
}
}
private void UpdateSelection()
{
for (int i = 0; i < _radioButtons.Count; i++)
{
_radioButtons[i].SetChecked(i == Value, false);
}
}
private void OnRadioChanged(int index, bool isChecked)
{
if (isChecked && index != Value)
{
// 다른 라디오 버튼 해제
for (int i = 0; i < _radioButtons.Count; i++)
{
if (i != index)
{
_radioButtons[i].SetChecked(false, false);
}
}
Value = index;
}
}
#endregion
}
}

View File

@@ -1,161 +1,54 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 문자열 속성 아이템
/// TextField를 사용한 텍스트 입력
/// 문자열 속성 데이터 클래스입니다.
/// UI는 UTKStringPropertyItemView에서 담당합니다.
/// </summary>
public class UTKStringPropertyItem : UTKPropertyItemBase<string>
{
#region Fields
private UTKInputField? _inputField;
private bool _isMultiline;
private int _maxLength;
private bool _isMultiline = false;
private int _maxLength = -1;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.String;
/// <summary>멀티라인 모드 여부</summary>
public bool IsMultiline
{
get => _isMultiline;
set
{
_isMultiline = value;
if (_inputField != null)
{
_inputField.multiline = value;
}
}
set => _isMultiline = value;
}
/// <summary>최대 문자 길이 (0 = 무제한)</summary>
/// <summary>최대 문자 길이 (-1 = 무제한)</summary>
public int MaxLength
{
get => _maxLength;
set
{
_maxLength = value;
if (_inputField != null)
{
_inputField.maxLength = value;
}
}
set => _maxLength = value;
}
#endregion
#region Constructor
public UTKStringPropertyItem(string id, string name, string initialValue = "", bool isReadOnly = false, bool isMultiline = false, int maxLength = 0)
/// <summary>
/// 문자열 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isMultiline">멀티라인 모드</param>
/// <param name="maxLength">최대 문자 길이</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKStringPropertyItem(string id, string name, string initialValue = "", bool isMultiline = false, int maxLength = -1, bool isReadOnly = false)
: base(id, name, initialValue ?? string.Empty)
{
base._isReadOnly = isReadOnly;
IsReadOnly = isReadOnly;
_isMultiline = isMultiline;
_maxLength = maxLength;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKStringPropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_inputField = container.Q<UTKInputField>("value-field");
if (_inputField != null)
{
_inputField.Value = Value;
_inputField.multiline = _isMultiline;
_inputField.isReadOnly = base._isReadOnly;
if (_maxLength > 0)
{
_inputField.maxLength = _maxLength;
}
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_inputField = new UTKInputField();
_inputField.name = "value-field";
_inputField.Value = Value;
_inputField.multiline = _isMultiline;
_inputField.isReadOnly = IsReadOnly;
if (_maxLength > 0)
{
_inputField.maxLength = _maxLength;
}
valueContainer.Add(_inputField);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_inputField = element.Q<UTKInputField>("value-field");
if (_inputField != null)
{
_inputField.Value = Value;
_inputField.multiline = _isMultiline;
_inputField.isReadOnly = IsReadOnly;
_inputField.OnValueChanged += OnTextChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_inputField != null)
{
_inputField.OnValueChanged -= OnTextChanged;
_inputField = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_inputField != null && _inputField.Value != Value)
{
_inputField.SetValue(Value, false);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_inputField != null)
{
_inputField.isReadOnly = IsReadOnly;
}
}
#endregion
#region Private Methods
private void OnTextChanged(string newValue)
{
DebounceValueChange(newValue).Forget();
}
#endregion
}
}

View File

@@ -1,122 +1,31 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 2D 벡터 속성 아이템
/// UTKVector2Field 사용
/// 2D 벡터 속성 데이터 클래스입니다.
/// UI는 UTKVector2PropertyItemView에서 담당합니다.
/// </summary>
public class UTKVector2PropertyItem : UTKPropertyItemBase<Vector2>
{
#region Fields
private UTKVector2Field? _vectorField;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Vector2;
#endregion
#region Constructor
/// <summary>
/// 2D 벡터 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKVector2PropertyItem(string id, string name, Vector2 initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue)
{
base._isReadOnly = isReadOnly;
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKVector2PropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_vectorField = container.Q<UTKVector2Field>("value-field");
if (_vectorField != null)
{
_vectorField.Value = Value;
_vectorField.label = "";
_vectorField.IsEnabled = !IsReadOnly;
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_vectorField = new UTKVector2Field();
_vectorField.name = "value-field";
_vectorField.label = "";
_vectorField.Value = Value;
_vectorField.AddToClassList("utk-property-item__vector2-field");
valueContainer.Add(_vectorField);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_vectorField = element.Q<UTKVector2Field>("value-field");
if (_vectorField != null)
{
_vectorField.Value = Value;
_vectorField.label = "";
_vectorField.IsEnabled = !IsReadOnly;
_vectorField.OnValueChanged += OnVectorChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_vectorField != null)
{
_vectorField.OnValueChanged -= OnVectorChanged;
_vectorField = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_vectorField != null && _vectorField.Value != Value)
{
_vectorField.SetValueWithoutNotify(Value);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_vectorField != null)
{
_vectorField.IsEnabled = !IsReadOnly;
}
}
#endregion
#region Private Methods
private void OnVectorChanged(Vector2 newValue)
{
DebounceValueChange(newValue, 100).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

@@ -1,120 +1,31 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 3D 벡터 속성 아이템
/// UTKVector3Field 사용
/// 3D 벡터 속성 데이터 클래스입니다.
/// UI는 UTKVector3PropertyItemView에서 담당합니다.
/// </summary>
public class UTKVector3PropertyItem : UTKPropertyItemBase<Vector3>
{
#region Fields
private UTKVector3Field? _vectorField;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.Vector3;
#endregion
#region Constructor
public UTKVector3PropertyItem(string id, string name, Vector3 initialValue = default)
/// <summary>
/// 3D 벡터 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="initialValue">초기 값</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKVector3PropertyItem(string id, string name, Vector3 initialValue = default, bool isReadOnly = false)
: base(id, name, initialValue)
{
}
#endregion
#region Override Methods
public override VisualElement CreateUI()
{
var container = CreateUIFromUxml("UTKVector3PropertyItem");
if (container == null)
{
return CreateUIFallback();
}
_vectorField = container.Q<UTKVector3Field>("value-field");
if (_vectorField != null)
{
_vectorField.Value = Value;
_vectorField.label = "";
}
return container;
}
private VisualElement CreateUIFallback()
{
var container = CreateContainer();
var label = CreateNameLabel();
container.Add(label);
var valueContainer = new VisualElement();
valueContainer.AddToClassList("utk-property-item__value");
_vectorField = new UTKVector3Field();
_vectorField.name = "value-field";
_vectorField.label = "";
_vectorField.Value = Value;
_vectorField.AddToClassList("utk-property-item__vector3-field");
valueContainer.Add(_vectorField);
container.Add(valueContainer);
return container;
}
public override void BindUI(VisualElement element)
{
base.BindUI(element);
_vectorField = element.Q<UTKVector3Field>("value-field");
if (_vectorField != null)
{
_vectorField.Value = Value;
_vectorField.label = "";
_vectorField.IsEnabled = !IsReadOnly;
_vectorField.OnValueChanged += OnVectorChanged;
}
}
public override void UnbindUI(VisualElement element)
{
if (_vectorField != null)
{
_vectorField.OnValueChanged -= OnVectorChanged;
_vectorField = null;
}
base.UnbindUI(element);
}
public override void RefreshUI()
{
if (_vectorField != null && _vectorField.Value != Value)
{
_vectorField.SetValueWithoutNotify(Value);
}
}
protected override void UpdateReadOnlyState()
{
base.UpdateReadOnlyState();
if (_vectorField != null)
{
_vectorField.IsEnabled = !IsReadOnly;
}
}
#endregion
#region Private Methods
private void OnVectorChanged(Vector3 newValue)
{
DebounceValueChange(newValue, 100).Forget();
IsReadOnly = isReadOnly;
}
#endregion
}

View File

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

View File

@@ -0,0 +1,50 @@
#nullable enable
using System;
namespace UVC.UIToolkit
{
/// <summary>
/// PropertyItem View의 기본 인터페이스입니다.
/// Data 클래스와 바인딩하여 사용하거나, 단독으로 사용할 수 있습니다.
/// </summary>
public interface IUTKPropertyItemView : IDisposable
{
/// <summary>라벨 텍스트</summary>
string Label { get; set; }
/// <summary>읽기 전용 여부</summary>
bool IsReadOnly { get; set; }
/// <summary>표시 여부</summary>
bool IsVisible { get; set; }
/// <summary>툴팁 텍스트</summary>
string? TooltipText { get; set; }
/// <summary>Data 클래스와 바인딩</summary>
void Bind(IUTKPropertyItem data);
/// <summary>Data 클래스와 바인딩 해제</summary>
void Unbind();
/// <summary>UI 상태 갱신</summary>
void RefreshUI();
}
/// <summary>
/// 제네릭 버전의 PropertyItem View 인터페이스입니다.
/// 타입 안전한 값 접근을 제공합니다.
/// </summary>
/// <typeparam name="T">값의 타입</typeparam>
public interface IUTKPropertyItemView<T> : IUTKPropertyItemView
{
/// <summary>현재 값</summary>
T Value { get; set; }
/// <summary>값 변경 이벤트 (View에서 직접 변경 시)</summary>
event Action<T>? OnValueChanged;
/// <summary>타입 안전한 Data 클래스와 바인딩</summary>
void Bind(IUTKPropertyItem<T> data);
}
}

View File

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

View File

@@ -0,0 +1,271 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Bool 속성 View 클래스입니다.
/// UTKToggle 또는 UTKLabel (ReadOnly 시)을 사용하여 bool 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
/// // C# 코드에서 생성
/// var view = new UTKBoolPropertyItemView();
/// view.Label = "활성화";
/// view.Value = true;
/// parent.Add(view);
///
/// // UXML에서 사용
/// &lt;utk:UTKBoolPropertyItemView label="활성화" value="true" /&gt;
/// </code>
/// </summary>
[UxmlElement]
public partial class UTKBoolPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<bool>
{
#region Fields
private UTKToggle? _toggle;
private UTKLabel? _stateLabel;
private bool _value;
private IUTKPropertyItem<bool>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKBoolPropertyItemView";
/// <summary>현재 값</summary>
[UxmlAttribute("value")]
public bool Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<bool>? OnValueChanged;
#endregion
#region Constructor
public UTKBoolPropertyItemView() : base()
{
InitializeUI();
}
public UTKBoolPropertyItemView(string label, bool value = false) : base()
{
_value = value;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--bool");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_toggle = this.Q<UTKToggle>("value-field");
_stateLabel = this.Q<UTKLabel>("state-label");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_toggle == null)
{
_toggle = new UTKToggle
{
name = "value-field",
IsOn = _value,
IsInteractive = true
};
_toggle.AddToClassList("utk-property-item-view__toggle");
_valueContainer.Add(_toggle);
}
if (_stateLabel == null)
{
_stateLabel = new UTKLabel(_value ? "True" : "False", UTKLabel.LabelSize.Body2)
{
name = "state-label"
};
_stateLabel.AddToClassList("utk-property-item-view__state-label");
_valueContainer.Add(_stateLabel);
}
}
}
private void RegisterEvents()
{
if (_toggle != null)
{
_toggle.OnValueChanged += OnToggleChanged;
}
}
private void UnregisterEvents()
{
if (_toggle != null)
{
_toggle.OnValueChanged -= OnToggleChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/USS 기반으로 display 조절하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
// USS에서 .utk-property-item-view--readonly 클래스로 display 조절
// Toggle의 상호작용 상태도 업데이트
if (_toggle != null)
{
_toggle.IsInteractive = !isReadOnly;
}
UpdateValueUI();
}
#endregion
#region Event Handling
private void OnToggleChanged(bool newValue)
{
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_toggle != null && _toggle.IsOn != _value)
{
_toggle.SetOn(_value, false);
}
if (_stateLabel != null)
{
_stateLabel.Text = _value ? "True" : "False";
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<bool> boolData)
{
Bind(boolData);
}
else
{
Debug.LogWarning($"[UTKBoolPropertyItemView] Cannot bind to non-bool data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<bool> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<bool> item, bool oldValue, bool newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_toggle = null;
_stateLabel = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 73bebdc74c6f5a54bb7633273f962959

View File

@@ -0,0 +1,410 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Color 속성 View 클래스입니다.
/// 색상 미리보기 + UTKColorPicker 연동을 통해 Color 값을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKColorPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<Color>
{
#region Fields
private VisualElement? _colorPreview;
private UTKButton? _pickerButton;
private UTKInputField? _hexField;
private UTKColorPicker? _currentPicker;
private Color _value = Color.white;
private bool _useAlpha;
private IUTKPropertyItem<Color>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKColorPropertyItemView";
/// <summary>현재 값</summary>
public Color Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
/// <summary>알파 채널 사용 여부</summary>
[UxmlAttribute("use-alpha")]
public bool UseAlpha
{
get => _useAlpha;
set
{
_useAlpha = value;
UpdateValueUI();
}
}
#endregion
#region Events
public event Action<Color>? OnValueChanged;
#endregion
#region Constructor
public UTKColorPropertyItemView() : base()
{
InitializeUI();
}
public UTKColorPropertyItemView(string label, Color value = default, bool useAlpha = false) : base()
{
_value = value == default ? Color.white : value;
_useAlpha = useAlpha;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--color");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_colorPreview = this.Q<VisualElement>("color-preview");
_hexField = this.Q<UTKInputField>("hex-field");
_pickerButton = this.Q<UTKButton>("picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_colorPreview == null)
{
_colorPreview = new VisualElement { name = "color-preview" };
_colorPreview.AddToClassList("utk-property-item-view__color-preview");
_valueContainer.Add(_colorPreview);
}
if (_hexField == null)
{
_hexField = new UTKInputField { name = "hex-field" };
_hexField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_hexField);
}
if (_pickerButton == null)
{
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "picker-btn"
};
_pickerButton.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_pickerButton);
}
}
// 초기 값 설정
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = _value;
}
if (_hexField != null)
{
_hexField.Value = ColorToHex(_value);
_hexField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_colorPreview != null)
{
_colorPreview.RegisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_hexField != null)
{
_hexField.OnValueChanged += OnHexChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
private void UnregisterEvents()
{
if (_colorPreview != null)
{
_colorPreview.UnregisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_hexField != null)
{
_hexField.OnValueChanged -= OnHexChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
_colorPreview?.SetEnabled(!isReadOnly);
if (_hexField != null) _hexField.isReadOnly = isReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !isReadOnly;
}
#endregion
#region Event Handling
private void OnPreviewClicked(ClickEvent evt)
{
if (!IsReadOnly)
{
OpenPicker();
}
}
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OnHexChanged(string newValue)
{
if (TryParseHex(newValue, out Color color))
{
if (_value != color)
{
_value = color;
if (_colorPreview != null) _colorPreview.style.backgroundColor = color;
OnValueChanged?.Invoke(color);
if (_boundData != null && _boundData.Value != color)
{
_boundData.Value = color;
}
}
}
}
private void OpenPicker()
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKColorPicker.Show(root, _value, Label, _useAlpha);
_currentPicker.OnColorChanged += OnPickerColorChanged;
_currentPicker.OnColorSelected += OnPickerColorSelected;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnColorChanged -= OnPickerColorChanged;
_currentPicker.OnColorSelected -= OnPickerColorSelected;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerColorChanged(Color color)
{
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = color;
}
if (_hexField != null)
{
_hexField.SetValue(ColorToHex(color), false);
}
}
private void OnPickerColorSelected(Color color)
{
Value = color;
ClosePicker();
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = _value;
}
if (_hexField != null)
{
var hex = ColorToHex(_value);
if (_hexField.Value != hex)
{
_hexField.SetValue(hex, false);
}
}
}
private string ColorToHex(Color color)
{
if (_useAlpha)
{
return "#" + ColorUtility.ToHtmlStringRGBA(color);
}
else
{
return "#" + ColorUtility.ToHtmlStringRGB(color);
}
}
private bool TryParseHex(string hex, out Color color)
{
if (!hex.StartsWith("#"))
{
hex = "#" + hex;
}
if (ColorUtility.TryParseHtmlString(hex, out color))
{
if (!_useAlpha)
{
color.a = 1f;
}
return true;
}
color = default;
return false;
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<Color> colorData)
{
Bind(colorData);
}
else
{
Debug.LogWarning($"[UTKColorPropertyItemView] Cannot bind to non-Color data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<Color> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKColorPropertyItem colorItem)
{
_useAlpha = colorItem.UseAlpha;
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<Color> item, Color oldValue, Color newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_colorPreview = null;
_hexField = null;
_pickerButton = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,339 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// ColorState 속성 View 클래스입니다.
/// 상태 라벨 + 색상 피커를 사용하여 상태+색상 복합 값을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKColorStatePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<UTKColorState>
{
#region Fields
private UTKLabel? _stateLabel;
private VisualElement? _colorPreview;
private UTKButton? _pickerButton;
private UTKColorPicker? _currentPicker;
private UTKColorState _value;
private IUTKPropertyItem<UTKColorState>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKColorStatePropertyItemView";
/// <summary>현재 값</summary>
public UTKColorState Value
{
get => _value;
set
{
if (!_value.Equals(value))
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && !_boundData.Value.Equals(value))
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<UTKColorState>? OnValueChanged;
#endregion
#region Constructor
public UTKColorStatePropertyItemView() : base()
{
InitializeUI();
}
public UTKColorStatePropertyItemView(string label, UTKColorState value = default) : base()
{
_value = value;
Label = label;
InitializeUI();
}
public UTKColorStatePropertyItemView(string label, string state, Color color) : base()
{
_value = new UTKColorState(state, color);
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--color-state");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_stateLabel = this.Q<UTKLabel>("state-label");
_colorPreview = this.Q<VisualElement>("color-preview");
_pickerButton = this.Q<UTKButton>("picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
_valueContainer.style.flexDirection = FlexDirection.Row;
if (_stateLabel == null)
{
_stateLabel = new UTKLabel(_value.State, UTKLabel.LabelSize.Body2)
{
name = "state-label"
};
_stateLabel.AddToClassList("utk-property-item-view__state-label");
_valueContainer.Add(_stateLabel);
}
if (_colorPreview == null)
{
_colorPreview = new VisualElement { name = "color-preview" };
_colorPreview.AddToClassList("utk-property-item-view__color-preview");
_valueContainer.Add(_colorPreview);
}
if (_pickerButton == null)
{
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "picker-btn"
};
_pickerButton.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_pickerButton);
}
}
// 초기 값 설정
if (_stateLabel != null)
{
_stateLabel.Text = _value.State;
}
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = _value.Color;
_colorPreview.SetEnabled(!IsReadOnly);
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_colorPreview != null)
{
_colorPreview.RegisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_pickerButton != null)
{
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
private void UnregisterEvents()
{
if (_colorPreview != null)
{
_colorPreview.UnregisterCallback<ClickEvent>(OnPreviewClicked);
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_pickerButton != null) _pickerButton.IsEnabled = !isReadOnly;
if (_colorPreview != null) _colorPreview.SetEnabled(!isReadOnly);
}
#endregion
#region Event Handling
private void OnPreviewClicked(ClickEvent evt)
{
if (!IsReadOnly) OpenPicker();
}
private void OnPickerButtonClicked() => OpenPicker();
private void OpenPicker()
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null) root = root.parent;
_currentPicker = UTKColorPicker.Show(root, _value.Color, $"{Label} - {_value.State}", false);
_currentPicker.OnColorChanged += OnPickerColorChanged;
_currentPicker.OnColorSelected += OnPickerColorSelected;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnColorChanged -= OnPickerColorChanged;
_currentPicker.OnColorSelected -= OnPickerColorSelected;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerColorChanged(Color color)
{
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = color;
}
}
private void OnPickerColorSelected(Color color)
{
Value = new UTKColorState(_value.State, color);
ClosePicker();
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_stateLabel != null && _stateLabel.Text != _value.State)
{
_stateLabel.Text = _value.State;
}
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = _value.Color;
}
}
#endregion
#region Public Methods
/// <summary>상태 텍스트만 변경</summary>
public void SetState(string state)
{
Value = new UTKColorState(state, _value.Color);
}
/// <summary>색상만 변경</summary>
public void SetColor(Color color)
{
Value = new UTKColorState(_value.State, color);
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<UTKColorState> colorStateData)
{
Bind(colorStateData);
}
else
{
Debug.LogWarning($"[UTKColorStatePropertyItemView] Cannot bind to non-ColorState data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<UTKColorState> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<UTKColorState> item, UTKColorState oldValue, UTKColorState newValue)
{
if (!_value.Equals(newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_stateLabel = null;
_colorPreview = null;
_pickerButton = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 25977c55de643d94ab9b6af995da701b

View File

@@ -0,0 +1,333 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Date 속성 View 클래스입니다.
/// UTKInputField + UTKDatePicker를 사용하여 날짜를 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKDatePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<DateTime>
{
#region Fields
private UTKInputField? _dateField;
private UTKButton? _pickerButton;
private UTKDatePicker? _currentPicker;
private DateTime _value = DateTime.Today;
private string _dateFormat = "yyyy-MM-dd";
private IUTKPropertyItem<DateTime>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKDatePropertyItemView";
/// <summary>현재 값</summary>
public DateTime Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
/// <summary>날짜 표시 형식</summary>
[UxmlAttribute("date-format")]
public string DateFormat
{
get => _dateFormat;
set
{
_dateFormat = value ?? "yyyy-MM-dd";
UpdateValueUI();
}
}
#endregion
#region Events
public event Action<DateTime>? OnValueChanged;
#endregion
#region Constructor
public UTKDatePropertyItemView() : base()
{
InitializeUI();
}
public UTKDatePropertyItemView(string label, DateTime value = default) : base()
{
_value = value == default ? DateTime.Today : value;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--date");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_dateField = this.Q<UTKInputField>("date-field");
_pickerButton = this.Q<UTKButton>("picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_dateField == null)
{
_dateField = new UTKInputField { name = "date-field" };
_dateField.style.flexGrow = 1;
_dateField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_dateField);
}
if (_pickerButton == null)
{
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "picker-btn"
};
_pickerButton.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_pickerButton);
}
}
// 초기 값 설정
if (_dateField != null)
{
_dateField.SetValue(_value.ToString(_dateFormat), false);
_dateField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_dateField != null)
{
_dateField.OnValueChanged += OnDateTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
private void UnregisterEvents()
{
if (_dateField != null)
{
_dateField.OnValueChanged -= OnDateTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_dateField != null) _dateField.isReadOnly = isReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !isReadOnly;
}
#endregion
#region Event Handling
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OnDateTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
if (_value.Date != date.Date)
{
_value = date.Date;
OnValueChanged?.Invoke(_value);
if (_boundData != null && _boundData.Value != _value)
{
_boundData.Value = _value;
}
}
}
}
private void OpenPicker()
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKDatePicker.Show(root, _value, UTKDatePicker.PickerMode.DateOnly, Label);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime date)
{
Value = date.Date;
ClosePicker();
}
private void OnPickerClosed()
{
_currentPicker = null;
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_dateField != null)
{
var formatted = _value.ToString(_dateFormat);
if (_dateField.Value != formatted)
{
_dateField.SetValue(formatted, false);
}
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<DateTime> dateData)
{
Bind(dateData);
}
else
{
Debug.LogWarning($"[UTKDatePropertyItemView] Cannot bind to non-DateTime data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<DateTime> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKDatePropertyItem dateItem)
{
_dateFormat = dateItem.DateFormat;
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<DateTime> item, DateTime oldValue, DateTime newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_dateField = null;
_pickerButton = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,440 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// DateRange 속성 View 클래스입니다.
/// 시작일, 종료일 두 개의 DatePicker를 사용하여 날짜 범위를 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKDateRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<UTKDateRange>
{
#region Fields
private UTKInputField? _startField;
private UTKInputField? _endField;
private UTKButton? _startPickerBtn;
private UTKButton? _endPickerBtn;
private UTKDatePicker? _currentPicker;
private bool _isEditingStart;
private UTKDateRange _value;
private string _dateFormat = "yyyy-MM-dd";
private IUTKPropertyItem<UTKDateRange>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKDateRangePropertyItemView";
/// <summary>현재 값</summary>
public UTKDateRange Value
{
get => _value;
set
{
if (!_value.Equals(value))
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && !_boundData.Value.Equals(value))
{
_boundData.Value = value;
}
}
}
}
/// <summary>날짜 표시 형식</summary>
[UxmlAttribute("date-format")]
public string DateFormat
{
get => _dateFormat;
set
{
_dateFormat = value ?? "yyyy-MM-dd";
UpdateValueUI();
}
}
#endregion
#region Events
public event Action<UTKDateRange>? OnValueChanged;
#endregion
#region Constructor
public UTKDateRangePropertyItemView() : base()
{
_value = new UTKDateRange(DateTime.Today, DateTime.Today);
InitializeUI();
}
public UTKDateRangePropertyItemView(string label, UTKDateRange value = default) : base()
{
_value = value.Start == default ? new UTKDateRange(DateTime.Today, DateTime.Today) : value;
Label = label;
InitializeUI();
}
public UTKDateRangePropertyItemView(string label, DateTime start, DateTime end) : base()
{
_value = new UTKDateRange(start, end);
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--date-range");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_startField = this.Q<UTKInputField>("start-field");
_startPickerBtn = this.Q<UTKButton>("start-picker-btn");
_endField = this.Q<UTKInputField>("end-field");
_endPickerBtn = this.Q<UTKButton>("end-picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
_valueContainer.style.flexDirection = FlexDirection.Row;
if (_startField == null)
{
_startField = new UTKInputField { name = "start-field" };
_startField.style.flexGrow = 1;
_startField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_startField);
}
if (_startPickerBtn == null)
{
_startPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "start-picker-btn"
};
_startPickerBtn.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_startPickerBtn);
}
// Separator 확인 및 생성
if (this.Q<UTKLabel>(className: "utk-property-item-view__range-separator") == null)
{
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item-view__range-separator");
_valueContainer.Add(separator);
}
if (_endField == null)
{
_endField = new UTKInputField { name = "end-field" };
_endField.style.flexGrow = 1;
_endField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_endField);
}
if (_endPickerBtn == null)
{
_endPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "end-picker-btn"
};
_endPickerBtn.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_endPickerBtn);
}
}
// 초기 값 설정
if (_startField != null)
{
_startField.SetValue(_value.Start.ToString(_dateFormat), false);
_startField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_endField != null)
{
_endField.SetValue(_value.End.ToString(_dateFormat), false);
_endField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
}
if (_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_startField != null)
{
_startField.OnValueChanged += OnStartTextChanged;
}
if (_endField != null)
{
_endField.OnValueChanged += OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked += OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked += OnEndPickerClicked;
}
}
private void UnregisterEvents()
{
if (_startField != null)
{
_startField.OnValueChanged -= OnStartTextChanged;
}
if (_endField != null)
{
_endField.OnValueChanged -= OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked -= OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked -= OnEndPickerClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_startField != null) _startField.isReadOnly = isReadOnly;
if (_endField != null) _endField.isReadOnly = isReadOnly;
if (_startPickerBtn != null) _startPickerBtn.IsEnabled = !isReadOnly;
if (_endPickerBtn != null) _endPickerBtn.IsEnabled = !isReadOnly;
}
#endregion
#region Event Handling
private void OnStartPickerClicked()
{
_isEditingStart = true;
OpenPicker(_value.Start);
}
private void OnEndPickerClicked()
{
_isEditingStart = false;
OpenPicker(_value.End);
}
private void OnStartTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
var range = new UTKDateRange(date.Date, _value.End);
if (!_value.Equals(range))
{
_value = range;
OnValueChanged?.Invoke(range);
if (_boundData != null && !_boundData.Value.Equals(range))
{
_boundData.Value = range;
}
}
}
}
private void OnEndTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime date))
{
var range = new UTKDateRange(_value.Start, date.Date);
if (!_value.Equals(range))
{
_value = range;
OnValueChanged?.Invoke(range);
if (_boundData != null && !_boundData.Value.Equals(range))
{
_boundData.Value = range;
}
}
}
}
private void OpenPicker(DateTime initialDate)
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null) root = root.parent;
string title = _isEditingStart ? $"{Label} - 시작일" : $"{Label} - 종료일";
_currentPicker = UTKDatePicker.Show(root, initialDate, UTKDatePicker.PickerMode.DateOnly, title);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime date)
{
if (_isEditingStart)
{
Value = new UTKDateRange(date.Date, _value.End);
}
else
{
Value = new UTKDateRange(_value.Start, date.Date);
}
ClosePicker();
}
private void OnPickerClosed() => _currentPicker = null;
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_startField != null)
{
var formatted = _value.Start.ToString(_dateFormat);
if (_startField.Value != formatted)
{
_startField.SetValue(formatted, false);
}
}
if (_endField != null)
{
var formatted = _value.End.ToString(_dateFormat);
if (_endField.Value != formatted)
{
_endField.SetValue(formatted, false);
}
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<UTKDateRange> rangeData)
{
Bind(rangeData);
}
else
{
Debug.LogWarning($"[UTKDateRangePropertyItemView] Cannot bind to non-DateRange data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<UTKDateRange> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKDateRangePropertyItem dateRangeItem)
{
_dateFormat = dateRangeItem.DateFormat;
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<UTKDateRange> item, UTKDateRange oldValue, UTKDateRange newValue)
{
if (!_value.Equals(newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_startField = null;
_endField = null;
_startPickerBtn = null;
_endPickerBtn = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 35155d0327a2886458817c2d45643df2

View File

@@ -0,0 +1,333 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// DateTime 속성 View 클래스입니다.
/// UTKInputField + UTKDatePicker (DateAndTime 모드)를 사용하여 날짜시간을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKDateTimePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<DateTime>
{
#region Fields
private UTKInputField? _dateTimeField;
private UTKButton? _pickerButton;
private UTKDatePicker? _currentPicker;
private DateTime _value = DateTime.Now;
private string _dateTimeFormat = "yyyy-MM-dd HH:mm";
private IUTKPropertyItem<DateTime>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKDateTimePropertyItemView";
/// <summary>현재 값</summary>
public DateTime Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
/// <summary>날짜시간 표시 형식</summary>
[UxmlAttribute("datetime-format")]
public string DateTimeFormat
{
get => _dateTimeFormat;
set
{
_dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
UpdateValueUI();
}
}
#endregion
#region Events
public event Action<DateTime>? OnValueChanged;
#endregion
#region Constructor
public UTKDateTimePropertyItemView() : base()
{
InitializeUI();
}
public UTKDateTimePropertyItemView(string label, DateTime value = default) : base()
{
_value = value == default ? DateTime.Now : value;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--datetime");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_dateTimeField = this.Q<UTKInputField>("datetime-field");
_pickerButton = this.Q<UTKButton>("picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_dateTimeField == null)
{
_dateTimeField = new UTKInputField { name = "datetime-field" };
_dateTimeField.style.flexGrow = 1;
_dateTimeField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_dateTimeField);
}
if (_pickerButton == null)
{
_pickerButton = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "picker-btn"
};
_pickerButton.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_pickerButton);
}
}
// 초기 값 설정
if (_dateTimeField != null)
{
_dateTimeField.SetValue(_value.ToString(_dateTimeFormat), false);
_dateTimeField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_pickerButton != null)
{
_pickerButton.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_dateTimeField != null)
{
_dateTimeField.OnValueChanged += OnDateTimeTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked += OnPickerButtonClicked;
}
}
private void UnregisterEvents()
{
if (_dateTimeField != null)
{
_dateTimeField.OnValueChanged -= OnDateTimeTextChanged;
}
if (_pickerButton != null)
{
_pickerButton.OnClicked -= OnPickerButtonClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_dateTimeField != null) _dateTimeField.isReadOnly = isReadOnly;
if (_pickerButton != null) _pickerButton.IsEnabled = !isReadOnly;
}
#endregion
#region Event Handling
private void OnPickerButtonClicked()
{
OpenPicker();
}
private void OnDateTimeTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
if (_value != dateTime)
{
_value = dateTime;
OnValueChanged?.Invoke(_value);
if (_boundData != null && _boundData.Value != _value)
{
_boundData.Value = _value;
}
}
}
}
private void OpenPicker()
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null)
{
root = root.parent;
}
_currentPicker = UTKDatePicker.Show(root, _value, UTKDatePicker.PickerMode.DateAndTime, Label);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime dateTime)
{
Value = dateTime;
ClosePicker();
}
private void OnPickerClosed()
{
_currentPicker = null;
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_dateTimeField != null)
{
var formatted = _value.ToString(_dateTimeFormat);
if (_dateTimeField.Value != formatted)
{
_dateTimeField.SetValue(formatted, false);
}
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<DateTime> dateTimeData)
{
Bind(dateTimeData);
}
else
{
Debug.LogWarning($"[UTKDateTimePropertyItemView] Cannot bind to non-DateTime data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<DateTime> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKDateTimePropertyItem dateTimeItem)
{
_dateTimeFormat = dateTimeItem.DateTimeFormat;
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<DateTime> item, DateTime oldValue, DateTime newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_dateTimeField = null;
_pickerButton = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4821fbbe4446fca45b2e313d095ae797

View File

@@ -0,0 +1,440 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// DateTimeRange 속성 View 클래스입니다.
/// 시작, 종료 두 개의 DateTimePicker를 사용하여 날짜시간 범위를 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKDateTimeRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<UTKDateTimeRange>
{
#region Fields
private UTKInputField? _startField;
private UTKInputField? _endField;
private UTKButton? _startPickerBtn;
private UTKButton? _endPickerBtn;
private UTKDatePicker? _currentPicker;
private bool _isEditingStart;
private UTKDateTimeRange _value;
private string _dateTimeFormat = "yyyy-MM-dd HH:mm";
private IUTKPropertyItem<UTKDateTimeRange>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKDateTimeRangePropertyItemView";
/// <summary>현재 값</summary>
public UTKDateTimeRange Value
{
get => _value;
set
{
if (!_value.Equals(value))
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && !_boundData.Value.Equals(value))
{
_boundData.Value = value;
}
}
}
}
/// <summary>날짜시간 표시 형식</summary>
[UxmlAttribute("datetime-format")]
public string DateTimeFormat
{
get => _dateTimeFormat;
set
{
_dateTimeFormat = value ?? "yyyy-MM-dd HH:mm";
UpdateValueUI();
}
}
#endregion
#region Events
public event Action<UTKDateTimeRange>? OnValueChanged;
#endregion
#region Constructor
public UTKDateTimeRangePropertyItemView() : base()
{
_value = new UTKDateTimeRange(DateTime.Now, DateTime.Now);
InitializeUI();
}
public UTKDateTimeRangePropertyItemView(string label, UTKDateTimeRange value = default) : base()
{
_value = value.Start == default ? new UTKDateTimeRange(DateTime.Now, DateTime.Now) : value;
Label = label;
InitializeUI();
}
public UTKDateTimeRangePropertyItemView(string label, DateTime start, DateTime end) : base()
{
_value = new UTKDateTimeRange(start, end);
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--datetime-range");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_startField = this.Q<UTKInputField>("start-field");
_startPickerBtn = this.Q<UTKButton>("start-picker-btn");
_endField = this.Q<UTKInputField>("end-field");
_endPickerBtn = this.Q<UTKButton>("end-picker-btn");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
_valueContainer.style.flexDirection = FlexDirection.Row;
if (_startField == null)
{
_startField = new UTKInputField { name = "start-field" };
_startField.style.flexGrow = 1;
_startField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_startField);
}
if (_startPickerBtn == null)
{
_startPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "start-picker-btn"
};
_startPickerBtn.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_startPickerBtn);
}
// Separator 확인 및 생성
if (this.Q<UTKLabel>(className: "utk-property-item-view__range-separator") == null)
{
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item-view__range-separator");
_valueContainer.Add(separator);
}
if (_endField == null)
{
_endField = new UTKInputField { name = "end-field" };
_endField.style.flexGrow = 1;
_endField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_endField);
}
if (_endPickerBtn == null)
{
_endPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary)
{
name = "end-picker-btn"
};
_endPickerBtn.AddToClassList("utk-property-item-view__picker-btn");
_valueContainer.Add(_endPickerBtn);
}
}
// 초기 값 설정
if (_startField != null)
{
_startField.SetValue(_value.Start.ToString(_dateTimeFormat), false);
_startField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_endField != null)
{
_endField.SetValue(_value.End.ToString(_dateTimeFormat), false);
_endField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가
}
if (_startPickerBtn != null)
{
_startPickerBtn.IsEnabled = !IsReadOnly;
}
if (_endPickerBtn != null)
{
_endPickerBtn.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_startField != null)
{
_startField.OnValueChanged += OnStartTextChanged;
}
if (_endField != null)
{
_endField.OnValueChanged += OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked += OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked += OnEndPickerClicked;
}
}
private void UnregisterEvents()
{
if (_startField != null)
{
_startField.OnValueChanged -= OnStartTextChanged;
}
if (_endField != null)
{
_endField.OnValueChanged -= OnEndTextChanged;
}
if (_startPickerBtn != null)
{
_startPickerBtn.OnClicked -= OnStartPickerClicked;
}
if (_endPickerBtn != null)
{
_endPickerBtn.OnClicked -= OnEndPickerClicked;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_startField != null) _startField.isReadOnly = isReadOnly;
if (_endField != null) _endField.isReadOnly = isReadOnly;
if (_startPickerBtn != null) _startPickerBtn.IsEnabled = !isReadOnly;
if (_endPickerBtn != null) _endPickerBtn.IsEnabled = !isReadOnly;
}
#endregion
#region Event Handling
private void OnStartPickerClicked()
{
_isEditingStart = true;
OpenPicker(_value.Start);
}
private void OnEndPickerClicked()
{
_isEditingStart = false;
OpenPicker(_value.End);
}
private void OnStartTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
var range = new UTKDateTimeRange(dateTime, _value.End);
if (!_value.Equals(range))
{
_value = range;
OnValueChanged?.Invoke(range);
if (_boundData != null && !_boundData.Value.Equals(range))
{
_boundData.Value = range;
}
}
}
}
private void OnEndTextChanged(string newValue)
{
if (DateTime.TryParse(newValue, out DateTime dateTime))
{
var range = new UTKDateTimeRange(_value.Start, dateTime);
if (!_value.Equals(range))
{
_value = range;
OnValueChanged?.Invoke(range);
if (_boundData != null && !_boundData.Value.Equals(range))
{
_boundData.Value = range;
}
}
}
}
private void OpenPicker(DateTime initialDateTime)
{
if (_currentPicker != null) return;
var root = this as VisualElement;
while (root.parent != null) root = root.parent;
string title = _isEditingStart ? $"{Label} - 시작" : $"{Label} - 종료";
_currentPicker = UTKDatePicker.Show(root, initialDateTime, UTKDatePicker.PickerMode.DateAndTime, title);
_currentPicker.OnDateSelected += OnPickerDateSelected;
_currentPicker.OnClosed += OnPickerClosed;
}
private void ClosePicker()
{
if (_currentPicker != null)
{
_currentPicker.OnDateSelected -= OnPickerDateSelected;
_currentPicker.OnClosed -= OnPickerClosed;
_currentPicker.Close();
_currentPicker = null;
}
}
private void OnPickerDateSelected(DateTime dateTime)
{
if (_isEditingStart)
{
Value = new UTKDateTimeRange(dateTime, _value.End);
}
else
{
Value = new UTKDateTimeRange(_value.Start, dateTime);
}
ClosePicker();
}
private void OnPickerClosed() => _currentPicker = null;
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_startField != null)
{
var formatted = _value.Start.ToString(_dateTimeFormat);
if (_startField.Value != formatted)
{
_startField.SetValue(formatted, false);
}
}
if (_endField != null)
{
var formatted = _value.End.ToString(_dateTimeFormat);
if (_endField.Value != formatted)
{
_endField.SetValue(formatted, false);
}
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<UTKDateTimeRange> rangeData)
{
Bind(rangeData);
}
else
{
Debug.LogWarning($"[UTKDateTimeRangePropertyItemView] Cannot bind to non-DateTimeRange data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<UTKDateTimeRange> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKDateTimeRangePropertyItem dateTimeRangeItem)
{
_dateTimeFormat = dateTimeRangeItem.DateTimeFormat;
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<UTKDateTimeRange> item, UTKDateTimeRange oldValue, UTKDateTimeRange newValue)
{
if (!_value.Equals(newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
ClosePicker();
UnregisterEvents();
Unbind();
OnValueChanged = null;
_startField = null;
_endField = null;
_startPickerBtn = null;
_endPickerBtn = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,319 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Dropdown 속성 View 클래스입니다.
/// UTKDropdown을 사용하여 문자열 목록에서 선택합니다.
/// </summary>
[UxmlElement]
public partial class UTKDropdownPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<string>
{
#region Fields
private UTKDropdown? _dropdown;
private string _value = string.Empty;
private List<string> _choices = new();
private IUTKPropertyItem<string>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKDropdownPropertyItemView";
/// <summary>현재 값</summary>
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value ?? string.Empty;
UpdateValueUI();
OnValueChanged?.Invoke(_value);
if (_boundData != null && _boundData.Value != _value)
{
_boundData.Value = _value;
}
}
}
}
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set
{
_choices = value ?? new List<string>();
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
}
}
}
#endregion
#region Events
public event Action<string>? OnValueChanged;
#endregion
#region Constructor
public UTKDropdownPropertyItemView() : base()
{
InitializeUI();
}
public UTKDropdownPropertyItemView(UTKDropdownPropertyItem data) : base()
{
_choices = data.Choices;
_value = data.Value ?? (_choices.Count > 0 ? _choices[0] : string.Empty);
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKDropdownPropertyItemView(string label, List<string> choices, string initialValue = "") : base()
{
_choices = choices ?? new List<string>();
_value = initialValue ?? (_choices.Count > 0 ? _choices[0] : string.Empty);
Label = label;
InitializeUI();
}
public UTKDropdownPropertyItemView(string label, IEnumerable<string> choices, int selectedIndex = 0) : base()
{
_choices = choices?.ToList() ?? new List<string>();
if (_choices.Count > 0 && selectedIndex >= 0 && selectedIndex < _choices.Count)
{
_value = _choices[selectedIndex];
}
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--dropdown");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_dropdown = this.Q<UTKDropdown>("dropdown-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _dropdown == null)
{
_dropdown = new UTKDropdown { name = "dropdown-field" };
_dropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_dropdown);
}
// 초기 값 설정
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
int selectedIndex = _choices.IndexOf(_value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_dropdown != null)
{
_dropdown.OnSelectionChanged += OnDropdownChanged;
}
}
private void UnregisterEvents()
{
if (_dropdown != null)
{
_dropdown.OnSelectionChanged -= OnDropdownChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_dropdown != null)
{
_dropdown.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void OnDropdownChanged(int index, string newValue)
{
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_dropdown != null && _dropdown.SelectedValue != _value)
{
_dropdown.SetSelectedValue(_value, notify: false);
}
}
#endregion
#region Public Methods
/// <summary>선택 항목 추가</summary>
public void AddChoice(string choice)
{
if (!_choices.Contains(choice))
{
_choices.Add(choice);
if (_dropdown != null)
{
_dropdown.AddOption(choice);
}
}
}
/// <summary>선택 항목 제거</summary>
public bool RemoveChoice(string choice)
{
bool removed = _choices.Remove(choice);
if (removed && _dropdown != null)
{
_dropdown.SetOptions(_choices);
if (_value == choice && _choices.Count > 0)
{
Value = _choices[0];
}
}
return removed;
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<string> stringData)
{
Bind(stringData);
}
else
{
Debug.LogWarning($"[UTKDropdownPropertyItemView] Cannot bind to non-string data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<string> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value ?? string.Empty;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKDropdownPropertyItem dropdownItem)
{
_choices = dropdownItem.Choices;
if (_dropdown != null)
{
_dropdown.SetOptions(_choices);
}
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<string> item, string oldValue, string newValue)
{
if (_value != newValue)
{
_value = newValue ?? string.Empty;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_dropdown = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,267 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Enum 속성 View 클래스입니다.
/// UTKEnumDropDown을 사용하여 열거형 값을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKEnumPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<Enum>
{
#region Fields
private UTKEnumDropDown? _enumDropdown;
private Enum? _value;
private Type? _enumType;
private IUTKPropertyItem<Enum>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKEnumPropertyItemView";
/// <summary>현재 값</summary>
public Enum Value
{
get => _value!;
set
{
if (_value != value)
{
_value = value;
_enumType = value?.GetType();
UpdateValueUI();
OnValueChanged?.Invoke(value!);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value!;
}
}
}
}
/// <summary>열거형 타입</summary>
public Type? EnumType => _enumType;
#endregion
#region Events
public event Action<Enum>? OnValueChanged;
#endregion
#region Constructor
public UTKEnumPropertyItemView() : base()
{
InitializeUI();
}
public UTKEnumPropertyItemView(UTKEnumPropertyItem data) : base()
{
if (data == null) throw new ArgumentNullException(nameof(data));
_value = data.Value;
_enumType = data.Value?.GetType();
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKEnumPropertyItemView(string label, Enum initialValue) : base()
{
_value = initialValue ?? throw new ArgumentNullException(nameof(initialValue));
_enumType = initialValue.GetType();
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--enum");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_enumDropdown = this.Q<UTKEnumDropDown>("enum-dropdown");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _enumDropdown == null)
{
_enumDropdown = new UTKEnumDropDown { name = "enum-dropdown" };
_enumDropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_enumDropdown);
}
// 초기 값 설정
if (_enumDropdown != null)
{
if (_value != null)
{
_enumDropdown.Init(_value);
}
_enumDropdown.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_enumDropdown != null)
{
_enumDropdown.OnValueChanged += OnEnumDropdownChanged;
}
}
private void UnregisterEvents()
{
if (_enumDropdown != null)
{
_enumDropdown.OnValueChanged -= OnEnumDropdownChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_enumDropdown != null)
{
_enumDropdown.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void OnEnumDropdownChanged(Enum? newValue)
{
if (newValue != null && !Equals(_value, newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !Equals(_boundData.Value, newValue))
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_enumDropdown != null && _value != null)
{
if (_enumDropdown.Value != _value)
{
_enumDropdown.Value = _value;
}
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<Enum> enumData)
{
Bind(enumData);
}
else
{
Debug.LogWarning($"[UTKEnumPropertyItemView] Cannot bind to non-Enum data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<Enum> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
_enumType = data.Value?.GetType();
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (_enumDropdown != null && _value != null)
{
_enumDropdown.Init(_value);
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<Enum> item, Enum oldValue, Enum newValue)
{
if (!Equals(_value, newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_enumDropdown = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,447 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Float 속성 View 클래스입니다.
/// UTKFloatField 또는 UTKSlider를 사용하여 float 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
/// // C# 코드에서 생성
/// var view = new UTKFloatPropertyItemView();
/// view.Label = "속도";
/// view.Value = 1.5f;
/// view.UseSlider = true;
/// view.MinValue = 0f;
/// view.MaxValue = 10f;
/// parent.Add(view);
///
/// // UXML에서 사용
/// &lt;utk:UTKFloatPropertyItemView label="속도" value="1.5" use-slider="true" min-value="0" max-value="10" /&gt;
/// </code>
///
/// <para><b>사용법 (Data 바인딩):</b></para>
/// <code>
/// var data = new UTKFloatPropertyItem("speed", "속도", 1.5f);
/// var view = new UTKFloatPropertyItemView();
/// view.Bind(data);
/// </code>
/// </summary>
[UxmlElement]
public partial class UTKFloatPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<float>
{
#region Fields
private UTKFloatField? _floatField;
private UTKSlider? _slider;
private float _value;
private float _minValue;
private float _maxValue = 1f;
private bool _useSlider;
private IUTKPropertyItem<float>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKFloatPropertyItemView";
/// <summary>현재 값</summary>
[UxmlAttribute("value")]
public float Value
{
get => _value;
set
{
if (!Mathf.Approximately(_value, value))
{
var oldValue = _value;
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
// 바인딩된 데이터가 있으면 동기화
if (_boundData != null && !Mathf.Approximately(_boundData.Value, value))
{
_boundData.Value = value;
}
}
}
}
/// <summary>최소값 (슬라이더 모드)</summary>
[UxmlAttribute("min-value")]
public float MinValue
{
get => _minValue;
set
{
_minValue = value;
if (_slider != null)
{
_slider.lowValue = value;
}
}
}
/// <summary>최대값 (슬라이더 모드)</summary>
[UxmlAttribute("max-value")]
public float MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
if (_slider != null)
{
_slider.highValue = value;
}
}
}
/// <summary>슬라이더 사용 여부</summary>
[UxmlAttribute("use-slider")]
public bool UseSlider
{
get => _useSlider;
set
{
if (_useSlider != value)
{
_useSlider = value;
UpdateSliderClass();
}
}
}
#endregion
#region Events
public event Action<float>? OnValueChanged;
#endregion
#region Constructor
public UTKFloatPropertyItemView() : base()
{
InitializeUI();
}
public UTKFloatPropertyItemView(string label, float value = 0f, bool useSlider = false) : base()
{
_value = value;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
public UTKFloatPropertyItemView(UTKFloatPropertyItem data) : base()
{
_value = data.Value;
_minValue = data.MinValue;
_maxValue = data.MaxValue;
_useSlider = data.UseSlider;
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKFloatPropertyItemView(string label, float value, float minValue, float maxValue, bool useSlider = true) : base()
{
_value = value;
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--float");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
// 슬라이더 클래스 업데이트
UpdateSliderClass();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_slider = this.Q<UTKSlider>("slider-field");
_floatField = this.Q<UTKFloatField>("value-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_slider == null)
{
_slider = new UTKSlider("", _minValue, _maxValue, _value)
{
name = "slider-field"
};
_slider.AddToClassList("utk-property-item-view__slider");
_valueContainer.Insert(0, _slider);
}
if (_floatField == null)
{
_floatField = new UTKFloatField { name = "value-field" };
_floatField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_floatField);
}
}
// 초기 값 설정
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.SetValueWithoutNotify(_value);
}
if (_floatField != null)
{
_floatField.SetValueWithoutNotify(_value);
_floatField.isReadOnly = IsReadOnly;
}
}
private void UpdateSliderClass()
{
if (_useSlider)
{
AddToClassList("utk-property-item-view--slider");
}
else
{
RemoveFromClassList("utk-property-item-view--slider");
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_floatField != null)
{
_floatField.isReadOnly = isReadOnly;
}
if (_slider != null)
{
_slider.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void RegisterEvents()
{
if (_floatField != null)
{
_floatField.OnValueChanged += OnFloatFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged += OnSliderChanged;
}
}
private void UnregisterEvents()
{
if (_floatField != null)
{
_floatField.OnValueChanged -= OnFloatFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged -= OnSliderChanged;
}
}
private void OnFloatFieldChanged(float newValue)
{
float clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue;
if (_slider != null && !Mathf.Approximately(_slider.Value, clampedValue))
{
_slider.SetValueWithoutNotify(clampedValue);
}
if (_floatField != null && !Mathf.Approximately(_floatField.Value, clampedValue))
{
_floatField.SetValueWithoutNotify(clampedValue);
}
// 값 설정 (이벤트 발생)
if (!Mathf.Approximately(_value, clampedValue))
{
_value = clampedValue;
OnValueChanged?.Invoke(clampedValue);
if (_boundData != null && !Mathf.Approximately(_boundData.Value, clampedValue))
{
_boundData.Value = clampedValue;
}
}
}
private void OnSliderChanged(float newValue)
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, newValue))
{
_floatField.SetValueWithoutNotify(newValue);
}
// 값 설정 (이벤트 발생)
if (!Mathf.Approximately(_value, newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !Mathf.Approximately(_boundData.Value, newValue))
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, _value))
{
_floatField.SetValueWithoutNotify(_value);
}
if (_slider != null && !Mathf.Approximately(_slider.Value, _value))
{
_slider.SetValueWithoutNotify(_value);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<float> floatData)
{
Bind(floatData);
}
else
{
Debug.LogWarning($"[UTKFloatPropertyItemView] Cannot bind to non-float data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<float> data)
{
// 기존 바인딩 해제
Unbind();
_boundData = data;
// 데이터에서 속성 동기화
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
// 슬라이더 관련 속성 동기화
if (data is UTKFloatPropertyItem floatItem)
{
_minValue = floatItem.MinValue;
_maxValue = floatItem.MaxValue;
if (_useSlider != floatItem.UseSlider)
{
_useSlider = floatItem.UseSlider;
UpdateSliderClass();
}
// 슬라이더 범위 업데이트
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
}
}
// 데이터 변경 이벤트 구독
data.OnTypedValueChanged += OnDataValueChanged;
// UI 갱신
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<float> item, float oldValue, float newValue)
{
if (!Mathf.Approximately(_value, newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_floatField = null;
_slider = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14166aaf18c01844fbb08bc1c5b794cc

View File

@@ -0,0 +1,296 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// FloatRange 속성 View 클래스입니다.
/// Min, Max 두 개의 UTKFloatField를 사용하여 실수 범위를 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKFloatRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<UTKFloatRange>
{
#region Fields
private UTKFloatField? _minField;
private UTKFloatField? _maxField;
private UTKFloatRange _value;
private IUTKPropertyItem<UTKFloatRange>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKFloatRangePropertyItemView";
/// <summary>현재 값</summary>
public UTKFloatRange Value
{
get => _value;
set
{
if (!_value.Equals(value))
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && !_boundData.Value.Equals(value))
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<UTKFloatRange>? OnValueChanged;
#endregion
#region Constructor
public UTKFloatRangePropertyItemView() : base()
{
InitializeUI();
}
public UTKFloatRangePropertyItemView(string label, UTKFloatRange value = default) : base()
{
_value = value;
Label = label;
InitializeUI();
}
public UTKFloatRangePropertyItemView(string label, float min, float max) : base()
{
_value = new UTKFloatRange(min, max);
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--float-range");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_minField = this.Q<UTKFloatField>("min-field");
_maxField = this.Q<UTKFloatField>("max-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
_valueContainer.style.flexDirection = FlexDirection.Row;
if (_minField == null)
{
_minField = new UTKFloatField { name = "min-field" };
_minField.style.flexGrow = 1;
_minField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_minField);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item-view__range-separator");
_valueContainer.Add(separator);
}
if (_maxField == null)
{
_maxField = new UTKFloatField { name = "max-field" };
_maxField.style.flexGrow = 1;
_maxField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_maxField);
}
}
// 초기 값 설정
if (_minField != null)
{
_minField.SetValueWithoutNotify(_value.Min);
_minField.isReadOnly = IsReadOnly;
}
if (_maxField != null)
{
_maxField.SetValueWithoutNotify(_value.Max);
_maxField.isReadOnly = IsReadOnly;
}
}
private void RegisterEvents()
{
if (_minField != null)
{
_minField.OnValueChanged += OnMinChanged;
}
if (_maxField != null)
{
_maxField.OnValueChanged += OnMaxChanged;
}
}
private void UnregisterEvents()
{
if (_minField != null)
{
_minField.OnValueChanged -= OnMinChanged;
}
if (_maxField != null)
{
_maxField.OnValueChanged -= OnMaxChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_minField != null) _minField.isReadOnly = isReadOnly;
if (_maxField != null) _maxField.isReadOnly = isReadOnly;
}
#endregion
#region Event Handling
private void OnMinChanged(float newMin)
{
var newValue = new UTKFloatRange(newMin, _value.Max);
if (!_value.Equals(newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !_boundData.Value.Equals(newValue))
{
_boundData.Value = newValue;
}
}
}
private void OnMaxChanged(float newMax)
{
var newValue = new UTKFloatRange(_value.Min, newMax);
if (!_value.Equals(newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !_boundData.Value.Equals(newValue))
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_minField != null && !Mathf.Approximately(_minField.Value, _value.Min))
{
_minField.SetValueWithoutNotify(_value.Min);
}
if (_maxField != null && !Mathf.Approximately(_maxField.Value, _value.Max))
{
_maxField.SetValueWithoutNotify(_value.Max);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<UTKFloatRange> rangeData)
{
Bind(rangeData);
}
else
{
Debug.LogWarning($"[UTKFloatRangePropertyItemView] Cannot bind to non-FloatRange data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<UTKFloatRange> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<UTKFloatRange> item, UTKFloatRange oldValue, UTKFloatRange newValue)
{
if (!_value.Equals(newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_minField = null;
_maxField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 362da87d1c7470e4d886e0dfc06dbdc8

View File

@@ -0,0 +1,431 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Int 속성 View 클래스입니다.
/// UTKIntegerField 또는 UTKSliderInt를 사용하여 int 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
/// // C# 코드에서 생성
/// var view = new UTKIntPropertyItemView();
/// view.Label = "수량";
/// view.Value = 10;
/// view.UseSlider = true;
/// view.MinValue = 0;
/// view.MaxValue = 100;
/// parent.Add(view);
///
/// // UXML에서 사용
/// &lt;utk:UTKIntPropertyItemView label="수량" value="10" use-slider="true" min-value="0" max-value="100" /&gt;
/// </code>
/// </summary>
[UxmlElement]
public partial class UTKIntPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<int>
{
#region Fields
private UTKIntegerField? _intField;
private UTKSliderInt? _slider;
private int _value;
private int _minValue;
private int _maxValue = 100;
private bool _useSlider;
private IUTKPropertyItem<int>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKIntPropertyItemView";
/// <summary>현재 값</summary>
[UxmlAttribute("value")]
public int Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
/// <summary>최소값 (슬라이더 모드)</summary>
[UxmlAttribute("min-value")]
public int MinValue
{
get => _minValue;
set
{
_minValue = value;
if (_slider != null)
{
_slider.lowValue = value;
}
}
}
/// <summary>최대값 (슬라이더 모드)</summary>
[UxmlAttribute("max-value")]
public int MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
if (_slider != null)
{
_slider.highValue = value;
}
}
}
/// <summary>슬라이더 사용 여부</summary>
[UxmlAttribute("use-slider")]
public bool UseSlider
{
get => _useSlider;
set
{
if (_useSlider != value)
{
_useSlider = value;
UpdateSliderClass();
}
}
}
#endregion
#region Events
public event Action<int>? OnValueChanged;
#endregion
#region Constructor
public UTKIntPropertyItemView() : base()
{
InitializeUI();
}
public UTKIntPropertyItemView(string label, int value = 0, bool useSlider = false) : base()
{
_value = value;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
public UTKIntPropertyItemView(UTKIntPropertyItem item) : base()
{
_value = item.Value;
_minValue = item.MinValue;
_maxValue = item.MaxValue;
_useSlider = item.UseSlider;
_isReadOnly = item.IsReadOnly;
InitializeUI();
Bind(item);
}
public UTKIntPropertyItemView(string label, int value, int minValue, int maxValue, bool useSlider = true) : base()
{
_value = value;
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--int");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
// 슬라이더 클래스 업데이트
UpdateSliderClass();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_slider = this.Q<UTKSliderInt>("slider-field");
_intField = this.Q<UTKIntegerField>("value-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_slider == null)
{
_slider = new UTKSliderInt("", _minValue, _maxValue, _value)
{
name = "slider-field"
};
_slider.AddToClassList("utk-property-item-view__slider");
_valueContainer.Insert(0, _slider);
}
if (_intField == null)
{
_intField = new UTKIntegerField { name = "value-field" };
_intField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_intField);
}
}
// 초기 값 설정
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.SetValueWithoutNotify(_value);
}
if (_intField != null)
{
_intField.SetValueWithoutNotify(_value);
_intField.isReadOnly = IsReadOnly;
}
}
private void UpdateSliderClass()
{
if (_useSlider)
{
AddToClassList("utk-property-item-view--slider");
}
else
{
RemoveFromClassList("utk-property-item-view--slider");
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_intField != null)
{
_intField.isReadOnly = isReadOnly;
}
if (_slider != null)
{
_slider.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void RegisterEvents()
{
if (_intField != null)
{
_intField.OnValueChanged += OnIntFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged += OnSliderChanged;
}
}
private void UnregisterEvents()
{
if (_intField != null)
{
_intField.OnValueChanged -= OnIntFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged -= OnSliderChanged;
}
}
private void OnIntFieldChanged(int newValue)
{
int clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue;
if (_slider != null && _slider.Value != clampedValue)
{
_slider.SetValueWithoutNotify(clampedValue);
}
if (_intField != null && _intField.Value != clampedValue)
{
_intField.SetValueWithoutNotify(clampedValue);
}
if (_value != clampedValue)
{
_value = clampedValue;
OnValueChanged?.Invoke(clampedValue);
if (_boundData != null && _boundData.Value != clampedValue)
{
_boundData.Value = clampedValue;
}
}
}
private void OnSliderChanged(int newValue)
{
if (_intField != null && _intField.Value != newValue)
{
_intField.SetValueWithoutNotify(newValue);
}
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_intField != null && _intField.Value != _value)
{
_intField.SetValueWithoutNotify(_value);
}
if (_slider != null && _slider.Value != _value)
{
_slider.SetValueWithoutNotify(_value);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<int> intData)
{
Bind(intData);
}
else
{
Debug.LogWarning($"[UTKIntPropertyItemView] Cannot bind to non-int data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<int> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKIntPropertyItem intItem)
{
_minValue = intItem.MinValue;
_maxValue = intItem.MaxValue;
if (_useSlider != intItem.UseSlider)
{
_useSlider = intItem.UseSlider;
UpdateSliderClass();
}
// 슬라이더 범위 업데이트
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
}
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<int> item, int oldValue, int newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_intField = null;
_slider = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61d3b9a295685a14c818d6b006bbae76

View File

@@ -0,0 +1,296 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// IntRange 속성 View 클래스입니다.
/// Min, Max 두 개의 UTKIntegerField를 사용하여 정수 범위를 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKIntRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<UTKIntRange>
{
#region Fields
private UTKIntegerField? _minField;
private UTKIntegerField? _maxField;
private UTKIntRange _value;
private IUTKPropertyItem<UTKIntRange>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKIntRangePropertyItemView";
/// <summary>현재 값</summary>
public UTKIntRange Value
{
get => _value;
set
{
if (!_value.Equals(value))
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && !_boundData.Value.Equals(value))
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<UTKIntRange>? OnValueChanged;
#endregion
#region Constructor
public UTKIntRangePropertyItemView() : base()
{
InitializeUI();
}
public UTKIntRangePropertyItemView(string label, UTKIntRange value = default) : base()
{
_value = value;
Label = label;
InitializeUI();
}
public UTKIntRangePropertyItemView(string label, int min, int max) : base()
{
_value = new UTKIntRange(min, max);
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--int-range");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_minField = this.Q<UTKIntegerField>("min-field");
_maxField = this.Q<UTKIntegerField>("max-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
_valueContainer.style.flexDirection = FlexDirection.Row;
if (_minField == null)
{
_minField = new UTKIntegerField { name = "min-field" };
_minField.style.flexGrow = 1;
_minField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_minField);
var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2);
separator.AddToClassList("utk-property-item-view__range-separator");
_valueContainer.Add(separator);
}
if (_maxField == null)
{
_maxField = new UTKIntegerField { name = "max-field" };
_maxField.style.flexGrow = 1;
_maxField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_maxField);
}
}
// 초기 값 설정
if (_minField != null)
{
_minField.SetValueWithoutNotify(_value.Min);
_minField.isReadOnly = IsReadOnly;
}
if (_maxField != null)
{
_maxField.SetValueWithoutNotify(_value.Max);
_maxField.isReadOnly = IsReadOnly;
}
}
private void RegisterEvents()
{
if (_minField != null)
{
_minField.OnValueChanged += OnMinChanged;
}
if (_maxField != null)
{
_maxField.OnValueChanged += OnMaxChanged;
}
}
private void UnregisterEvents()
{
if (_minField != null)
{
_minField.OnValueChanged -= OnMinChanged;
}
if (_maxField != null)
{
_maxField.OnValueChanged -= OnMaxChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_minField != null) _minField.isReadOnly = isReadOnly;
if (_maxField != null) _maxField.isReadOnly = isReadOnly;
}
#endregion
#region Event Handling
private void OnMinChanged(int newMin)
{
var newValue = new UTKIntRange(newMin, _value.Max);
if (!_value.Equals(newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !_boundData.Value.Equals(newValue))
{
_boundData.Value = newValue;
}
}
}
private void OnMaxChanged(int newMax)
{
var newValue = new UTKIntRange(_value.Min, newMax);
if (!_value.Equals(newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !_boundData.Value.Equals(newValue))
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_minField != null && _minField.Value != _value.Min)
{
_minField.SetValueWithoutNotify(_value.Min);
}
if (_maxField != null && _maxField.Value != _value.Max)
{
_maxField.SetValueWithoutNotify(_value.Max);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<UTKIntRange> rangeData)
{
Bind(rangeData);
}
else
{
Debug.LogWarning($"[UTKIntRangePropertyItemView] Cannot bind to non-IntRange data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<UTKIntRange> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<UTKIntRange> item, UTKIntRange oldValue, UTKIntRange newValue)
{
if (!_value.Equals(newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_minField = null;
_maxField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 66594df42e9b4204aa5d6c05342ea4e3

View File

@@ -0,0 +1,365 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 모든 PropertyItem View의 기본 추상 클래스입니다.
/// VisualElement를 상속하여 UXML/C#에서 단독 사용이 가능합니다.
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>테마 자동 적용 (UTKThemeManager)</item>
/// <item>공통/개별 USS 자동 로드</item>
/// <item>Label + Value 레이아웃 제공</item>
/// <item>Data 클래스 바인딩 지원</item>
/// <item>단독 사용 시 UXML 속성 지원</item>
/// </list>
///
/// <para><b>관련 리소스:</b></para>
/// <list type="bullet">
/// <item>Resources/UIToolkit/Property/Views/{ViewName}.uxml - 각 뷰 UXML</item>
/// <item>Resources/UIToolkit/Property/Views/{ViewName}Uss.uss - 각 뷰 USS</item>
/// <item>Resources/UIToolkit/Property/Views/UTKPropertyItemViewCommonUss.uss - 공통 USS</item>
/// </list>
/// </summary>
public abstract class UTKPropertyItemViewBase : VisualElement, IDisposable
{
#region Constants
protected const string UXML_BASE_PATH = "UIToolkit/Property/Views/";
protected const string USS_BASE_PATH = "UIToolkit/Property/Views/";
protected const string USS_COMMON_PATH = "UIToolkit/Property/Views/UTKPropertyItemViewCommonUss";
protected const string USS_CLASS_READONLY = "utk-property-item-view--readonly";
protected const string USS_CLASS_HIDDEN = "utk-property-item-view--hidden";
#endregion
#region Static Cache
private static readonly Dictionary<string, VisualTreeAsset> _uxmlCache = new();
private static readonly Dictionary<string, StyleSheet> _ussCache = new();
private static StyleSheet? _commonUssCache;
#endregion
#region Fields
protected bool _disposed;
protected UTKLabel? _labelElement;
protected VisualElement? _valueContainer;
protected string _label = string.Empty;
protected bool _isReadOnly = false;
protected bool _isVisible = true;
protected string? _tooltipText;
#endregion
#region Properties
/// <summary>라벨 텍스트</summary>
[UxmlAttribute("label")]
public string Label
{
get => _label;
set
{
_label = value;
if (_labelElement != null)
{
_labelElement.Text = value;
}
}
}
/// <summary>읽기 전용 여부</summary>
[UxmlAttribute("is-read-only")]
public bool IsReadOnly
{
get => _isReadOnly;
set
{
if (_isReadOnly != value)
{
_isReadOnly = value;
UpdateReadOnlyState();
}
}
}
/// <summary>표시 여부</summary>
[UxmlAttribute("is-visible")]
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
UpdateVisibility();
}
}
}
/// <summary>툴팁 텍스트</summary>
[UxmlAttribute("tooltip-text")]
public string? TooltipText
{
get => _tooltipText;
set
{
_tooltipText = value;
UpdateTooltip();
}
}
#endregion
#region Constructor
protected UTKPropertyItemViewBase()
{
// 1. 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 2. 공통 USS 로드
ApplyCommonStyleSheet();
// 3. 테마 변경 구독
SubscribeToThemeChanges();
}
#endregion
#region Abstract Methods
/// <summary>View 타입 이름 (UXML/USS 파일명에 사용)</summary>
protected abstract string ViewTypeName { get; }
/// <summary>값 영역 UI 생성 (하위 클래스에서 구현)</summary>
protected abstract void CreateValueUI(VisualElement container);
/// <summary>UI 상태 갱신 (하위 클래스에서 구현)</summary>
public abstract void RefreshUI();
/// <summary>읽기 전용 상태 업데이트 (하위 클래스에서 오버라이드)</summary>
protected abstract void OnReadOnlyStateChanged(bool isReadOnly);
#endregion
#region Protected Methods
/// <summary>
/// UXML 템플릿에서 UI를 생성합니다.
/// </summary>
/// <returns>성공 여부</returns>
protected bool CreateUIFromUxml()
{
var asset = LoadUxmlAsset(ViewTypeName);
if (asset == null)
{
return false;
}
var root = asset.Instantiate();
Add(root);
// 개별 USS 로드
ApplyStyleSheet(ViewTypeName + "Uss");
// 공통 요소 참조 획득
_labelElement = this.Q<UTKLabel>("label");
_valueContainer = this.Q<VisualElement>("value-container");
if (_labelElement != null)
{
_labelElement.Text = _label;
}
return true;
}
/// <summary>
/// 코드로 기본 UI 레이아웃을 생성합니다 (Fallback).
/// </summary>
protected void CreateUIFallback()
{
AddToClassList("utk-property-item-view");
// 개별 USS 로드
ApplyStyleSheet(ViewTypeName + "Uss");
// Label
_labelElement = new UTKLabel(_label, UTKLabel.LabelSize.Body2)
{
name = "label"
};
_labelElement.AddToClassList("utk-property-item-view__label");
Add(_labelElement);
// Value Container
_valueContainer = new VisualElement { name = "value-container" };
_valueContainer.AddToClassList("utk-property-item-view__value");
Add(_valueContainer);
// 하위 클래스에서 값 UI 생성
CreateValueUI(_valueContainer);
}
/// <summary>읽기 전용 상태 업데이트</summary>
protected virtual void UpdateReadOnlyState()
{
if (_isReadOnly)
{
AddToClassList(USS_CLASS_READONLY);
}
else
{
RemoveFromClassList(USS_CLASS_READONLY);
}
OnReadOnlyStateChanged(_isReadOnly);
}
/// <summary>표시 상태 업데이트</summary>
protected virtual void UpdateVisibility()
{
style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None;
if (_isVisible)
{
RemoveFromClassList(USS_CLASS_HIDDEN);
}
else
{
AddToClassList(USS_CLASS_HIDDEN);
}
}
/// <summary>툴팁 업데이트</summary>
protected virtual void UpdateTooltip()
{
if (!string.IsNullOrEmpty(_tooltipText))
{
this.SetTooltip(_tooltipText);
}
else
{
this.ClearTooltip();
}
}
#endregion
#region Theme
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region StyleSheet Loading
/// <summary>공통 USS 스타일시트를 적용합니다.</summary>
protected void ApplyCommonStyleSheet()
{
var commonUss = LoadCommonUssAsset();
if (commonUss != null)
{
styleSheets.Add(commonUss);
}
}
/// <summary>개별 USS 스타일시트를 적용합니다.</summary>
protected void ApplyStyleSheet(string ussName)
{
var uss = LoadUssAsset(ussName);
if (uss != null)
{
styleSheets.Add(uss);
}
}
/// <summary>UXML 에셋 로드 (캐시 사용)</summary>
protected static VisualTreeAsset? LoadUxmlAsset(string uxmlName)
{
string path = UXML_BASE_PATH + uxmlName;
if (_uxmlCache.TryGetValue(path, out var cached))
{
return cached;
}
var asset = Resources.Load<VisualTreeAsset>(path);
if (asset != null)
{
_uxmlCache[path] = asset;
}
return asset;
}
/// <summary>공통 USS 에셋 로드 (캐시 사용)</summary>
protected static StyleSheet? LoadCommonUssAsset()
{
if (_commonUssCache != null)
{
return _commonUssCache;
}
_commonUssCache = Resources.Load<StyleSheet>(USS_COMMON_PATH);
return _commonUssCache;
}
/// <summary>USS 에셋 로드 (캐시 사용)</summary>
protected static StyleSheet? LoadUssAsset(string ussName)
{
string path = USS_BASE_PATH + ussName;
if (_ussCache.TryGetValue(path, out var cached))
{
return cached;
}
var asset = Resources.Load<StyleSheet>(path);
if (asset != null)
{
_ussCache[path] = asset;
}
return asset;
}
/// <summary>모든 캐시 클리어 (에디터 용도)</summary>
public static void ClearAllCache()
{
_uxmlCache.Clear();
_ussCache.Clear();
_commonUssCache = null;
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (disposing)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
this.ClearTooltip();
_labelElement = null;
_valueContainer = null;
}
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 55df5e23ee322e643ae650eea2bd4120

View File

@@ -0,0 +1,215 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// PropertyItem 데이터에 해당하는 View를 생성하는 팩토리 클래스입니다.
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>PropertyType에 따라 적절한 View 인스턴스 생성</item>
/// <item>Data 클래스와 View 자동 바인딩</item>
/// <item>커스텀 View 등록 지원</item>
/// </list>
///
/// <para><b>사용법:</b></para>
/// <code>
/// var data = new UTKFloatPropertyItem("speed", "속도", 1.5f);
/// var view = UTKPropertyItemViewFactory.CreateView(data);
/// parent.Add(view);
/// </code>
/// </summary>
public static class UTKPropertyItemViewFactory
{
#region Fields
private static readonly Dictionary<UTKPropertyType, Func<IUTKPropertyItemView>> _customViewFactories = new();
#endregion
#region Public Methods
/// <summary>
/// PropertyItem 데이터에 해당하는 View를 생성하고 바인딩합니다.
/// </summary>
/// <param name="data">PropertyItem 데이터</param>
/// <returns>바인딩된 View (VisualElement)</returns>
public static VisualElement CreateView(IUTKPropertyItem data)
{
var view = CreateViewInstance(data);
view.Bind(data);
if (view is VisualElement element)
{
return element;
}
Debug.LogError($"[UTKPropertyItemViewFactory] View is not a VisualElement: {view.GetType().Name}");
return new VisualElement();
}
/// <summary>
/// PropertyItem 데이터에 해당하는 View를 생성합니다 (바인딩 없음).
/// </summary>
/// <param name="data">PropertyItem 데이터 (View 설정에 필요한 정보 제공)</param>
/// <returns>View 인스턴스</returns>
public static IUTKPropertyItemView CreateViewInstance(IUTKPropertyItem data)
{
// 커스텀 팩토리 우선
if (_customViewFactories.TryGetValue(data.PropertyType, out var customFactory))
{
return customFactory();
}
// 기본 View 생성 - View의 Bind 메서드에서 데이터 속성 동기화 처리
return data.PropertyType switch
{
UTKPropertyType.String => CreateStringView(data),
UTKPropertyType.Int => CreateIntView(data),
UTKPropertyType.Float => CreateFloatView(data),
UTKPropertyType.Bool => new UTKBoolPropertyItemView(),
UTKPropertyType.Vector2 => new UTKVector2PropertyItemView(),
UTKPropertyType.Vector3 => new UTKVector3PropertyItemView(),
UTKPropertyType.Color => new UTKColorPropertyItemView(),
UTKPropertyType.Date => new UTKDatePropertyItemView(),
UTKPropertyType.DateTime => new UTKDateTimePropertyItemView(),
UTKPropertyType.Enum => CreateEnumView(data),
UTKPropertyType.DropdownList => CreateDropdownView(data),
UTKPropertyType.RadioGroup => CreateRadioView(data),
UTKPropertyType.IntRange => new UTKIntRangePropertyItemView(),
UTKPropertyType.FloatRange => new UTKFloatRangePropertyItemView(),
UTKPropertyType.DateRange => new UTKDateRangePropertyItemView(),
UTKPropertyType.DateTimeRange => new UTKDateTimeRangePropertyItemView(),
UTKPropertyType.ColorState => new UTKColorStatePropertyItemView(),
_ => throw new ArgumentException($"Unknown property type: {data.PropertyType}")
};
}
/// <summary>
/// PropertyType에 해당하는 빈 View 인스턴스를 생성합니다.
/// </summary>
/// <param name="propertyType">속성 타입</param>
/// <returns>View 인스턴스</returns>
public static IUTKPropertyItemView CreateViewInstance(UTKPropertyType propertyType)
{
// 커스텀 팩토리 우선
if (_customViewFactories.TryGetValue(propertyType, out var customFactory))
{
return customFactory();
}
return propertyType switch
{
UTKPropertyType.String => new UTKStringPropertyItemView(),
UTKPropertyType.Int => new UTKIntPropertyItemView(),
UTKPropertyType.Float => new UTKFloatPropertyItemView(),
UTKPropertyType.Bool => new UTKBoolPropertyItemView(),
UTKPropertyType.Vector2 => new UTKVector2PropertyItemView(),
UTKPropertyType.Vector3 => new UTKVector3PropertyItemView(),
UTKPropertyType.Color => new UTKColorPropertyItemView(),
UTKPropertyType.Date => new UTKDatePropertyItemView(),
UTKPropertyType.DateTime => new UTKDateTimePropertyItemView(),
UTKPropertyType.Enum => new UTKEnumPropertyItemView(),
UTKPropertyType.DropdownList => new UTKDropdownPropertyItemView(),
UTKPropertyType.RadioGroup => new UTKRadioPropertyItemView(),
UTKPropertyType.IntRange => new UTKIntRangePropertyItemView(),
UTKPropertyType.FloatRange => new UTKFloatRangePropertyItemView(),
UTKPropertyType.DateRange => new UTKDateRangePropertyItemView(),
UTKPropertyType.DateTimeRange => new UTKDateTimeRangePropertyItemView(),
UTKPropertyType.ColorState => new UTKColorStatePropertyItemView(),
_ => throw new ArgumentException($"Unknown property type: {propertyType}")
};
}
/// <summary>
/// 커스텀 View 팩토리를 등록합니다.
/// </summary>
/// <param name="propertyType">속성 타입</param>
/// <param name="factory">View 생성 팩토리 함수</param>
public static void RegisterCustomView(UTKPropertyType propertyType, Func<IUTKPropertyItemView> factory)
{
_customViewFactories[propertyType] = factory;
}
/// <summary>
/// 커스텀 View 팩토리를 제거합니다.
/// </summary>
/// <param name="propertyType">속성 타입</param>
public static void UnregisterCustomView(UTKPropertyType propertyType)
{
_customViewFactories.Remove(propertyType);
}
/// <summary>
/// 모든 커스텀 View 팩토리를 제거합니다.
/// </summary>
public static void ClearCustomViews()
{
_customViewFactories.Clear();
}
#endregion
#region Private Methods
private static IUTKPropertyItemView CreateStringView(IUTKPropertyItem data)
{
// UTKStringPropertyItem의 Multiline 속성에 따라 View 생성
if (data is UTKStringPropertyItem stringItem)
{
return new UTKStringPropertyItemView(stringItem.Id, stringItem.Value ?? "", stringItem.IsMultiline, stringItem.MaxLength, stringItem.IsReadOnly);
}
return new UTKStringPropertyItemView();
}
private static IUTKPropertyItemView CreateIntView(IUTKPropertyItem data)
{
// UTKIntPropertyItem의 UseSlider 속성에 따라 View 생성
if (data is UTKIntPropertyItem intItem && intItem.UseSlider)
{
return new UTKIntPropertyItemView("", intItem.Value, intItem.MinValue, intItem.MaxValue, true);
}
return new UTKIntPropertyItemView();
}
private static IUTKPropertyItemView CreateFloatView(IUTKPropertyItem data)
{
// UTKFloatPropertyItem의 UseSlider 속성에 따라 View 생성
if (data is UTKFloatPropertyItem floatItem && floatItem.UseSlider)
{
return new UTKFloatPropertyItemView("", floatItem.Value, floatItem.MinValue, floatItem.MaxValue, true);
}
return new UTKFloatPropertyItemView();
}
private static IUTKPropertyItemView CreateEnumView(IUTKPropertyItem data)
{
// UTKEnumPropertyItem의 Value로 EnumDropDown 초기화
if (data is UTKEnumPropertyItem enumItem && enumItem.Value != null)
{
return new UTKEnumPropertyItemView("", enumItem.Value);
}
return new UTKEnumPropertyItemView();
}
private static IUTKPropertyItemView CreateDropdownView(IUTKPropertyItem data)
{
// UTKDropdownPropertyItem의 Choices로 Dropdown 초기화
if (data is UTKDropdownPropertyItem dropdownItem)
{
return new UTKDropdownPropertyItemView("", dropdownItem.Choices);
}
return new UTKDropdownPropertyItemView();
}
private static IUTKPropertyItemView CreateRadioView(IUTKPropertyItem data)
{
// UTKRadioPropertyItem의 Choices로 RadioGroup 초기화
if (data is UTKRadioPropertyItem radioItem)
{
return new UTKRadioPropertyItemView("", radioItem.Choices);
}
return new UTKRadioPropertyItemView();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 826804598deec784a9ee7ab5aab35e3f

View File

@@ -0,0 +1,319 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Radio 속성 View 클래스입니다.
/// UTKRadioButton 그룹을 사용하여 단일 선택을 제공합니다.
/// </summary>
[UxmlElement]
public partial class UTKRadioPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<int>
{
#region Fields
private VisualElement? _radioContainer;
private List<UTKRadioButton> _radioButtons = new();
private int _value;
private List<string> _choices = new();
private IUTKPropertyItem<int>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKRadioPropertyItemView";
/// <summary>현재 선택된 인덱스</summary>
public int Value
{
get => _value;
set
{
if (_value != value && value >= 0 && value < _choices.Count)
{
_value = value;
UpdateSelection();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set
{
_choices = value ?? new List<string>();
RebuildRadioButtons();
}
}
/// <summary>현재 선택된 항목의 텍스트</summary>
public string? SelectedText
{
get
{
if (_value >= 0 && _value < _choices.Count)
{
return _choices[_value];
}
return null;
}
}
#endregion
#region Events
public event Action<int>? OnValueChanged;
#endregion
#region Constructor
public UTKRadioPropertyItemView() : base()
{
InitializeUI();
}
public UTKRadioPropertyItemView(UTKRadioPropertyItem data) : this()
{
_choices = data.Choices;
_value = data.Value;
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKRadioPropertyItemView(string label, List<string> choices, int selectedIndex = 0) : base()
{
_choices = choices ?? new List<string>();
_value = Math.Max(0, Math.Min(selectedIndex, _choices.Count - 1));
Label = label;
InitializeUI();
}
public UTKRadioPropertyItemView(string label, IEnumerable<string> choices, int selectedIndex = 0) : this(label, choices?.ToList() ?? new List<string>(), selectedIndex)
{
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--radio");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 라디오 버튼 생성
CreateRadioButtons();
UpdateSelection();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_radioContainer = this.Q<VisualElement>("radio-container");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _radioContainer == null)
{
_radioContainer = new VisualElement { name = "radio-container" };
_radioContainer.AddToClassList("utk-property-item-view__radio-container");
_valueContainer.Add(_radioContainer);
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateSelection();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
foreach (var radio in _radioButtons)
{
radio.IsEnabled = !isReadOnly;
}
}
#endregion
#region UI Creation
private void CreateRadioButtons()
{
if (_radioContainer == null) return;
_radioContainer.Clear();
_radioButtons.Clear();
for (int i = 0; i < _choices.Count; i++)
{
var radio = new UTKRadioButton(_choices[i]);
radio.name = $"radio-{i}";
radio.AddToClassList("utk-property-item-view__radio");
int index = i;
radio.OnValueChanged += (isChecked) => OnRadioChanged(index, isChecked);
radio.IsEnabled = !IsReadOnly;
_radioButtons.Add(radio);
_radioContainer.Add(radio);
}
UpdateSelection();
}
private void RebuildRadioButtons()
{
if (_radioContainer != null)
{
CreateRadioButtons();
}
}
#endregion
#region Event Handling
private void OnRadioChanged(int index, bool isChecked)
{
if (isChecked && index != _value)
{
// 다른 라디오 버튼 해제
for (int i = 0; i < _radioButtons.Count; i++)
{
if (i != index)
{
_radioButtons[i].SetChecked(false, false);
}
}
_value = index;
OnValueChanged?.Invoke(index);
if (_boundData != null && _boundData.Value != index)
{
_boundData.Value = index;
}
}
}
#endregion
#region Value Update
private void UpdateSelection()
{
for (int i = 0; i < _radioButtons.Count; i++)
{
_radioButtons[i].SetChecked(i == _value, false);
}
}
#endregion
#region Public Methods
/// <summary>텍스트로 선택</summary>
public void SelectByText(string text)
{
int index = _choices.IndexOf(text);
if (index >= 0)
{
Value = index;
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<int> intData)
{
Bind(intData);
}
else
{
Debug.LogWarning($"[UTKRadioPropertyItemView] Cannot bind to non-int data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<int> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKRadioPropertyItem radioItem)
{
_choices = radioItem.Choices;
RebuildRadioButtons();
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateSelection();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<int> item, int oldValue, int newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateSelection();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_radioButtons.Clear();
Unbind();
OnValueChanged = null;
_radioContainer = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,314 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// String 속성 View 클래스입니다.
/// UTKInputField를 사용하여 문자열 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
/// // C# 코드에서 생성
/// var view = new UTKStringPropertyItemView();
/// view.Label = "이름";
/// view.Value = "홍길동";
/// view.IsMultiline = false;
/// parent.Add(view);
///
/// // UXML에서 사용
/// &lt;utk:UTKStringPropertyItemView label="이름" value="홍길동" is-multiline="false" /&gt;
/// </code>
/// </summary>
[UxmlElement]
public partial class UTKStringPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<string>
{
#region Fields
private UTKInputField? _inputField;
private string _value = string.Empty;
private bool _isMultiline = false;
private int _maxLength = -1;
private IUTKPropertyItem<string>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKStringPropertyItemView";
/// <summary>현재 값</summary>
[UxmlAttribute("value")]
public string Value
{
get => _value;
set
{
var newValue = value ?? string.Empty;
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
}
/// <summary>멀티라인 모드 여부</summary>
[UxmlAttribute("is-multiline")]
public bool IsMultiline
{
get => _isMultiline;
set
{
_isMultiline = value;
if (_inputField != null)
{
_inputField.multiline = value;
}
}
}
/// <summary>최대 문자 길이 (-1 = 무제한)</summary>
[UxmlAttribute("max-length")]
public int MaxLength
{
get => _maxLength;
set
{
_maxLength = value;
if (_inputField != null)
{
_inputField.maxLength = value;
}
}
}
#endregion
#region Events
public event Action<string>? OnValueChanged;
#endregion
#region Constructor
public UTKStringPropertyItemView() : base()
{
InitializeUI();
}
public UTKStringPropertyItemView(UTKStringPropertyItem item) : base()
{
_value = item.Value ?? string.Empty;
_isMultiline = item.IsMultiline;
_maxLength = item.MaxLength;
Label = item.Name;
_isReadOnly = item.IsReadOnly;
InitializeUI();
Bind(item);
}
public UTKStringPropertyItemView(string label, string value = "", bool isMultiline = false, int maxLength = -1, bool isReadOnly = false) : base()
{
_value = value ?? string.Empty;
_isMultiline = isMultiline;
_maxLength = maxLength;
Label = label;
_isReadOnly = isReadOnly;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--string");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_inputField = this.Q<UTKInputField>("value-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _inputField == null)
{
_inputField = new UTKInputField { name = "value-field" };
_inputField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_inputField);
}
// 초기 값 설정
if (_inputField != null)
{
_inputField.SetValue(_value, false);
_inputField.multiline = _isMultiline;
_inputField.isReadOnly = IsReadOnly;
if (_maxLength >= 0)
{
_inputField.maxLength = _maxLength;
}
}
}
private void RegisterEvents()
{
if (_inputField != null)
{
_inputField.OnValueChanged += OnInputChanged;
}
}
private void UnregisterEvents()
{
if (_inputField != null)
{
_inputField.OnValueChanged -= OnInputChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_inputField != null)
{
_inputField.isReadOnly = isReadOnly;
}
}
#endregion
#region Event Handling
private void OnInputChanged(string newValue)
{
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_inputField != null && _inputField.Value != _value)
{
_inputField.SetValue(_value, false);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<string> stringData)
{
Bind(stringData);
}
else
{
Debug.LogWarning($"[UTKStringPropertyItemView] Cannot bind to non-string data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<string> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value ?? string.Empty;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKStringPropertyItem stringItem)
{
_isMultiline = stringItem.IsMultiline;
_maxLength = stringItem.MaxLength;
if (_inputField != null)
{
_inputField.multiline = _isMultiline;
_inputField.maxLength = _maxLength;
}
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<string> item, string oldValue, string newValue)
{
if (_value != newValue)
{
_value = newValue ?? string.Empty;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_inputField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6c530f4a9382c264b871bc439ca1d527

View File

@@ -0,0 +1,241 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Vector2 속성 View 클래스입니다.
/// UTKVector2Field를 사용하여 Vector2 값을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKVector2PropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<Vector2>
{
#region Fields
private UTKVector2Field? _vectorField;
private Vector2 _value;
private IUTKPropertyItem<Vector2>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKVector2PropertyItemView";
/// <summary>현재 값</summary>
public Vector2 Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<Vector2>? OnValueChanged;
#endregion
#region Constructor
public UTKVector2PropertyItemView() : base()
{
InitializeUI();
}
public UTKVector2PropertyItemView(string label, Vector2 value = default) : base()
{
_value = value;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--vector2");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_vectorField = this.Q<UTKVector2Field>("value-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _vectorField == null)
{
_vectorField = new UTKVector2Field
{
name = "value-field",
label = ""
};
_vectorField.AddToClassList("utk-property-item-view__vector-field");
_valueContainer.Add(_vectorField);
}
// 초기 값 설정
if (_vectorField != null)
{
_vectorField.label = "";
_vectorField.SetValueWithoutNotify(_value);
_vectorField.IsReadOnly = IsReadOnly;
}
}
private void RegisterEvents()
{
if (_vectorField != null)
{
_vectorField.OnValueChanged += OnVectorChanged;
}
}
private void UnregisterEvents()
{
if (_vectorField != null)
{
_vectorField.OnValueChanged -= OnVectorChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_vectorField != null)
{
_vectorField.IsReadOnly = isReadOnly;
}
}
#endregion
#region Event Handling
private void OnVectorChanged(Vector2 newValue)
{
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_vectorField != null && _vectorField.Value != _value)
{
_vectorField.SetValueWithoutNotify(_value);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<Vector2> vectorData)
{
Bind(vectorData);
}
else
{
Debug.LogWarning($"[UTKVector2PropertyItemView] Cannot bind to non-Vector2 data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<Vector2> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<Vector2> item, Vector2 oldValue, Vector2 newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_vectorField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,241 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Vector3 속성 View 클래스입니다.
/// UTKVector3Field를 사용하여 Vector3 값을 표시/편집합니다.
/// </summary>
[UxmlElement]
public partial class UTKVector3PropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<Vector3>
{
#region Fields
private UTKVector3Field? _vectorField;
private Vector3 _value;
private IUTKPropertyItem<Vector3>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKVector3PropertyItemView";
/// <summary>현재 값</summary>
public Vector3 Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
if (_boundData != null && _boundData.Value != value)
{
_boundData.Value = value;
}
}
}
}
#endregion
#region Events
public event Action<Vector3>? OnValueChanged;
#endregion
#region Constructor
public UTKVector3PropertyItemView() : base()
{
InitializeUI();
}
public UTKVector3PropertyItemView(string label, Vector3 value = default) : base()
{
_value = value;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--vector3");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_vectorField = this.Q<UTKVector3Field>("value-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _vectorField == null)
{
_vectorField = new UTKVector3Field
{
name = "value-field",
label = ""
};
_vectorField.AddToClassList("utk-property-item-view__vector-field");
_valueContainer.Add(_vectorField);
}
// 초기 값 설정
if (_vectorField != null)
{
_vectorField.label = "";
_vectorField.SetValueWithoutNotify(_value);
_vectorField.IsEnabled = !IsReadOnly;
}
}
private void RegisterEvents()
{
if (_vectorField != null)
{
_vectorField.OnValueChanged += OnVectorChanged;
}
}
private void UnregisterEvents()
{
if (_vectorField != null)
{
_vectorField.OnValueChanged -= OnVectorChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_vectorField != null)
{
_vectorField.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void OnVectorChanged(Vector3 newValue)
{
if (_value != newValue)
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && _boundData.Value != newValue)
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_vectorField != null && _vectorField.Value != _value)
{
_vectorField.SetValueWithoutNotify(_value);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<Vector3> vectorData)
{
Bind(vectorData);
}
else
{
Debug.LogWarning($"[UTKVector3PropertyItemView] Cannot bind to non-Vector3 data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<Vector3> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value;
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<Vector3> item, Vector3 oldValue, Vector3 newValue)
{
if (_value != newValue)
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_vectorField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 58b72644d6ad6274b91d72aeb2cc68d2

View File

@@ -12,11 +12,11 @@ namespace UVC.UIToolkit
/// 헤더, 타이틀, 닫기 버튼 등 윈도우 프레임 제공
/// </summary>
[UxmlElement]
public partial class UTKPropertyWindow : VisualElement, IDisposable
public partial class UTKPropertyListWindow : VisualElement, IDisposable
{
#region Constants
private const string UXML_PATH = "UIToolkit/Property/UTKPropertyWindow";
private const string USS_PATH = "UIToolkit/Property/UTKPropertyWindowUss";
private const string UXML_PATH = "UIToolkit/Window/UTKPropertyListWindow";
private const string USS_PATH = "UIToolkit/Window/UTKPropertyListWindowUss";
#endregion
#region Fields
@@ -92,7 +92,7 @@ namespace UVC.UIToolkit
#endregion
#region Constructor
public UTKPropertyWindow()
public UTKPropertyListWindow()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
SubscribeToThemeChanges();
@@ -106,7 +106,7 @@ namespace UVC.UIToolkit
CreateUI();
}
public UTKPropertyWindow(string title) : this()
public UTKPropertyListWindow(string title) : this()
{
Title = title;
}

View File

@@ -162,7 +162,7 @@ namespace UVC.UIToolkit
#region UI (UI Component References)
/// <summary>검색어 입력 필드</summary>
private TextField? _searchField;
private UTKInputField? _searchField;
/// <summary>Unity UI Toolkit의 TreeView 컴포넌트</summary>
private TreeView? _treeView;
@@ -303,7 +303,7 @@ namespace UVC.UIToolkit
}
// 3. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
_searchField = this.Q<TextField>("search-field");
_searchField = this.Q<UTKInputField>("search-field");
_treeView = this.Q<TreeView>("main-tree-view");
_titleLabel = this.Q<Label>("title");
_closeButton = this.Q<UTKButton>("close-btn");
@@ -335,8 +335,7 @@ namespace UVC.UIToolkit
// 검색창 이벤트: Enter 키를 눌렀을 때 또는 포커스를 잃었을 때 필터링 실행
if (_searchField != null)
{
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit += OnSearch;
}
// TreeView 설정
@@ -349,7 +348,7 @@ namespace UVC.UIToolkit
_treeView.selectionChanged += OnTreeViewSelectionChanged;
_treeView.itemsChosen += OnTreeViewItemsChosen;
_treeView.itemExpandedChanged += OnTreeViewItemExpandedChanged;
_treeView.RegisterCallback<KeyDownEvent>(OnTreeViewKeyDown);
_treeView.RegisterCallback<KeyDownEvent>(OnTreeViewKeyDown, TrickleDown.TrickleDown);
}
// 닫기 버튼: 트리 리스트를 숨기고 이벤트 발생
@@ -1248,28 +1247,7 @@ namespace UVC.UIToolkit
#endregion
#region (Search Functionality)
/// <summary>
/// 검색 필드에서 Enter 키를 눌렀을 때 검색을 실행합니다.
/// </summary>
/// <param name="evt">키 입력 이벤트</param>
private void OnSearchFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
OnSearch(_searchField?.value ?? string.Empty);
evt.StopPropagation();
}
}
/// <summary>
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
/// </summary>
/// <param name="evt">포커스 아웃 이벤트</param>
private void OnSearchFieldFocusOut(FocusOutEvent evt)
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// 검색어에 따라 트리를 필터링합니다.
/// 검색어가 비어있으면 원본 데이터로 복원됩니다.
@@ -1428,8 +1406,7 @@ namespace UVC.UIToolkit
// 검색 필드 이벤트 해제
if (_searchField != null)
{
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
_searchField.OnSubmit -= OnSearch;
}
// TreeView 이벤트 핸들러 해제
@@ -1438,7 +1415,7 @@ namespace UVC.UIToolkit
_treeView.selectionChanged -= OnTreeViewSelectionChanged;
_treeView.itemsChosen -= OnTreeViewItemsChosen;
_treeView.itemExpandedChanged -= OnTreeViewItemExpandedChanged;
_treeView.UnregisterCallback<KeyDownEvent>(OnTreeViewKeyDown);
_treeView.UnregisterCallback<KeyDownEvent>(OnTreeViewKeyDown, TrickleDown.TrickleDown);
_treeView.bindItem = null;
_treeView.makeItem = null;
}