UTKFloatStepper 추가. UTKFloatPropertyItem, UTKFloatPropertyItemView에 추가

This commit is contained in:
logonkhi
2026-02-09 20:28:09 +09:00
parent a38efd756e
commit 97bbb789ed
65 changed files with 2785 additions and 131 deletions

View File

@@ -35,7 +35,7 @@ Assets/Scripts/UVC/UIToolkit/
│ ├── UTKInputField.cs # 텍스트 입력
│ ├── UTKIntegerField.cs # Integer 입력
│ ├── UTKLongField.cs # Long 입력
│ ├── UTKNumberStepper.cs # 숫자 스테퍼 (+/-)
│ ├── UTKIntStepper.cs # 숫자 스테퍼 (+/-)
│ ├── UTKRectField.cs # Rect 입력
│ ├── UTKVector2Field.cs # Vector2 입력
│ ├── UTKVector3Field.cs # Vector3 입력
@@ -417,12 +417,12 @@ var vector3Field = new UTKVector3Field("위치");
vector3Field.Value = Vector3.zero;
```
### UTKNumberStepper
### UTKIntStepper
증가/감소 버튼이 있는 숫자 입력 필드입니다.
```csharp
var stepper = new UTKNumberStepper();
var stepper = new UTKIntStepper();
stepper.Value = 5;
stepper.Min = 0;
stepper.Max = 100;

View File

@@ -0,0 +1,481 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 실수 입력 필드에 위/아래 스테퍼 버튼이 붙은 컴포넌트.
/// TextInput 오른쪽에 위, 아래 버튼이 세로로 배치됩니다.
/// 키보드 화살표, 마우스 휠, 버튼 클릭으로 값을 조절할 수 있습니다.
/// </summary>
/// <remarks>
/// <para><b>UTKFloatStepper란?</b></para>
/// <para>실수를 편리하게 증감할 수 있는 입력 컴포넌트입니다.</para>
/// <list type="bullet">
/// <item><description><b>버튼 클릭</b>: ▲/▼ 버튼으로 값 증감</description></item>
/// <item><description><b>키보드</b>: ↑/↓ 화살표 키로 값 증감</description></item>
/// <item><description><b>마우스 휠</b>: 호버 상태에서 휠로 값 조절</description></item>
/// <item><description><b>직접 입력</b>: 텍스트 필드에 숫자 직접 입력</description></item>
/// </list>
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>최소/최대값 제한 (MinValue, MaxValue)</description></item>
/// <item><description>증감 단위 설정 (Step)</description></item>
/// <item><description>순환 모드 (WrapAround) - 최대에서 최소로, 최소에서 최대로</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 스테퍼 생성
/// var stepper = new UTKFloatStepper();
/// stepper.MinValue = 0f;
/// stepper.MaxValue = 10f;
/// stepper.Value = 5.5f;
/// stepper.Step = 0.5f; // 0.5씩 증감
///
/// // 값 변경 이벤트
/// stepper.OnValueChanged += (value) => {
/// Debug.Log($"현재 값: {value}");
/// };
///
/// // 범위와 초기값을 지정하는 생성자
/// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.8f, 0.1f); // min, max, initial, step
///
/// // 순환 모드 활성화 (10.0 → 0.0, 0.0 → 10.0)
/// stepper.WrapAround = true;
///
/// // 프로그래밍 방식으로 값 변경
/// stepper.Increment(); // Step만큼 증가
/// stepper.Decrement(); // Step만큼 감소
/// stepper.SetValue(7.5f); // 직접 설정
///
/// // 읽기 전용 (사용자가 수정할 수 없음)
/// var readOnlyStepper = new UTKFloatStepper(0f, 10f, 5f, 0.1f);
/// readOnlyStepper.IsReadOnly = true;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 스테퍼 -->
/// <utk:UTKFloatStepper value="5.5" min-value="0" max-value="10" />
///
/// <!-- 증감 단위 설정 -->
/// <utk:UTKFloatStepper value="1.0" step="0.5" />
///
/// <!-- 순환 모드 -->
/// <utk:UTKFloatStepper value="0.5" min-value="0" max-value="1" wrap-around="true" />
///
/// <!-- 읽기 전용 -->
/// <utk:UTKFloatStepper value="5.0" is-readonly="true" />
/// ]]></code>
/// <para><b>실제 활용 예시 (볼륨 조절):</b></para>
/// <code>
/// // 볼륨 스테퍼 (0.0~1.0)
/// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.8f, 0.1f);
/// volumeStepper.OnValueChanged += (volume) => {
/// AudioListener.volume = volume;
/// };
/// </code>
/// </example>
[UxmlElement]
public partial class UTKFloatStepper : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Input/UTKFloatStepper";
#endregion
#region UXML Attributes
[UxmlAttribute("value")]
public float Value
{
get => _value;
set => SetValue(value);
}
[UxmlAttribute("min-value")]
public float MinValue
{
get => _minValue;
set
{
_minValue = value;
ClampValue();
}
}
[UxmlAttribute("max-value")]
public float MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
ClampValue();
}
}
[UxmlAttribute("step")]
public float Step
{
get => _step;
set => _step = value > 0 ? value : 0.1f;
}
[UxmlAttribute("wrap-around")]
public bool WrapAround
{
get => _wrapAround;
set => _wrapAround = value;
}
/// <summary>읽기 전용 상태. true일 때 사용자가 값을 수정할 수 없음</summary>
[UxmlAttribute("is-readonly")]
public bool IsReadOnly
{
get => _isReadOnly;
set
{
_isReadOnly = value;
UpdateReadOnlyState();
EnableInClassList("utk-number-stepper--readonly", value);
}
}
#endregion
#region Fields
private bool _disposed;
private bool _isReadOnly;
private float _value;
private float _minValue = float.MinValue;
private float _maxValue = float.MaxValue;
private float _step = 1.0f;
private bool _wrapAround;
private bool _isUpdating;
private bool _isHovered;
private TextField? _textField;
private Button? _upButton;
private Button? _downButton;
#endregion
#region Events
/// <summary>값이 변경될 때 발생</summary>
public event Action<float>? OnValueChanged;
/// <summary>Tab 키가 눌렸을 때 발생 (다음 요소로 포커스 이동용)</summary>
public event Action? OnTabPressed;
/// <summary>Shift+Tab 키가 눌렸을 때 발생 (이전 요소로 포커스 이동용)</summary>
public event Action? OnShiftTabPressed;
#endregion
#region Constructor
public UTKFloatStepper()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
LoadStyleSheet();
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
}
public UTKFloatStepper(bool isReadOnly = false): this()
{
_isReadOnly = isReadOnly;
}
public UTKFloatStepper(float minValue, float maxValue, float initialValue = 0f, float step = 0.1f, bool isReadOnly = false): this()
{
_isReadOnly = isReadOnly;
_minValue = minValue;
_maxValue = maxValue;
_step = step > 0 ? step : 1.0f;
_value = Mathf.Clamp(initialValue, minValue, maxValue);
}
private void LoadStyleSheet()
{
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
// 패널에서 분리될 때 이벤트 구독 해제
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Public Methods
public void SetValue(float newValue, bool notify = true)
{
float clampedValue = ClampValueInternal(newValue);
if (!Mathf.Approximately(_value, clampedValue))
{
_value = clampedValue;
UpdateDisplay();
if (notify)
{
OnValueChanged?.Invoke(_value);
}
}
}
public void Increment()
{
if (_wrapAround && _value + _step > _maxValue)
{
SetValue(_minValue);
}
else
{
SetValue(_value + _step);
}
}
public void Decrement()
{
if (_wrapAround && _value - _step < _minValue)
{
SetValue(_maxValue);
}
else
{
SetValue(_value - _step);
}
}
public void SetRange(float min, float max)
{
_minValue = min;
_maxValue = max;
ClampValue();
}
/// <summary>컴포넌트의 활성화 상태를 설정합니다.</summary>
public new void SetEnabled(bool enabled)
{
base.SetEnabled(enabled);
EnableInClassList("utk-number-stepper--disabled", !enabled);
}
/// <summary>텍스트 필드에 포커스를 설정합니다.</summary>
public new void Focus()
{
_textField?.Focus();
}
#endregion
#region Private Methods - UI Creation
private void CreateUI()
{
AddToClassList("utk-number-stepper");
// Text Field
_textField = new TextField { name = "stepper-input" };
_textField.AddToClassList("utk-number-stepper__input");
_textField.isReadOnly = _isReadOnly;
// TextField 내부 input 스타일링
_textField.RegisterCallback<AttachToPanelEvent>(_ =>
{
var input = _textField.Q<VisualElement>("unity-text-input");
if (input != null)
{
input.AddToClassList("utk-number-stepper__text-input");
}
});
Add(_textField);
// Button Container (위/아래 버튼을 세로로 배치)
var buttonContainer = new VisualElement { name = "stepper-buttons" };
buttonContainer.AddToClassList("utk-number-stepper__buttons");
// Up Button
_upButton = new Button { name = "stepper-up", text = UTKMaterialIcons.KeyboardArrowUp };
_upButton.AddToClassList("utk-number-stepper__btn");
_upButton.AddToClassList("utk-number-stepper__btn--up");
_upButton.SetEnabled(!_isReadOnly);
UTKMaterialIcons.ApplyIconStyle(_upButton, 14);
buttonContainer.Add(_upButton);
// Down Button
_downButton = new Button { name = "stepper-down", text = UTKMaterialIcons.KeyboardArrowDown };
_downButton.AddToClassList("utk-number-stepper__btn");
_downButton.AddToClassList("utk-number-stepper__btn--down");
_downButton.SetEnabled(!_isReadOnly);
UTKMaterialIcons.ApplyIconStyle(_downButton, 14);
buttonContainer.Add(_downButton);
Add(buttonContainer);
UpdateDisplay();
}
private void SetupEvents()
{
_upButton?.RegisterCallback<ClickEvent>(OnUpButtonClick);
_downButton?.RegisterCallback<ClickEvent>(OnDownButtonClick);
_textField?.RegisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
RegisterCallback<MouseEnterEvent>(OnMouseEnter);
RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
RegisterCallback<WheelEvent>(OnWheelEvent);
}
#endregion
#region Event Handlers
private void OnUpButtonClick(ClickEvent evt) => Increment();
private void OnDownButtonClick(ClickEvent evt) => Decrement();
private void OnTextFieldChanged(ChangeEvent<string> evt)
{
if (_isUpdating) return;
if (float.TryParse(evt.newValue, out float parsed))
{
SetValue(parsed);
}
else
{
// 유효하지 않은 입력이면 이전 값으로 복원
UpdateDisplay();
}
}
private void OnTextFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.UpArrow)
{
Increment();
evt.StopPropagation();
}
else if (evt.keyCode == KeyCode.DownArrow)
{
Decrement();
evt.StopPropagation();
}
else if (evt.keyCode == KeyCode.Tab)
{
if (evt.shiftKey && OnShiftTabPressed != null)
{
OnShiftTabPressed.Invoke();
evt.StopImmediatePropagation();
}
else if (!evt.shiftKey && OnTabPressed != null)
{
OnTabPressed.Invoke();
evt.StopImmediatePropagation();
}
}
}
private void OnMouseEnter(MouseEnterEvent evt) => _isHovered = true;
private void OnMouseLeave(MouseLeaveEvent evt) => _isHovered = false;
private void OnWheelEvent(WheelEvent evt)
{
if (!_isHovered) return;
if (evt.delta.y < 0)
{
Increment();
}
else if (evt.delta.y > 0)
{
Decrement();
}
evt.StopPropagation();
}
#endregion
#region Private Methods - Logic
private void UpdateDisplay()
{
if (_textField == null) return;
_isUpdating = true;
_textField.value = _value.ToString();//.ToString("F2");
_isUpdating = false;
}
private void ClampValue()
{
SetValue(_value, notify: false);
}
private float ClampValueInternal(float value)
{
return Mathf.Clamp(value, _minValue, _maxValue);
}
private void UpdateReadOnlyState()
{
if (_textField != null)
{
_textField.isReadOnly = _isReadOnly;
}
if (_upButton != null)
{
_upButton.SetEnabled(!_isReadOnly);
}
if (_downButton != null)
{
_downButton.SetEnabled(!_isReadOnly);
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
// 이벤트 콜백 해제
_upButton?.UnregisterCallback<ClickEvent>(OnUpButtonClick);
_downButton?.UnregisterCallback<ClickEvent>(OnDownButtonClick);
_textField?.UnregisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
UnregisterCallback<MouseEnterEvent>(OnMouseEnter);
UnregisterCallback<MouseLeaveEvent>(OnMouseLeave);
UnregisterCallback<WheelEvent>(OnWheelEvent);
// 이벤트 null 처리
OnValueChanged = null;
OnTabPressed = null;
OnShiftTabPressed = null;
// UI 참조 정리
_textField = null;
_upButton = null;
_downButton = null;
}
#endregion
}
}

