UTKPropertyItem 개선
This commit is contained in:
@@ -466,7 +466,7 @@ namespace UVC.UIToolkit
|
||||
private void SetupEvents()
|
||||
{
|
||||
RegisterCallback<ClickEvent>(OnClick);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace UVC.UIToolkit
|
||||
private void SetupEvents()
|
||||
{
|
||||
RegisterCallback<ClickEvent>(OnClick);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -271,7 +271,7 @@ namespace UVC.UIToolkit
|
||||
private void SetupEvents()
|
||||
{
|
||||
RegisterCallback<ClickEvent>(OnClick);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
|
||||
@@ -218,7 +218,7 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
OnSubmit?.Invoke(value);
|
||||
}
|
||||
});
|
||||
}, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -220,7 +220,7 @@ namespace UVC.UIToolkit
|
||||
Close();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
});
|
||||
}, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// UTKPropertyWindow에서 지원하는 속성 타입
|
||||
/// UTKPropertyListWindow에서 지원하는 속성 타입
|
||||
/// 기존 PropertyType과 동일한 열거형
|
||||
/// </summary>
|
||||
public enum UTKPropertyType
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
8
Assets/Scripts/UVC/UIToolkit/Property/Views.meta
Normal file
8
Assets/Scripts/UVC/UIToolkit/Property/Views.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac922ee72791e234cb778a5f9042a0d5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7bd5b74e1fdb61469e5c3573d226f63
|
||||
@@ -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에서 사용
|
||||
/// <utk:UTKBoolPropertyItemView label="활성화" value="true" />
|
||||
/// </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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73bebdc74c6f5a54bb7633273f962959
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8ebfcf0c3e0a1048aaeefd7b019ca45
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25977c55de643d94ab9b6af995da701b
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a462d7492ea6b43469717e78b69397eb
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35155d0327a2886458817c2d45643df2
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4821fbbe4446fca45b2e313d095ae797
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0016d4ed9a446c409d3a5d5770c1ce6
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cad9ff03a39d354ebe7ff61da124dcc
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5ba35b319a44f74cb2e8874799d1110
|
||||
@@ -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에서 사용
|
||||
/// <utk:UTKFloatPropertyItemView label="속도" value="1.5" use-slider="true" min-value="0" max-value="10" />
|
||||
/// </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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14166aaf18c01844fbb08bc1c5b794cc
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 362da87d1c7470e4d886e0dfc06dbdc8
|
||||
@@ -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에서 사용
|
||||
/// <utk:UTKIntPropertyItemView label="수량" value="10" use-slider="true" min-value="0" max-value="100" />
|
||||
/// </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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61d3b9a295685a14c818d6b006bbae76
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66594df42e9b4204aa5d6c05342ea4e3
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55df5e23ee322e643ae650eea2bd4120
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 826804598deec784a9ee7ab5aab35e3f
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba59162ee3e10fc4084a40df609a3ae7
|
||||
@@ -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에서 사용
|
||||
/// <utk:UTKStringPropertyItemView label="이름" value="홍길동" is-multiline="false" />
|
||||
/// </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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c530f4a9382c264b871bc439ca1d527
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f10f82c210cf5b646805a99675dfef0b
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58b72644d6ad6274b91d72aeb2cc68d2
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user