UTKShortcutList 개발 완료. Modal 개선

This commit is contained in:
logonkhi
2026-02-24 20:01:56 +09:00
parent b9b394935e
commit 8ca8bd0df9
72 changed files with 3466 additions and 68 deletions

View File

@@ -35,7 +35,8 @@
"Bash(/bin/mkdir -p:*)",
"Bash(/bin/rm:*)",
"WebFetch(domain:docs.unity3d.com)",
"Bash(ls:*)"
"Bash(ls:*)",
"WebFetch(domain:discussions.unity.com)"
],
"deny": [],
"ask": []

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="root" style="padding-left: 20px;">
<utk:UTKInputField label="Database IP" value="127.0.0.3" label-min-width="200" style="width: 400px;" />
<utk:UTKInputField label="Database PORT" value="3304" label-min-width="200" style="width: 400px;" />
<utk:UTKInputField label="Database ID" value="" label-min-width="200" style="width: 400px;" />
<utk:UTKInputField label="Database PASSWORD" value="" is-password-field="true" label-min-width="200" style="width: 400px;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d27b20f78aec0354b8b97b9af0765701
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,24 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="root" style="padding-left: 20px;">
<ui:VisualElement style="flex-direction: row;">
<utk:UTKIntegerField label="Auto Save Time" value="5" label-min-width="200" style="width: 400px;margin-right: 5px;" />
<utk:UTKLabel text="Minutes" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row;">
<utk:UTKFloatField label="Grid Spacing" value="1" label-min-width="200" style="width: 400px;margin-right: 5px;" />
<utk:UTKLabel text="Meters" />
</ui:VisualElement>
<ui:VisualElement name="VisualElement" style="flex-direction: row;">
<utk:UTKFloatField label="Position Snap" value="0.5" label-min-width="200" style="width: 400px;margin-right: 5px;" />
<utk:UTKLabel text="Meters" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row;">
<utk:UTKFloatField label="Rotation Snap" value="10" label-min-width="200" style="width: 400px;margin-right: 5px;" />
<utk:UTKLabel text="Meters" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row;">
<utk:UTKFloatField label="Scale Snap" value="0.5" label-min-width="200" style="width: 400px;margin-right: 5px;" />
<utk:UTKLabel text="Meters" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0a709a335acffbd4c9d4358c153fad81
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,5 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<ui:VisualElement name="root" style="padding-left: 10px;">
<utk:UTKShortcutList name="window" style="background-color: transparent;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: dcdd51dd452d3e94ca374da0307608a4
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -35,7 +35,7 @@
.utk-input > .unity-base-text-field__input {
flex-grow: 1;
height: var(--size-input-height);
min-width: 120px;
min-width: 60px;
padding-left: var(--space-m);
padding-right: var(--space-m);
background-color: var(--color-bg-input);

View File

@@ -0,0 +1,56 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ui="UnityEngine.UIElements"
xmlns:utk="UVC.UIToolkit"
noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd"
editor-extension-mode="False">
<!--
UTKShortcutList.uxml
단축키 설정 리스트 컴포넌트 메인 레이아웃.
구조:
- container (.utk-shortcut-list)
- search-field : UTKInputField (Command 이름 검색)
- header : 컬럼 헤더 행
- header-command : 명령 이름 컬럼 헤더 (flex-grow)
- header-ctrl : Ctrl 컬럼 헤더
- header-shift : Shift 컬럼 헤더
- header-alt : Alt 컬럼 헤더
- header-key : Key 컬럼 헤더
- list-view : UTKListView (가상화 지원)
Item 레이아웃: UTKShortcutListItem.uxml 참조
스타일: Resources/UIToolkit/List/UTKShortcutListUss.uss (C#에서 로드)
-->
<ui:VisualElement name="container" class="utk-shortcut-list-container">
<!-- 검색 영역 -->
<ui:VisualElement name="search-container" class="utk-shortcut-list__search-container">
<utk:UTKInputField name="search-field"
placeholder="Search command..."
class="utk-shortcut-list__search" />
<utk:UTKButton name="clear-btn"
class="utk-shortcut-list__clear-btn"
variant="Text"
icon="Close"
icon-size="12"
icon-only="true" />
</ui:VisualElement>
<!-- 컬럼 헤더 행 -->
<ui:VisualElement name="header" class="utk-shortcut-list__header">
<ui:Label name="header-command" text="" class="utk-shortcut-list__header-command" />
<ui:Label name="header-ctrl" text="Ctrl" class="utk-shortcut-list__header-modifier" />
<ui:Label name="header-shift" text="Shift" class="utk-shortcut-list__header-modifier" />
<ui:Label name="header-alt" text="Alt" class="utk-shortcut-list__header-modifier" />
<ui:Label name="header-key" text="Key" class="utk-shortcut-list__header-key" />
</ui:VisualElement>
<!-- ListView (가상화 지원) -->
<utk:UTKListView name="list-view"
class="utk-shortcut-list__listview"
fixed-item-height="36"
selection-type="None"
show-alternating-row-backgrounds="None" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 009b695fb58d74b43a48be6fb5a00895
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,40 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ui="UnityEngine.UIElements"
xmlns:utk="UVC.UIToolkit"
noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd"
editor-extension-mode="False">
<!--
UTKShortcutListItem.uxml
단축키 리스트 단일 행(아이템) 레이아웃.
UTKShortcutList.cs 의 makeItem/bindItem 에서 사용됩니다.
구조 (flex-direction: row):
- item-container (.utk-shortcut-list-item)
- command-label : UTKLabel - 명령 이름 (flex-grow: 1)
- ctrl-checkbox : UTKCheckBox - Ctrl 수정자 토글
- shift-checkbox : UTKCheckBox - Shift 수정자 토글
- alt-checkbox : UTKCheckBox - Alt 수정자 토글
- key-field : UTKInputField - 주요 키 (읽기전용 + 클릭 캡처)
스타일: UTKShortcutListUss.uss (상위 컴포넌트에서 cascade)
USS는 UXML에 지정하지 않고 C#에서 로드합니다 (테마 적용 순서 보장).
-->
<ui:VisualElement name="item-container" class="utk-shortcut-list-item">
<!-- 명령 이름 (좌측, 남은 공간 전부 사용) -->
<utk:UTKLabel name="command-label" class="utk-shortcut-list-item__command" />
<!-- Ctrl 수정자 체크박스 (텍스트 없음, 아이콘만) -->
<utk:UTKCheckBox name="ctrl-checkbox" class="utk-shortcut-list-item__modifier" />
<!-- Shift 수정자 체크박스 -->
<utk:UTKCheckBox name="shift-checkbox" class="utk-shortcut-list-item__modifier" />
<!-- Alt 수정자 체크박스 -->
<utk:UTKCheckBox name="alt-checkbox" class="utk-shortcut-list-item__modifier" />
<!-- 주요 키 입력 필드 (읽기전용 표시 + 클릭 시 키 캡처 모드 진입) -->
<utk:UTKInputField name="key-field" class="utk-shortcut-list-item__key" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0d18cdf18ee19874aaf8decc2b8805ff
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,262 @@
/*
* ===================================
* UTKShortcutListUss.uss
* 단축키 설정 리스트 컴포넌트 전용 스타일
* ===================================
*
* 이 파일은 UTKThemeManager 의 CSS 변수를 참조합니다.
* - 색상 : var(--color-*)
* - 간격 : var(--space-*)
* - 반지름: var(--radius-*)
* - 폰트 : var(--font-size-*)
*
* 컬럼 너비 (컨테이너 · 헤더 · 아이템 공통으로 맞춤):
* command flex-grow:1 명령 이름 (나머지 공간 전부)
* modifier 52px Ctrl / Shift / Alt 체크박스 셀
* key 76px 주요 키 입력 셀
*
* 관련 파일:
* UTKShortcutList.uxml 메인 레이아웃
* UTKShortcutListItem.uxml 아이템 행 레이아웃
* UTKShortcutList.cs 컴포넌트 로직
*/
/* =============================================
루트 컨테이너
============================================= */
.utk-shortcut-list {
flex-grow: 1;
flex-direction: column;
background-color: var(--color-bg-panel);
}
.utk-shortcut-list-container {
flex-grow: 1;
flex-direction: column;
}
/* =============================================
검색 영역 (검색 필드 + Clear 버튼)
============================================= */
.utk-shortcut-list__search-container {
position: relative;
margin: var(--space-m) var(--space-m) 0 var(--space-m);
justify-content: center;
}
.utk-shortcut-list__search {
flex-grow: 1;
}
/* 검색어 있을 때 텍스트가 버튼 뒤로 가리지 않도록 오른쪽 패딩 확보 */
.utk-shortcut-list__search .unity-text-field__input {
padding-right: 28px;
}
.utk-shortcut-list__clear-btn {
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;
border-width: 0;
margin: 0;
padding: 0;
align-self: center;
position: absolute;
right: 4px;
}
.utk-shortcut-list__search .unity-text-field__input {
background-color: var(--color-bg-input);
border-width: var(--border-width);
border-color: var(--color-border);
border-radius: var(--radius-s);
padding: var(--space-xs) var(--space-m);
font-size: var(--font-size-body2);
min-height: 28px;
}
.utk-shortcut-list__search:focus .unity-text-field__input {
border-color: var(--color-border-focus);
}
/* UTKInputField 내부 label 숨김 (placeholder 모드에서 label 없이 사용) */
.utk-shortcut-list__search .unity-label {
display: none;
min-width: 0;
width: 0;
padding: 0;
}
/* =============================================
컬럼 헤더 행
============================================= */
.utk-shortcut-list__header {
flex-direction: row;
align-items: center;
padding: var(--space-s) var(--space-m);
margin-top: var(--space-s);
border-bottom-width: 1px;
border-color: var(--color-border);
}
.utk-shortcut-list__header-command {
flex-grow: 1;
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
-unity-font-style: bold;
overflow: hidden;
min-width: 0;
}
.utk-shortcut-list__header-modifier {
width: 52px;
flex-shrink: 0;
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
-unity-text-align: middle-center;
-unity-font-style: bold;
}
.utk-shortcut-list__header-key {
width: 90px;
flex-shrink: 0;
font-size: var(--font-size-body2);
color: var(--color-text-secondary);
-unity-text-align: middle-center;
-unity-font-style: bold;
}
/* =============================================
UTKListView 컨테이너
============================================= */
.utk-shortcut-list__listview {
flex-grow: 1;
background-color: transparent;
}
/* ListView 자체의 테두리·배경 제거 (디자인 가이드: 패널 배경 사용) */
.utk-shortcut-list__listview.utk-listview {
border-width: 0;
border-radius: 0;
background-color: transparent;
}
/* =============================================
아이템 행 컨테이너
============================================= */
.utk-shortcut-list-item {
flex-direction: row;
align-items: center;
padding: 0 var(--space-m);
min-height: 36px;
flex-grow: 1;
border-bottom-width: 1px;
border-color: var(--color-border);
transition-duration: var(--anim-fast);
transition-property: background-color;
}
.utk-shortcut-list-item:hover {
background-color: var(--color-btn-hover);
}
/* =============================================
아이템 명령 이름 레이블
============================================= */
.utk-shortcut-list-item__command {
flex-grow: 1;
min-width: 0;
overflow: hidden;
}
/* UTKLabel 내부 Label 텍스트 스타일 */
.utk-shortcut-list-item__command .unity-label {
font-size: var(--font-size-body2);
color: var(--color-text-primary);
-unity-text-align: middle-left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* =============================================
아이템 수정자 키 체크박스 (Ctrl / Shift / Alt)
============================================= */
.utk-shortcut-list-item__modifier {
width: 52px;
flex-shrink: 0;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
}
/* UTKCheckBox 내부 컨테이너를 중앙 정렬 */
.utk-shortcut-list-item__modifier .utk-checkbox {
justify-content: center;
margin: 0;
}
/* 체크박스 레이블(텍스트) 숨김 헤더가 컬럼 레이블 역할을 담당 */
.utk-shortcut-list-item__modifier .utk-checkbox__label {
display: none;
}
/* =============================================
아이템 주요 키 입력 필드
============================================= */
.utk-shortcut-list-item__key {
width: 90px;
flex-shrink: 0;
margin: 0;
padding: 0;
}
/* UTKInputField label 숨김 */
.utk-shortcut-list-item__key .unity-label {
display: none;
min-width: 0;
width: 0;
padding: 0;
}
/* 텍스트 입력 영역: 키 뱃지 스타일 */
.utk-shortcut-list-item__key .unity-text-field__input {
background-color: var(--color-bg-secondary);
border-width: var(--border-width);
border-color: var(--color-border);
border-radius: var(--radius-s);
-unity-text-align: middle-center;
font-size: var(--font-size-body2);
color: var(--color-text-primary);
padding: 0 var(--space-s);
min-height: 24px;
cursor: link;
transition-duration: var(--anim-fast);
transition-property: border-color, background-color;
}
.utk-shortcut-list-item__key .unity-text-field__input:hover {
border-color: var(--color-border-focus);
}
/* 캡처 모드: 키 입력 대기 중 */
.utk-shortcut-list-item__key--capturing .unity-text-field__input {
border-color: var(--color-border-focus);
background-color: var(--color-bg-input);
color: var(--color-text-secondary);
}
/* 텍스트 커서(caret) 숨김 읽기전용이므로 커서 불필요 */
.utk-shortcut-list-item__key .unity-text-field__input > TextElement {
cursor: link;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9fafe6cd359d2c4899d36f1fb6ad99a
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -69,10 +69,12 @@
justify-content: flex-end;
}
.utk-alert__buttons .last-child {
margin-left: var(--space-s);
}
.utk-alert__btn {
min-width: 80px;
margin-left: var(--space-s);
margin-right: var(--space-s);
}
/* ===================================

View File

@@ -33,7 +33,8 @@
}
.utk-modal--large {
width: 640px;
width: 800px;
height: 600px;
max-height: 800px;
}
@@ -112,6 +113,14 @@
justify-content: flex-end;
align-items: center;
padding: var(--space-m) var(--space-l);
border-top-width: var(--border-width);
border-top-width: 0;
border-top-color: var(--color-border);
}
.utk-modal__footer--has-children {
border-top-width: var(--border-width);
}
.utk-modal__footer .last-child {
margin-left: var(--space-s);
}

View File

@@ -131,3 +131,7 @@
justify-content: flex-end;
margin-top: var(--space-s);
}
.utk-notification__actions .last-child {
margin-left: var(--space-s);
}

View File

@@ -29,3 +29,8 @@
height: 150px;
width: 200px;
}
.utk-sample-shortcut-list {
height: 320px;
width: 620px;
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
<Style src="../UTKSampleCommon.uss" />
<Style src="UTKListSample.uss" />
<VisualElement class="utk-sample-container">
<Label class="utk-sample-desc" text="단축키(Command + Modifier Keys + Key) 매핑을 설정하는 리스트 컴포넌트" />
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="ShortcutList" />
<utk:UTKShortcutList name="shortcut-list-sample" class="utk-sample-shortcut-list" />
</VisualElement>
<!-- Code Sample -->
<VisualElement class="utk-code-sample-container">
<utk:UTKCodeBlock name="code-csharp" title="C#" />
<utk:UTKCodeBlock name="code-uxml" title="UXML" />
</VisualElement>
</VisualElement>
</UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: db17838bfbe1351449a155173abfc075
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -61,6 +61,7 @@
margin-right: var(--space-s);
transition-duration: var(--anim-fast);
transition-property: background-color;
align-self: stretch;
}
.utk-tabview .unity-tab__header:hover {
@@ -150,11 +151,13 @@
border-bottom-width: 0;
border-top-width: 0;
border-left-width: 0;
flex-grow: 1;
}
.utk-tabview.utk-tabview--align-left .unity-tab__header {
margin-right: 0;
margin-bottom: var(--space-s);
position: relative;
}
.utk-tabview.utk-tabview--align-left .unity-tab__header-underline {
@@ -183,6 +186,7 @@
.utk-tabview.utk-tabview--align-right .unity-tab__header {
margin-right: 0;
margin-bottom: var(--space-s);
position: relative;
}
.utk-tabview.utk-tabview--align-right .unity-tab__header-underline {

View File

@@ -166,7 +166,7 @@ MonoBehaviour:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 54e4f33c8b08cb54f97dbdb5edd79e1e, type: 3}
sourceAsset: {fileID: 9197481963319205126, guid: 6c8eae7ee21b96245b325f08111b214b, type: 3}
m_SortingOrder: 1
m_Position: 0
m_WorldSpaceSizeMode: 1
@@ -184,9 +184,9 @@ MonoBehaviour:
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 39265a781c40bdb4a90aa56b0fbf44a6, type: 3}
m_Script: {fileID: 11500000, guid: e88ad13f58976fb4a837242a7e1c8282, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.Sample.UIToolkit.UTKToolBarSample
m_EditorClassIdentifier: Assembly-CSharp::UVC.Sample.UIToolkit.UTKSettingModalSample
_uiDocument: {fileID: 1097328754}
initialTheme: 0
--- !u!1 &1331954412

View File

@@ -5,6 +5,7 @@ using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
using UVC.Log;
using UVC.Studio.UIToolkit.Modal;
namespace UVC.Sample.UIToolkit
{
@@ -21,6 +22,10 @@ namespace UVC.Sample.UIToolkit
private UTKToggle? _themeToggle;
private VisualElement? _root;
private UTKButton? _openButton0;
private UTKButton? _openButton1;
private UTKButton? _openButton2;
private void Start()
{
@@ -33,7 +38,7 @@ namespace UVC.Sample.UIToolkit
}
_uiDocument = doc;
_root = _uiDocument.rootVisualElement;
UTKModal.SetRoot(_root);
UTKThemeManager.Instance.RegisterRoot(_root);
UTKThemeManager.Instance.SetTheme(initialTheme);
@@ -47,6 +52,39 @@ namespace UVC.Sample.UIToolkit
};
}
_openButton0 = _root.Q<UTKButton>("openButton0");
if (_openButton0 != null) {
_openButton0.OnClicked += async () =>
{
var modal = UTKModal.Create("Settings", UTKModal.ModalSize.Large);
var content = new UTKSettingModalContent(0); // 초기 탭 인덱스 설정
modal.Add(content);
await modal.ShowAsync();
};
}
_openButton1 = _root.Q<UTKButton>("openButton1");
if (_openButton1 != null) {
_openButton1.OnClicked += async () =>
{
var modal = UTKModal.Create("Settings", UTKModal.ModalSize.Large);
var content = new UTKSettingModalContent(1); // 초기 탭 인덱스 설정
modal.Add(content);
await modal.ShowAsync();
};
}
_openButton2 = _root.Q<UTKButton>("openButton2");
if (_openButton2 != null) {
_openButton2.OnClicked += async () =>
{
var modal = UTKModal.Create("Settings", UTKModal.ModalSize.Large);
var content = new UTKSettingModalContent(2); // 초기 탭 인덱스 설정
modal.Add(content);
await modal.ShowAsync();
};
}
}
private void OnDestroy()

View File

@@ -1,6 +1,8 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<VisualElement style="width: 100%; height: 100%; flex-direction: column;">
<VisualElement name="toolbar-area" style="flex-grow: 1; flex-direction: column; padding: 8px;" />
<utk:UTKToggle name="toggle" label="테마 변경" style="position: absolute; top: 8px; right: 10px; z-index: 10;" />
<utk:UTKButton name="openButton0" text="Open0" style="position: absolute; top: 8px; left: 10px; z-index: 10;" />
<utk:UTKButton name="openButton1" text="Open1" style="position: absolute; top: 38px; left: 10px; z-index: 10;" />
<utk:UTKButton name="openButton2" text="Open2" style="position: absolute; top: 68px; left: 10px; z-index: 10;" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,497 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &1097328750
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1097328752}
- component: {fileID: 1097328754}
- component: {fileID: 1097328755}
m_Layer: 0
m_Name: Sample
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1097328752
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1097328754
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 274352955998a6e478eb57d04c49969b, type: 3}
m_SortingOrder: 0
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!114 &1097328755
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1097328750}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 85433d2caa763104f86b4b44ded2fc2d, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UTKShortcutListSample
uiDocument: {fileID: 1097328754}
initialTheme: 0
--- !u!1 &1331954412
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1331954415}
- component: {fileID: 1331954414}
- component: {fileID: 1331954413}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1331954413
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_MoveRepeatDelay: 0.5
m_MoveRepeatRate: 0.1
m_XRTrackingOrigin: {fileID: 0}
m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
m_DeselectOnBackgroundClick: 0
m_PointerBehavior: 0
m_CursorLockBehavior: 0
m_ScrollDeltaPerTick: 6
--- !u!114 &1331954414
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1331954415
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331954412}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1414861612
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1414861614}
- component: {fileID: 1414861613}
- component: {fileID: 1414861615}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &1414861613
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
serializedVersion: 11
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &1414861614
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!114 &1414861615
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1414861612}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &2136621999
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2136622002}
- component: {fileID: 2136622001}
- component: {fileID: 2136622000}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &2136622000
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
--- !u!20 &2136622001
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &2136622002
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2136621999}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 2136622002}
- {fileID: 1414861614}
- {fileID: 1331954415}
- {fileID: 1097328752}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f46f8f4e748fbd34ab6be590a5584d89
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,213 @@
#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
/// <summary>
/// UTKShortcutList의 기능을 테스트하기 위한 샘플 MonoBehaviour입니다.
/// <list type="bullet">
/// <item>Studio 단축키 목록 SetData</item>
/// <item>OnDataChanged 이벤트 핸들링</item>
/// <item>GetData / 리셋 / 항목 추가 버튼</item>
/// <item>라이트/다크 테마 토글</item>
/// </list>
/// </summary>
public class UTKShortcutListSample : MonoBehaviour
{
[SerializeField]
public UIDocument uiDocument;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle? _themeToggle;
private UTKShortcutList? _shortcutList;
// ── 샘플 데이터 원본 (리셋용) ──────────────────────────────────────────
private static readonly List<UTKShortcutItemData> DefaultShortcuts = new()
{
new() { Id = "file.new_project", CommandName = "File > New Project", UseCtrl = true, UseShift = false, UseAlt = false, Key = "N" },
new() { Id = "file.open_project", CommandName = "File > Open Project", UseCtrl = true, UseShift = true, UseAlt = false, Key = "O" },
new() { Id = "file.save_project", CommandName = "File > Save Project", UseCtrl = true, UseShift = false, UseAlt = true, Key = "S" },
new() { Id = "file.save_as", CommandName = "File > Save As...", UseCtrl = true, UseShift = true, UseAlt = true, Key = "S" },
new() { Id = "file.insert_database", CommandName = "File > Insert Database", UseCtrl = true, UseShift = true, UseAlt = false, Key = "I" },
new() { Id = "file.export_layout", CommandName = "File > Export > Layout", UseCtrl = true, UseShift = true, UseAlt = false, Key = "L" },
new() { Id = "file.export_metadata", CommandName = "File > Export > Metadata",UseCtrl = true, UseShift = true, UseAlt = false, Key = "M" },
new() { Id = "file.export_gltf", CommandName = "File > Export > glTF", UseCtrl = true, UseShift = true, UseAlt = false, Key = "G" },
new() { Id = "edit.undo", CommandName = "Edit > Undo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Z" },
new() { Id = "edit.redo", CommandName = "Edit > Redo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Y" },
new() { Id = "edit.duplicate", CommandName = "Edit > Duplicate", UseCtrl = true, UseShift = true, UseAlt = false, Key = "D" },
new() { Id = "edit.delete", CommandName = "Edit > Delete", UseCtrl = false, UseShift = true, UseAlt = false, Key = "Delete" },
new() { Id = "create.plane", CommandName = "Create > Plane", UseCtrl = true, UseShift = true, UseAlt = false, Key = "V" },
new() { Id = "tool.select", CommandName = "Select Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "1" },
new() { Id = "tool.move", CommandName = "Move Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "2" },
new() { Id = "tool.rotate", CommandName = "Rotate Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "3" },
new() { Id = "tool.scale", CommandName = "Scale Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "4" },
new() { Id = "view.focus", CommandName = "View > Focus Selected", UseCtrl = false, UseShift = false, UseAlt = false, Key = "F" },
new() { Id = "view.toggle_grid", CommandName = "View > Toggle Grid", UseCtrl = false, UseShift = false, UseAlt = false, Key = "G" },
new() { Id = "view.toggle_wireframe", CommandName = "View > Toggle Wireframe", UseCtrl = false, UseShift = false, UseAlt = false, Key = "W" },
};
// ─────────────────────────────────────────────────────────────────────────
private void Start()
{
// UIDocument 참조
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("[UTKShortcutListSample] UIDocument 컴포넌트가 없습니다.");
return;
}
uiDocument = doc;
var root = uiDocument.rootVisualElement;
// 테마 토글
_themeToggle = root.Q<UTKToggle>("toggle");
if (_themeToggle == null)
Debug.LogWarning("[UTKShortcutListSample] UXML에서 UTKToggle(name='toggle')을 찾을 수 없습니다.");
// ShortcutList
_shortcutList = root.Q<UTKShortcutList>("shortcut-list");
if (_shortcutList == null)
{
// 이름을 못 찾으면 타입으로 검색
_shortcutList = root.Q<UTKShortcutList>();
}
if (_shortcutList == null)
{
Debug.LogError("[UTKShortcutListSample] UXML에서 UTKShortcutList를 찾을 수 없습니다.");
return;
}
// 테마 초기화
UTKThemeManager.Instance.RegisterRoot(root);
UTKThemeManager.Instance.SetTheme(initialTheme);
_themeToggle!.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(isOn ? UTKTheme.Light : UTKTheme.Dark);
};
// 이벤트 핸들러
_shortcutList.OnDataChanged += OnShortcutDataChanged;
// 기본 데이터 로드 (DeepCopy 로 원본 보존)
LoadDefaultData();
// 테스트 버튼 생성
CreateButtons(root);
}
// ── 이벤트 핸들러 ─────────────────────────────────────────────────────────
/// <summary>단축키 데이터가 변경될 때 호출됩니다.</summary>
private void OnShortcutDataChanged(UTKShortcutItemData item)
{
Debug.Log($"[UTKShortcutListSample] 변경 → id={item.Id} " +
$"Ctrl={item.UseCtrl} Shift={item.UseShift} Alt={item.UseAlt} Key={item.Key}");
}
// ── 데이터 관리 ───────────────────────────────────────────────────────────
/// <summary>DefaultShortcuts 의 복사본을 리스트에 로드합니다.</summary>
private void LoadDefaultData()
{
var copy = new List<UTKShortcutItemData>(DefaultShortcuts.Count);
foreach (var src in DefaultShortcuts)
{
copy.Add(new UTKShortcutItemData
{
Id = src.Id,
CommandName = src.CommandName,
UseCtrl = src.UseCtrl,
UseShift = src.UseShift,
UseAlt = src.UseAlt,
Key = src.Key,
});
}
_shortcutList?.SetData(copy);
Debug.Log($"[UTKShortcutListSample] {copy.Count}개 단축키 로드 완료");
}
// ── 테스트 버튼 ───────────────────────────────────────────────────────────
private void CreateButtons(VisualElement root)
{
var bar = new VisualElement();
bar.style.flexDirection = FlexDirection.Row;
bar.style.justifyContent = Justify.Center;
bar.style.paddingTop = 8;
bar.style.paddingBottom = 8;
bar.style.flexWrap = Wrap.Wrap;
// GetData 버튼: 현재 데이터를 콘솔에 출력
var getDataBtn = new UTKButton("GetData", variant: UTKButton.ButtonVariant.Primary);
getDataBtn.OnClicked += OnGetDataClicked;
getDataBtn.style.marginRight = 4;
bar.Add(getDataBtn);
// 리셋 버튼: 기본값으로 복원
var resetBtn = new UTKButton("리셋", variant: UTKButton.ButtonVariant.Normal);
resetBtn.OnClicked += LoadDefaultData;
resetBtn.style.marginRight = 4;
bar.Add(resetBtn);
// 항목 추가 버튼: 빈 항목 하나 추가
var addBtn = new UTKButton("항목 추가", variant: UTKButton.ButtonVariant.OutlinePrimary);
addBtn.OnClicked += OnAddItemClicked;
bar.Add(addBtn);
root.Add(bar);
}
/// <summary>현재 최종 데이터를 콘솔에 출력합니다.</summary>
private void OnGetDataClicked()
{
if (_shortcutList == null) return;
var data = _shortcutList.GetData();
Debug.Log($"[UTKShortcutListSample] ── GetData ({data.Count}개) ──");
foreach (var item in data)
{
var mods = $"{(item.UseCtrl ? "Ctrl+" : "")}" +
$"{(item.UseShift ? "Shift+" : "")}" +
$"{(item.UseAlt ? "Alt+" : "")}";
Debug.Log($" [{item.Id}] {item.CommandName} → {mods}{item.Key}");
}
}
/// <summary>빈 항목을 목록 끝에 추가하고 리스트를 갱신합니다.</summary>
private void OnAddItemClicked()
{
if (_shortcutList == null) return;
var current = _shortcutList.GetData();
var newItem = new UTKShortcutItemData
{
Id = $"custom.{current.Count + 1}",
CommandName = $"Custom Command {current.Count + 1}",
UseCtrl = false,
UseShift = false,
UseAlt = false,
Key = "",
};
current.Add(newItem);
_shortcutList.SetData(current);
Debug.Log($"[UTKShortcutListSample] 항목 추가: {newItem.CommandName}");
}
// ─────────────────────────────────────────────────────────────────────────
private void OnDestroy()
{
if (_shortcutList != null)
{
_shortcutList.OnDataChanged -= OnShortcutDataChanged;
_shortcutList.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 85433d2caa763104f86b4b44ded2fc2d

View File

@@ -0,0 +1,6 @@
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<VisualElement style="width: 100%; height: 100%;">
<utk:UTKShortcutList name="window" style="width: 500px;" />
<utk:UTKToggle name="toggle" label="테마 변경" style="position: absolute; top: 10px; right: 10px;" />
</VisualElement>
</UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 274352955998a6e478eb57d04c49969b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -102,7 +102,11 @@ input.ErrorMessage = """"; // 오류 제거
// 비활성화 / 읽기 전용
input.IsEnabled = false;
input.isReadOnly = true;
input.multiline = true;",
input.multiline = true;
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
input.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -124,6 +128,9 @@ input.multiline = true;",
<!-- 비활성화 -->
<utk:UTKInputField label=""읽기전용"" is-enabled=""false"" value=""수정 불가"" />
<!-- label min-width 설정 -->
<utk:UTKInputField label=""이름"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -200,7 +207,10 @@ ageField.ClearError();
// 5) 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
intField.ErrorMessage = ""서버 오류가 발생했습니다."";
intField.ErrorMessage = """"; // 오류 제거",
intField.ErrorMessage = """"; // 오류 제거
// ── Label Min-Width ──────────────────────────
intField.LabelMinWidth = 120f; // label의 min-width 설정",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -213,6 +223,9 @@ intField.ErrorMessage = """"; // 오류 제거",
<!-- 비활성화 -->
<utk:UTKIntegerField label=""읽기전용"" is-enabled=""false"" value=""50"" />
<!-- label min-width 설정 -->
<utk:UTKIntegerField label=""수량"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -276,7 +289,10 @@ sizeField.Validation = () => sizeField.Value >= 0 && sizeField.Value <= 1000000;
bool isValid = sizeField.Validate();
// 3) 에러 수동 해제
sizeField.ClearError();",
sizeField.ClearError();
// ── Label Min-Width ──────────────────────────
longField.LabelMinWidth = 120f; // label의 min-width 설정",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -289,6 +305,9 @@ sizeField.ClearError();",
<!-- 비활성화 -->
<utk:UTKLongField label=""읽기전용"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKLongField label=""ID"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -353,7 +372,10 @@ speedField.Validation = () => speedField.Value >= 0f && speedField.Value <= 100f
bool isValid = speedField.Validate();
// 3) 에러 수동 해제
speedField.ClearError();",
speedField.ClearError();
// ── Label Min-Width ──────────────────────────
floatField.LabelMinWidth = 120f; // label의 min-width 설정",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -366,6 +388,9 @@ speedField.ClearError();",
<!-- 비활성화 -->
<utk:UTKFloatField label=""읽기전용"" is-enabled=""false"" value=""3.14"" />
<!-- label min-width 설정 -->
<utk:UTKFloatField label=""속도"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -430,7 +455,10 @@ percentField.Validation = () => percentField.Value >= 0.0 && percentField.Value
bool isValid = percentField.Validate();
// 3) 에러 수동 해제
percentField.ClearError();",
percentField.ClearError();
// ── Label Min-Width ──────────────────────────
doubleField.LabelMinWidth = 120f; // label의 min-width 설정",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -443,6 +471,9 @@ percentField.ClearError();",
<!-- 비활성화 -->
<utk:UTKDoubleField label=""읽기전용"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKDoubleField label=""정밀도"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -695,7 +726,11 @@ dirField.ErrorMessage = ""방향 벡터는 (0,0)이 될 수 없습니다."";
dirField.Validation = () => dirField.Value != Vector2.zero;
bool isValid = dirField.Validate();
dirField.ClearError();",
dirField.ClearError();
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
positionField.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -708,6 +743,9 @@ dirField.ClearError();",
<!-- 비활성화 -->
<utk:UTKVector2Field label=""ReadOnly"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKVector2Field label=""Position"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -767,7 +805,11 @@ scaleField.ErrorMessage = ""스케일 벡터는 (0,0,0)이 될 수 없습니다.
scaleField.Validation = () => scaleField.Value != Vector3.zero;
bool isValid = scaleField.Validate();
scaleField.ClearError();",
scaleField.ClearError();
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
positionField.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -780,6 +822,9 @@ scaleField.ClearError();",
<!-- 비활성화 -->
<utk:UTKVector3Field label=""Disabled"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKVector3Field label=""Position"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -853,7 +898,11 @@ rgbaField.Validation = () =>
};
bool isValid = rgbaField.Validate();
rgbaField.ClearError();",
rgbaField.ClearError();
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
colorField.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -866,6 +915,9 @@ rgbaField.ClearError();",
<!-- 비활성화 -->
<utk:UTKVector4Field label=""Disabled"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKVector4Field label=""Color"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -927,7 +979,11 @@ viewportField.Validation = () =>
viewportField.Value.width > 0 && viewportField.Value.height > 0;
bool isValid = viewportField.Validate();
viewportField.ClearError();",
viewportField.ClearError();
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
areaField.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -940,6 +996,9 @@ viewportField.ClearError();",
<!-- 비활성화 -->
<utk:UTKRectField label=""Disabled"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKRectField label=""Area"" label-min-width=""120"" />
</ui:UXML>");
}
@@ -1009,7 +1068,11 @@ colliderField.Validation = () =>
};
bool isValid = colliderField.Validate();
colliderField.ClearError();",
colliderField.ClearError();
// ── Label Min-Width ──────────────────────────
// label이 있을 때 .unity-label의 min-width를 설정
boundsField.LabelMinWidth = 120f; // 120px (-1이면 미설정)",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -1022,6 +1085,9 @@ colliderField.ClearError();",
<!-- 비활성화 -->
<utk:UTKBoundsField label=""Disabled"" is-enabled=""false"" />
<!-- label min-width 설정 -->
<utk:UTKBoundsField label=""Bounds"" label-min-width=""120"" />
</ui:UXML>");
}

View File

@@ -456,6 +456,72 @@ treeView.OnItemSelected += (index) => Debug.Log($""선택된 항목: {index}"");
<!-- C#에서 컴럼과 데이터 설정 필요 -->");
}
private void InitializeShortcutListSample(VisualElement root)
{
var shortcutList = root.Q<UTKShortcutList>("shortcut-list-sample");
if (shortcutList != null)
{
shortcutList.SetData(new System.Collections.Generic.List<UTKShortcutItemData>
{
new() { Id = "file.new_project", CommandName = "File > New Project", UseCtrl = true, UseShift = false, UseAlt = false, Key = "N" },
new() { Id = "file.open_project", CommandName = "File > Open Project", UseCtrl = true, UseShift = true, UseAlt = false, Key = "O" },
new() { Id = "file.save_project", CommandName = "File > Save Project", UseCtrl = true, UseShift = false, UseAlt = true, Key = "S" },
new() { Id = "file.save_as", CommandName = "File > Save As...", UseCtrl = true, UseShift = true, UseAlt = true, Key = "S" },
new() { Id = "file.export_layout", CommandName = "File > Export > Layout", UseCtrl = true, UseShift = true, UseAlt = false, Key = "L" },
new() { Id = "file.export_gltf", CommandName = "File > Export > glTF", UseCtrl = true, UseShift = true, UseAlt = false, Key = "G" },
new() { Id = "edit.undo", CommandName = "Edit > Undo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Z" },
new() { Id = "edit.redo", CommandName = "Edit > Redo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Y" },
new() { Id = "edit.duplicate", CommandName = "Edit > Duplicate", UseCtrl = true, UseShift = true, UseAlt = false, Key = "D" },
new() { Id = "edit.delete", CommandName = "Edit > Delete", UseCtrl = false, UseShift = true, UseAlt = false, Key = "Delete" },
new() { Id = "create.plane", CommandName = "Create > Plane", UseCtrl = true, UseShift = true, UseAlt = false, Key = "V" },
new() { Id = "tool.select", CommandName = "Select Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "1" },
});
shortcutList.OnDataChanged += item =>
Debug.Log($"[StyleGuide] 단축키 변경: {item.CommandName} → {(item.UseCtrl ? "Ctrl+" : "")}{(item.UseShift ? "Shift+" : "")}{(item.UseAlt ? "Alt+" : "")}{item.Key}");
}
SetCodeSamples(root,
csharpCode:
@"// 데이터 생성
var data = new List<UTKShortcutItemData>
{
new() { Id = ""file.new"", CommandName = ""File > New Project"", UseCtrl = true, UseShift = false, UseAlt = false, Key = ""N"" },
new() { Id = ""edit.undo"", CommandName = ""Edit > Undo"", UseCtrl = true, UseShift = true, UseAlt = false, Key = ""Z"" },
new() { Id = ""edit.delete"", CommandName = ""Edit > Delete"", UseCtrl = false, UseShift = true, UseAlt = false, Key = ""Delete"" },
};
// 리스트 생성 & 데이터 설정
var list = new UTKShortcutList();
list.SetData(data);
// 변경 이벤트 (Ctrl/Shift/Alt 체크 또는 Key 캡처 시 발생)
list.OnDataChanged += item =>
{
Debug.Log($""{item.CommandName}: Ctrl={item.UseCtrl} Key={item.Key}"");
};
// Key 캡처 방법:
// 1. Key 필드 클릭 → ""···"" 표시
// 2. 원하는 키 입력 → 자동 저장
// 3. Escape → 취소 (이전 값 복원)
// 현재 데이터 가져오기
var current = list.GetData();
// 업데이트된 데이터 다시 적용
current[0].Key = ""F1"";
list.SetData(current);",
uxmlCode:
@"<!-- 기본 사용 -->
<utk:UTKShortcutList name=""shortcut-list"" />
<!-- 고정 크기 -->
<utk:UTKShortcutList style=""height: 300px;"" />
<!-- 데이터와 이벤트는 C#에서 설정 -->");
}
private void InitializeScrollViewSample(VisualElement root)
{
// Horizontal scroll

View File

@@ -430,7 +430,7 @@ for (int i = 0; i < buttons.Length; i++)
var btnFooter = root.Q<UTKButton>("btn-modal-footer");
btnFooter?.RegisterCallback<ClickEvent>(_ =>
{
var modal = UTKModal.Show("푸터 버튼 모달", UTKModal.ModalSize.Medium);
var modal = UTKModal.Create("푸터 버튼 모달", UTKModal.ModalSize.Medium);
modal.Add(new Label("확인 또는 취소를 선택하세요."));
var confirmBtn = new UTKButton("확인") { Variant = UTKButton.ButtonVariant.Primary };
@@ -449,29 +449,33 @@ for (int i = 0; i < buttons.Length; i++)
modal.AddToFooter(cancelBtn);
modal.AddToFooter(confirmBtn);
modal.Show();
});
var btnNoClose = root.Q<UTKButton>("btn-modal-no-close");
btnNoClose?.RegisterCallback<ClickEvent>(_ =>
{
var modal = UTKModal.Show("닫기 버튼 없음", UTKModal.ModalSize.Small);
var modal = UTKModal.Create("닫기 버튼 없음", UTKModal.ModalSize.Small);
modal.ShowCloseButton = false;
modal.CloseOnBackdropClick = true;
modal.Add(new Label("닫기 버튼이 없습니다.\n배경을 클릭하면 닫힙니다."));
modal.Show();
});
var btnNoBackdrop = root.Q<UTKButton>("btn-modal-no-backdrop");
btnNoBackdrop?.RegisterCallback<ClickEvent>(_ =>
{
var modal = UTKModal.Show("배경 클릭 비활성화", UTKModal.ModalSize.Small);
var modal = UTKModal.Create("배경 클릭 비활성화", UTKModal.ModalSize.Small);
modal.CloseOnBackdropClick = false;
modal.Add(new Label("배경을 클릭해도 닫히지 않습니다.\nX 버튼으로만 닫을 수 있습니다."));
modal.Show();
});
// Custom Content - Form Modal
var btnForm = root.Q<UTKButton>("btn-modal-form");
btnForm?.RegisterCallback<ClickEvent>(_ =>
{
var modal = UTKModal.Show("사용자 정보 입력", UTKModal.ModalSize.Medium);
var modal = UTKModal.Create("사용자 정보 입력", UTKModal.ModalSize.Medium);
var nameField = new UTKInputField("이름");
var emailField = new UTKInputField("이메일");
@@ -490,6 +494,7 @@ for (int i = 0; i < buttons.Length; i++)
modal.AddToFooter(cancelBtn);
modal.AddToFooter(submitBtn);
modal.Show();
});
SetCodeSamples(root,
@@ -500,33 +505,40 @@ UTKModal.SetRoot(rootVisualElement);
// 1. 기본 모달 표시
// ========================================
var modal = UTKModal.Show(""제목"", UTKModal.ModalSize.Medium);
var modal = UTKModal.Create(""제목"", UTKModal.ModalSize.Medium);
modal.Add(new Label(""모달 내용""));
// 닫힘 이벤트
modal.OnClosed += () => Debug.Log(""모달 닫힘"");
// 화면에 표시
modal.Show();
// ========================================
// 2. 모달 크기
// ========================================
// Small
UTKModal.Show(""Small"", UTKModal.ModalSize.Small);
var small = UTKModal.Create(""Small"", UTKModal.ModalSize.Small);
small.Show();
// Medium (기본)
UTKModal.Show(""Medium"", UTKModal.ModalSize.Medium);
var medium = UTKModal.Create(""Medium"", UTKModal.ModalSize.Medium);
medium.Show();
// Large
UTKModal.Show(""Large"", UTKModal.ModalSize.Large);
var large = UTKModal.Create(""Large"", UTKModal.ModalSize.Large);
large.Show();
// FullScreen
UTKModal.Show(""FullScreen"", UTKModal.ModalSize.FullScreen);
var full = UTKModal.Create(""FullScreen"", UTKModal.ModalSize.FullScreen);
full.Show();
// ========================================
// 3. 옵션 설정
// ========================================
var modal = UTKModal.Show(""설정"");
var modal = UTKModal.Create(""설정"");
// 닫기 버튼 숨기기
modal.ShowCloseButton = false;
@@ -537,11 +549,14 @@ modal.CloseOnBackdropClick = false;
// 푸터 숨기기
modal.SetFooterVisible(false);
// 옵션 설정 후 표시
modal.Show();
// ========================================
// 4. 푸터 버튼 추가
// ========================================
var modal = UTKModal.Show(""확인 모달"");
var modal = UTKModal.Create(""확인 모달"");
modal.Add(new Label(""정말 삭제하시겠습니까?""));
var confirmBtn = new UTKButton(""삭제"") { Variant = UTKButton.ButtonVariant.Danger };
@@ -556,12 +571,13 @@ cancelBtn.RegisterCallback<ClickEvent>(_ => modal.Close());
modal.AddToFooter(cancelBtn);
modal.AddToFooter(confirmBtn);
modal.Show();
// ========================================
// 5. 폼 모달
// ========================================
var modal = UTKModal.Show(""사용자 정보"");
var modal = UTKModal.Create(""사용자 정보"");
var nameField = new UTKInputField(""이름"");
var emailField = new UTKInputField(""이메일"");
@@ -575,18 +591,83 @@ submitBtn.RegisterCallback<ClickEvent>(_ =>
modal.Close();
});
modal.AddToFooter(submitBtn);
modal.Show();
// ========================================
// 6. 프로그래밍 방식 닫기
// ========================================
var modal = UTKModal.Show(""타이머 모달"");
var modal = UTKModal.Create(""타이머 모달"");
modal.Add(new Label(""3초 후 자동으로 닫힙니다.""));
modal.ShowCloseButton = false;
modal.CloseOnBackdropClick = false;
modal.Show();
// 3초 후 닫기
modal.schedule.Execute(() => modal.Close()).StartingIn(3000);",
modal.schedule.Execute(() => modal.Close()).StartingIn(3000);
// ========================================
// 7. Async/Await 방식 (닫힐 때까지 대기)
// ========================================
var modal = UTKModal.Create(""설정"");
modal.Add(new Label(""모달 내용""));
var closeBtn = new UTKButton(""닫기"") { Variant = UTKButton.ButtonVariant.Primary };
closeBtn.OnClicked += () => modal.Close();
modal.AddToFooter(closeBtn);
// 모달이 닫힐 때까지 대기
await modal.ShowAsync();
Debug.Log(""모달이 닫혔습니다."");
// Async 폼 예시
var formModal = UTKModal.Create(""사용자 정보"");
var nameField = new UTKInputField(""이름"");
formModal.Add(nameField);
var submitBtn = new UTKButton(""제출"") { Variant = UTKButton.ButtonVariant.Primary };
submitBtn.OnClicked += () => formModal.Close();
formModal.AddToFooter(submitBtn);
await formModal.ShowAsync();
Debug.Log($""입력된 이름: {nameField.value}"");
// ========================================
// 8. IUTKModalContent<T> 결과 반환
// ========================================
// IUTKModalContent<T>를 구현한 콘텐츠 클래스 정의
// public class UserFormContent : VisualElement, IUTKModalContent<UserData>
// {
// private UTKInputField _nameField = new(""이름"");
// private UTKInputField _emailField = new(""이메일"");
//
// public UserFormContent()
// {
// Add(_nameField);
// Add(_emailField);
// }
//
// public UserData? GetResult()
// {
// return new UserData(_nameField.value, _emailField.value);
// }
// }
// 사용
var modal = UTKModal.Create(""사용자 정보"");
var form = new UserFormContent();
modal.Add(form);
var submitBtn = new UTKButton(""제출"") { Variant = UTKButton.ButtonVariant.Primary };
submitBtn.OnClicked += () => modal.Close();
modal.AddToFooter(submitBtn);
// Close() 시 자동으로 form.GetResult() 호출
UserData? result = await modal.ShowAsync<UserData>();
if (result != null)
Debug.Log($""이름: {result.Name}, 이메일: {result.Email}"");",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
@@ -612,9 +693,10 @@ modal.schedule.Execute(() => modal.Close()).StartingIn(3000);",
{
if (_root == null) return;
var modal = UTKModal.Show($"{size} Modal", size);
var modal = UTKModal.Create($"{size} Modal", size);
modal.Add(new Label($"이것은 {size} 크기의 모달입니다."));
modal.OnClosed += () => Debug.Log($"{size} modal closed");
modal.Show();
}
#endregion

View File

@@ -88,6 +88,7 @@ public partial class UTKStyleGuideSample : MonoBehaviour
["UTKMultiColumnTreeView"] = "UIToolkit/Sample/List/UTKMultiColumnTreeViewSample",
["UTKFoldout"] = "UIToolkit/Sample/List/UTKFoldoutSample",
["UTKScrollView"] = "UIToolkit/Sample/List/UTKScrollViewSample",
["UTKShortcutList"] = "UIToolkit/Sample/List/UTKShortcutListSample",
// Card
["UTKCard"] = "UIToolkit/Sample/Card/UTKCardSample",
["UTKPanel"] = "UIToolkit/Sample/Card/UTKPanelSample",
@@ -123,7 +124,7 @@ public partial class UTKStyleGuideSample : MonoBehaviour
["Slider"] = new[] { "UTKSlider", "UTKSliderInt", "UTKMinMaxSlider", "UTKProgressBar" },
["Dropdown"] = new[] { "UTKDropdown", "UTKEnumDropDown", "UTKMultiSelectDropdown" },
["Label"] = new[] { "UTKLabel", "UTKHelpBox" },
["List"] = new[] { "UTKListView", "UTKTreeView", "UTKMultiColumnListView", "UTKMultiColumnTreeView", "UTKFoldout", "UTKScrollView" },
["List"] = new[] { "UTKListView", "UTKTreeView", "UTKMultiColumnListView", "UTKMultiColumnTreeView", "UTKFoldout", "UTKScrollView", "UTKShortcutList" },
["Card"] = new[] { "UTKCard", "UTKPanel" },
["Tab"] = new[] { "UTKTabView" },
["Modal"] = new[] { "UTKAlert", "UTKToast", "UTKTooltip", "UTKNotification", "UTKModal" },
@@ -527,6 +528,9 @@ public partial class UTKStyleGuideSample : MonoBehaviour
case "UTKScrollView":
InitializeScrollViewSample(root);
break;
case "UTKShortcutList":
InitializeShortcutListSample(root);
break;
// Card
case "UTKCard":
InitializeCardSample(root);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
namespace UVC.Studio.UIToolkit.Modal
{
[UxmlElement]
public partial class UTKSettingModalContent : VisualElement, IDisposable, IUTKModalContent<object>
{
private UTKTabView tabView;
public UTKSettingModalContent()
{
style.flexGrow = 1;
tabView = new UTKTabView();
tabView.style.flexGrow = 1;
tabView.TabWidth = 140; // 탭 너비 설정
// 탭 정렬 방향 설정
tabView.Align = TabAlign.Left; // 탭을 왼쪽에 세로로 배치
// 탭 추가
var tab1 = tabView.AddUTKTab("Database");
tab1.Add(new UTKSettingModalContentDB());
var tab2 = tabView.AddUTKTab("General");
tab2.Add(new UTKSettingModalContentGeneral());
var tab3 = tabView.AddUTKTab("Shortcut");
tab3.Add(new UTKSettingModalContentShortcut());
tabView.OnTabChanged += OnTabChanged;
tabView.tabClosed += OnTabClosed;
Add(tabView);
}
public UTKSettingModalContent(int tabIndex = 0, object? data = null) : this()
{
// 생성자에서 탭 인덱스를 받아서 초기 탭 설정
tabView.SelectedIndex = tabIndex;
}
private void OnTabChanged(int index, UnityEngine.UIElements.Tab? tab)
{
Debug.Log($"Selected Tab Index: {index}");
}
private void OnTabClosed(UnityEngine.UIElements.Tab tab, int index)
{
Debug.Log($"Closed Tab Index: {index}");
}
public object? GetResult()
{
IUTKTabContent? content = UTKTabView.FindTabContent(tabView.activeTab);
if (content != null) {
return content.Hide();
}
return null;
}
public void Dispose()
{
// 필요한 경우 리소스 정리
tabView.OnTabChanged -= OnTabChanged;
tabView.tabClosed -= OnTabClosed;
}
}
}

View File

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

View File

@@ -0,0 +1,97 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
namespace UVC.Studio.UIToolkit.Modal
{
/// <summary>
/// 설정 모달 - Database 탭 콘텐츠
/// </summary>
[UxmlElement]
public partial class UTKSettingModalContentDB : VisualElement, IDisposable, IUTKTabContent
{
#region Constants
private const string UXML_PATH = "Studio/UIToolkit/Modal/UTKSettingModalContentDBUXML";
#endregion
#region Fields
private bool _disposed;
private UTKInputField? _dbIpField;
private UTKInputField? _dbPortField;
private UTKInputField? _dbIdField;
private UTKInputField? _dbPasswordField;
#endregion
#region Properties
/// <summary>Database IP</summary>
public string DbIp => _dbIpField?.value ?? "";
/// <summary>Database PORT</summary>
public string DbPort => _dbPortField?.value ?? "";
/// <summary>Database ID</summary>
public string DbId => _dbIdField?.value ?? "";
/// <summary>Database PASSWORD</summary>
public string DbPassword => _dbPasswordField?.value ?? "";
#endregion
#region Constructor
public UTKSettingModalContentDB()
{
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (asset != null)
{
var root = asset.Instantiate();
root.style.flexGrow = 1;
// UTKInputField 참조 가져오기
var inputFields = root.Query<UTKInputField>().ToList();
foreach (var field in inputFields)
{
switch (field.label)
{
case "Database IP":
_dbIpField = field;
break;
case "Database PORT":
_dbPortField = field;
break;
case "Database ID":
_dbIdField = field;
break;
case "Database PASSWORD":
_dbPasswordField = field;
break;
}
}
Add(root);
}
}
#endregion
#region IUTKTabContent
public void Show(object? data)
{
}
public async UniTask Hide()
{
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,113 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
namespace UVC.Studio.UIToolkit.Modal
{
/// <summary>
/// 설정 모달 - General 탭 콘텐츠
/// </summary>
[UxmlElement]
public partial class UTKSettingModalContentGeneral : VisualElement, IDisposable, IUTKTabContent
{
#region Constants
private const string UXML_PATH = "Studio/UIToolkit/Modal/UTKSettingModalContentGeneralUXML";
#endregion
#region Fields
private bool _disposed;
private UTKIntegerField? _autoSaveTimeField;
private UTKFloatField? _gridSpacingField;
private UTKFloatField? _positionSnapField;
private UTKFloatField? _rotationSnapField;
private UTKFloatField? _scaleSnapField;
#endregion
#region Properties
/// <summary>Auto Save Time 값 (분)</summary>
public int AutoSaveTime => _autoSaveTimeField?.value ?? 5;
/// <summary>Grid Spacing 값</summary>
public float GridSpacing => _gridSpacingField?.value ?? 1f;
/// <summary>Position Snap 값</summary>
public float PositionSnap => _positionSnapField?.value ?? 0.5f;
/// <summary>Rotation Snap 값</summary>
public float RotationSnap => _rotationSnapField?.value ?? 10f;
/// <summary>Scale Snap 값</summary>
public float ScaleSnap => _scaleSnapField?.value ?? 0.5f;
#endregion
#region Constructor
public UTKSettingModalContentGeneral()
{
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (asset != null)
{
var root = asset.Instantiate();
root.style.flexGrow = 1;
// UTKIntegerField 참조 가져오기
var intFields = root.Query<UTKIntegerField>().ToList();
foreach (var field in intFields)
{
switch (field.label)
{
case "Auto Save Time":
_autoSaveTimeField = field;
break;
}
}
// UTKFloatField 참조 가져오기
var floatFields = root.Query<UTKFloatField>().ToList();
foreach (var field in floatFields)
{
switch (field.label)
{
case "Grid Spacing":
_gridSpacingField = field;
break;
case "Position Snap":
_positionSnapField = field;
break;
case "Rotation Snap":
_rotationSnapField = field;
break;
case "Scale Snap":
_scaleSnapField = field;
break;
}
}
Add(root);
}
}
#endregion
#region Public Methods
public void Show(object? data)
{
}
public async UniTask Hide()
{
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,111 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
namespace UVC.Studio.UIToolkit.Modal
{
/// <summary>
/// 설정 모달 - Database 탭 콘텐츠
/// </summary>
[UxmlElement]
public partial class UTKSettingModalContentShortcut : VisualElement, IDisposable, IUTKTabContent
{
#region Constants
private const string UXML_PATH = "Studio/UIToolkit/Modal/UTKSettingModalContentShortcutUXML";
#endregion
#region Fields
private bool _disposed;
private UTKShortcutList? _shortcutList;
#endregion
#region Properties
#endregion
#region Constructor
public UTKSettingModalContentShortcut()
{
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (asset != null)
{
var root = asset.Instantiate();
root.style.flexGrow = 1;
// 참조 가져오기
_shortcutList = root.Q<UTKShortcutList>("window");
if (_shortcutList != null)
{
// 변경 이벤트 구독
_shortcutList.OnDataChanged += OnDataChanged;
}
Add(root);
}
}
#endregion
private void OnDataChanged(UTKShortcutItemData item)
{
Debug.Log($"{item.CommandName}: Ctrl={item.UseCtrl} Key={item.Key}");
}
#region IUTKTabContent
public void Show(object? data)
{
Debug.Log($"[UTKSettingModalContentShortcut] Show called with data: {data}");
if (_shortcutList != null)
{
if(data is List<UTKShortcutItemData> shortcuts)
{
_shortcutList.SetData(shortcuts);
return;
}
// 예시 데이터 설정
var tempData = new List<UTKShortcutItemData>()
{
new() { Id = "file.new_project", CommandName = "File > New Project", UseCtrl = true, UseShift = false, UseAlt = false, Key = "N" },
new() { Id = "file.open_project", CommandName = "File > Open Project", UseCtrl = true, UseShift = true, UseAlt = false, Key = "O" },
new() { Id = "file.save_project", CommandName = "File > Save Project", UseCtrl = true, UseShift = false, UseAlt = true, Key = "S" },
new() { Id = "file.save_as", CommandName = "File > Save As...", UseCtrl = true, UseShift = true, UseAlt = true, Key = "S" },
new() { Id = "file.insert_database", CommandName = "File > Insert Database", UseCtrl = true, UseShift = true, UseAlt = false, Key = "I" },
new() { Id = "file.export_layout", CommandName = "File > Export > Layout", UseCtrl = true, UseShift = true, UseAlt = false, Key = "L" },
new() { Id = "file.export_metadata", CommandName = "File > Export > Metadata",UseCtrl = true, UseShift = true, UseAlt = false, Key = "M" },
new() { Id = "file.export_gltf", CommandName = "File > Export > glTF", UseCtrl = true, UseShift = true, UseAlt = false, Key = "G" },
new() { Id = "edit.undo", CommandName = "Edit > Undo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Z" },
new() { Id = "edit.redo", CommandName = "Edit > Redo", UseCtrl = true, UseShift = true, UseAlt = false, Key = "Y" },
new() { Id = "edit.duplicate", CommandName = "Edit > Duplicate", UseCtrl = true, UseShift = true, UseAlt = false, Key = "D" },
new() { Id = "edit.delete", CommandName = "Edit > Delete", UseCtrl = false, UseShift = true, UseAlt = false, Key = "Delete" },
new() { Id = "create.plane", CommandName = "Create > Plane", UseCtrl = true, UseShift = true, UseAlt = false, Key = "V" },
new() { Id = "tool.select", CommandName = "Select Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "1" },
new() { Id = "tool.move", CommandName = "Move Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "2" },
new() { Id = "tool.rotate", CommandName = "Rotate Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "3" },
new() { Id = "tool.scale", CommandName = "Scale Tool", UseCtrl = false, UseShift = false, UseAlt = false, Key = "4" },
};
_shortcutList.SetData(tempData);
}
}
public async UniTask Hide()
{
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_shortcutList != null)
{
_shortcutList.OnDataChanged -= OnDataChanged;
}
}
#endregion
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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; } = "";
}
}

View File

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

View 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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 700c91f027891a64c8d4a91e4107a90d

View File

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

View File

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

View File

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

View 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();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3f5401783ae68ec49b0dca98d448e209

View File

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

View File

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

View 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");
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26dda967fd6d0d84bb1f40f8a6ad9c5f

View File

@@ -5,6 +5,17 @@
---
## 0) 작업 진행 규칙
**⚠️ 최우선 규칙: 임의로 진행하지 않고, 반드시 사용자에게 확인 후 진행합니다.**
- 코드 수정, 파일 생성/삭제, 리팩토링 등 **모든 변경 작업은 사전에 계획을 설명하고 승인을 받은 후** 진행합니다.
- 요구사항이 모호하거나 여러 접근 방식이 가능한 경우, **추측하지 말고 질문**합니다.
- 버그 수정이라도 원인 분석 결과를 먼저 공유하고, 수정 방향에 대해 합의 후 코드를 변경합니다.
- 단순한 오타 수정, 한 줄 변경 등 **명백하고 사소한 작업**만 즉시 진행할 수 있습니다.
---
## 1) 핵심 원칙
### UI 프레임워크