View File

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

View File

@@ -11,7 +11,7 @@ namespace UVC.UIToolkit
/// 키보드 화살표, 마우스 휠, 버튼 클릭으로 값을 조절할 수 있습니다.
/// </summary>
/// <remarks>
/// <para><b>UTKNumberStepper란?</b></para>
/// <para><b>UTKIntStepper란?</b></para>
/// <para>숫자를 편리하게 증감할 수 있는 입력 컴포넌트입니다.</para>
/// <list type="bullet">
/// <item><description><b>버튼 클릭</b>: ▲/▼ 버튼으로 값 증감</description></item>
@@ -30,7 +30,7 @@ namespace UVC.UIToolkit
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 스테퍼 생성
/// var stepper = new UTKNumberStepper();
/// var stepper = new UTKIntStepper();
/// stepper.MinValue = 0;
/// stepper.MaxValue = 100;
/// stepper.Value = 50;
@@ -42,7 +42,7 @@ namespace UVC.UIToolkit
/// };
///
/// // 범위와 초기값을 지정하는 생성자
/// var volumeStepper = new UTKNumberStepper(0, 100, 80, 10); // min, max, initial, step
/// var volumeStepper = new UTKIntStepper(0, 100, 80, 10); // min, max, initial, step
///
/// // 순환 모드 활성화 (100 → 0, 0 → 100)
/// stepper.WrapAround = true;
@@ -53,27 +53,27 @@ namespace UVC.UIToolkit
/// stepper.SetValue(75); // 직접 설정
///
/// // 읽기 전용 (사용자가 수정할 수 없음)
/// var readOnlyStepper = new UTKNumberStepper(0, 100, 50, 1);
/// var readOnlyStepper = new UTKIntStepper(0, 100, 50, 1);
/// readOnlyStepper.IsReadOnly = true;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 스테퍼 -->
/// <utk:UTKNumberStepper value="50" min-value="0" max-value="100" />
/// <utk:UTKIntStepper value="50" min-value="0" max-value="100" />
///
/// <!-- 증감 단위 설정 -->
/// <utk:UTKNumberStepper value="10" step="5" />
/// <utk:UTKIntStepper value="10" step="5" />
///
/// <!-- 순환 모드 -->
/// <utk:UTKNumberStepper value="1" min-value="1" max-value="12" wrap-around="true" />
/// <utk:UTKIntStepper value="1" min-value="1" max-value="12" wrap-around="true" />
///
/// <!-- 읽기 전용 -->
/// <utk:UTKNumberStepper value="50" is-readonly="true" />
/// <utk:UTKIntStepper value="50" is-readonly="true" />
/// ]]></code>
/// <para><b>실제 활용 예시 (월 선택기):</b></para>
/// <code>
/// // 월 선택 스테퍼 (1~12 순환)
/// var monthStepper = new UTKNumberStepper(1, 12, DateTime.Now.Month, 1);
/// var monthStepper = new UTKIntStepper(1, 12, DateTime.Now.Month, 1);
/// monthStepper.WrapAround = true; // 12월 다음 1월, 1월 이전 12월
/// monthStepper.OnValueChanged += (month) => {
/// UpdateCalendar(month);
@@ -81,10 +81,10 @@ namespace UVC.UIToolkit
/// </code>
/// </example>
[UxmlElement]
public partial class UTKNumberStepper : VisualElement, IDisposable
public partial class UTKIntStepper : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Input/UTKNumberStepper";
private const string USS_PATH = "UIToolkit/Input/UTKIntStepper";
#endregion
#region UXML Attributes
@@ -174,7 +174,7 @@ namespace UVC.UIToolkit
#region Constructor
public UTKNumberStepper()
public UTKIntStepper()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
LoadStyleSheet();
@@ -183,12 +183,12 @@ namespace UVC.UIToolkit
SubscribeToThemeChanges();
}
public UTKNumberStepper(bool isReadOnly = false): this()
public UTKIntStepper(bool isReadOnly = false): this()
{
_isReadOnly = isReadOnly;
}
public UTKNumberStepper(int minValue, int maxValue, int initialValue = 0, int step = 1, bool isReadOnly = false): this()
public UTKIntStepper(int minValue, int maxValue, int initialValue = 0, int step = 1, bool isReadOnly = false): this()
{
_isReadOnly = isReadOnly;
_minValue = minValue;

View File

@@ -64,6 +64,9 @@ namespace UVC.UIToolkit
/// <summary>속성 클릭 이벤트</summary>
public event Action<IUTKPropertyItem>? OnPropertyClicked;
/// <summary>버튼 클릭 이벤트 (액션 이름 전달)</summary>
public event Action<string, string>? OnPropertyButtonClicked;
#endregion
#region Constructor
@@ -576,6 +579,12 @@ namespace UVC.UIToolkit
{
view.Unbind();
}
// IDisposable 구현체인 경우 Dispose 호출
if (child is IDisposable disposable)
{
disposable.Dispose();
}
}
element.Clear();
@@ -601,11 +610,21 @@ namespace UVC.UIToolkit
groupElement.Add(title);
groupElement.Add(count);
groupElement.RegisterCallback<ClickEvent>(_ =>
// 그룹 클릭 이벤트 - DetachFromPanelEvent에서 자동 정리됨
EventCallback<ClickEvent> clickCallback = null!;
clickCallback = _ =>
{
ToggleGroupExpanded(group.GroupId);
expandIcon.SetMaterialIcon(group.IsExpanded ? UTKMaterialIcons.ExpandMore : UTKMaterialIcons.ChevronRight, 16);
OnGroupExpandedChanged?.Invoke(group, group.IsExpanded);
};
groupElement.RegisterCallback(clickCallback);
// DetachFromPanelEvent에서 이벤트 해제
groupElement.RegisterCallback<DetachFromPanelEvent>(evt =>
{
groupElement.UnregisterCallback(clickCallback);
});
container.Add(groupElement);
@@ -616,7 +635,47 @@ namespace UVC.UIToolkit
// View Factory를 사용하여 View 생성 및 바인딩
var itemView = UTKPropertyItemViewFactory.CreateView(item);
itemView.RegisterCallback<ClickEvent>(_ => OnPropertyClicked?.Invoke(item));
// 클릭 이벤트 등록 - DetachFromPanelEvent에서 자동 정리됨
EventCallback<ClickEvent> clickCallback = _ => OnPropertyClicked?.Invoke(item);
itemView.RegisterCallback(clickCallback);
// 버튼 아이템인 경우 버튼 클릭 이벤트 구독
Action<string>? buttonClickHandler = null;
if (itemView is UTKButtonItemView buttonView)
{
buttonClickHandler = (actionName) =>
{
OnPropertyButtonClicked?.Invoke(item.Id, actionName);
};
buttonView.OnButtonClicked += buttonClickHandler;
}
// String 아이템에 ActionButton이 있는 경우 이벤트 구독
Action<string>? actionButtonClickHandler = null;
if (itemView is UTKStringPropertyItemView stringView)
{
actionButtonClickHandler = (actionName) =>
{
OnPropertyButtonClicked?.Invoke(item.Id, actionName);
};
stringView.OnActionButtonClicked += actionButtonClickHandler;
}
// DetachFromPanelEvent에서 이벤트 해제
itemView.RegisterCallback<DetachFromPanelEvent>(evt =>
{
itemView.UnregisterCallback(clickCallback);
if (itemView is UTKButtonItemView btnView && buttonClickHandler != null)
{
btnView.OnButtonClicked -= buttonClickHandler;
}
if (itemView is UTKStringPropertyItemView strView && actionButtonClickHandler != null)
{
strView.OnActionButtonClicked -= actionButtonClickHandler;
}
});
container.Add(itemView);
}
@@ -720,6 +779,7 @@ namespace UVC.UIToolkit
OnPropertyValueChanged = null;
OnGroupExpandedChanged = null;
OnPropertyClicked = null;
OnPropertyButtonClicked = null;
// UI 참조 정리
_treeView = null;

