UTKShortcutList 개발 완료. Modal 개선
This commit is contained in:
@@ -97,8 +97,17 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
|
||||
/// <utk:UTKBoundsField label="경계" error-message="크기는 양수여야 합니다." />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKBoundsField label="경계" label-min-width="120" />
|
||||
/// </UXML>
|
||||
/// </code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var boundsField = new UTKBoundsField("경계");
|
||||
/// boundsField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시:</b></para>
|
||||
/// <code>
|
||||
/// // 게임 오브젝트의 콜라이더 경계 설정
|
||||
@@ -130,6 +139,7 @@ namespace UVC.UIToolkit
|
||||
private string _yLabel = "Y";
|
||||
private string _zLabel = "Z";
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -253,6 +263,18 @@ namespace UVC.UIToolkit
|
||||
get => _validation;
|
||||
set => _validation = value;
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -283,7 +305,11 @@ namespace UVC.UIToolkit
|
||||
AddToClassList("utk-boundsfield");
|
||||
|
||||
// 초기 라벨 설정
|
||||
schedule.Execute(() => UpdateAxisLabels());
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
UpdateAxisLabels();
|
||||
ApplyLabelMinWidth();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -348,6 +374,22 @@ namespace UVC.UIToolkit
|
||||
textInput.isReadOnly = _isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -67,7 +67,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 비활성화 상태 -->
|
||||
/// <utk:UTKDoubleField label="PI" value="3.141592653589793" is-enabled="false" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKDoubleField label="경도" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var doubleField = new UTKDoubleField("경도");
|
||||
/// doubleField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (GPS 좌표):</b></para>
|
||||
/// <code>
|
||||
/// // GPS 좌표 입력 (높은 정밀도 필요)
|
||||
@@ -89,6 +98,7 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private bool _isEnabled = true;
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -140,7 +150,19 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용</summary>
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용</summary>
|
||||
public new bool isReadOnly
|
||||
{
|
||||
get => base.isReadOnly;
|
||||
@@ -180,6 +202,9 @@ namespace UVC.UIToolkit
|
||||
private void SetupStyles()
|
||||
{
|
||||
AddToClassList("utk-double-field");
|
||||
|
||||
// label 설정 후 LabelMinWidth 적용
|
||||
schedule.Execute(() => ApplyLabelMinWidth());
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -212,6 +237,22 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -78,7 +78,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 비활성화 상태 -->
|
||||
/// <utk:UTKFloatField label="고정 값" value="3.14" is-enabled="false" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKFloatField label="속도" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var floatField = new UTKFloatField("속도");
|
||||
/// floatField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (캐릭터 스탯):</b></para>
|
||||
/// <code>
|
||||
/// // 캐릭터 이동 속도 편집
|
||||
@@ -101,6 +110,7 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private bool _isEnabled = true;
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -152,6 +162,18 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용</summary>
|
||||
public new bool isReadOnly
|
||||
{
|
||||
@@ -191,6 +213,9 @@ namespace UVC.UIToolkit
|
||||
private void SetupStyles()
|
||||
{
|
||||
AddToClassList("utk-float-field");
|
||||
|
||||
// label 설정 후 LabelMinWidth 적용
|
||||
schedule.Execute(() => ApplyLabelMinWidth());
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -223,6 +248,22 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -80,8 +80,18 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 비활성화 -->
|
||||
/// <utk:UTKInputField label="읽기전용" is-enabled="false" value="수정 불가" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKInputField label="이름" label-min-width="120" />
|
||||
/// </ui:UXML>
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var input = new UTKInputField("이름");
|
||||
/// input.LabelMinWidth = 120f; // 120px
|
||||
/// // -1이면 미설정 (기본값)
|
||||
/// </code>
|
||||
/// </example>
|
||||
[UxmlElement]
|
||||
public partial class UTKInputField : TextField, IDisposable
|
||||
@@ -94,6 +104,7 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private bool _isEnabled = true;
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private InputFieldVariant _variant = InputFieldVariant.Default;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
@@ -160,6 +171,18 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용</summary>
|
||||
public new bool isReadOnly
|
||||
{
|
||||
@@ -237,6 +260,9 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
AddToClassList("utk-input");
|
||||
UpdateVariant();
|
||||
|
||||
// label 설정 후 LabelMinWidth 적용
|
||||
schedule.Execute(() => ApplyLabelMinWidth());
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -271,6 +297,22 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -76,7 +76,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 비활성화 상태 -->
|
||||
/// <utk:UTKIntegerField label="고정 값" value="100" is-enabled="false" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKIntegerField label="수량" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var intField = new UTKIntegerField("수량");
|
||||
/// intField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (인벤토리 수량):</b></para>
|
||||
/// <code>
|
||||
/// // 인벤토리 아이템 수량 편집
|
||||
@@ -100,6 +109,7 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private bool _isEnabled = true;
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -151,6 +161,18 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 읽기 전용
|
||||
/// </summary>
|
||||
@@ -192,6 +214,9 @@ namespace UVC.UIToolkit
|
||||
private void SetupStyles()
|
||||
{
|
||||
AddToClassList("utk-integer-field");
|
||||
|
||||
// label 설정 후 LabelMinWidth 적용
|
||||
schedule.Execute(() => ApplyLabelMinWidth());
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -224,6 +249,22 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -68,7 +68,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 비활성화 상태 -->
|
||||
/// <utk:UTKLongField label="고정 ID" is-enabled="false" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKLongField label="ID" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var longField = new UTKLongField("ID");
|
||||
/// longField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (파일 정보):</b></para>
|
||||
/// <code>
|
||||
/// // 파일 크기 표시
|
||||
@@ -87,6 +96,7 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private bool _isEnabled = true;
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -138,6 +148,18 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 읽기 전용
|
||||
/// </summary>
|
||||
@@ -179,6 +201,9 @@ namespace UVC.UIToolkit
|
||||
private void SetupStyles()
|
||||
{
|
||||
AddToClassList("utk-long-field");
|
||||
|
||||
// label 설정 후 LabelMinWidth 적용
|
||||
schedule.Execute(() => ApplyLabelMinWidth());
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -211,6 +236,22 @@ namespace UVC.UIToolkit
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -84,7 +84,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 읽기 전용 -->
|
||||
/// <utk:UTKRectField label="고정 영역" is-readonly="true" />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKRectField label="영역" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var rectField = new UTKRectField("영역");
|
||||
/// rectField.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (스프라이트 영역 편집):</b></para>
|
||||
/// <code>
|
||||
/// // 스프라이트 UV 영역 편집기
|
||||
@@ -114,6 +123,7 @@ namespace UVC.UIToolkit
|
||||
private string _wLabel = "W";
|
||||
private string _hLabel = "H";
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -213,6 +223,18 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>읽기 전용 상태</summary>
|
||||
[UxmlAttribute("is-readonly")]
|
||||
public bool IsReadOnly
|
||||
@@ -263,7 +285,11 @@ namespace UVC.UIToolkit
|
||||
AddToClassList("utk-rectfield");
|
||||
|
||||
// 초기 라벨 설정
|
||||
schedule.Execute(() => UpdateAxisLabels());
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
UpdateAxisLabels();
|
||||
ApplyLabelMinWidth();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -318,6 +344,22 @@ namespace UVC.UIToolkit
|
||||
textInput.isReadOnly = _isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -89,8 +89,17 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
|
||||
/// <utk:UTKVector2Field label="크기" error-message="크기는 양수여야 합니다." />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKVector2Field label="크기" label-min-width="120" />
|
||||
/// </UXML>
|
||||
/// </code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var vec2Field = new UTKVector2Field("크기");
|
||||
/// vec2Field.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시:</b></para>
|
||||
/// <code>
|
||||
/// // RectTransform 크기 조절
|
||||
@@ -121,6 +130,7 @@ namespace UVC.UIToolkit
|
||||
private string _xLabel = "X";
|
||||
private string _yLabel = "Y";
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -208,6 +218,18 @@ namespace UVC.UIToolkit
|
||||
get => _validation;
|
||||
set => _validation = value;
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -246,7 +268,11 @@ namespace UVC.UIToolkit
|
||||
AddToClassList("utk-vector2-field");
|
||||
|
||||
// 초기 라벨 설정
|
||||
schedule.Execute(() => UpdateAxisLabels());
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
UpdateAxisLabels();
|
||||
ApplyLabelMinWidth();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -299,6 +325,22 @@ namespace UVC.UIToolkit
|
||||
textInput.isReadOnly = _isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -89,7 +89,16 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
|
||||
/// <utk:UTKVector3Field label="위치" error-message="유효하지 않은 좌표입니다." />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKVector3Field label="위치" label-min-width="120" />
|
||||
/// ]]></code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var vec3Field = new UTKVector3Field("위치");
|
||||
/// vec3Field.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시 (Transform 편집기):</b></para>
|
||||
/// <code>
|
||||
/// // GameObject Transform 편집
|
||||
@@ -121,6 +130,7 @@ namespace UVC.UIToolkit
|
||||
private string _yLabel = "Y";
|
||||
private string _zLabel = "Z";
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -229,6 +239,18 @@ namespace UVC.UIToolkit
|
||||
get => _validation;
|
||||
set => _validation = value;
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -267,7 +289,11 @@ namespace UVC.UIToolkit
|
||||
AddToClassList("utk-vector3-field");
|
||||
|
||||
// 초기 라벨 설정
|
||||
schedule.Execute(() => UpdateAxisLabels());
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
UpdateAxisLabels();
|
||||
ApplyLabelMinWidth();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -321,6 +347,22 @@ namespace UVC.UIToolkit
|
||||
textInput.isReadOnly = _isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -91,8 +91,17 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
|
||||
/// <utk:UTKVector4Field label="색상" error-message="알파 값은 0~1 사이여야 합니다." />
|
||||
///
|
||||
/// <!-- label min-width 설정 -->
|
||||
/// <utk:UTKVector4Field label="값" label-min-width="120" />
|
||||
/// </UXML>
|
||||
/// </code>
|
||||
/// <para><b>Label Min-Width 설정:</b></para>
|
||||
/// <code>
|
||||
/// // label이 있을 때 .unity-label의 min-width를 설정
|
||||
/// var vec4Field = new UTKVector4Field("값");
|
||||
/// vec4Field.LabelMinWidth = 120f; // 120px
|
||||
/// </code>
|
||||
/// <para><b>실제 활용 예시:</b></para>
|
||||
/// <code>
|
||||
/// // 머티리얼 쉐이더 파라미터 설정
|
||||
@@ -123,6 +132,7 @@ namespace UVC.UIToolkit
|
||||
private string _zLabel = "Z";
|
||||
private string _wLabel = "W";
|
||||
private string _errorMessage = "";
|
||||
private float _labelMinWidth = -1f;
|
||||
private Func<bool>? _validation;
|
||||
private Label? _errorLabel;
|
||||
#endregion
|
||||
@@ -234,6 +244,18 @@ namespace UVC.UIToolkit
|
||||
get => _validation;
|
||||
set => _validation = value;
|
||||
}
|
||||
|
||||
/// <summary>label이 있을 때 .unity-label의 min-width (px). -1이면 미설정</summary>
|
||||
[UxmlAttribute("label-min-width")]
|
||||
public float LabelMinWidth
|
||||
{
|
||||
get => _labelMinWidth;
|
||||
set
|
||||
{
|
||||
_labelMinWidth = value;
|
||||
ApplyLabelMinWidth();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -272,7 +294,11 @@ namespace UVC.UIToolkit
|
||||
AddToClassList("utk-vector4field");
|
||||
|
||||
// 초기 라벨 설정
|
||||
schedule.Execute(() => UpdateAxisLabels());
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
UpdateAxisLabels();
|
||||
ApplyLabelMinWidth();
|
||||
});
|
||||
}
|
||||
|
||||
private void SetupEvents()
|
||||
@@ -327,6 +353,22 @@ namespace UVC.UIToolkit
|
||||
textInput.isReadOnly = _isReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLabelMinWidth()
|
||||
{
|
||||
if (string.IsNullOrEmpty(label)) return;
|
||||
var labelElement = this.Q<Label>(className: "unity-label");
|
||||
if (labelElement == null) return;
|
||||
|
||||
if (_labelMinWidth >= 0)
|
||||
{
|
||||
labelElement.style.minWidth = _labelMinWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
labelElement.style.minWidth = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
42
Assets/Scripts/UVC/UIToolkit/List/UTKShortcutItemData.cs
Normal file
42
Assets/Scripts/UVC/UIToolkit/List/UTKShortcutItemData.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
#nullable enable
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 단축키 아이템 데이터.
|
||||
/// <see cref="UTKShortcutList"/>의 각 행에 대한 데이터를 담습니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var data = new UTKShortcutItemData
|
||||
/// {
|
||||
/// Id = "file.new_project",
|
||||
/// CommandName = "File > New Project",
|
||||
/// UseCtrl = true,
|
||||
/// UseShift = false,
|
||||
/// UseAlt = false,
|
||||
/// Key = "N"
|
||||
/// };
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class UTKShortcutItemData
|
||||
{
|
||||
/// <summary>단축키 고유 ID (예: "file.new_project")</summary>
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
/// <summary>표시 명령 이름 (예: "File > New Project")</summary>
|
||||
public string CommandName { get; set; } = "";
|
||||
|
||||
/// <summary>Ctrl 키 사용 여부</summary>
|
||||
public bool UseCtrl { get; set; }
|
||||
|
||||
/// <summary>Shift 키 사용 여부</summary>
|
||||
public bool UseShift { get; set; }
|
||||
|
||||
/// <summary>Alt 키 사용 여부</summary>
|
||||
public bool UseAlt { get; set; }
|
||||
|
||||
/// <summary>주요 키 표시 텍스트 (예: "N", "Delete", "F1")</summary>
|
||||
public string Key { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: caa768e3069c01c49863e091a09d622f
|
||||
639
Assets/Scripts/UVC/UIToolkit/List/UTKShortcutList.cs
Normal file
639
Assets/Scripts/UVC/UIToolkit/List/UTKShortcutList.cs
Normal file
@@ -0,0 +1,639 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 단축키 설정 리스트 컴포넌트.
|
||||
/// Command 이름, Ctrl / Shift / Alt 체크박스, Key 입력(읽기전용 + 클릭 시 키 캡처)으로 구성됩니다.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para><b>열 구성 (왼쪽 → 오른쪽):</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Command 이름 (flex-grow)</item>
|
||||
/// <item>Ctrl 체크박스 (52 px)</item>
|
||||
/// <item>Shift 체크박스 (52 px)</item>
|
||||
/// <item>Alt 체크박스 (52 px)</item>
|
||||
/// <item>Key 입력 필드 (76 px) – 클릭 시 다음 키 자동 캡처</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>Key 캡처 방법:</b></para>
|
||||
/// <list type="number">
|
||||
/// <item>Key 필드 클릭 → 캡처 모드 진입 ("···" 표시)</item>
|
||||
/// <item>원하는 키 입력 → 자동 저장 후 캡처 종료</item>
|
||||
/// <item>Escape 키 → 취소 (이전 값 복원)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>가상화:</b> UTKListView(ListView) 를 사용하여 대량 항목도 성능 저하 없이 표시합니다.</para>
|
||||
///
|
||||
/// <para><b>관련 리소스:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Resources/UIToolkit/List/UTKShortcutList.uxml</item>
|
||||
/// <item>Resources/UIToolkit/List/UTKShortcutListItem.uxml</item>
|
||||
/// <item>Resources/UIToolkit/List/UTKShortcutListUss.uss</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var list = new UTKShortcutList();
|
||||
/// list.OnDataChanged += (item) => Debug.Log($"변경: {item.CommandName}");
|
||||
///
|
||||
/// list.SetData(new List<UTKShortcutItemData>
|
||||
/// {
|
||||
/// new() { Id = "file.new", CommandName = "File > New Project", UseCtrl = true, Key = "N" },
|
||||
/// new() { Id = "edit.undo", CommandName = "Edit > Undo", UseCtrl = true, Key = "Z" },
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
[UxmlElement]
|
||||
public partial class UTKShortcutList : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
private const string UXML_PATH = "UIToolkit/List/UTKShortcutList";
|
||||
private const string USS_PATH = "UIToolkit/List/UTKShortcutListUss";
|
||||
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKShortcutListItem";
|
||||
private const float ITEM_HEIGHT = 36f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool _disposed;
|
||||
|
||||
private UTKListView? _listView;
|
||||
private UTKInputField? _searchField;
|
||||
private UTKButton? _clearButton;
|
||||
|
||||
// 전체 데이터 · 검색 필터링된 데이터 분리
|
||||
private List<UTKShortcutItemData> _allItems = new();
|
||||
private readonly List<UTKShortcutItemData> _filteredItems = new();
|
||||
|
||||
// UXML 캐싱 (makeItem 호출마다 Resources.Load 방지)
|
||||
private VisualTreeAsset? _itemTemplate;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// 단축키 데이터가 변경될 때 발생합니다.
|
||||
/// 변경된 <see cref="UTKShortcutItemData"/> 인스턴스를 전달합니다.
|
||||
/// </summary>
|
||||
public event Action<UTKShortcutItemData>? OnDataChanged;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public UTKShortcutList() : base()
|
||||
{
|
||||
// 1. 테마 적용
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
// 2. USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨)
|
||||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (uss != null)
|
||||
styleSheets.Add(uss);
|
||||
else
|
||||
Debug.LogWarning($"[UTKShortcutList] USS not found: {USS_PATH}");
|
||||
|
||||
// 3. UXML 로드 → 요소 구성
|
||||
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||||
if (visualTree != null)
|
||||
{
|
||||
visualTree.CloneTree(this);
|
||||
InitializeFromUxml();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[UTKShortcutList] UXML not found: {UXML_PATH}, using fallback");
|
||||
CreateFallbackUI();
|
||||
}
|
||||
|
||||
// 4. 테마 변경 구독
|
||||
SubscribeToThemeChanges();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UI Creation
|
||||
|
||||
/// <summary>UXML 로드 성공 시 – 자식 요소 참조 획득 및 초기화.</summary>
|
||||
private void InitializeFromUxml()
|
||||
{
|
||||
AddToClassList("utk-shortcut-list");
|
||||
|
||||
_searchField = this.Q<UTKInputField>("search-field");
|
||||
_clearButton = this.Q<UTKButton>("clear-btn");
|
||||
_listView = this.Q<UTKListView>("list-view");
|
||||
|
||||
BindSearchField();
|
||||
SetupListView();
|
||||
}
|
||||
|
||||
/// <summary>UXML 로드 실패 시 – 코드로 UI 구성.</summary>
|
||||
private void CreateFallbackUI()
|
||||
{
|
||||
AddToClassList("utk-shortcut-list");
|
||||
|
||||
// 검색 영역
|
||||
var searchContainer = new VisualElement { name = "search-container" };
|
||||
searchContainer.AddToClassList("utk-shortcut-list__search-container");
|
||||
Add(searchContainer);
|
||||
|
||||
_searchField = new UTKInputField { name = "search-field" };
|
||||
_searchField.AddToClassList("utk-shortcut-list__search");
|
||||
searchContainer.Add(_searchField);
|
||||
|
||||
_clearButton = new UTKButton { name = "clear-btn" };
|
||||
_clearButton.Variant = UTKButton.ButtonVariant.Text;
|
||||
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
|
||||
_clearButton.AddToClassList("utk-shortcut-list__clear-btn");
|
||||
searchContainer.Add(_clearButton);
|
||||
|
||||
// 컬럼 헤더
|
||||
var header = new VisualElement { name = "header" };
|
||||
header.AddToClassList("utk-shortcut-list__header");
|
||||
header.Add(MakeHeaderLabel("", "utk-shortcut-list__header-command"));
|
||||
header.Add(MakeHeaderLabel("Ctrl", "utk-shortcut-list__header-modifier"));
|
||||
header.Add(MakeHeaderLabel("Shift", "utk-shortcut-list__header-modifier"));
|
||||
header.Add(MakeHeaderLabel("Alt", "utk-shortcut-list__header-modifier"));
|
||||
header.Add(MakeHeaderLabel("Key", "utk-shortcut-list__header-key"));
|
||||
Add(header);
|
||||
|
||||
// ListView
|
||||
_listView = new UTKListView { name = "list-view" };
|
||||
_listView.AddToClassList("utk-shortcut-list__listview");
|
||||
Add(_listView);
|
||||
|
||||
BindSearchField();
|
||||
SetupListView();
|
||||
}
|
||||
|
||||
private static Label MakeHeaderLabel(string text, string className)
|
||||
{
|
||||
var label = new Label(text);
|
||||
label.AddToClassList(className);
|
||||
return label;
|
||||
}
|
||||
|
||||
/// <summary>검색 필드 이벤트 연결.</summary>
|
||||
private void BindSearchField()
|
||||
{
|
||||
// Clear 버튼 초기 숨김
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = DisplayStyle.None;
|
||||
_clearButton.OnClicked += OnClearButtonClicked;
|
||||
}
|
||||
|
||||
if (_searchField == null) return;
|
||||
// Enter 키 또는 포커스 잃을 때 검색 실행 (UTKComponentList 방식)
|
||||
_searchField.OnSubmit += OnSearch;
|
||||
}
|
||||
|
||||
/// <summary>ListView makeItem / bindItem / unbindItem 설정.</summary>
|
||||
private void SetupListView()
|
||||
{
|
||||
if (_listView == null) return;
|
||||
|
||||
_listView.makeItem = MakeItem;
|
||||
_listView.bindItem = BindItem;
|
||||
_listView.unbindItem = UnbindItem;
|
||||
_listView.fixedItemHeight = ITEM_HEIGHT;
|
||||
_listView.selectionType = SelectionType.None;
|
||||
_listView.itemsSource = _filteredItems;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Theme
|
||||
private void SubscribeToThemeChanges()
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||||
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||||
}
|
||||
|
||||
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Search
|
||||
/// <summary>Clear 버튼 클릭 처리 – 검색어 초기화 및 재검색.</summary>
|
||||
private void OnClearButtonClicked()
|
||||
{
|
||||
if (_searchField != null && _searchField.value.Length > 0)
|
||||
{
|
||||
_searchField.value = string.Empty;
|
||||
OnSearch(string.Empty);
|
||||
}
|
||||
if (_clearButton != null)
|
||||
_clearButton.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
/// <summary>검색 실행 (Enter 키 또는 포커스 잃을 때 호출).</summary>
|
||||
private void OnSearch(string query)
|
||||
{
|
||||
// Clear 버튼 표시/숨김
|
||||
if (_clearButton != null)
|
||||
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
|
||||
_filteredItems.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
_filteredItems.AddRange(_allItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lower = query.ToLowerInvariant();
|
||||
foreach (var item in _allItems)
|
||||
{
|
||||
if (item.CommandName.ToLowerInvariant().Contains(lower))
|
||||
_filteredItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
_listView?.RefreshItems();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ListView Callbacks
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 VisualElement 생성 (가상화 재사용 요소).
|
||||
/// UXML을 캐싱하여 매 호출마다 Resources.Load 를 방지합니다.
|
||||
/// </summary>
|
||||
private VisualElement MakeItem()
|
||||
{
|
||||
_itemTemplate ??= Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
|
||||
|
||||
if (_itemTemplate != null)
|
||||
return _itemTemplate.Instantiate();
|
||||
|
||||
Debug.LogWarning($"[UTKShortcutList] Item UXML not found: {ITEM_UXML_PATH}, using fallback");
|
||||
return CreateItemFallback();
|
||||
}
|
||||
|
||||
/// <summary>UXML 로드 실패 시 코드로 아이템 행 생성.</summary>
|
||||
private static VisualElement CreateItemFallback()
|
||||
{
|
||||
var container = new VisualElement { name = "item-container" };
|
||||
container.AddToClassList("utk-shortcut-list-item");
|
||||
|
||||
var cmd = new UTKLabel { name = "command-label" };
|
||||
cmd.AddToClassList("utk-shortcut-list-item__command");
|
||||
container.Add(cmd);
|
||||
|
||||
foreach (var nm in new[] { "ctrl-checkbox", "shift-checkbox", "alt-checkbox" })
|
||||
{
|
||||
var cb = new UTKCheckBox { name = nm };
|
||||
cb.AddToClassList("utk-shortcut-list-item__modifier");
|
||||
container.Add(cb);
|
||||
}
|
||||
|
||||
var key = new UTKInputField { name = "key-field" };
|
||||
key.AddToClassList("utk-shortcut-list-item__key");
|
||||
container.Add(key);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터를 VisualElement 에 바인딩합니다.
|
||||
/// 이전 콜백을 해제하고 새 콜백을 등록하여 중복 이벤트를 방지합니다.
|
||||
/// </summary>
|
||||
private void BindItem(VisualElement element, int index)
|
||||
{
|
||||
if (index < 0 || index >= _filteredItems.Count) return;
|
||||
|
||||
// ListView 가 내부적으로 flex-grow: 0 을 인라인으로 강제하므로 덮어씁니다.
|
||||
element.style.flexGrow = 1;
|
||||
|
||||
var data = _filteredItems[index];
|
||||
|
||||
// 요소 참조 획득
|
||||
var root = element.Q<VisualElement>("item-container");
|
||||
var cmdLabel = root?.Q<UTKLabel>("command-label");
|
||||
var ctrlBox = root?.Q<UTKCheckBox>("ctrl-checkbox");
|
||||
var shiftBox = root?.Q<UTKCheckBox>("shift-checkbox");
|
||||
var altBox = root?.Q<UTKCheckBox>("alt-checkbox");
|
||||
var keyField = root?.Q<UTKInputField>("key-field");
|
||||
|
||||
if (cmdLabel == null || ctrlBox == null || shiftBox == null || altBox == null || keyField == null)
|
||||
{
|
||||
Debug.LogWarning("[UTKShortcutList] BindItem: 일부 자식 요소를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 이전 바인딩 해제
|
||||
CleanupItemCallbacks(element);
|
||||
|
||||
// ── 값 설정 (notify: false → 이벤트 미발생) ──────────────
|
||||
cmdLabel.Text = data.CommandName;
|
||||
ctrlBox.SetChecked(data.UseCtrl, notify: false);
|
||||
shiftBox.SetChecked(data.UseShift, notify: false);
|
||||
altBox.SetChecked(data.UseAlt, notify: false);
|
||||
keyField.SetValue(data.Key, notify: false);
|
||||
keyField.isReadOnly = true; // 직접 타이핑 방지 (캡처만으로 설정)
|
||||
|
||||
// ── 수정자 키 체크박스 콜백 ──────────────────────────────
|
||||
Action<bool> onCtrl = v => { data.UseCtrl = v; OnDataChanged?.Invoke(data); };
|
||||
Action<bool> onShift = v => { data.UseShift = v; OnDataChanged?.Invoke(data); };
|
||||
Action<bool> onAlt = v => { data.UseAlt = v; OnDataChanged?.Invoke(data); };
|
||||
|
||||
ctrlBox.OnValueChanged += onCtrl;
|
||||
shiftBox.OnValueChanged += onShift;
|
||||
altBox.OnValueChanged += onAlt;
|
||||
|
||||
// ── Key 캡처 콜백 ─────────────────────────────────────────
|
||||
var capture = new KeyCaptureState(data, keyField);
|
||||
|
||||
EventCallback<FocusInEvent> onFocusIn = _ => capture.StartCapture();
|
||||
EventCallback<KeyDownEvent> onKeyDown = evt => capture.HandleKeyDown(evt, () => OnDataChanged?.Invoke(data));
|
||||
EventCallback<FocusOutEvent> onFocusOut = _ => capture.CancelCapture();
|
||||
|
||||
keyField.RegisterCallback(onFocusIn);
|
||||
// TrickleDown 으로 등록 → 내부 TextElement 가 키 이벤트를 받기 전에 가로채기
|
||||
keyField.RegisterCallback(onKeyDown, TrickleDown.TrickleDown);
|
||||
keyField.RegisterCallback(onFocusOut);
|
||||
|
||||
// 해제 정보 저장
|
||||
element.userData = new ShortcutItemCallbackInfo(
|
||||
ctrlBox, shiftBox, altBox, keyField,
|
||||
onCtrl, onShift, onAlt,
|
||||
onFocusIn, onKeyDown, onFocusOut);
|
||||
}
|
||||
|
||||
/// <summary>가상화 재사용 전 콜백 정리.</summary>
|
||||
private void UnbindItem(VisualElement element, int index)
|
||||
{
|
||||
CleanupItemCallbacks(element);
|
||||
}
|
||||
|
||||
/// <summary>element.userData 에 저장된 모든 이벤트 콜백을 해제합니다.</summary>
|
||||
private static void CleanupItemCallbacks(VisualElement element)
|
||||
{
|
||||
if (element.userData is not ShortcutItemCallbackInfo info) return;
|
||||
|
||||
info.CtrlBox.OnValueChanged -= info.OnCtrlHandler;
|
||||
info.ShiftBox.OnValueChanged -= info.OnShiftHandler;
|
||||
info.AltBox.OnValueChanged -= info.OnAltHandler;
|
||||
|
||||
info.KeyField.UnregisterCallback(info.OnFocusIn);
|
||||
info.KeyField.UnregisterCallback(info.OnKeyDown, TrickleDown.TrickleDown);
|
||||
info.KeyField.UnregisterCallback(info.OnFocusOut);
|
||||
|
||||
element.userData = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// 단축키 목록을 설정하고 ListView 를 갱신합니다.
|
||||
/// </summary>
|
||||
/// <param name="items">표시할 단축키 데이터 목록.</param>
|
||||
public void SetData(List<UTKShortcutItemData> items)
|
||||
{
|
||||
_allItems = items ?? new List<UTKShortcutItemData>();
|
||||
OnSearch(_searchField?.value ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 단축키 목록(원본 전체)을 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>복사본 목록.</returns>
|
||||
public List<UTKShortcutItemData> GetData() => new(_allItems);
|
||||
|
||||
/// <summary>
|
||||
/// ListView 를 강제로 새로고침합니다.
|
||||
/// 외부에서 데이터를 직접 변경한 후 호출하세요.
|
||||
/// </summary>
|
||||
public void RefreshItems() => _listView?.RefreshItems();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Types
|
||||
|
||||
/// <summary>
|
||||
/// Key 캡처 상태 관리.
|
||||
/// FocusIn → 캡처 시작, KeyDown → 키 저장, FocusOut/Escape → 취소.
|
||||
/// </summary>
|
||||
private sealed class KeyCaptureState
|
||||
{
|
||||
private readonly UTKShortcutItemData _data;
|
||||
private readonly UTKInputField _keyField;
|
||||
private bool _isCapturing;
|
||||
private string _originalKey = "";
|
||||
|
||||
/// <summary>캡처 대기 중 표시 문자열.</summary>
|
||||
private const string CAPTURE_PLACEHOLDER = "···";
|
||||
|
||||
public KeyCaptureState(UTKShortcutItemData data, UTKInputField keyField)
|
||||
{
|
||||
_data = data;
|
||||
_keyField = keyField;
|
||||
}
|
||||
|
||||
/// <summary>캡처 모드 진입 – 대기 표시 문자열로 교체.</summary>
|
||||
public void StartCapture()
|
||||
{
|
||||
if (_isCapturing) return;
|
||||
|
||||
_isCapturing = true;
|
||||
_originalKey = _data.Key;
|
||||
|
||||
_keyField.AddToClassList("utk-shortcut-list-item__key--capturing");
|
||||
_keyField.SetValue(CAPTURE_PLACEHOLDER, notify: false);
|
||||
}
|
||||
|
||||
/// <summary>캡처 취소 – 원래 값 복원.</summary>
|
||||
public void CancelCapture()
|
||||
{
|
||||
if (!_isCapturing) return;
|
||||
|
||||
_isCapturing = false;
|
||||
_keyField.RemoveFromClassList("utk-shortcut-list-item__key--capturing");
|
||||
_keyField.SetValue(_originalKey, notify: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// KeyDown 이벤트 처리.
|
||||
/// <list type="bullet">
|
||||
/// <item>Escape → 캡처 취소.</item>
|
||||
/// <item>수정자 키(Ctrl/Shift/Alt 단독) → 무시.</item>
|
||||
/// <item>그 외 → 키 이름 저장 후 캡처 종료.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public void HandleKeyDown(KeyDownEvent evt, Action onChanged)
|
||||
{
|
||||
if (!_isCapturing) return;
|
||||
|
||||
var code = evt.keyCode;
|
||||
|
||||
// Escape: 취소
|
||||
if (code == KeyCode.Escape)
|
||||
{
|
||||
evt.StopImmediatePropagation();
|
||||
CancelCapture();
|
||||
_keyField.Blur();
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정자 키 단독 입력 → 무시 (Ctrl/Shift/Alt 체크박스로 설정)
|
||||
if (code is KeyCode.LeftControl or KeyCode.RightControl or
|
||||
KeyCode.LeftShift or KeyCode.RightShift or
|
||||
KeyCode.LeftAlt or KeyCode.RightAlt or
|
||||
KeyCode.LeftCommand or KeyCode.RightCommand or
|
||||
KeyCode.None)
|
||||
return;
|
||||
|
||||
// 키 저장
|
||||
_isCapturing = false;
|
||||
_keyField.RemoveFromClassList("utk-shortcut-list-item__key--capturing");
|
||||
|
||||
var keyName = ResolveKeyName(code, evt.character);
|
||||
_data.Key = keyName;
|
||||
_keyField.SetValue(keyName, notify: false);
|
||||
|
||||
onChanged.Invoke();
|
||||
|
||||
evt.StopImmediatePropagation();
|
||||
evt.PreventDefault();
|
||||
|
||||
_keyField.Blur();
|
||||
}
|
||||
|
||||
/// <summary>KeyCode → 표시 문자열 변환.</summary>
|
||||
private static string ResolveKeyName(KeyCode code, char character)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
KeyCode.Delete => "Delete",
|
||||
KeyCode.Backspace => "Backspace",
|
||||
KeyCode.Return => "Enter",
|
||||
KeyCode.KeypadEnter => "Enter",
|
||||
KeyCode.Tab => "Tab",
|
||||
KeyCode.Space => "Space",
|
||||
KeyCode.Insert => "Insert",
|
||||
KeyCode.Home => "Home",
|
||||
KeyCode.End => "End",
|
||||
KeyCode.PageUp => "PgUp",
|
||||
KeyCode.PageDown => "PgDn",
|
||||
KeyCode.UpArrow => "↑",
|
||||
KeyCode.DownArrow => "↓",
|
||||
KeyCode.LeftArrow => "←",
|
||||
KeyCode.RightArrow => "→",
|
||||
KeyCode.F1 => "F1",
|
||||
KeyCode.F2 => "F2",
|
||||
KeyCode.F3 => "F3",
|
||||
KeyCode.F4 => "F4",
|
||||
KeyCode.F5 => "F5",
|
||||
KeyCode.F6 => "F6",
|
||||
KeyCode.F7 => "F7",
|
||||
KeyCode.F8 => "F8",
|
||||
KeyCode.F9 => "F9",
|
||||
KeyCode.F10 => "F10",
|
||||
KeyCode.F11 => "F11",
|
||||
KeyCode.F12 => "F12",
|
||||
KeyCode.Keypad0 => "Num0",
|
||||
KeyCode.Keypad1 => "Num1",
|
||||
KeyCode.Keypad2 => "Num2",
|
||||
KeyCode.Keypad3 => "Num3",
|
||||
KeyCode.Keypad4 => "Num4",
|
||||
KeyCode.Keypad5 => "Num5",
|
||||
KeyCode.Keypad6 => "Num6",
|
||||
KeyCode.Keypad7 => "Num7",
|
||||
KeyCode.Keypad8 => "Num8",
|
||||
KeyCode.Keypad9 => "Num9",
|
||||
KeyCode.KeypadPlus => "Num+",
|
||||
KeyCode.KeypadMinus => "Num-",
|
||||
KeyCode.KeypadMultiply => "Num*",
|
||||
KeyCode.KeypadDivide => "Num/",
|
||||
KeyCode.KeypadPeriod => "Num.",
|
||||
// 일반 문자 키 – character 우선, 없으면 KeyCode.ToString()
|
||||
_ => character != '\0' && !char.IsControl(character)
|
||||
? character.ToString().ToUpperInvariant()
|
||||
: code.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 바인딩 시 등록하는 이벤트 콜백 참조 보관.
|
||||
/// UnbindItem 에서 정확히 해제하기 위해 사용합니다.
|
||||
/// </summary>
|
||||
private sealed class ShortcutItemCallbackInfo
|
||||
{
|
||||
public readonly UTKCheckBox CtrlBox;
|
||||
public readonly UTKCheckBox ShiftBox;
|
||||
public readonly UTKCheckBox AltBox;
|
||||
public readonly UTKInputField KeyField;
|
||||
|
||||
public readonly Action<bool> OnCtrlHandler;
|
||||
public readonly Action<bool> OnShiftHandler;
|
||||
public readonly Action<bool> OnAltHandler;
|
||||
|
||||
public readonly EventCallback<FocusInEvent> OnFocusIn;
|
||||
public readonly EventCallback<KeyDownEvent> OnKeyDown;
|
||||
public readonly EventCallback<FocusOutEvent> OnFocusOut;
|
||||
|
||||
public ShortcutItemCallbackInfo(
|
||||
UTKCheckBox ctrl, UTKCheckBox shift, UTKCheckBox alt, UTKInputField key,
|
||||
Action<bool> onCtrl, Action<bool> onShift, Action<bool> onAlt,
|
||||
EventCallback<FocusInEvent> onFocusIn,
|
||||
EventCallback<KeyDownEvent> onKeyDown,
|
||||
EventCallback<FocusOutEvent> onFocusOut)
|
||||
{
|
||||
CtrlBox = ctrl; ShiftBox = shift; AltBox = alt; KeyField = key;
|
||||
OnCtrlHandler = onCtrl; OnShiftHandler = onShift; OnAltHandler = onAlt;
|
||||
OnFocusIn = onFocusIn;
|
||||
OnKeyDown = onKeyDown;
|
||||
OnFocusOut = onFocusOut;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 테마 구독 해제
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||||
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||||
|
||||
// 검색 필드 이벤트 해제
|
||||
if (_searchField != null)
|
||||
_searchField.OnSubmit -= OnSearch;
|
||||
|
||||
// Clear 버튼 이벤트 해제
|
||||
if (_clearButton != null)
|
||||
_clearButton.OnClicked -= OnClearButtonClicked;
|
||||
|
||||
// ListView 정리
|
||||
_listView?.Dispose();
|
||||
|
||||
// 이벤트 · 캐시 정리
|
||||
OnDataChanged = null;
|
||||
_itemTemplate = null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 700c91f027891a64c8d4a91e4107a90d
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.Util;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
@@ -478,6 +479,7 @@ namespace UVC.UIToolkit
|
||||
okBtn.OnClicked += Close;
|
||||
_buttonContainer.Add(okBtn);
|
||||
}
|
||||
UTKChildAnnotator.AnnotateChild(_buttonContainer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.Util;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
@@ -21,8 +23,8 @@ namespace UVC.UIToolkit
|
||||
/// // 초기화 (root 설정 필요)
|
||||
/// UTKModal.SetRoot(rootVisualElement);
|
||||
///
|
||||
/// // Static Factory로 표시
|
||||
/// var modal = UTKModal.Show("설정", UTKModal.ModalSize.Medium);
|
||||
/// // Static Factory로 생성
|
||||
/// var modal = UTKModal.Create("설정", UTKModal.ModalSize.Medium);
|
||||
/// modal.OnClosed += () => Debug.Log("모달 닫힘");
|
||||
///
|
||||
/// // 콘텐츠 추가
|
||||
@@ -32,6 +34,37 @@ namespace UVC.UIToolkit
|
||||
/// // 푸터에 버튼 추가
|
||||
/// modal.AddToFooter(new UTKButton("확인", "", UTKButton.ButtonVariant.Primary));
|
||||
/// modal.AddToFooter(new UTKButton("취소", "", UTKButton.ButtonVariant.Normal));
|
||||
///
|
||||
/// // 화면에 표시
|
||||
/// modal.Show();
|
||||
/// </code>
|
||||
/// <para><b>Async/Await 방식:</b></para>
|
||||
/// <code>
|
||||
/// // 모달이 닫힐 때까지 대기
|
||||
/// var modal = UTKModal.Create("설정", UTKModal.ModalSize.Medium);
|
||||
/// modal.Add(new Label("모달 내용"));
|
||||
///
|
||||
/// var closeBtn = new UTKButton("닫기", "", UTKButton.ButtonVariant.Primary);
|
||||
/// closeBtn.OnClicked += () => modal.Close();
|
||||
/// modal.AddToFooter(closeBtn);
|
||||
///
|
||||
/// await modal.ShowAsync();
|
||||
/// Debug.Log("모달이 닫혔습니다.");
|
||||
/// </code>
|
||||
/// <para><b>Async/Await + IUTKModalContent 방식:</b></para>
|
||||
/// <code>
|
||||
/// // IUTKModalContent<T> 구현 콘텐츠에서 결과 반환
|
||||
/// var modal = UTKModal.Create("사용자 정보", UTKModal.ModalSize.Medium);
|
||||
/// var form = new UserFormContent(); // VisualElement + IUTKModalContent<UserData>
|
||||
/// modal.Add(form);
|
||||
///
|
||||
/// var submitBtn = new UTKButton("제출", "", UTKButton.ButtonVariant.Primary);
|
||||
/// submitBtn.OnClicked += () => modal.Close();
|
||||
/// modal.AddToFooter(submitBtn);
|
||||
///
|
||||
/// UserData? result = await modal.ShowAsync<UserData>();
|
||||
/// if (result != null)
|
||||
/// Debug.Log($"이름: {result.Name}");
|
||||
/// </code>
|
||||
/// <para><b>UXML에서 사용:</b></para>
|
||||
/// <code>
|
||||
@@ -42,6 +75,41 @@ namespace UVC.UIToolkit
|
||||
/// </ui:UXML>
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <summary>
|
||||
/// 모달 콘텐츠 인터페이스.
|
||||
/// VisualElement를 상속한 클래스에서 구현하여 모달의 결과 값을 반환합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">결과 타입</typeparam>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class UserFormContent : VisualElement, IUTKModalContent<UserData>
|
||||
/// {
|
||||
/// private UTKInputField _nameField;
|
||||
/// private UTKInputField _emailField;
|
||||
///
|
||||
/// public UserFormContent()
|
||||
/// {
|
||||
/// _nameField = new UTKInputField("이름");
|
||||
/// _emailField = new UTKInputField("이메일");
|
||||
/// Add(_nameField);
|
||||
/// Add(_emailField);
|
||||
/// }
|
||||
///
|
||||
/// public UserData? GetResult()
|
||||
/// {
|
||||
/// return new UserData(_nameField.value, _emailField.value);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public interface IUTKModalContent<T> where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// 모달 닫힐 때 호출되어 결과 값을 반환합니다.
|
||||
/// </summary>
|
||||
T? GetResult();
|
||||
}
|
||||
|
||||
[UxmlElement]
|
||||
public partial class UTKModal : VisualElement, IDisposable
|
||||
{
|
||||
@@ -64,6 +132,9 @@ namespace UVC.UIToolkit
|
||||
private bool _showCloseButton = true;
|
||||
private bool _closeOnBackdropClick = false;
|
||||
private ModalSize _size = ModalSize.Medium;
|
||||
|
||||
private UniTaskCompletionSource? _closeTcs;
|
||||
private Action? _onCloseResultHandler;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
@@ -104,7 +175,12 @@ namespace UVC.UIToolkit
|
||||
public bool CloseOnBackdropClick
|
||||
{
|
||||
get => _closeOnBackdropClick;
|
||||
set => _closeOnBackdropClick = value;
|
||||
set
|
||||
{
|
||||
if (_closeOnBackdropClick == value) return;
|
||||
_closeOnBackdropClick = value;
|
||||
UpdateBackdropClickHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>모달 크기</summary>
|
||||
@@ -186,31 +262,12 @@ namespace UVC.UIToolkit
|
||||
|
||||
#region Static Factory
|
||||
/// <summary>
|
||||
/// 모달 표시
|
||||
/// 모달 생성 (화면에 표시하려면 Show() 호출 필요)
|
||||
/// </summary>
|
||||
public static UTKModal Show(string title, ModalSize size = ModalSize.Medium)
|
||||
public static UTKModal Create(string title, ModalSize size = ModalSize.Medium)
|
||||
{
|
||||
ValidateRoot();
|
||||
|
||||
var modal = new UTKModal(title, size);
|
||||
|
||||
modal._blocker = UTKModalBlocker.Show(_root!, 0.5f, false);
|
||||
if (modal._closeOnBackdropClick)
|
||||
{
|
||||
modal._blocker.OnBlockerClicked += modal.Close;
|
||||
}
|
||||
|
||||
// panel.visualTree에 직접 추가
|
||||
var root = _root!.panel?.visualTree ?? _root!;
|
||||
root.Add(modal);
|
||||
|
||||
// 중앙 정렬
|
||||
modal.style.position = Position.Absolute;
|
||||
modal.style.left = Length.Percent(50);
|
||||
modal.style.top = Length.Percent(50);
|
||||
modal.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
|
||||
|
||||
return modal;
|
||||
return new UTKModal(title, size);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -305,6 +362,22 @@ namespace UVC.UIToolkit
|
||||
AddToClassList(sizeClass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// backdrop 클릭 핸들러 등록/해제
|
||||
/// </summary>
|
||||
private void UpdateBackdropClickHandler()
|
||||
{
|
||||
if (_blocker == null) return;
|
||||
|
||||
// 항상 먼저 해제하여 중복 등록 방지
|
||||
_blocker.OnBlockerClicked -= Close;
|
||||
|
||||
if (_closeOnBackdropClick)
|
||||
{
|
||||
_blocker.OnBlockerClicked += Close;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 콘텐츠 추가
|
||||
/// </summary>
|
||||
@@ -319,6 +392,8 @@ namespace UVC.UIToolkit
|
||||
public void AddToFooter(VisualElement element)
|
||||
{
|
||||
_footer?.Add(element);
|
||||
_footer?.AddToClassList("utk-modal__footer--has-children");
|
||||
UTKChildAnnotator.AnnotateChild(_footer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -332,11 +407,81 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달을 화면에 표시
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
ValidateRoot();
|
||||
|
||||
_blocker = UTKModalBlocker.Show(_root!, 0.5f, false);
|
||||
UpdateBackdropClickHandler();
|
||||
|
||||
// panel.visualTree에 직접 추가
|
||||
var root = _root!.panel?.visualTree ?? _root!;
|
||||
root.Add(this);
|
||||
|
||||
// 중앙 정렬
|
||||
style.position = Position.Absolute;
|
||||
style.left = Length.Percent(50);
|
||||
style.top = Length.Percent(50);
|
||||
style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달을 화면에 표시하고 닫힐 때까지 대기
|
||||
/// </summary>
|
||||
public UniTask ShowAsync()
|
||||
{
|
||||
_closeTcs = new UniTaskCompletionSource();
|
||||
Show();
|
||||
return _closeTcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달을 화면에 표시하고 닫힐 때 IUTKModalContent<T> 콘텐츠의 결과를 반환.
|
||||
/// Add()된 자식 중 IUTKModalContent<T>를 구현한 첫 번째 요소에서 GetResult()를 호출합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">결과 타입</typeparam>
|
||||
/// <returns>콘텐츠의 결과 값. 콘텐츠가 없으면 default(T)</returns>
|
||||
public UniTask<T?> ShowAsync<T>() where T : class
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<T?>();
|
||||
|
||||
_onCloseResultHandler = () =>
|
||||
{
|
||||
var result = FindModalContent<T>()?.GetResult();
|
||||
tcs.TrySetResult(result);
|
||||
};
|
||||
|
||||
Show();
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 콘텐츠에서 IUTKModalContent<T>를 구현한 첫 번째 요소를 찾습니다.
|
||||
/// </summary>
|
||||
private IUTKModalContent<T>? FindModalContent<T>() where T : class
|
||||
{
|
||||
if (_content == null) return null;
|
||||
|
||||
for (int i = 0; i < _content.childCount; i++)
|
||||
{
|
||||
if (_content[i] is IUTKModalContent<T> modalContent)
|
||||
return modalContent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모달 닫기
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
// 결과 핸들러 먼저 호출 (ShowAsync<T> 사용 시)
|
||||
_onCloseResultHandler?.Invoke();
|
||||
_onCloseResultHandler = null;
|
||||
|
||||
OnClosed?.Invoke();
|
||||
RemoveFromHierarchy();
|
||||
if (_blocker != null)
|
||||
@@ -345,6 +490,8 @@ namespace UVC.UIToolkit
|
||||
_blocker.Hide();
|
||||
}
|
||||
_blocker = null;
|
||||
_closeTcs?.TrySetResult();
|
||||
_closeTcs = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.Util;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
@@ -411,6 +412,7 @@ namespace UVC.UIToolkit
|
||||
btn.Size = UTKButton.ButtonSize.Small;
|
||||
btn.OnClicked += () => OnActionClicked?.Invoke(actionId);
|
||||
_actions.Add(btn);
|
||||
UTKChildAnnotator.AnnotateChild(_actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
18
Assets/Scripts/UVC/UIToolkit/Tab/IUTKTabContent.cs
Normal file
18
Assets/Scripts/UVC/UIToolkit/Tab/IUTKTabContent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠 인터페이스.
|
||||
/// UTKTabView에서 탭 전환 시 자동으로 Show/Hide를 호출합니다.
|
||||
/// </summary>
|
||||
public interface IUTKTabContent
|
||||
{
|
||||
/// <summary>탭이 선택되어 표시될 때 호출</summary>
|
||||
void Show(object? data);
|
||||
|
||||
/// <summary>탭이 선택 해제되어 숨겨질 때 호출</summary>
|
||||
UniTask Hide();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/Tab/IUTKTabContent.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/Tab/IUTKTabContent.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f5401783ae68ec49b0dca98d448e209
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
@@ -126,6 +127,10 @@ namespace UVC.UIToolkit
|
||||
private bool _disposed;
|
||||
private readonly List<UTKTab> _utkTabs = new();
|
||||
private TabAlign _align = TabAlign.Top;
|
||||
private float _tabWidth = 0;
|
||||
private float _tabHeight = 0;
|
||||
private VisualElement? _contentViewport;
|
||||
private int _previousTabIndex = -1;
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
@@ -156,6 +161,30 @@ namespace UVC.UIToolkit
|
||||
ApplyAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭 콘텐츠 영역 너비 (0 이하이면 미설정)</summary>
|
||||
[UxmlAttribute("tab-width")]
|
||||
public float TabWidth
|
||||
{
|
||||
get => _tabWidth;
|
||||
set
|
||||
{
|
||||
_tabWidth = value;
|
||||
ApplyContentViewportSize();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭 콘텐츠 영역 높이 (0 이하이면 미설정)</summary>
|
||||
[UxmlAttribute("tab-height")]
|
||||
public float TabHeight
|
||||
{
|
||||
get => _tabHeight;
|
||||
set
|
||||
{
|
||||
_tabHeight = value;
|
||||
ApplyContentViewportSize();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -185,6 +214,7 @@ namespace UVC.UIToolkit
|
||||
private void SetupEvents()
|
||||
{
|
||||
this.RegisterCallback<ChangeEvent<int>>(OnTabIndexChanged);
|
||||
activeTabChanged += OnActiveTabChanged;
|
||||
}
|
||||
|
||||
private void SubscribeToThemeChanges()
|
||||
@@ -213,12 +243,71 @@ namespace UVC.UIToolkit
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
/// <summary>
|
||||
/// 코드에서 selectedTabIndex를 변경했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
private void OnTabIndexChanged(ChangeEvent<int> evt)
|
||||
{
|
||||
_previousTabIndex = evt.previousValue;
|
||||
NotifyTabContent(evt.previousValue, evt.newValue).Forget();
|
||||
UpdateTabSelection();
|
||||
OnTabChanged?.Invoke(evt.newValue, activeTab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 클릭 등으로 탭이 변경되었을 때 호출됩니다.
|
||||
/// </summary>
|
||||
private void OnActiveTabChanged(Tab previousTab, Tab newTab)
|
||||
{
|
||||
int prevIndex = previousTab != null ? _utkTabs.FindIndex(t => t == previousTab) : -1;
|
||||
int newIndex = newTab != null ? _utkTabs.FindIndex(t => t == newTab) : -1;
|
||||
|
||||
// ChangeEvent<int>와 중복 호출 방지
|
||||
if (prevIndex == _previousTabIndex && newIndex == selectedTabIndex)
|
||||
return;
|
||||
|
||||
_previousTabIndex = prevIndex;
|
||||
NotifyTabContent(prevIndex, newIndex).Forget();
|
||||
UpdateTabSelection();
|
||||
OnTabChanged?.Invoke(newIndex, newTab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이전 탭 콘텐츠의 Hide, 새 탭 콘텐츠의 Show를 호출합니다.
|
||||
/// </summary>
|
||||
private async UniTaskVoid NotifyTabContent(int previousIndex, int newIndex)
|
||||
{
|
||||
// 이전 탭 Hide
|
||||
if (previousIndex >= 0 && previousIndex < _utkTabs.Count)
|
||||
{
|
||||
if (FindTabContent(_utkTabs[previousIndex]) is IUTKTabContent prevContent)
|
||||
{
|
||||
await prevContent.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 새 탭 Show
|
||||
if (newIndex >= 0 && newIndex < _utkTabs.Count)
|
||||
{
|
||||
if (FindTabContent(_utkTabs[newIndex]) is IUTKTabContent newContent)
|
||||
{
|
||||
newContent.Show(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 내부에서 IUTKTabContent를 구현한 자식 요소를 찾습니다.
|
||||
/// </summary>
|
||||
public static IUTKTabContent? FindTabContent(Tab tab)
|
||||
{
|
||||
for (int i = 0; i < tab.childCount; i++)
|
||||
{
|
||||
if (tab[i] is IUTKTabContent content) return content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UpdateTabSelection()
|
||||
{
|
||||
for (int i = 0; i < _utkTabs.Count; i++)
|
||||
@@ -258,6 +347,23 @@ namespace UVC.UIToolkit
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠 영역 크기 적용
|
||||
/// </summary>
|
||||
private void ApplyContentViewportSize()
|
||||
{
|
||||
_contentViewport ??= this.Q(className: "unity-tab-view__content-viewport");
|
||||
if (_contentViewport == null) return;
|
||||
|
||||
_contentViewport.style.width = _tabWidth > 0
|
||||
? new StyleLength(_tabWidth)
|
||||
: new StyleLength(StyleKeyword.Auto);
|
||||
|
||||
_contentViewport.style.height = _tabHeight > 0
|
||||
? new StyleLength(_tabHeight)
|
||||
: new StyleLength(StyleKeyword.Auto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UTK 탭 추가
|
||||
/// </summary>
|
||||
@@ -322,6 +428,8 @@ namespace UVC.UIToolkit
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||||
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||||
UnregisterCallback<ChangeEvent<int>>(OnTabIndexChanged);
|
||||
activeTabChanged -= OnActiveTabChanged;
|
||||
|
||||
foreach (var tab in _utkTabs)
|
||||
{
|
||||
|
||||
8
Assets/Scripts/UVC/UIToolkit/Util.meta
Normal file
8
Assets/Scripts/UVC/UIToolkit/Util.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad370f2935493cc439100336b1d0c334
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
41
Assets/Scripts/UVC/UIToolkit/Util/UTKChildAnnotator.cs
Normal file
41
Assets/Scripts/UVC/UIToolkit/Util/UTKChildAnnotator.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// UTKChildAnnotator는 VisualElement의 자식 요소들을 순회하며 첫 번째 자식에게 "first-child" 클래스를, 마지막 자식에게 "last-child" 클래스를 추가하는 유틸리티 클래스입니다.
|
||||
/// </summary>
|
||||
public static class UTKChildAnnotator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 주어진 VisualElement의 자식 요소들을 순회하며 첫 번째 자식에게는 "first-child" 클래스를, 마지막 자식에게는 "last-child" 클래스를 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="parent">클래스를 추가할 VisualElement 부모 요소</param>
|
||||
public static void AnnotateChild(VisualElement parent)
|
||||
{
|
||||
var children = parent.Children().ToList();
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
var child = children[i];
|
||||
if (i == 0)
|
||||
{
|
||||
child.RemoveFromClassList("first-child");
|
||||
child.AddToClassList("first-child");
|
||||
}
|
||||
else if (i == children.Count - 1)
|
||||
{
|
||||
child.RemoveFromClassList("last-child");
|
||||
child.AddToClassList("last-child");
|
||||
}
|
||||
else
|
||||
{
|
||||
child.RemoveFromClassList("first-child");
|
||||
child.RemoveFromClassList("last-child");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26dda967fd6d0d84bb1f40f8a6ad9c5f
|
||||
Reference in New Issue
Block a user