View File

@@ -188,8 +188,8 @@ namespace UVC.UIToolkit
private Button? _nextYearButton;
private VisualElement? _dayNamesRow;
private VisualElement? _timeRow;
private UTKNumberStepper? _hourStepper;
private UTKNumberStepper? _minuteStepper;
private UTKIntStepper? _hourStepper;
private UTKIntStepper? _minuteStepper;
private UTKButton? _cancelButton;
private UTKButton? _confirmButton;
private VisualElement? _header;
@@ -648,14 +648,14 @@ namespace UVC.UIToolkit
var timeLabel = new Label("Time:");
timeLabel.AddToClassList("utk-date-picker__time-label");
_hourStepper = new UTKNumberStepper(0, 23, 0, 1) { name = "hour-stepper" };
_hourStepper = new UTKIntStepper(0, 23, 0, 1) { name = "hour-stepper" };
_hourStepper.AddToClassList("utk-date-picker__time-stepper");
_hourStepper.WrapAround = true;
var colonLabel = new Label(":");
colonLabel.AddToClassList("utk-date-picker__time-separator");
_minuteStepper = new UTKNumberStepper(0, 59, 0, 1) { name = "minute-stepper" };
_minuteStepper = new UTKIntStepper(0, 59, 0, 1) { name = "minute-stepper" };
_minuteStepper.AddToClassList("utk-date-picker__time-stepper");
_minuteStepper.WrapAround = true;
@@ -709,8 +709,8 @@ namespace UVC.UIToolkit
_nextYearButton ??= this.Q<Button>("next-year");
_dayNamesRow ??= this.Q<VisualElement>("day-names");
_timeRow ??= this.Q<VisualElement>("time-row");
_hourStepper ??= this.Q<UTKNumberStepper>("hour-stepper");
_minuteStepper ??= this.Q<UTKNumberStepper>("minute-stepper");
_hourStepper ??= this.Q<UTKIntStepper>("hour-stepper");
_minuteStepper ??= this.Q<UTKIntStepper>("minute-stepper");
_cancelButton ??= this.Q<UTKButton>("cancel-btn");
_confirmButton ??= this.Q<UTKButton>("confirm-btn");
_rangeInfoLabel ??= this.Q<Label>("range-info");

View File

@@ -28,6 +28,9 @@ namespace UVC.UIToolkit
/// <summary>읽기 전용 여부</summary>
bool IsReadOnly { get; set; }
/// <summary>라벨 표시 여부 (false면 value가 전체 너비 사용)</summary>
bool ShowLabel { get; set; }
/// <summary>속성 타입</summary>
UTKPropertyType PropertyType { get; }

View File

@@ -41,6 +41,9 @@ namespace UVC.UIToolkit
/// <summary>문자열 목록 드롭다운</summary>
DropdownList,
/// <summary>다중 선택 드롭다운</summary>
MultiSelectDropdownList,
/// <summary>라디오 버튼 그룹</summary>
RadioGroup,
@@ -57,6 +60,9 @@ namespace UVC.UIToolkit
DateTimeRange,
/// <summary>상태 + 색상 복합 타입</summary>
ColorState
ColorState,
/// <summary>버튼 (액션 트리거)</summary>
Button
}
}

View File

@@ -29,6 +29,7 @@ namespace UVC.UIToolkit
private T _value;
private bool _isReadOnly;
private bool _isVisible = true;
private bool _showLabel = true;
private string? _description;
private string? _tooltip;
private string? _groupId;
@@ -103,6 +104,13 @@ namespace UVC.UIToolkit
get => _groupId;
set => _groupId = value;
}
/// <summary>라벨 표시 여부 (false면 value가 전체 너비 사용)</summary>
public bool ShowLabel
{
get => _showLabel;
set => _showLabel = value;
}
#endregion
#region Events

View File

@@ -0,0 +1,119 @@
#nullable enable
using System;
using UnityEngine;
namespace UVC.UIToolkit
{
/// <summary>
/// 버튼 속성 Item 클래스입니다.
/// UTKButton 설정을 포함하며, 클릭 시 Action 이름을 전달합니다.
/// </summary>
public class UTKButtonItem : UTKPropertyItemBase<string>
{
#region Properties
public override UTKPropertyType PropertyType => UTKPropertyType.Button;
/// <summary>버튼 텍스트</summary>
public string Text { get; set; }
/// <summary>버튼 아이콘</summary>
public string Icon { get; set; }
/// <summary>아이콘 크기</summary>
public int IconSize { get; set; }
/// <summary>버튼 스타일 변형</summary>
public UTKButton.ButtonVariant Variant { get; set; }
/// <summary>버튼 크기</summary>
public UTKButton.ButtonSize Size { get; set; }
/// <summary>커스텀 배경 색상</summary>
public Color? BackgroundColor { get; set; }
/// <summary>외곽선 굵기</summary>
public int BorderWidth { get; set; }
/// <summary>아이콘만 표시 모드</summary>
public bool IconOnly { get; set; }
/// <summary>라벨 표시 여부 (false면 버튼이 전체 너비 사용)</summary>
public bool ShowLabel { get; set; }
/// <summary>액션 이름 (버튼 클릭 시 전달되는 고유 이름)</summary>
public string ActionName { get; }
#endregion
#region Constructor
/// <summary>
/// 기본 버튼 아이템을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="actionName">액션 이름 (클릭 시 전달)</param>
/// <param name="text">버튼 텍스트</param>
/// <param name="icon">버튼 아이콘</param>
/// <param name="variant">버튼 스타일</param>
/// <param name="size">버튼 크기</param>
public UTKButtonItem(
string id,
string actionName,
string text = "",
string icon = "",
UTKButton.ButtonVariant variant = UTKButton.ButtonVariant.Normal,
UTKButton.ButtonSize size = UTKButton.ButtonSize.Medium)
: base(id, actionName, actionName)
{
ActionName = actionName;
Text = text;
Icon = icon;
IconSize = 12;
Variant = variant;
Size = size;
BackgroundColor = null;
BorderWidth = -1;
IconOnly = false;
ShowLabel = true; // 기본적으로 라벨 표시
}
/// <summary>
/// 전체 설정을 포함한 버튼 아이템을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="actionName">액션 이름</param>
/// <param name="text">버튼 텍스트</param>
/// <param name="icon">버튼 아이콘</param>
/// <param name="iconSize">아이콘 크기</param>
/// <param name="variant">버튼 스타일</param>
/// <param name="size">버튼 크기</param>
/// <param name="backgroundColor">배경 색상</param>
/// <param name="borderWidth">외곽선 굵기</param>
/// <param name="iconOnly">아이콘만 표시</param>
/// <param name="showLabel">라벨 표시 여부</param>
public UTKButtonItem(
string id,
string actionName,
string text,
string icon,
int iconSize,
UTKButton.ButtonVariant variant,
UTKButton.ButtonSize size,
Color? backgroundColor,
int borderWidth,
bool iconOnly,
bool showLabel = true)
: base(id, actionName, actionName)
{
ActionName = actionName;
Text = text;
Icon = icon;
IconSize = iconSize;
Variant = variant;
Size = size;
BackgroundColor = backgroundColor;
BorderWidth = borderWidth;
IconOnly = iconOnly;
ShowLabel = showLabel;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 180e39abb0136364c90d52fbd5b63fd6

View File

@@ -10,8 +10,10 @@ namespace UVC.UIToolkit
{
#region Fields
private bool _useSlider;
private bool _useStepper;
private float _minValue;
private float _maxValue = 1f;
private float _step = 1.0f;
#endregion
#region Properties
@@ -25,6 +27,20 @@ namespace UVC.UIToolkit
set => _useSlider = value;
}
/// <summary>스테퍼(증감 버튼) 사용 여부</summary>
public bool UseStepper
{
get => _useStepper;
set => _useStepper = value;
}
/// <summary>스테퍼 증감 단위 (기본값: 0.1)</summary>
public float Step
{
get => _step;
set => _step = value > 0 ? value : 0.1f;
}
/// <summary>최소값 (슬라이더 모드)</summary>
public float MinValue
{
@@ -63,13 +79,15 @@ namespace UVC.UIToolkit
/// <param name="minValue">최소값</param>
/// <param name="maxValue">최대값</param>
/// <param name="useSlider">슬라이더 사용 여부</param>
/// <param name="useStepper">스테퍼 사용 여부</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKFloatPropertyItem(string id, string name, float initialValue, float minValue = float.MinValue, float maxValue = float.MaxValue, bool useSlider = true, bool isReadOnly = false)
public UTKFloatPropertyItem(string id, string name, float initialValue, float minValue = float.MinValue, float maxValue = float.MaxValue, bool useSlider = true, bool useStepper = false, bool isReadOnly = false)
: base(id, name, initialValue)
{
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
_useStepper = useStepper;
IsReadOnly = isReadOnly;
}
#endregion

View File

@@ -0,0 +1,162 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
namespace UVC.UIToolkit
{
/// <summary>
/// 다중 선택 드롭다운 속성 데이터 클래스입니다.
/// UI는 UTKMultiSelectDropdownPropertyItemView에서 담당합니다.
/// </summary>
public class UTKMultiSelectDropdownPropertyItem : UTKPropertyItemBase<List<string>>
{
#region Fields
private List<string> _choices;
#endregion
#region Properties
/// <summary>속성 타입</summary>
public override UTKPropertyType PropertyType => UTKPropertyType.MultiSelectDropdownList;
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set => _choices = value ?? new List<string>();
}
/// <summary>선택된 인덱스 목록</summary>
public List<int> SelectedIndices
{
get
{
if (Value == null || Value.Count == 0)
return new List<int>();
return Value
.Select(v => _choices.IndexOf(v))
.Where(i => i >= 0)
.OrderBy(i => i)
.ToList();
}
}
#endregion
#region Constructor
/// <summary>
/// 다중 선택 드롭다운 속성을 생성합니다.
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="initialValues">초기 선택 값 목록</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKMultiSelectDropdownPropertyItem(
string id,
string name,
List<string> choices,
List<string>? initialValues = null,
bool isReadOnly = false)
: base(id, name, initialValues ?? new List<string>())
{
_choices = choices ?? new List<string>();
// initialValues가 유효한지 확인하고 필터링
if (initialValues != null && initialValues.Count > 0)
{
var validValues = initialValues.Where(v => _choices.Contains(v)).ToList();
Value = validValues;
}
IsReadOnly = isReadOnly;
}
/// <summary>
/// 다중 선택 드롭다운 속성을 생성합니다 (인덱스 기반).
/// </summary>
/// <param name="id">고유 ID</param>
/// <param name="name">표시 이름</param>
/// <param name="choices">선택 항목 목록</param>
/// <param name="selectedIndices">초기 선택 인덱스 목록</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public UTKMultiSelectDropdownPropertyItem(
string id,
string name,
IEnumerable<string> choices,
IEnumerable<int>? selectedIndices = null,
bool isReadOnly = false)
: base(id, name, new List<string>())
{
_choices = choices?.ToList() ?? new List<string>();
if (selectedIndices != null)
{
var validValues = selectedIndices
.Where(i => i >= 0 && i < _choices.Count)
.Select(i => _choices[i])
.ToList();
Value = validValues;
}
IsReadOnly = isReadOnly;
}
#endregion
#region Public Methods
/// <summary>선택 항목 추가</summary>
public void AddChoice(string choice)
{
if (!_choices.Contains(choice))
{
_choices.Add(choice);
}
}
/// <summary>선택 항목 제거</summary>
public bool RemoveChoice(string choice)
{
bool removed = _choices.Remove(choice);
if (removed && Value != null && Value.Contains(choice))
{
// 값 목록에서도 제거
var newValue = Value.Where(v => v != choice).ToList();
Value = newValue;
}
return removed;
}
/// <summary>인덱스로 선택 설정</summary>
public void SetSelectedIndices(List<int> indices)
{
var validValues = indices
.Where(i => i >= 0 && i < _choices.Count)
.Select(i => _choices[i])
.Distinct()
.ToList();
Value = validValues;
}
/// <summary>값으로 선택 설정</summary>
public void SetSelectedValues(List<string> values)
{
var validValues = values
.Where(v => _choices.Contains(v))
.Distinct()
.ToList();
Value = validValues;
}
/// <summary>모든 항목 선택</summary>
public void SelectAll()
{
Value = new List<string>(_choices);
}
/// <summary>모든 선택 해제</summary>
public void ClearSelection()
{
Value = new List<string>();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8ffce5db6e1fd4748b4e6c1d72812fb7

View File

@@ -30,6 +30,9 @@ namespace UVC.UIToolkit
get => _maxLength;
set => _maxLength = value;
}
/// <summary>액션 버튼 설정 (null이면 버튼 미표시)</summary>
public UTKButtonItem? ActionButton { get; set; }
#endregion
#region Constructor
@@ -42,12 +45,14 @@ namespace UVC.UIToolkit
/// <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)
/// <param name="actionButton">액션 버튼</param>
public UTKStringPropertyItem(string id, string name, string initialValue = "", bool isMultiline = false, int maxLength = -1, bool isReadOnly = false, UTKButtonItem? actionButton = null)
: base(id, name, initialValue ?? string.Empty)
{
IsReadOnly = isReadOnly;
_isMultiline = isMultiline;
_maxLength = maxLength;
ActionButton = actionButton;
}
#endregion
}

View File

@@ -0,0 +1,222 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 버튼 속성 View 클래스입니다.
/// UTKButton을 표시하고 클릭 시 Action 이름을 이벤트로 전달합니다.
/// </summary>
[UxmlElement]
public partial class UTKButtonItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<string>
{
#region Fields
private UTKButton? _button;
private IUTKPropertyItem<string>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKButtonItemView";
/// <summary>값 변경 이벤트 (IUTKPropertyItemView 인터페이스 구현)</summary>
public event Action<string>? OnValueChanged;
/// <summary>버튼 클릭 이벤트</summary>
public event Action<string>? OnButtonClicked;
/// <summary>액션 이름 (현재 Value)</summary>
public string Value
{
get => _boundData?.Value ?? "";
set
{
if (_boundData != null)
{
_boundData.Value = value;
}
}
}
#endregion
#region Constructor
public UTKButtonItemView() : base()
{
InitializeUI();
}
public UTKButtonItemView(UTKButtonItem data) : this()
{
Bind(data);
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--button");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
QueryUIElements();
}
private void QueryUIElements()
{
if (_valueContainer == null) return;
_button = this.Q<UTKButton>("button-field");
if (_button == null)
{
_button = new UTKButton { name = "button-field" };
_button.AddToClassList("utk-property-item-view__button");
_valueContainer.Add(_button);
}
// 버튼 클릭 이벤트 등록
_button.OnClicked += OnButtonClickedInternal;
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// Fallback: UXML 로드 실패 시 코드로 생성
_button = new UTKButton { name = "button-field" };
_button.AddToClassList("utk-property-item-view__button");
container.Add(_button);
_button.OnClicked += OnButtonClickedInternal;
}
public override void RefreshUI()
{
// 버튼은 정적이므로 별도 갱신 불필요
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
// 버튼은 ReadOnly 개념이 적용되지 않으므로 IsEnabled로 제어
if (_button != null)
{
_button.IsEnabled = !isReadOnly;
}
}
#endregion
#region Event Handling
private void OnButtonClickedInternal()
{
if (_boundData is UTKButtonItem buttonItem)
{
OnButtonClicked?.Invoke(buttonItem.ActionName);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<string> stringData)
{
Bind(stringData);
}
else
{
Debug.LogWarning($"[UTKButtonItemView] Cannot bind to non-string data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<string> data)
{
Unbind();
_boundData = data;
// 라벨 텍스트 설정
if (_labelElement != null)
{
_labelElement.Text = data.DisplayName;
}
// UTKButtonItem 설정 적용
if (data is UTKButtonItem buttonItem && _button != null)
{
_button.Text = buttonItem.Text;
_button.Icon = buttonItem.Icon;
_button.IconSize = buttonItem.IconSize;
_button.Variant = buttonItem.Variant;
_button.Size = buttonItem.Size;
_button.IconOnly = buttonItem.IconOnly;
if (buttonItem.BackgroundColor.HasValue)
{
_button.BackgroundColor = buttonItem.BackgroundColor;
}
if (buttonItem.BorderWidth >= 0)
{
_button.BorderWidth = buttonItem.BorderWidth;
}
_button.IsEnabled = !buttonItem.IsReadOnly;
// 라벨 표시 여부 제어
if (_labelElement != null)
{
if (buttonItem.ShowLabel)
{
_labelElement.style.display = DisplayStyle.Flex;
RemoveFromClassList("utk-property-item-view--button-no-label");
}
else
{
_labelElement.style.display = DisplayStyle.None;
AddToClassList("utk-property-item-view--button-no-label");
}
}
}
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
}
public void Unbind()
{
if (_boundData != null)
{
_boundData = null;
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
if (_button != null)
{
_button.OnClicked -= OnButtonClickedInternal;
_button.Dispose();
}
Unbind();
OnValueChanged = null;
OnButtonClicked = null;
_button = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -10,12 +10,14 @@ namespace UVC.UIToolkit
/// <summary>
/// Dropdown 속성 View 클래스입니다.
/// UTKDropdown을 사용하여 문자열 목록에서 선택합니다.
/// ReadOnly 상태에서는 UTKInputField로 표시됩니다.
/// </summary>
[UxmlElement]
public partial class UTKDropdownPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<string>
{
#region Fields
private UTKDropdown? _dropdown;
private UTKInputField? _readOnlyField;
private string _value = string.Empty;
private List<string> _choices = new();
@@ -125,23 +127,48 @@ namespace UVC.UIToolkit
private void QueryUIElements()
{
_dropdown = this.Q<UTKDropdown>("dropdown-field");
if (_valueContainer == null) return;
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _dropdown == null)
if (IsReadOnly)
{
_dropdown = new UTKDropdown { name = "dropdown-field" };
_dropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_dropdown);
// ReadOnly: UTKInputField 사용
_readOnlyField = this.Q<UTKInputField>("readonly-field");
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = _value;
_readOnlyField.isReadOnly = true;
// Dropdown 숨기기
if (_dropdown != null)
{
_dropdown.style.display = DisplayStyle.None;
}
}
// 초기 값 설정
if (_dropdown != null)
else
{
// Editable: UTKDropdown 사용
_dropdown = this.Q<UTKDropdown>("dropdown-field");
if (_dropdown == null)
{
_dropdown = new UTKDropdown { name = "dropdown-field" };
_dropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_dropdown);
}
_dropdown.SetOptions(_choices);
int selectedIndex = _choices.IndexOf(_value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = !IsReadOnly;
_dropdown.IsEnabled = true;
_dropdown.style.display = DisplayStyle.Flex;
// ReadOnly 필드 숨기기
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
@@ -175,9 +202,46 @@ namespace UVC.UIToolkit
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_dropdown != null)
if (_valueContainer == null) return;
if (isReadOnly)
{
_dropdown.IsEnabled = !isReadOnly;
// Dropdown → InputField 전환
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = _value;
_readOnlyField.isReadOnly = true;
_readOnlyField.style.display = DisplayStyle.Flex;
if (_dropdown != null)
{
_dropdown.style.display = DisplayStyle.None;
}
}
else
{
// InputField → Dropdown 전환
if (_dropdown == null)
{
_dropdown = new UTKDropdown { name = "dropdown-field" };
_dropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_dropdown);
_dropdown.SetOptions(_choices);
RegisterEvents();
}
int selectedIndex = _choices.IndexOf(_value);
_dropdown.SelectedIndex = Math.Max(0, selectedIndex);
_dropdown.IsEnabled = true;
_dropdown.style.display = DisplayStyle.Flex;
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
#endregion
@@ -201,7 +265,11 @@ namespace UVC.UIToolkit
#region Value Update
private void UpdateValueUI()
{
if (_dropdown != null && _dropdown.SelectedValue != _value)
if (IsReadOnly && _readOnlyField != null)
{
_readOnlyField.Value = _value;
}
else if (_dropdown != null && _dropdown.SelectedValue != _value)
{
_dropdown.SetSelectedValue(_value, notify: false);
}
@@ -310,6 +378,7 @@ namespace UVC.UIToolkit
OnValueChanged = null;
_dropdown = null;
_readOnlyField = null;
}
base.Dispose(disposing);

View File

@@ -8,12 +8,14 @@ namespace UVC.UIToolkit
/// <summary>
/// Enum 속성 View 클래스입니다.
/// UTKEnumDropDown을 사용하여 열거형 값을 표시/편집합니다.
/// ReadOnly 상태에서는 UTKInputField로 표시됩니다.
/// </summary>
[UxmlElement]
public partial class UTKEnumPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<Enum>
{
#region Fields
private UTKEnumDropDown? _enumDropdown;
private UTKInputField? _readOnlyField;
private Enum? _value;
private Type? _enumType;
@@ -103,24 +105,49 @@ namespace UVC.UIToolkit
private void QueryUIElements()
{
_enumDropdown = this.Q<UTKEnumDropDown>("enum-dropdown");
if (_valueContainer == null) return;
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _enumDropdown == null)
if (IsReadOnly)
{
_enumDropdown = new UTKEnumDropDown { name = "enum-dropdown" };
_enumDropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_enumDropdown);
// ReadOnly: UTKInputField 사용
_readOnlyField = this.Q<UTKInputField>("readonly-field");
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = _value?.ToString() ?? "";
_readOnlyField.isReadOnly = true;
// Dropdown 숨기기
if (_enumDropdown != null)
{
_enumDropdown.style.display = DisplayStyle.None;
}
}
// 초기 값 설정
if (_enumDropdown != null)
else
{
// Editable: UTKEnumDropDown 사용
_enumDropdown = this.Q<UTKEnumDropDown>("enum-dropdown");
if (_enumDropdown == null)
{
_enumDropdown = new UTKEnumDropDown { name = "enum-dropdown" };
_enumDropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_enumDropdown);
}
if (_value != null)
{
_enumDropdown.Init(_value);
}
_enumDropdown.IsEnabled = !IsReadOnly;
_enumDropdown.IsEnabled = true;
_enumDropdown.style.display = DisplayStyle.Flex;
// ReadOnly 필드 숨기기
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
@@ -154,9 +181,47 @@ namespace UVC.UIToolkit
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_enumDropdown != null)
if (_valueContainer == null) return;
if (isReadOnly)
{
_enumDropdown.IsEnabled = !isReadOnly;
// Dropdown → InputField 전환
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = _value?.ToString() ?? "";
_readOnlyField.isReadOnly = true;
_readOnlyField.style.display = DisplayStyle.Flex;
if (_enumDropdown != null)
{
_enumDropdown.style.display = DisplayStyle.None;
}
}
else
{
// InputField → Dropdown 전환
if (_enumDropdown == null)
{
_enumDropdown = new UTKEnumDropDown { name = "enum-dropdown" };
_enumDropdown.AddToClassList("utk-property-item-view__dropdown");
_valueContainer.Add(_enumDropdown);
if (_value != null)
{
_enumDropdown.Init(_value);
}
RegisterEvents();
}
_enumDropdown.IsEnabled = true;
_enumDropdown.style.display = DisplayStyle.Flex;
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
#endregion
@@ -180,7 +245,11 @@ namespace UVC.UIToolkit
#region Value Update
private void UpdateValueUI()
{
if (_enumDropdown != null && _value != null)
if (IsReadOnly && _readOnlyField != null)
{
_readOnlyField.Value = _value?.ToString() ?? "";
}
else if (_enumDropdown != null && _value != null)
{
if (_enumDropdown.Value != _value)
{
@@ -258,6 +327,7 @@ namespace UVC.UIToolkit
OnValueChanged = null;
_enumDropdown = null;
_readOnlyField = null;
}
base.Dispose(disposing);

View File

@@ -37,11 +37,14 @@ namespace UVC.UIToolkit
#region Fields
private UTKFloatField? _floatField;
private UTKSlider? _slider;
private UTKFloatStepper? _stepper;
private float _value;
private float _minValue;
private float _maxValue = 1f;
private float _step = 0.1f;
private bool _useSlider;
private bool _useStepper;
private IUTKPropertyItem<float>? _boundData;
#endregion
@@ -112,10 +115,35 @@ namespace UVC.UIToolkit
if (_useSlider != value)
{
_useSlider = value;
UpdateSliderClass();
if (value) _useStepper = false;
UpdateModeClass();
}
}
}
/// <summary>스테퍼(증감 버튼) 사용 여부</summary>
[UxmlAttribute("use-stepper")]
public bool UseStepper
{
get => _useStepper;
set
{
if (_useStepper != value)
{
_useStepper = value;
if (value) _useSlider = false;
UpdateModeClass();
}
}
}
/// <summary>스테퍼 증감 단위</summary>
[UxmlAttribute("step")]
public float Step
{
get => _step;
set => _step = value > 0 ? value : 0.1f;
}
#endregion
#region Events
@@ -141,7 +169,9 @@ namespace UVC.UIToolkit
_value = data.Value;
_minValue = data.MinValue;
_maxValue = data.MaxValue;
_step = data.Step;
_useSlider = data.UseSlider;
_useStepper = data.UseStepper;
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
@@ -176,8 +206,8 @@ namespace UVC.UIToolkit
// 이벤트 등록
RegisterEvents();
// 슬라이더 클래스 업데이트
UpdateSliderClass();
// 모드 클래스 업데이트
UpdateModeClass();
UpdateValueUI();
UpdateReadOnlyState();
@@ -187,6 +217,7 @@ namespace UVC.UIToolkit
{
_slider = this.Q<UTKSlider>("slider-field");
_floatField = this.Q<UTKFloatField>("value-field");
_stepper = this.Q<UTKFloatStepper>("stepper-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
@@ -207,6 +238,16 @@ namespace UVC.UIToolkit
_floatField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_floatField);
}
if (_stepper == null)
{
_stepper = new UTKFloatStepper(_minValue, _maxValue, _value, _step, IsReadOnly)
{
name = "stepper-field"
};
_stepper.AddToClassList("utk-property-item-view__stepper");
_valueContainer.Add(_stepper);
}
}
// 초기 값 설정
@@ -221,10 +262,28 @@ namespace UVC.UIToolkit
_floatField.SetValueWithoutNotify(_value);
_floatField.isReadOnly = IsReadOnly;
}
if (_stepper != null)
{
_stepper.MinValue = _minValue;
_stepper.MaxValue = _maxValue;
_stepper.Step = _step;
_stepper.SetValue(_value, false);
_stepper.IsReadOnly = IsReadOnly;
}
}
private void UpdateSliderClass()
private void UpdateModeClass()
{
// ReadOnly 상태에서는 무조건 FloatField만 표시
if (IsReadOnly)
{
RemoveFromClassList("utk-property-item-view--slider");
RemoveFromClassList("utk-property-item-view--stepper");
UpdateControlVisibility();
return;
}
// 슬라이더 클래스
if (_useSlider)
{
AddToClassList("utk-property-item-view--slider");
@@ -233,6 +292,52 @@ namespace UVC.UIToolkit
{
RemoveFromClassList("utk-property-item-view--slider");
}
// 스테퍼 클래스
if (_useStepper)
{
AddToClassList("utk-property-item-view--stepper");
}
else
{
RemoveFromClassList("utk-property-item-view--stepper");
}
UpdateControlVisibility();
}
/// <summary>컨트롤 가시성을 업데이트합니다.</summary>
private void UpdateControlVisibility()
{
bool isReadOnlyMode = IsReadOnly;
bool showSlider = !isReadOnlyMode && _useSlider;
bool showStepper = !isReadOnlyMode && _useStepper;
bool showFloatField = !showStepper; // 스테퍼 모드가 아니면 항상 FloatField 표시
if (_slider != null)
{
_slider.style.display = showSlider ? DisplayStyle.Flex : DisplayStyle.None;
}
if (_stepper != null)
{
_stepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None;
}
if (_floatField != null)
{
_floatField.style.display = showFloatField ? DisplayStyle.Flex : DisplayStyle.None;
// 슬라이더 모드일 때 필드 너비 조정
if (showSlider)
{
_floatField.AddToClassList("utk-property-item-view__field--with-slider");
}
else
{
_floatField.RemoveFromClassList("utk-property-item-view__field--with-slider");
}
}
}
#endregion
@@ -258,6 +363,14 @@ namespace UVC.UIToolkit
{
_slider.IsEnabled = !isReadOnly;
}
if (_stepper != null)
{
_stepper.IsReadOnly = isReadOnly;
}
// ReadOnly 상태 변경 시 모드 클래스 업데이트
UpdateModeClass();
}
#endregion
@@ -273,6 +386,11 @@ namespace UVC.UIToolkit
{
_slider.OnValueChanged += OnSliderChanged;
}
if (_stepper != null)
{
_stepper.OnValueChanged += OnStepperChanged;
}
}
private void UnregisterEvents()
@@ -286,6 +404,11 @@ namespace UVC.UIToolkit
{
_slider.OnValueChanged -= OnSliderChanged;
}
if (_stepper != null)
{
_stepper.OnValueChanged -= OnStepperChanged;
}
}
private void OnFloatFieldChanged(float newValue)
@@ -322,6 +445,11 @@ namespace UVC.UIToolkit
_floatField.SetValueWithoutNotify(newValue);
}
if (_stepper != null && !Mathf.Approximately(_stepper.Value, newValue))
{
_stepper.SetValue(newValue, false);
}
// 값 설정 (이벤트 발생)
if (!Mathf.Approximately(_value, newValue))
{
@@ -334,6 +462,20 @@ namespace UVC.UIToolkit
}
}
}
private void OnStepperChanged(float 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
@@ -348,6 +490,11 @@ namespace UVC.UIToolkit
{
_slider.SetValueWithoutNotify(_value);
}
if (_stepper != null && !Mathf.Approximately(_stepper.Value, _value))
{
_stepper.SetValue(_value, false);
}
}
#endregion
@@ -378,16 +525,28 @@ namespace UVC.UIToolkit
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
// 슬라이더 관련 속성 동기화
// 슬라이더 및 스테퍼 관련 속성 동기화
if (data is UTKFloatPropertyItem floatItem)
{
_minValue = floatItem.MinValue;
_maxValue = floatItem.MaxValue;
_step = floatItem.Step;
// 모드 변경 확인
bool modeChanged = false;
if (_useSlider != floatItem.UseSlider)
{
_useSlider = floatItem.UseSlider;
UpdateSliderClass();
modeChanged = true;
}
if (_useStepper != floatItem.UseStepper)
{
_useStepper = floatItem.UseStepper;
modeChanged = true;
}
if (modeChanged)
{
UpdateModeClass();
}
// 슬라이더 범위 업데이트
@@ -396,6 +555,14 @@ namespace UVC.UIToolkit
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
}
// 스테퍼 설정 업데이트
if (_stepper != null)
{
_stepper.MinValue = _minValue;
_stepper.MaxValue = _maxValue;
_stepper.Step = _step;
}
}
// 데이터 변경 이벤트 구독
@@ -438,6 +605,8 @@ namespace UVC.UIToolkit
OnValueChanged = null;
_floatField = null;
_slider = null;
_stepper?.Dispose();
_stepper = null;
}
base.Dispose(disposing);

View File

@@ -7,7 +7,7 @@ namespace UVC.UIToolkit
{
/// <summary>
/// Int 속성 View 클래스입니다.
/// UTKIntegerField, UTKSliderInt, 또는 UTKNumberStepper를 사용하여 int 값을 표시/편집합니다.
/// UTKIntegerField, UTKSliderInt, 또는 UTKIntStepper를 사용하여 int 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
@@ -36,7 +36,7 @@ namespace UVC.UIToolkit
#region Fields
private UTKIntegerField? _intField;
private UTKSliderInt? _slider;
private UTKNumberStepper? _stepper;
private UTKIntStepper? _stepper;
private int _value;
private int _minValue;
@@ -219,7 +219,7 @@ namespace UVC.UIToolkit
{
_slider = this.Q<UTKSliderInt>("slider-field");
_intField = this.Q<UTKIntegerField>("value-field");
_stepper = this.Q<UTKNumberStepper>("stepper-field");
_stepper = this.Q<UTKIntStepper>("stepper-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
@@ -243,7 +243,7 @@ namespace UVC.UIToolkit
if (_stepper == null)
{
_stepper = new UTKNumberStepper(_minValue, _maxValue, _value, _step, IsReadOnly)
_stepper = new UTKIntStepper(_minValue, _maxValue, _value, _step, IsReadOnly)
{
name = "stepper-field"
};
@@ -347,7 +347,7 @@ namespace UVC.UIToolkit
bool isReadOnlyMode = IsReadOnly;
bool showSlider = !isReadOnlyMode && _useSlider;
bool showStepper = !isReadOnlyMode && _useStepper;
bool showIntField = isReadOnlyMode || (!_useSlider && !_useStepper);
bool showIntField = !showStepper; // 스테퍼 모드가 아니면 항상 IntField 표시
if (_slider != null)
{
@@ -362,6 +362,16 @@ namespace UVC.UIToolkit
if (_intField != null)
{
_intField.style.display = showIntField ? DisplayStyle.Flex : DisplayStyle.None;
// 슬라이더 모드일 때 필드 너비 조정
if (showSlider)
{
_intField.AddToClassList("utk-property-item-view__field--with-slider");
}
else
{
_intField.RemoveFromClassList("utk-property-item-view__field--with-slider");
}
}
}
#endregion
@@ -436,6 +446,11 @@ namespace UVC.UIToolkit
_intField.SetValueWithoutNotify(newValue);
}
if (_stepper != null && _stepper.Value != newValue)
{
_stepper.SetValue(newValue, false);
}
if (_value != newValue)
{
_value = newValue;

View File

@@ -15,8 +15,8 @@ namespace UVC.UIToolkit
#region Fields
private UTKIntegerField? _minField;
private UTKIntegerField? _maxField;
private UTKNumberStepper? _minStepper;
private UTKNumberStepper? _maxStepper;
private UTKIntStepper? _minStepper;
private UTKIntStepper? _maxStepper;
private UTKIntRange _value;
private bool _useStepper;
@@ -150,8 +150,8 @@ namespace UVC.UIToolkit
{
_minField = this.Q<UTKIntegerField>("min-field");
_maxField = this.Q<UTKIntegerField>("max-field");
_minStepper = this.Q<UTKNumberStepper>("min-stepper");
_maxStepper = this.Q<UTKNumberStepper>("max-stepper");
_minStepper = this.Q<UTKIntStepper>("min-stepper");
_maxStepper = this.Q<UTKIntStepper>("max-stepper");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
@@ -182,7 +182,7 @@ namespace UVC.UIToolkit
// NumberStepper 생성
if (_minStepper == null)
{
_minStepper = new UTKNumberStepper(_stepperMinValue, _stepperMaxValue, _value.Min, _stepperStep, IsReadOnly)
_minStepper = new UTKIntStepper(_stepperMinValue, _stepperMaxValue, _value.Min, _stepperStep, IsReadOnly)
{
name = "min-stepper"
};
@@ -193,7 +193,7 @@ namespace UVC.UIToolkit
if (_maxStepper == null)
{
_maxStepper = new UTKNumberStepper(_stepperMinValue, _stepperMaxValue, _value.Max, _stepperStep, IsReadOnly)
_maxStepper = new UTKIntStepper(_stepperMinValue, _stepperMaxValue, _value.Max, _stepperStep, IsReadOnly)
{
name = "max-stepper"
};

View File

@@ -0,0 +1,448 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// MultiSelectDropdown 속성 View 클래스입니다.
/// UTKMultiSelectDropdown을 사용하여 문자열 목록에서 다중 선택합니다.
/// ReadOnly 상태에서는 UTKInputField로 표시됩니다.
/// </summary>
[UxmlElement]
public partial class UTKMultiSelectDropdownPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<List<string>>
{
#region Fields
private UTKMultiSelectDropdown? _multiSelectDropdown;
private UTKInputField? _readOnlyField;
private List<string> _value = new();
private List<string> _choices = new();
private IUTKPropertyItem<List<string>>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKMultiSelectDropdownPropertyItemView";
/// <summary>현재 값 (선택된 항목 목록)</summary>
public List<string> Value
{
get => _value;
set
{
var newValue = value ?? new List<string>();
// 리스트 내용 비교
if (!AreListsEqual(_value, newValue))
{
_value = new List<string>(newValue);
UpdateValueUI();
OnValueChanged?.Invoke(_value);
if (_boundData != null && !AreListsEqual(_boundData.Value, _value))
{
_boundData.Value = new List<string>(_value);
}
}
}
}
/// <summary>선택 가능한 항목 목록</summary>
public List<string> Choices
{
get => _choices;
set
{
_choices = value ?? new List<string>();
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.SetOptions(_choices);
}
}
}
#endregion
#region Events
public event Action<List<string>>? OnValueChanged;
#endregion
#region Constructor
public UTKMultiSelectDropdownPropertyItemView() : base()
{
InitializeUI();
}
public UTKMultiSelectDropdownPropertyItemView(UTKMultiSelectDropdownPropertyItem data) : base()
{
_choices = data.Choices;
_value = data.Value != null ? new List<string>(data.Value) : new List<string>();
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKMultiSelectDropdownPropertyItemView(
string label,
List<string> choices,
List<string>? initialValues = null) : base()
{
_choices = choices ?? new List<string>();
_value = initialValues != null ? new List<string>(initialValues) : new List<string>();
Label = label;
InitializeUI();
}
public UTKMultiSelectDropdownPropertyItemView(
string label,
IEnumerable<string> choices,
IEnumerable<int>? selectedIndices = null) : base()
{
_choices = choices?.ToList() ?? new List<string>();
if (selectedIndices != null)
{
_value = selectedIndices
.Where(i => i >= 0 && i < _choices.Count)
.Select(i => _choices[i])
.ToList();
}
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--multiselect-dropdown");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
if (_valueContainer == null) return;
if (IsReadOnly)
{
// ReadOnly: UTKInputField 사용
_readOnlyField = this.Q<UTKInputField>("readonly-field");
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = string.Join(", ", _value);
_readOnlyField.isReadOnly = true;
// Dropdown 숨기기
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.style.display = DisplayStyle.None;
}
}
else
{
// Editable: UTKMultiSelectDropdown 사용
_multiSelectDropdown = this.Q<UTKMultiSelectDropdown>("multiselect-dropdown-field");
if (_multiSelectDropdown == null)
{
_multiSelectDropdown = new UTKMultiSelectDropdown { name = "multiselect-dropdown-field" };
_multiSelectDropdown.AddToClassList("utk-property-item-view__multiselect-dropdown");
_valueContainer.Add(_multiSelectDropdown);
}
_multiSelectDropdown.SetOptions(_choices);
_multiSelectDropdown.SetSelectedValues(_value, notify: false);
_multiSelectDropdown.IsEnabled = true;
_multiSelectDropdown.style.display = DisplayStyle.Flex;
// ReadOnly 필드 숨기기
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
private void RegisterEvents()
{
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.OnSelectionChanged += OnMultiSelectDropdownChanged;
}
}
private void UnregisterEvents()
{
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.OnSelectionChanged -= OnMultiSelectDropdownChanged;
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// Fallback: UXML 로드 실패 시 코드로 생성
_multiSelectDropdown = new UTKMultiSelectDropdown { name = "multiselect-dropdown-field" };
_multiSelectDropdown.AddToClassList("utk-property-item-view__multiselect-dropdown");
container.Add(_multiSelectDropdown);
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_valueContainer == null) return;
if (isReadOnly)
{
// Dropdown → InputField 전환
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = string.Join(", ", _value);
_readOnlyField.isReadOnly = true;
_readOnlyField.style.display = DisplayStyle.Flex;
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.style.display = DisplayStyle.None;
}
}
else
{
// InputField → Dropdown 전환
if (_multiSelectDropdown == null)
{
_multiSelectDropdown = new UTKMultiSelectDropdown { name = "multiselect-dropdown-field" };
_multiSelectDropdown.AddToClassList("utk-property-item-view__multiselect-dropdown");
_valueContainer.Add(_multiSelectDropdown);
_multiSelectDropdown.SetOptions(_choices);
RegisterEvents();
}
_multiSelectDropdown.SetSelectedValues(_value, notify: false);
_multiSelectDropdown.IsEnabled = true;
_multiSelectDropdown.style.display = DisplayStyle.Flex;
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
#endregion
#region Event Handling
private void OnMultiSelectDropdownChanged(List<int> indices, List<string> values)
{
if (!AreListsEqual(_value, values))
{
_value = new List<string>(values);
OnValueChanged?.Invoke(_value);
if (_boundData != null && !AreListsEqual(_boundData.Value, _value))
{
_boundData.Value = new List<string>(_value);
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (IsReadOnly && _readOnlyField != null)
{
_readOnlyField.Value = string.Join(", ", _value);
}
else if (_multiSelectDropdown != null && !AreListsEqual(_multiSelectDropdown.SelectedValues, _value))
{
_multiSelectDropdown.SetSelectedValues(_value, notify: false);
}
}
#endregion
#region Helper Methods
/// <summary>두 리스트의 내용이 같은지 비교 (순서 무관)</summary>
private bool AreListsEqual(List<string>? list1, List<string>? list2)
{
if (list1 == null && list2 == null) return true;
if (list1 == null || list2 == null) return false;
if (list1.Count != list2.Count) return false;
var set1 = new HashSet<string>(list1);
var set2 = new HashSet<string>(list2);
return set1.SetEquals(set2);
}
#endregion
#region Public Methods
/// <summary>선택 항목 추가</summary>
public void AddChoice(string choice)
{
if (!_choices.Contains(choice))
{
_choices.Add(choice);
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.SetOptions(_choices);
}
}
}
/// <summary>선택 항목 제거</summary>
public bool RemoveChoice(string choice)
{
bool removed = _choices.Remove(choice);
if (removed && _multiSelectDropdown != null)
{
_multiSelectDropdown.SetOptions(_choices);
if (_value.Contains(choice))
{
var newValue = _value.Where(v => v != choice).ToList();
Value = newValue;
}
}
return removed;
}
/// <summary>인덱스로 선택 설정</summary>
public void SetSelectedIndices(List<int> indices, bool notify = true)
{
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.SetSelectedIndices(indices, notify);
}
}
/// <summary>값으로 선택 설정</summary>
public void SetSelectedValues(List<string> values, bool notify = true)
{
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.SetSelectedValues(values, notify);
}
}
/// <summary>모든 항목 선택</summary>
public void SelectAll()
{
_multiSelectDropdown?.SelectAll();
}
/// <summary>모든 선택 해제</summary>
public void ClearSelection()
{
_multiSelectDropdown?.ClearSelection();
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<List<string>> listData)
{
Bind(listData);
}
else
{
Debug.LogWarning($"[UTKMultiSelectDropdownPropertyItemView] Cannot bind to non-List<string> data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<List<string>> data)
{
Unbind();
_boundData = data;
Label = data.Name;
_value = data.Value != null ? new List<string>(data.Value) : new List<string>();
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKMultiSelectDropdownPropertyItem multiSelectItem)
{
_choices = multiSelectItem.Choices;
if (_multiSelectDropdown != null)
{
_multiSelectDropdown.SetOptions(_choices);
}
}
data.OnTypedValueChanged += OnDataValueChanged;
UpdateValueUI();
UpdateReadOnlyState();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<List<string>> item, List<string> oldValue, List<string> newValue)
{
if (!AreListsEqual(_value, newValue))
{
_value = newValue != null ? new List<string>(newValue) : new List<string>();
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_multiSelectDropdown = null;
_readOnlyField = null;
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -35,6 +35,7 @@ namespace UVC.UIToolkit
protected const string USS_CLASS_READONLY = "utk-property-item-view--readonly";
protected const string USS_CLASS_HIDDEN = "utk-property-item-view--hidden";
protected const string USS_CLASS_NO_LABEL = "utk-property-item-view--no-label";
#endregion
#region Static Cache
@@ -51,6 +52,7 @@ namespace UVC.UIToolkit
protected string _label = string.Empty;
protected bool _isReadOnly = false;
protected bool _isVisible = true;
protected bool _showLabel = true;
protected string? _tooltipText;
#endregion
@@ -111,6 +113,21 @@ namespace UVC.UIToolkit
UpdateTooltip();
}
}
/// <summary>라벨 표시 여부 (false면 value가 전체 너비 사용)</summary>
[UxmlAttribute("show-label")]
public bool ShowLabel
{
get => _showLabel;
set
{
if (_showLabel != value)
{
_showLabel = value;
UpdateShowLabelState();
}
}
}
#endregion
#region Constructor
@@ -241,6 +258,27 @@ namespace UVC.UIToolkit
this.ClearTooltip();
}
}
/// <summary>라벨 표시 상태 업데이트</summary>
protected virtual void UpdateShowLabelState()
{
if (_showLabel)
{
RemoveFromClassList(USS_CLASS_NO_LABEL);
if (_labelElement != null)
{
_labelElement.style.display = DisplayStyle.Flex;
}
}
else
{
AddToClassList(USS_CLASS_NO_LABEL);
if (_labelElement != null)
{
_labelElement.style.display = DisplayStyle.None;
}
}
}
#endregion
#region Theme
@@ -339,6 +377,21 @@ namespace UVC.UIToolkit
}
#endregion
#region Helper Methods
/// <summary>
/// PropertyItem의 공통 속성을 View에 적용합니다.
/// 하위 클래스의 Bind 메서드에서 호출하세요.
/// </summary>
protected void ApplyCommonProperties(IUTKPropertyItem item)
{
Label = item.Name;
IsReadOnly = item.IsReadOnly;
IsVisible = item.IsVisible;
TooltipText = item.Tooltip;
ShowLabel = item.ShowLabel;
}
#endregion
#region IDisposable
public void Dispose()
{

View File

@@ -76,12 +76,14 @@ namespace UVC.UIToolkit
UTKPropertyType.DateTime => new UTKDateTimePropertyItemView(),
UTKPropertyType.Enum => CreateEnumView(data),
UTKPropertyType.DropdownList => CreateDropdownView(data),
UTKPropertyType.MultiSelectDropdownList => CreateMultiSelectDropdownView(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(),
UTKPropertyType.Button => CreateButtonView(data),
_ => throw new ArgumentException($"Unknown property type: {data.PropertyType}")
};
}
@@ -112,12 +114,14 @@ namespace UVC.UIToolkit
UTKPropertyType.DateTime => new UTKDateTimePropertyItemView(),
UTKPropertyType.Enum => new UTKEnumPropertyItemView(),
UTKPropertyType.DropdownList => new UTKDropdownPropertyItemView(),
UTKPropertyType.MultiSelectDropdownList => new UTKMultiSelectDropdownPropertyItemView(),
UTKPropertyType.RadioGroup => new UTKRadioPropertyItemView(),
UTKPropertyType.IntRange => new UTKIntRangePropertyItemView(),
UTKPropertyType.FloatRange => new UTKFloatRangePropertyItemView(),
UTKPropertyType.DateRange => new UTKDateRangePropertyItemView(),
UTKPropertyType.DateTimeRange => new UTKDateTimeRangePropertyItemView(),
UTKPropertyType.ColorState => new UTKColorStatePropertyItemView(),
UTKPropertyType.Button => new UTKButtonItemView(),
_ => throw new ArgumentException($"Unknown property type: {propertyType}")
};
}
@@ -210,6 +214,26 @@ namespace UVC.UIToolkit
}
return new UTKRadioPropertyItemView();
}
private static IUTKPropertyItemView CreateMultiSelectDropdownView(IUTKPropertyItem data)
{
// UTKMultiSelectDropdownPropertyItem의 Choices로 MultiSelectDropdown 초기화
if (data is UTKMultiSelectDropdownPropertyItem multiSelectItem)
{
return new UTKMultiSelectDropdownPropertyItemView("", multiSelectItem.Choices);
}
return new UTKMultiSelectDropdownPropertyItemView();
}
private static IUTKPropertyItemView CreateButtonView(IUTKPropertyItem data)
{
// UTKButtonItem으로 ButtonView 초기화
if (data is UTKButtonItem buttonItem)
{
return new UTKButtonItemView(buttonItem);
}
return new UTKButtonItemView();
}
#endregion
}
}

View File

@@ -10,6 +10,7 @@ namespace UVC.UIToolkit
/// <summary>
/// Radio 속성 View 클래스입니다.
/// UTKRadioButton 그룹을 사용하여 단일 선택을 제공합니다.
/// ReadOnly 상태에서는 UTKInputField로 표시됩니다.
/// </summary>
[UxmlElement]
public partial class UTKRadioPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<int>
@@ -17,6 +18,7 @@ namespace UVC.UIToolkit
#region Fields
private VisualElement? _radioContainer;
private List<UTKRadioButton> _radioButtons = new();
private UTKInputField? _readOnlyField;
private int _value;
private List<string> _choices = new();
@@ -129,14 +131,45 @@ namespace UVC.UIToolkit
private void QueryUIElements()
{
_radioContainer = this.Q<VisualElement>("radio-container");
if (_valueContainer == null) return;
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null && _radioContainer == null)
if (IsReadOnly)
{
_radioContainer = new VisualElement { name = "radio-container" };
_radioContainer.AddToClassList("utk-property-item-view__radio-container");
_valueContainer.Add(_radioContainer);
// ReadOnly: UTKInputField 사용
_readOnlyField = this.Q<UTKInputField>("readonly-field");
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = SelectedText ?? "";
_readOnlyField.isReadOnly = true;
// Radio 컨테이너 숨기기
_radioContainer = this.Q<VisualElement>("radio-container");
if (_radioContainer != null)
{
_radioContainer.style.display = DisplayStyle.None;
}
}
else
{
// Editable: Radio 버튼 사용
_radioContainer = this.Q<VisualElement>("radio-container");
if (_radioContainer == null)
{
_radioContainer = new VisualElement { name = "radio-container" };
_radioContainer.AddToClassList("utk-property-item-view__radio-container");
_valueContainer.Add(_radioContainer);
}
_radioContainer.style.display = DisplayStyle.Flex;
// ReadOnly 필드 숨기기
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
#endregion
@@ -154,9 +187,48 @@ namespace UVC.UIToolkit
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
foreach (var radio in _radioButtons)
if (_valueContainer == null) return;
if (isReadOnly)
{
radio.IsEnabled = !isReadOnly;
// Radio → InputField 전환
if (_readOnlyField == null)
{
_readOnlyField = new UTKInputField { name = "readonly-field" };
_readOnlyField.AddToClassList("utk-property-item-view__readonly-field");
_valueContainer.Add(_readOnlyField);
}
_readOnlyField.Value = SelectedText ?? "";
_readOnlyField.isReadOnly = true;
_readOnlyField.style.display = DisplayStyle.Flex;
if (_radioContainer != null)
{
_radioContainer.style.display = DisplayStyle.None;
}
}
else
{
// InputField → Radio 전환
if (_radioContainer == null)
{
_radioContainer = new VisualElement { name = "radio-container" };
_radioContainer.AddToClassList("utk-property-item-view__radio-container");
_valueContainer.Add(_radioContainer);
CreateRadioButtons();
}
_radioContainer.style.display = DisplayStyle.Flex;
// 라디오 버튼 활성화
foreach (var radio in _radioButtons)
{
radio.IsEnabled = true;
}
if (_readOnlyField != null)
{
_readOnlyField.style.display = DisplayStyle.None;
}
}
}
#endregion
@@ -223,9 +295,16 @@ namespace UVC.UIToolkit
#region Value Update
private void UpdateSelection()
{
for (int i = 0; i < _radioButtons.Count; i++)
if (IsReadOnly && _readOnlyField != null)
{
_radioButtons[i].SetChecked(i == _value, false);
_readOnlyField.Value = SelectedText ?? "";
}
else
{
for (int i = 0; i < _radioButtons.Count; i++)
{
_radioButtons[i].SetChecked(i == _value, false);
}
}
}
#endregion
@@ -310,6 +389,7 @@ namespace UVC.UIToolkit
OnValueChanged = null;
_radioContainer = null;
_readOnlyField = null;
}
base.Dispose(disposing);

View File

@@ -27,6 +27,8 @@ namespace UVC.UIToolkit
{
#region Fields
private UTKInputField? _inputField;
private UTKButton? _actionButton;
private Action? _actionButtonClickHandler; // 이벤트 핸들러 참조 보관
private string _value = string.Empty;
private bool _isMultiline = false;
@@ -93,6 +95,9 @@ namespace UVC.UIToolkit
#region Events
public event Action<string>? OnValueChanged;
/// <summary>액션 버튼 클릭 이벤트 (actionName 전달)</summary>
public event Action<string>? OnActionButtonClicked;
#endregion
#region Constructor
@@ -232,6 +237,67 @@ namespace UVC.UIToolkit
}
#endregion
#region Action Button
private void CreateActionButton(UTKButtonItem buttonItem)
{
// 기존 버튼 제거
RemoveActionButton();
// 새 버튼 생성
_actionButton = new UTKButton
{
name = buttonItem.Name,
Text = buttonItem.Text,
Icon = buttonItem.Icon,
IconSize = buttonItem.IconSize,
Variant = buttonItem.Variant,
Size = buttonItem.Size,
IconOnly = buttonItem.IconOnly
};
if(buttonItem.IconOnly == false){
_actionButton.AddToClassList("utk-property-item-view__action-button");
}
else
{
_actionButton.AddToClassList("utk-property-item-view__action-button-icon");
}
if (buttonItem.BackgroundColor.HasValue)
{
_actionButton.BackgroundColor = buttonItem.BackgroundColor;
}
if (buttonItem.BorderWidth >= 0)
{
_actionButton.BorderWidth = buttonItem.BorderWidth;
}
// 버튼 클릭 이벤트 등록 (핸들러 참조 보관)
_actionButtonClickHandler = () => OnActionButtonClicked?.Invoke(buttonItem.ActionName);
_actionButton.OnClicked += _actionButtonClickHandler;
// value-container에 추가 (InputField 다음)
_valueContainer?.Add(_actionButton);
}
private void RemoveActionButton()
{
if (_actionButton != null)
{
// 보관된 핸들러로 이벤트 해제
if (_actionButtonClickHandler != null)
{
_actionButton.OnClicked -= _actionButtonClickHandler;
_actionButtonClickHandler = null;
}
_actionButton.RemoveFromHierarchy();
_actionButton.Dispose();
_actionButton = null;
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
@@ -256,7 +322,7 @@ namespace UVC.UIToolkit
IsReadOnly = data.IsReadOnly;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
if (data is UTKStringPropertyItem stringItem)
{
_isMultiline = stringItem.IsMultiline;
@@ -266,6 +332,16 @@ namespace UVC.UIToolkit
_inputField.multiline = _isMultiline;
_inputField.maxLength = _maxLength;
}
// ActionButton이 있으면 버튼 생성
if (stringItem.ActionButton != null && _valueContainer != null)
{
CreateActionButton(stringItem.ActionButton);
}
else
{
RemoveActionButton();
}
}
data.OnTypedValueChanged += OnDataValueChanged;
@@ -281,6 +357,9 @@ namespace UVC.UIToolkit
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData = null;
}
// ActionButton 제거
RemoveActionButton();
}
private void OnDataValueChanged(IUTKPropertyItem<string> item, string oldValue, string newValue)
@@ -303,7 +382,14 @@ namespace UVC.UIToolkit
UnregisterEvents();
Unbind();
// UTKInputField Dispose 호출
if (_inputField is IDisposable disposableField)
{
disposableField.Dispose();
}
OnValueChanged = null;
OnActionButtonClicked = null;
_inputField = null;
}

View File

@@ -89,6 +89,13 @@ namespace UVC.UIToolkit
add => PropertyList.OnPropertyClicked += value;
remove => PropertyList.OnPropertyClicked -= value;
}
/// <summary>버튼 클릭 이벤트 (PropertyList 위임)</summary>
public event Action<string, string>? OnPropertyButtonClicked
{
add => PropertyList.OnPropertyButtonClicked += value;
remove => PropertyList.OnPropertyButtonClicked -= value;
}
#endregion
#region Constructor