UTKToolBar 개발 완료

This commit is contained in:
logonkhi
2026-02-19 18:40:37 +09:00
parent 0333b83b57
commit 739a62eb9b
112 changed files with 7496 additions and 46 deletions

View File

@@ -29,7 +29,11 @@
"Bash(del:*)",
"Bash(cmd /c \"del /q \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\Property\\\\Views\\\\UTKIntPropertyItemViewSliderUss.uss\"\" \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\Property\\\\Views\\\\UTKFloatPropertyItemViewSliderUss.uss\"\"\")",
"mcp__UnityMCP__refresh_unity",
"Bash(powershell \"Get-Process Unity -ErrorAction SilentlyContinue | Select-Object -First 1 | Format-List\")"
"Bash(powershell \"Get-Process Unity -ErrorAction SilentlyContinue | Select-Object -First 1 | Format-List\")",
"Bash(cmd /c \"mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Data\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Items\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\ToolBar\"\"\")",
"Bash(/bin/ls:*)",
"Bash(/bin/mkdir -p:*)",
"Bash(/bin/rm:*)"
],
"deny": [],
"ask": []

View File

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

View File

@@ -0,0 +1,30 @@
/*
* ===================================
* UTKTopMenuSample.uss
* Styles for UTKTopMenu sample
* ===================================
*/
.utk-menu-sample-container {
min-height: 44px;
background-color: var(--color-bg-secondary);
border-radius: 6px;
padding: 0;
overflow: visible;
}
.utk-menu-sample-container--vertical {
min-height: 180px;
max-width: 160px;
}
.utk-menu-sample-result {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 8px;
padding: 8px 12px;
background-color: var(--color-bg-secondary);
border-radius: 4px;
min-height: 20px;
white-space: normal;
}

View File

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

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
<Style src="../UTKSampleCommon.uss" />
<Style src="UTKTopMenuSample.uss" />
<VisualElement class="utk-sample-container">
<Label class="utk-sample-desc" text="UIToolkit 기반 Top Menu 컴포넌트입니다. 가로/세로 정렬, 서브메뉴, 아이콘 메뉴, ItemSpacing, Command 실행 등을 지원합니다." />
<!-- 가로 메뉴 (Horizontal) -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="Horizontal Menu (기본)" />
<VisualElement name="horizontal-menu-container" class="utk-menu-sample-container" />
</VisualElement>
<!-- 가로 메뉴 + 간격 + 이미지 아이콘 -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="Image Icon Menu (ItemSpacing)" />
<VisualElement name="icon-menu-container" class="utk-menu-sample-container" />
</VisualElement>
<!-- 세로 메뉴 (Vertical) -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="Vertical Menu (세로 정렬)" />
<VisualElement name="vertical-menu-container" class="utk-menu-sample-container utk-menu-sample-container--vertical" />
</VisualElement>
<!-- API 테스트 -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="API 테스트" />
<VisualElement name="api-test-container" class="utk-sample-row" />
<Label name="api-result-label" class="utk-menu-sample-result" text="" />
</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: 50dc639c380919a4884c8f844748f8f1
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

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

View File

@@ -0,0 +1,44 @@
/*
* ===================================
* UTKToolBarSample.uss
* Styles for UTKToolBar sample
* ===================================
*/
.utk-toolbar-sample-container {
min-height: 48px;
background-color: var(--color-bg-secondary);
border-radius: 6px;
padding: 4px;
overflow: visible;
align-items: center;
align-self: flex-start;
}
.utk-toolbar-sample-container--vertical {
min-height: 240px;
max-width: 52px;
}
.utk-toolbar-sample-result {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 8px;
padding: 8px 12px;
background-color: var(--color-bg-secondary);
border-radius: 4px;
min-height: 20px;
white-space: normal;
}
.utk-toolbar-sample-log {
font-size: 11px;
color: var(--color-text-secondary);
padding: 8px 12px;
background-color: var(--color-bg-secondary);
border-radius: 4px;
min-height: 60px;
max-height: 120px;
white-space: normal;
overflow: hidden;
}

View File

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

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
<Style src="../UTKSampleCommon.uss" />
<Style src="UTKToolBarSample.uss" />
<VisualElement class="utk-sample-container">
<Label class="utk-sample-desc" text="UIToolkit 기반 ToolBar 컴포넌트입니다. 가로/세로 배치 전환, Standard/Toggle/Radio/Expandable 버튼, 구분선을 지원합니다." />
<!-- 가로 툴바 (Horizontal) -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="Horizontal Toolbar (기본)" />
<VisualElement name="horizontal-toolbar-container" class="utk-toolbar-sample-container" />
</VisualElement>
<!-- 세로 툴바 (Vertical) -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="Vertical Toolbar (세로 배치)" />
<VisualElement name="vertical-toolbar-container" class="utk-toolbar-sample-container utk-toolbar-sample-container--vertical" />
</VisualElement>
<!-- API 테스트 -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="API 테스트" />
<VisualElement name="api-test-container" class="utk-sample-row" />
<Label name="api-result-label" class="utk-toolbar-sample-result" text="" />
</VisualElement>
<!-- 이벤트 로그 -->
<VisualElement class="utk-sample-section">
<Label class="utk-sample-section__title" text="이벤트 로그" />
<Label name="event-log-label" class="utk-toolbar-sample-log" text="(버튼을 클릭하면 여기에 이벤트가 표시됩니다)" />
</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: a3d648b54b5194f4d9577a6de6dd703a
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -136,6 +136,7 @@
--color-bg-panel: var(--color-base-20);
--color-bg-input: var(--color-base-19);
--color-bg-hover: var(--color-base-16);
--color-bg-active: var(--color-base-14);
--color-bg-tooltip: rgba(38, 38, 38, 0.95);
--color-bg-overlay: rgba(0, 0, 0, 0.5);
@@ -169,6 +170,13 @@
--color-calendar-range: var(--color-blue-06);
--color-calendar-range-hover: var(--color-blue-07);
/* ===================================
Semantic Colors - Primary (color-primary-*)
=================================== */
--color-primary: var(--color-blue-05);
--color-primary-hover: var(--color-blue-06);
--color-primary-active: var(--color-blue-08);
/* ===================================
Semantic Colors - State (color-state-*)
=================================== */

View File

@@ -147,6 +147,7 @@
--color-bg-panel: var(--color-base-01);
--color-bg-input: var(--color-base-01);
--color-bg-hover: var(--color-base-04);
--color-bg-active: var(--color-base-05);
--color-bg-tooltip: rgba(50, 50, 50, 0.95);
--color-bg-overlay: rgba(0, 0, 0, 0.3);
@@ -180,6 +181,13 @@
--color-calendar-range: var(--color-blue-02);
--color-calendar-range-hover: var(--color-blue-03);
/* ===================================
Semantic Colors - Primary (color-primary-*)
=================================== */
--color-primary: var(--color-blue-05);
--color-primary-hover: var(--color-blue-06);
--color-primary-active: var(--color-blue-07);
/* ===================================
Semantic Colors - State (color-state-*)
=================================== */

View File

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

View File

@@ -0,0 +1,5 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="toolbar-root" class="utk-toolbar">
<ui:VisualElement name="toolbar-container" class="utk-toolbar__container" />
</ui:VisualElement>
</ui:UXML>

View File

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

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="button-root" class="utk-toolbar-btn">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
</ui:VisualElement>
</ui:UXML>

View File

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

View File

@@ -0,0 +1,57 @@
/* ===================================
* UTKToolBarButtonUss.uss
* 툴바 버튼 스타일
* =================================== */
.utk-toolbar-btn {
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 32px;
min-height: 32px;
padding: var(--space-xs);
border-radius: var(--radius-s);
border-width: 0;
background-color: transparent;
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
transition: background-color var(--anim-fast);
}
.utk-toolbar-btn:hover {
background-color: var(--color-bg-hover);
}
.utk-toolbar-btn:active {
background-color: var(--color-bg-active);
}
.utk-toolbar-btn--disabled {
opacity: 0.4;
}
.utk-toolbar-btn__icon {
width: var(--size-icon-btn);
height: var(--size-icon-btn);
font-size: 18px;
-unity-text-align: middle-center;
color: var(--color-text-primary);
padding: 0;
margin: 4px;
-unity-font-definition: resource('Fonts/Icons/MaterialSymbolsOutlined');
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
}
.utk-toolbar-btn__icon--image {
-unity-background-scale-mode: scale-to-fit;
-unity-font-definition: none;
font-size: 0;
}
.utk-toolbar-btn__label {
font-size: var(--font-size-label4);
color: var(--color-text-secondary);
-unity-text-align: upper-center;
margin-top: 1px;
display: none;
cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
}

View File

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

View File

@@ -0,0 +1,7 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="button-root" class="utk-toolbar-btn utk-toolbar-expandable">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
<ui:VisualElement name="arrow" class="utk-toolbar-expandable__arrow" />
</ui:VisualElement>
</ui:UXML>

View File

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

View File

@@ -0,0 +1,38 @@
/* ===================================
* UTKToolBarExpandableButtonUss.uss
* 확장 버튼 스타일 (서브 메뉴 화살표)
* =================================== */
.utk-toolbar-expandable {
position: relative;
}
.utk-toolbar-expandable__arrow {
width: 6px;
height: 6px;
position: absolute;
bottom: 2px;
right: 2px;
}
/* 가로 배치: 아래 화살표 ▼ */
.utk-toolbar--horizontal .utk-toolbar-expandable__arrow {
border-left-width: 3px;
border-right-width: 3px;
border-top-width: 4px;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: var(--color-text-secondary);
border-bottom-width: 0;
}
/* 세로 배치: 오른쪽 화살표 ▶ */
.utk-toolbar--vertical .utk-toolbar-expandable__arrow {
border-top-width: 3px;
border-bottom-width: 3px;
border-left-width: 4px;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: var(--color-text-secondary);
border-right-width: 0;
}

View File

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

View File

@@ -0,0 +1,3 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="separator-root" class="utk-toolbar-separator" />
</ui:UXML>

View File

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

View File

@@ -0,0 +1,20 @@
/* ===================================
* UTKToolBarSeparatorUss.uss
* 구분선 스타일
* =================================== */
/* 가로 배치: 세로 구분선 */
.utk-toolbar--horizontal .utk-toolbar-separator {
width: var(--border-width);
height: 20px;
margin: 0 var(--space-s);
background-color: var(--color-border);
}
/* 세로 배치: 가로 구분선 */
.utk-toolbar--vertical .utk-toolbar-separator {
height: var(--border-width);
width: 20px;
margin: var(--space-s) 0;
background-color: var(--color-border);
}

View File

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

View File

@@ -0,0 +1,5 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="submenu-root" class="utk-toolbar-submenu">
<ui:VisualElement name="submenu-container" class="utk-toolbar-submenu__container" />
</ui:VisualElement>
</ui:UXML>

View File

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

View File

@@ -0,0 +1,39 @@
/* ===================================
* UTKToolBarSubMenuUss.uss
* 확장 버튼 서브 메뉴 스타일
* =================================== */
.utk-toolbar-submenu {
position: absolute;
background-color: var(--color-bg-primary);
border-width: var(--border-width);
border-color: var(--color-border);
border-radius: var(--radius-m);
padding: var(--space-xs);
min-width: 120px;
}
.utk-toolbar-submenu__container {
flex-direction: column;
}
/* 서브 메뉴 내 버튼은 가로로 펼침 */
.utk-toolbar-submenu .utk-toolbar-btn {
flex-direction: row;
min-width: 100px;
min-height: 28px;
justify-content: flex-start;
padding: var(--space-xs) var(--space-m);
margin: 1px 0;
}
.utk-toolbar-submenu .utk-toolbar-btn__icon {
margin-right: var(--space-s);
}
.utk-toolbar-submenu .utk-toolbar-btn__label {
display: flex;
font-size: var(--font-size-body2);
color: var(--color-text-primary);
-unity-text-align: middle-left;
}

View File

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

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="button-root" class="utk-toolbar-btn utk-toolbar-toggle">
<ui:Label name="icon" class="utk-toolbar-btn__icon" />
<ui:Label name="label" class="utk-toolbar-btn__label" />
</ui:VisualElement>
</ui:UXML>

View File

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

View File

@@ -0,0 +1,22 @@
/* ===================================
* UTKToolBarToggleButtonUss.uss
* 토글/라디오 버튼 선택 상태 스타일
* =================================== */
/* 선택 상태: 특수성을 높여 기본 스타일 오버라이드 */
.utk-toolbar-btn.utk-toolbar-toggle--selected {
background-color: var(--color-primary);
border-radius: var(--radius-s);
}
.utk-toolbar-btn.utk-toolbar-toggle--selected:hover {
background-color: var(--color-primary-hover);
}
.utk-toolbar-btn.utk-toolbar-toggle--selected:active {
background-color: var(--color-primary-active);
}
.utk-toolbar-toggle--selected .utk-toolbar-btn__icon {
color: var(--color-text-on-primary);
}

View File

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

View File

@@ -0,0 +1,49 @@
/* ===================================
* UTKToolBarUss.uss
* 툴바 메인 스타일
* =================================== */
/* === 공통 === */
.utk-toolbar {
overflow: visible;
flex-grow: 1;
}
.utk-toolbar__container {
overflow: visible;
}
/* === 가로 배치 === */
.utk-toolbar--horizontal {
flex-direction: row;
align-items: center;
align-self: flex-start;
height: 40px;
background-color: var(--color-bg-secondary);
/* border-bottom-width: var(--border-width); */
/* border-bottom-color: var(--color-border); */
padding: 0 var(--space-s);
}
.utk-toolbar--horizontal > .utk-toolbar__container {
flex-direction: row;
align-items: center;
}
/* === 세로 배치 === */
.utk-toolbar--vertical {
flex-direction: column;
align-items: center;
width: 40px;
height: auto;
background-color: var(--color-bg-secondary);
/* border-right-width: var(--border-width);
border-right-color: var(--color-border); */
padding: var(--space-s) 0;
}
.utk-toolbar--vertical > .utk-toolbar__container {
flex-direction: column;
align-items: center;
flex-grow: 1;
}

View File

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

View File

@@ -13,13 +13,13 @@ namespace Sample
/// <para><b>[ 특징 ]</b></para>
/// <list type="bullet">
/// <item><description>프리팹 없이 런타임에 새 GameObject가 자동 생성됨</description></item>
/// <item><description>Injector가 new GameObject().AddComponent&lt;T&gt;()로 생성</description></item>
/// <item><description>Injector가 new GameObject().AddComponent<T>()로 생성</description></item>
/// <item><description>App 라이프사이클이면 DontDestroyOnLoad 자동 적용</description></item>
/// <item><description>다른 서비스([Inject] ILogService)에 대한 의존성 주입 지원</description></item>
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.Register&lt;IAudioManager, InjectorSampleAudioManager&gt;(ServiceLifetime.App);</code>
/// <code>Injector.Register<IAudioManager, InjectorSampleAudioManager>(ServiceLifetime.App);</code>
///
/// <para><b>[ 사용 방법 ]</b></para>
/// <code>
@@ -41,8 +41,8 @@ namespace Sample
///
/// private void Awake()
/// {
/// bgmSource = gameObject.AddComponent&lt;AudioSource&gt;();
/// sfxSource = gameObject.AddComponent&lt;AudioSource&gt;();
/// bgmSource = gameObject.AddComponent<AudioSource>();
/// sfxSource = gameObject.AddComponent<AudioSource>();
/// bgmSource.loop = true;
/// }
/// </code>

View File

@@ -7,7 +7,7 @@ namespace Sample
/// Type D 예시: MonoBehaviour SingletonApp - 네트워크 관리자
/// </summary>
/// <remarks>
/// <para><b>[ 타입 ]</b> Type D - MonoBehaviour SingletonApp (SingletonApp&lt;T&gt; 상속)</para>
/// <para><b>[ 타입 ]</b> Type D - MonoBehaviour SingletonApp (SingletonApp<T> 상속)</para>
/// <para><b>[ 라이프사이클 ]</b> App - 씬 전환 시에도 유지 (DontDestroyOnLoad)</para>
///
/// <para><b>[ 특징 ]</b></para>
@@ -19,7 +19,7 @@ namespace Sample
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.RegisterSingleton&lt;InjectorSampleNetworkManager&gt;();</code>
/// <code>Injector.RegisterSingleton<InjectorSampleNetworkManager>();</code>
///
/// <para><b>[ 씬 설정 (선택) ]</b></para>
/// <list type="number">
@@ -40,8 +40,8 @@ namespace Sample
///
/// <para><b>[ InjectorSampleSettingsManager와의 차이점 ]</b></para>
/// <list type="table">
/// <item><term>SettingsManager</term><description>Singleton&lt;T&gt; - 순수 C#, GameObject 없음</description></item>
/// <item><term>NetworkManager</term><description>SingletonApp&lt;T&gt; - MonoBehaviour, GameObject 필요</description></item>
/// <item><term>SettingsManager</term><description>Singleton<T> - 순수 C#, GameObject 없음</description></item>
/// <item><term>NetworkManager</term><description>SingletonApp<T> - MonoBehaviour, GameObject 필요</description></item>
/// </list>
/// </remarks>
public class InjectorSampleNetworkManager : SingletonApp<InjectorSampleNetworkManager>

View File

@@ -7,7 +7,7 @@ namespace Sample
/// Type D 예시: 순수 C# Singleton - 게임 설정 관리자
/// </summary>
/// <remarks>
/// <para><b>[ 타입 ]</b> Type D - 순수 C# Singleton (Singleton&lt;T&gt; 상속)</para>
/// <para><b>[ 타입 ]</b> Type D - 순수 C# Singleton (Singleton<T> 상속)</para>
/// <para><b>[ 라이프사이클 ]</b> App - 애플리케이션 전체 유지</para>
///
/// <para><b>[ 특징 ]</b></para>
@@ -19,7 +19,7 @@ namespace Sample
/// </list>
///
/// <para><b>[ 등록 방법 ]</b></para>
/// <code>Injector.RegisterSingleton&lt;InjectorSampleSettingsManager&gt;();</code>
/// <code>Injector.RegisterSingleton<InjectorSampleSettingsManager>();</code>
///
/// <para><b>[ 사용 방법 ]</b></para>
/// <code>

View File

@@ -34,7 +34,7 @@ namespace Sample
///
/// protected override void RegisterServices()
/// {
/// Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab.gameObject, ServiceLifetime.App);
/// Injector.RegisterPrefab<IUIManager>(uiManagerPrefab.gameObject, ServiceLifetime.App);
/// }
/// </code>
///

View File

@@ -33,7 +33,7 @@ namespace Sample
/// </summary>
/// <remarks>
/// <para><b>구현체:</b> ConsoleLogger</para>
/// <para><b>등록:</b> Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App)</para>
/// <para><b>등록:</b> Injector.Register<ILogService, ConsoleLogger>(ServiceLifetime.App)</para>
/// <para><b>사용:</b> [Inject] private ILogService _logger;</para>
/// </remarks>
public interface ILogService
@@ -73,8 +73,8 @@ namespace Sample
/// </summary>
/// <remarks>
/// <para><b>구현체:</b> SceneConfig</para>
/// <para><b>등록:</b> Injector.Register&lt;ISceneConfig, SceneConfig&gt;(ServiceLifetime.Scene)</para>
/// <para><b>또는 Factory:</b> Injector.RegisterFactory&lt;ISceneConfig&gt;(injector => new SceneConfig { ... })</para>
/// <para><b>등록:</b> Injector.Register<ISceneConfig, SceneConfig>(ServiceLifetime.Scene)</para>
/// <para><b>또는 Factory:</b> Injector.RegisterFactory<ISceneConfig>(injector => new SceneConfig { ... })</para>
/// </remarks>
public interface ISceneConfig
{
@@ -129,7 +129,7 @@ namespace Sample
/// <example>
/// <code>
/// // 등록
/// Injector.Register&lt;IGameService, GameService&gt;(ServiceLifetime.App);
/// Injector.Register<IGameService, GameService>(ServiceLifetime.App);
///
/// // 사용
/// [Inject] private IGameService _gameService;
@@ -154,7 +154,7 @@ namespace Sample
/// <remarks>
/// <para><b>구현체:</b> InjectorSampleAudioManager (별도 파일)</para>
/// <para><b>타입:</b> Type B - MonoBehaviour 동적 생성</para>
/// <para><b>등록:</b> Injector.Register&lt;IAudioManager, InjectorSampleAudioManager&gt;(ServiceLifetime.App)</para>
/// <para><b>등록:</b> Injector.Register<IAudioManager, InjectorSampleAudioManager>(ServiceLifetime.App)</para>
/// <para><b>특징:</b> 런타임에 새 GameObject가 생성되고 AudioSource 등 Unity 컴포넌트 활용 가능</para>
/// </remarks>
public interface IAudioManager
@@ -173,7 +173,7 @@ namespace Sample
/// <remarks>
/// <para><b>구현체:</b> InjectorSampleEnemySpawner (별도 파일)</para>
/// <para><b>타입:</b> Type B - MonoBehaviour 동적 생성</para>
/// <para><b>등록:</b> Injector.Register&lt;IEnemySpawner, InjectorSampleEnemySpawner&gt;(ServiceLifetime.Scene)</para>
/// <para><b>등록:</b> Injector.Register<IEnemySpawner, InjectorSampleEnemySpawner>(ServiceLifetime.Scene)</para>
/// <para><b>라이프사이클:</b> Scene - 씬 전환 시 적 스포너와 생성된 적들이 함께 정리됨</para>
/// </remarks>
public interface IEnemySpawner
@@ -194,7 +194,7 @@ namespace Sample
/// <remarks>
/// <para><b>구현체:</b> InjectorSampleUIManager (별도 파일)</para>
/// <para><b>타입:</b> Type C - Prefab 기반 MonoBehaviour</para>
/// <para><b>등록:</b> Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab, ServiceLifetime.App)</para>
/// <para><b>등록:</b> Injector.RegisterPrefab<IUIManager>(uiManagerPrefab, ServiceLifetime.App)</para>
/// <para><b>특징:</b></para>
/// <list type="bullet">
/// <item><description>프리팹에 미리 설정된 UI 요소들이 유지됨</description></item>
@@ -220,7 +220,7 @@ namespace Sample
/// <remarks>
/// <para><b>구현체:</b> InjectorSampleSceneUI (별도 파일)</para>
/// <para><b>타입:</b> Type C - Prefab 기반 MonoBehaviour</para>
/// <para><b>등록:</b> Injector.RegisterPrefab&lt;ISceneUI&gt;(sceneUIPrefab, ServiceLifetime.Scene)</para>
/// <para><b>등록:</b> Injector.RegisterPrefab<ISceneUI>(sceneUIPrefab, ServiceLifetime.Scene)</para>
/// <para><b>라이프사이클:</b> Scene - 씬 전환 시 UI가 함께 정리됨</para>
/// </remarks>
public interface ISceneUI
@@ -268,11 +268,11 @@ namespace Sample
/// <example>
/// <code>
/// // 등록
/// Injector.Register&lt;IRequestHandler, RequestHandler&gt;(ServiceLifetime.Transient);
/// Injector.Register<IRequestHandler, RequestHandler>(ServiceLifetime.Transient);
///
/// // 사용 - 매번 다른 인스턴스
/// var handler1 = Injector.Resolve&lt;IRequestHandler&gt;(); // RequestId: "a1b2c3d4"
/// var handler2 = Injector.Resolve&lt;IRequestHandler&gt;(); // RequestId: "e5f6g7h8"
/// var handler1 = Injector.Resolve<IRequestHandler>(); // RequestId: "a1b2c3d4"
/// var handler2 = Injector.Resolve<IRequestHandler>(); // RequestId: "e5f6g7h8"
/// </code>
/// </example>
public class RequestHandler : IRequestHandler

View File

@@ -0,0 +1,344 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
/// <summary>
/// UTKStyleGuideSample의 Menu 카테고리 Initialize 메서드들
/// </summary>
public partial class UTKStyleGuideSample
{
#region Menu Fields
private UTKTopMenu? _sampleMenuHorizontal;
private UTKTopMenuModel? _sampleModelHorizontal;
private UTKTopMenu? _sampleMenuIcon;
private UTKTopMenuModel? _sampleModelIcon;
private UTKTopMenu? _sampleMenuVertical;
private UTKTopMenuModel? _sampleModelVertical;
#endregion
#region Menu Initializers
private void InitializeTopMenuSample(VisualElement root)
{
InitializeHorizontalMenu(root);
InitializeIconMenu(root);
InitializeVerticalMenu(root);
InitializeApiTest(root);
SetCodeSamples(root,
csharpCode: @"// === 기본 가로 메뉴 ===
var menuView = new UTKTopMenu();
var menuModel = new UTKTopMenuModel();
// 메뉴 아이템 추가
var fileMenu = new UTKMenuItemData(""file"", ""파일"");
fileMenu.AddSubMenuItem(new UTKMenuItemData(
""file_new"", ""새 파일"",
new DebugLogCommand(""새 파일 생성""),
shortcut: ""Ctrl+N""
));
fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
fileMenu.AddSubMenuItem(new UTKMenuItemData(
""file_save"", ""저장"",
new DebugLogCommand(""저장""),
shortcut: ""Ctrl+S""
));
menuModel.AddMenuItem(fileMenu);
// View에 메뉴 생성
menuView.CreateMenuItems(
menuModel.MenuItems, menuView.MenuContainer);
// 클릭 이벤트
menuView.OnMenuItemClicked += (data) =>
Debug.Log($""클릭: {data.ItemId}"");
// === 세로 메뉴 ===
var vertMenu = new UTKTopMenu();
vertMenu.Orientation = UTKMenuOrientation.Vertical;
vertMenu.ItemSpacing = 4f;
// === 아이콘 메뉴 ===
var iconMenu = new UTKMenuImageItemData(
""settings"",
UTKMaterialIcons.Settings,
useMaterialIcon: true,
imageSize: 24f
);
// === API ===
// ItemId로 Command 실행
bool ok = menuView.ExecuteCommand(""file_new"");
// ItemId로 데이터 조회
if (menuView.TryGetMenuItemData(""file_save"", out var d))
Debug.Log($""Enabled: {d?.IsEnabled}"");",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
<!-- 기본 가로 메뉴 (C#에서 동적 생성) -->
<utk:UTKTopMenu />
<!-- UXML 어트리뷰트로 설정 -->
<utk:UTKTopMenu
orientation=""Vertical""
item-spacing=""4"" />
</ui:UXML>");
}
/// <summary>
/// 가로 메뉴 초기화
/// </summary>
private void InitializeHorizontalMenu(VisualElement root)
{
var container = root.Q<VisualElement>("horizontal-menu-container");
if (container == null) return;
_sampleMenuHorizontal = new UTKTopMenu();
_sampleModelHorizontal = new UTKTopMenuModel();
// 파일 메뉴
var fileMenu = new UTKMenuItemData("s_file", "파일");
fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_new", "새 파일", new DebugLogCommand("새 파일 생성"), shortcut: "Ctrl+N"));
fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_open", "열기", new DebugLogCommand("파일 열기"), shortcut: "Ctrl+O"));
fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_save", "저장", new DebugLogCommand("파일 저장"), shortcut: "Ctrl+S"));
fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_exit", "종료", new DebugLogCommand("종료"), shortcut: "Alt+F4"));
_sampleModelHorizontal.AddMenuItem(fileMenu);
// 편집 메뉴
var editMenu = new UTKMenuItemData("s_edit", "편집");
editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_undo", "실행 취소", new DebugLogCommand("실행 취소"), shortcut: "Ctrl+Z"));
editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_redo", "다시 실행", new DebugLogCommand("다시 실행"), shortcut: "Ctrl+Y"));
editMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_copy", "복사", new DebugLogCommand("복사"), shortcut: "Ctrl+C"));
editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_paste", "붙여넣기", new DebugLogCommand("붙여넣기"), shortcut: "Ctrl+V"));
_sampleModelHorizontal.AddMenuItem(editMenu);
// 보기 메뉴 (2 depth)
var viewMenu = new UTKMenuItemData("s_view", "보기");
var layoutMenu = new UTKMenuItemData("s_view_layout", "레이아웃");
layoutMenu.AddSubMenuItem(new UTKMenuItemData("s_view_layout_default", "기본", new DebugLogCommand("기본 레이아웃")));
layoutMenu.AddSubMenuItem(new UTKMenuItemData("s_view_layout_wide", "와이드", new DebugLogCommand("와이드 레이아웃")));
viewMenu.AddSubMenuItem(layoutMenu);
viewMenu.AddSubMenuItem(new UTKMenuItemData("s_view_fullscreen", "전체 화면", new DebugLogCommand("전체 화면"), shortcut: "F11"));
_sampleModelHorizontal.AddMenuItem(viewMenu);
// 도움말 메뉴
var helpMenu = new UTKMenuItemData("s_help", "도움말");
helpMenu.AddSubMenuItem(new UTKMenuItemData("s_help_doc", "문서", new DebugLogCommand("문서 열기"), shortcut: "F1"));
helpMenu.AddSubMenuItem(new UTKMenuItemData("s_help_about", "정보", new DebugLogCommand("정보")));
_sampleModelHorizontal.AddMenuItem(helpMenu);
// View에 메뉴 생성
if (_sampleMenuHorizontal.MenuContainer != null)
{
_sampleMenuHorizontal.CreateMenuItems(_sampleModelHorizontal.MenuItems, _sampleMenuHorizontal.MenuContainer);
}
_sampleMenuHorizontal.OnMenuItemClicked += OnSampleMenuClicked;
container.Add(_sampleMenuHorizontal);
}
/// <summary>
/// 아이콘 메뉴 초기화 (ItemSpacing 적용)
/// </summary>
private void InitializeIconMenu(VisualElement root)
{
var container = root.Q<VisualElement>("icon-menu-container");
if (container == null) return;
_sampleMenuIcon = new UTKTopMenu();
_sampleMenuIcon.ItemSpacing = 4f;
_sampleModelIcon = new UTKTopMenuModel();
// 홈 아이콘
var homeMenu = new UTKMenuImageItemData("s_icon_home", UTKMaterialIcons.Home, useMaterialIcon: true, imageSize: 24f);
homeMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_home_dashboard", "대시보드", new DebugLogCommand("대시보드")));
homeMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_home_recent", "최근 항목", new DebugLogCommand("최근 항목")));
_sampleModelIcon.AddMenuItem(homeMenu);
// 설정 아이콘
var settingsMenu = new UTKMenuImageItemData("s_icon_settings", UTKMaterialIcons.Settings, useMaterialIcon: true, imageSize: 24f);
settingsMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_settings_general", "일반", new DebugLogCommand("일반 설정")));
settingsMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_settings_display", "화면", new DebugLogCommand("화면 설정")));
_sampleModelIcon.AddMenuItem(settingsMenu);
// 알림 아이콘
var notifMenu = new UTKMenuImageItemData("s_icon_notif", UTKMaterialIcons.Notifications, useMaterialIcon: true, imageSize: 24f);
notifMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_notif_all", "전체 알림", new DebugLogCommand("전체 알림")));
notifMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_notif_clear", "알림 지우기", new DebugLogCommand("알림 지우기")));
_sampleModelIcon.AddMenuItem(notifMenu);
// 계정 아이콘
var accountMenu = new UTKMenuImageItemData("s_icon_account", UTKMaterialIcons.AccountCircle, useMaterialIcon: true, imageSize: 24f);
accountMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_account_profile", "프로필", new DebugLogCommand("프로필")));
accountMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_account_logout", "로그아웃", new DebugLogCommand("로그아웃")));
_sampleModelIcon.AddMenuItem(accountMenu);
if (_sampleMenuIcon.MenuContainer != null)
{
_sampleMenuIcon.CreateMenuItems(_sampleModelIcon.MenuItems, _sampleMenuIcon.MenuContainer);
}
_sampleMenuIcon.OnMenuItemClicked += OnSampleMenuClicked;
container.Add(_sampleMenuIcon);
}
/// <summary>
/// 세로 메뉴 초기화
/// </summary>
private void InitializeVerticalMenu(VisualElement root)
{
var container = root.Q<VisualElement>("vertical-menu-container");
if (container == null) return;
_sampleMenuVertical = new UTKTopMenu();
_sampleMenuVertical.Orientation = UTKMenuOrientation.Vertical;
_sampleMenuVertical.ItemSpacing = 2f;
_sampleMenuVertical.SubMenuOffsetX = -10f;
_sampleMenuVertical.SubMenuOffsetY = 4f;
_sampleModelVertical = new UTKTopMenuModel();
// 파일
var fileMenu = new UTKMenuItemData("sv_file", "파일");
fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_new", "새 파일", new DebugLogCommand("세로: 새 파일"), shortcut: "Ctrl+N"));
fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_open", "열기", new DebugLogCommand("세로: 열기"), shortcut: "Ctrl+O"));
fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_save", "저장", new DebugLogCommand("세로: 저장"), shortcut: "Ctrl+S"));
_sampleModelVertical.AddMenuItem(fileMenu);
// 편집
var editMenu = new UTKMenuItemData("sv_edit", "편집");
editMenu.AddSubMenuItem(new UTKMenuItemData("sv_edit_undo", "실행 취소", new DebugLogCommand("세로: 실행 취소"), shortcut: "Ctrl+Z"));
editMenu.AddSubMenuItem(new UTKMenuItemData("sv_edit_redo", "다시 실행", new DebugLogCommand("세로: 다시 실행"), shortcut: "Ctrl+Y"));
_sampleModelVertical.AddMenuItem(editMenu);
// 보기
var viewMenu = new UTKMenuItemData("sv_view", "보기");
viewMenu.AddSubMenuItem(new UTKMenuItemData("sv_view_fullscreen", "전체 화면", new DebugLogCommand("세로: 전체 화면"), shortcut: "F11"));
_sampleModelVertical.AddMenuItem(viewMenu);
// 도움말
var helpMenu = new UTKMenuItemData("sv_help", "도움말");
helpMenu.AddSubMenuItem(new UTKMenuItemData("sv_help_about", "정보", new DebugLogCommand("세로: 정보")));
_sampleModelVertical.AddMenuItem(helpMenu);
if (_sampleMenuVertical.MenuContainer != null)
{
_sampleMenuVertical.CreateMenuItems(_sampleModelVertical.MenuItems, _sampleMenuVertical.MenuContainer);
}
_sampleMenuVertical.OnMenuItemClicked += OnSampleMenuClicked;
container.Add(_sampleMenuVertical);
}
/// <summary>
/// API 테스트 버튼 초기화
/// </summary>
private void InitializeApiTest(VisualElement root)
{
var container = root.Q<VisualElement>("api-test-container");
var resultLabel = root.Q<Label>("api-result-label");
if (container == null || resultLabel == null) return;
// ExecuteCommand 테스트
var executeBtn = new UTKButton("ExecuteCommand");
executeBtn.OnClicked += () =>
{
if (_sampleMenuHorizontal == null) return;
bool result = _sampleMenuHorizontal.ExecuteCommand("s_file_new");
resultLabel.text = $"ExecuteCommand(\"s_file_new\"): {result}";
};
container.Add(executeBtn);
// TryGetMenuItemData 테스트
var getDataBtn = new UTKButton("TryGetMenuItemData");
getDataBtn.OnClicked += () =>
{
if (_sampleMenuHorizontal == null) return;
if (_sampleMenuHorizontal.TryGetMenuItemData("s_file_save", out var data))
{
resultLabel.text = $"TryGetMenuItemData(\"s_file_save\"): " +
$"DisplayName={data?.DisplayName}, " +
$"IsEnabled={data?.IsEnabled}, " +
$"Shortcut={data?.Shortcut}";
}
else
{
resultLabel.text = "TryGetMenuItemData(\"s_file_save\"): 찾을 수 없음";
}
};
container.Add(getDataBtn);
// CloseAllOpenSubMenus 테스트
var closeBtn = new UTKButton("CloseAll");
closeBtn.OnClicked += () =>
{
_sampleMenuHorizontal?.CloseAllOpenSubMenus();
_sampleMenuIcon?.CloseAllOpenSubMenus();
_sampleMenuVertical?.CloseAllOpenSubMenus();
resultLabel.text = "모든 서브메뉴 닫기 완료";
};
container.Add(closeBtn);
}
/// <summary>
/// 샘플 메뉴 클릭 핸들러
/// </summary>
private void OnSampleMenuClicked(UTKMenuItemData itemData)
{
if (itemData == null) return;
Debug.Log($"[UTKTopMenu Sample] 클릭: {itemData.ItemId} - {itemData.DisplayName}");
if (itemData.Command != null)
{
itemData.Command.Execute(itemData.CommandParameter);
}
}
/// <summary>
/// 메뉴 샘플 리소스 정리
/// </summary>
private void CleanupTopMenuSample()
{
if (_sampleMenuHorizontal != null)
{
_sampleMenuHorizontal.OnMenuItemClicked -= OnSampleMenuClicked;
_sampleMenuHorizontal.Dispose();
_sampleMenuHorizontal = null;
}
if (_sampleMenuIcon != null)
{
_sampleMenuIcon.OnMenuItemClicked -= OnSampleMenuClicked;
_sampleMenuIcon.Dispose();
_sampleMenuIcon = null;
}
if (_sampleMenuVertical != null)
{
_sampleMenuVertical.OnMenuItemClicked -= OnSampleMenuClicked;
_sampleMenuVertical.Dispose();
_sampleMenuVertical = null;
}
_sampleModelHorizontal?.Dispose();
_sampleModelHorizontal = null;
_sampleModelIcon?.Dispose();
_sampleModelIcon = null;
_sampleModelVertical?.Dispose();
_sampleModelVertical = null;
}
#endregion
}

View File

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

View File

@@ -0,0 +1,328 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
/// <summary>
/// UTKStyleGuideSample의 ToolBar 카테고리 Initialize 메서드들
/// </summary>
public partial class UTKStyleGuideSample
{
#region ToolBar Fields
private UTKToolBar? _sampleToolBarHorizontal;
private UTKToolBarModel? _sampleToolBarModelHorizontal;
private UTKToolBar? _sampleToolBarVertical;
private UTKToolBarModel? _sampleToolBarModelVertical;
private Label? _toolBarEventLogLabel;
private readonly List<string> _toolBarEventLogs = new();
private const int MaxToolBarLogLines = 5;
#endregion
#region ToolBar Initializers
private void InitializeToolBarSample(VisualElement root)
{
_toolBarEventLogLabel = root.Q<Label>("event-log-label");
InitializeHorizontalToolBar(root);
InitializeVerticalToolBar(root);
InitializeToolBarApiTest(root);
SetCodeSamples(root,
csharpCode: @"// === 가로 툴바 ===
var model = new UTKToolBarModel();
// Standard 버튼
model.AddStandardButton(""저장"", UTKMaterialIcons.Save);
// 구분선
model.AddSeparator();
// Toggle 버튼
model.AddToggleButton(""그리드"", false,
UTKMaterialIcons.GridOn,
UTKMaterialIcons.GridOff);
// Radio 버튼 그룹
model.AddRadioButton(""tool"", ""선택"", true,
UTKMaterialIcons.NearMe);
model.AddRadioButton(""tool"", ""이동"", false,
UTKMaterialIcons.OpenWith);
model.AddSeparator();
// Expandable 버튼 (서브 메뉴)
var shapes = model.AddExpandableButton(""도형"",
UTKMaterialIcons.Category, updateIconOnSelection: true);
shapes.SubButtons.Add(
new UTKToolBarStandardButtonData
{ Text = ""사각형"", IconPath = UTKMaterialIcons.CropSquare,
UseMaterialIcon = true });
shapes.SubButtons.Add(
new UTKToolBarStandardButtonData
{ Text = ""원형"", IconPath = UTKMaterialIcons.Circle,
UseMaterialIcon = true });
// View 생성 및 빌드
var toolbar = new UTKToolBar();
toolbar.Orientation = UTKToolBarOrientation.Horizontal;
parent.Add(toolbar);
toolbar.BuildToolBar(model);
// 이벤트 구독
toolbar.OnAction += args =>
Debug.Log($""{args.Text}: {args.ActionType}"");
// === 세로 배치로 전환 ===
toolbar.SetOrientation(UTKToolBarOrientation.Vertical);
// === API ===
// 토글 상태 변경
model.SetToggleButtonStateByText(""그리드"", true);
// 라디오 선택 변경
model.SetRadioButtonSelectionByText(""tool"", ""이동"");",
uxmlCode: @"<!-- 네임스페이스 선언 -->
<ui:UXML xmlns:utk=""UVC.UIToolkit"">
<!-- UXML에서 직접 사용 -->
<utk:UTKToolBar orientation=""Horizontal""
item-spacing=""2""
toolbar-size=""40"" />
<!-- 세로 배치 -->
<utk:UTKToolBar orientation=""Vertical"" />
</ui:UXML>");
}
/// <summary>
/// 가로 툴바 초기화
/// </summary>
private void InitializeHorizontalToolBar(VisualElement root)
{
var container = root.Q<VisualElement>("horizontal-toolbar-container");
if (container == null) return;
_sampleToolBarModelHorizontal = new UTKToolBarModel();
// Standard 버튼들
_sampleToolBarModelHorizontal.AddStandardButton("저장", UTKMaterialIcons.Save, new DebugLogCommand("저장"), "파일 저장");
_sampleToolBarModelHorizontal.AddStandardButton("실행 취소", UTKMaterialIcons.Undo, new DebugLogCommand("실행 취소"), "실행 취소");
_sampleToolBarModelHorizontal.AddStandardButton("다시 실행", UTKMaterialIcons.Redo, new DebugLogCommand("다시 실행"), "다시 실행");
_sampleToolBarModelHorizontal.AddSeparator();
// Toggle 버튼들
_sampleToolBarModelHorizontal.AddToggleButton("그리드", false,
UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff,
isOn => AppendToolBarLog($"그리드: {(isOn ? "ON" : "OFF")}"),
tooltip: "그리드 표시/숨김");
_sampleToolBarModelHorizontal.AddToggleButton("스냅", false,
UTKMaterialIcons.FilterCenterFocus, UTKMaterialIcons.CenterFocusWeak,
isOn => AppendToolBarLog($"스냅: {(isOn ? "ON" : "OFF")}"),
tooltip: "스냅 활성화/비활성화");
_sampleToolBarModelHorizontal.AddSeparator();
// Radio 버튼 그룹 (도구 선택)
_sampleToolBarModelHorizontal.AddRadioButton("tool", "선택", true,
UTKMaterialIcons.NearMe, null,
isOn => { if (isOn) AppendToolBarLog("도구: 선택"); },
tooltip: "선택 도구");
_sampleToolBarModelHorizontal.AddRadioButton("tool", "이동", false,
UTKMaterialIcons.OpenWith, null,
isOn => { if (isOn) AppendToolBarLog("도구: 이동"); },
tooltip: "이동 도구");
_sampleToolBarModelHorizontal.AddRadioButton("tool", "회전", false,
UTKMaterialIcons.Refresh, null,
isOn => { if (isOn) AppendToolBarLog("도구: 회전"); },
tooltip: "회전 도구");
_sampleToolBarModelHorizontal.AddRadioButton("tool", "크기", false,
UTKMaterialIcons.AspectRatio, null,
isOn => { if (isOn) AppendToolBarLog("도구: 크기"); },
tooltip: "크기 도구");
_sampleToolBarModelHorizontal.AddSeparator();
// Expandable 버튼 (도형)
var shapeBtn = _sampleToolBarModelHorizontal.AddExpandableButton("도형",
UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "삼각형", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true });
// View 생성
_sampleToolBarHorizontal = new UTKToolBar();
_sampleToolBarHorizontal.Orientation = UTKToolBarOrientation.Horizontal;
_sampleToolBarHorizontal.OnAction += OnToolBarSampleAction;
_sampleToolBarHorizontal.BuildToolBar(_sampleToolBarModelHorizontal);
container.Add(_sampleToolBarHorizontal);
}
/// <summary>
/// 세로 툴바 초기화
/// </summary>
private void InitializeVerticalToolBar(VisualElement root)
{
var container = root.Q<VisualElement>("vertical-toolbar-container");
if (container == null) return;
_sampleToolBarModelVertical = new UTKToolBarModel();
// Standard 버튼들 (아이콘만)
_sampleToolBarModelVertical.AddStandardButton("", UTKMaterialIcons.Save, new DebugLogCommand("세로: 저장"), "저장");
_sampleToolBarModelVertical.AddStandardButton("", UTKMaterialIcons.Undo, new DebugLogCommand("세로: 실행 취소"), "실행 취소");
_sampleToolBarModelVertical.AddSeparator();
// Toggle 버튼
_sampleToolBarModelVertical.AddToggleButton("", false,
UTKMaterialIcons.Visibility, UTKMaterialIcons.VisibilityOff,
tooltip: "표시/숨김");
_sampleToolBarModelVertical.AddToggleButton("", false,
UTKMaterialIcons.Lock, UTKMaterialIcons.LockOpen,
tooltip: "잠금/해제");
_sampleToolBarModelVertical.AddSeparator();
// Radio 버튼 그룹
_sampleToolBarModelVertical.AddRadioButton("vtool", "", true,
UTKMaterialIcons.NearMe, null, tooltip: "선택 도구");
_sampleToolBarModelVertical.AddRadioButton("vtool", "", false,
UTKMaterialIcons.OpenWith, null, tooltip: "이동 도구");
_sampleToolBarModelVertical.AddRadioButton("vtool", "", false,
UTKMaterialIcons.Refresh, null, tooltip: "회전 도구");
_sampleToolBarModelVertical.AddSeparator();
// Expandable 버튼 (도형)
var shapeBtn = _sampleToolBarModelVertical.AddExpandableButton("",
UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "삼각형", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true });
// View 생성
_sampleToolBarVertical = new UTKToolBar();
_sampleToolBarVertical.Orientation = UTKToolBarOrientation.Vertical;
_sampleToolBarVertical.OnAction += OnToolBarSampleAction;
_sampleToolBarVertical.BuildToolBar(_sampleToolBarModelVertical);
container.Add(_sampleToolBarVertical);
}
/// <summary>
/// API 테스트 버튼 초기화
/// </summary>
private void InitializeToolBarApiTest(VisualElement root)
{
var container = root.Q<VisualElement>("api-test-container");
var resultLabel = root.Q<Label>("api-result-label");
if (container == null || resultLabel == null) return;
// SetToggleButtonState 테스트
var toggleBtn = new UTKButton("Toggle Grid ON");
toggleBtn.OnClicked += () =>
{
if (_sampleToolBarModelHorizontal == null) return;
_sampleToolBarModelHorizontal.SetToggleButtonStateByText("그리드", true, true);
resultLabel.text = "SetToggleButtonStateByText(\"그리드\", true) 실행됨";
};
container.Add(toggleBtn);
// SetRadioButtonSelection 테스트
var radioBtn = new UTKButton("Select 이동");
radioBtn.OnClicked += () =>
{
if (_sampleToolBarModelHorizontal == null) return;
_sampleToolBarModelHorizontal.SetRadioButtonSelectionByText("tool", "이동", true);
resultLabel.text = "SetRadioButtonSelectionByText(\"tool\", \"이동\") 실행됨";
};
container.Add(radioBtn);
// ClearRadioButtonSelection 테스트
var clearBtn = new UTKButton("Clear Radio");
clearBtn.OnClicked += () =>
{
if (_sampleToolBarModelHorizontal == null) return;
_sampleToolBarModelHorizontal.ClearRadioButtonSelection("tool", true);
resultLabel.text = "ClearRadioButtonSelection(\"tool\") 실행됨";
};
container.Add(clearBtn);
// GetToggleButtonState 테스트
var getStateBtn = new UTKButton("Get Grid State");
getStateBtn.OnClicked += () =>
{
if (_sampleToolBarModelHorizontal == null) return;
bool state = _sampleToolBarModelHorizontal.GetToggleButtonState("그리드");
resultLabel.text = $"GetToggleButtonState(\"\"): {state}";
};
container.Add(getStateBtn);
}
/// <summary>
/// 툴바 액션 이벤트 핸들러
/// </summary>
private void OnToolBarSampleAction(UTKToolBarActionEventArgs args)
{
string valueStr = args.Value != null ? args.Value.ToString() ?? "" : "null";
string logEntry = $"[{args.ActionType}] {args.Text}: {valueStr}";
Debug.Log($"[UTKToolBar Sample] {logEntry}");
AppendToolBarLog(logEntry);
}
/// <summary>
/// 이벤트 로그에 항목을 추가합니다.
/// </summary>
private void AppendToolBarLog(string message)
{
_toolBarEventLogs.Add(message);
while (_toolBarEventLogs.Count > MaxToolBarLogLines)
{
_toolBarEventLogs.RemoveAt(0);
}
if (_toolBarEventLogLabel != null)
{
_toolBarEventLogLabel.text = string.Join("\n", _toolBarEventLogs);
}
}
/// <summary>
/// 툴바 샘플 리소스 정리
/// </summary>
private void CleanupToolBarSample()
{
if (_sampleToolBarHorizontal != null)
{
_sampleToolBarHorizontal.OnAction -= OnToolBarSampleAction;
_sampleToolBarHorizontal.Dispose();
_sampleToolBarHorizontal = null;
}
if (_sampleToolBarVertical != null)
{
_sampleToolBarVertical.OnAction -= OnToolBarSampleAction;
_sampleToolBarVertical.Dispose();
_sampleToolBarVertical = null;
}
_sampleToolBarModelHorizontal?.Dispose();
_sampleToolBarModelHorizontal = null;
_sampleToolBarModelVertical?.Dispose();
_sampleToolBarModelVertical = null;
_toolBarEventLogs.Clear();
_toolBarEventLogLabel = null;
}
#endregion
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9442e732448071f4eb1183d73ac6421b

View File

@@ -100,6 +100,10 @@ public partial class UTKStyleGuideSample : MonoBehaviour
// Picker
["UTKColorPicker"] = "UIToolkit/Sample/Picker/UTKColorPickerSample",
["UTKDatePicker"] = "UIToolkit/Sample/Picker/UTKDatePickerSample",
// Menu
["UTKTopMenu"] = "UIToolkit/Sample/Menu/UTKTopMenuSample",
// ToolBar
["UTKToolBar"] = "UIToolkit/Sample/ToolBar/UTKToolBarSample",
// Window
["UTKAccordionListWindow"] = "UIToolkit/Sample/Window/UTKAccordionListWindowSample",
["UTKComponentListWindow"] = "UIToolkit/Sample/Window/UTKComponentListWindowSample",
@@ -122,6 +126,8 @@ public partial class UTKStyleGuideSample : MonoBehaviour
["Tab"] = new[] { "UTKTabView" },
["Modal"] = new[] { "UTKAlert", "UTKToast", "UTKTooltip" },
["Picker"] = new[] { "UTKColorPicker", "UTKDatePicker" },
["Menu"] = new[] { "UTKTopMenu" },
["ToolBar"] = new[] { "UTKToolBar" },
["Window"] = new[] { "UTKAccordionListWindow", "UTKComponentListWindow", "UTKComponentTabListWindow", "UTKImageListWindow", "UTKTreeListWindow", "UTKPropertyListWindow" },
};
@@ -558,6 +564,14 @@ public partial class UTKStyleGuideSample : MonoBehaviour
case "UTKImage":
InitializeUTKImageSample(root);
break;
// Menu
case "UTKTopMenu":
InitializeTopMenuSample(root);
break;
// ToolBar
case "UTKToolBar":
InitializeToolBarSample(root);
break;
// Window
case "UTKAccordionListWindow":
InitializeAccordionListWindowSample(root);

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: 54e4f33c8b08cb54f97dbdb5edd79e1e, type: 3}
m_SortingOrder: 1
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: 39265a781c40bdb4a90aa56b0fbf44a6, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.Sample.UIToolkit.UTKToolBarSample
_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: 735821fd3c67e3f49a83cb87e5cf142e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
using UVC.Log;
namespace UVC.Sample.UIToolkit
{
/// <summary>
/// UTKToolBar 독립 실행 샘플 코드입니다.
/// 가로/세로 배치 전환, 4가지 버튼 타입, 구분선을 데모합니다.
/// </summary>
public class UTKToolBarSample : MonoBehaviour
{
[SerializeField] private UIDocument? _uiDocument;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle? _themeToggle;
private VisualElement? _root;
private UTKToolBar? _horizontalToolBar;
private UTKToolBar? _verticalToolBar;
private UTKToolBarModel? _horizontalModel;
private UTKToolBarModel? _verticalModel;
private Label? _eventLabel;
private readonly List<string> _eventLogs = new();
private const int MaxLogLines = 8;
private void Start()
{
// UIDocument 참조 확인
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("UIDocument가 할당되지 않았습니다.");
return;
}
_uiDocument = doc;
_root = _uiDocument.rootVisualElement;
UTKThemeManager.Instance.RegisterRoot(_root);
UTKThemeManager.Instance.SetTheme(initialTheme);
// 테마 토글
_themeToggle = _root.Q<UTKToggle>("toggle");
if (_themeToggle != null)
{
_themeToggle.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
};
}
// 툴바 영역
var toolbarArea = _root.Q<VisualElement>("toolbar-area");
if (toolbarArea == null)
{
toolbarArea = new VisualElement();
toolbarArea.style.flexDirection = FlexDirection.Column;
toolbarArea.style.paddingTop = 8;
toolbarArea.style.paddingLeft = 8;
toolbarArea.style.paddingRight = 8;
_root.Add(toolbarArea);
}
// 이벤트 라벨
_eventLabel = new Label("(버튼을 클릭하면 여기에 이벤트가 표시됩니다)");
_eventLabel.style.fontSize = 11;
_eventLabel.style.whiteSpace = WhiteSpace.Normal;
_eventLabel.style.marginTop = 12;
CreateHorizontalToolBar(toolbarArea);
CreateVerticalToolBar(toolbarArea);
toolbarArea.Add(_eventLabel);
}
/// <summary>
/// 가로 툴바를 생성합니다.
/// </summary>
private void CreateHorizontalToolBar(VisualElement parent)
{
var label = new Label("Horizontal Toolbar");
label.style.fontSize = 14;
label.style.marginTop = 8;
label.style.marginBottom = 4;
parent.Add(label);
_horizontalModel = new UTKToolBarModel();
// Standard
_horizontalModel.AddStandardButton("저장", UTKMaterialIcons.Save, new DebugLogCommand("저장"), "파일 저장");
_horizontalModel.AddStandardButton("실행 취소", UTKMaterialIcons.Undo, new DebugLogCommand("실행 취소"));
_horizontalModel.AddStandardButton("다시 실행", UTKMaterialIcons.Redo, new DebugLogCommand("다시 실행"));
_horizontalModel.AddSeparator();
// Toggle
_horizontalModel.AddToggleButton("그리드", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff, tooltip: "그리드 표시/숨김");
_horizontalModel.AddToggleButton("스냅", false, UTKMaterialIcons.FilterCenterFocus, UTKMaterialIcons.CenterFocusWeak, tooltip: "스냅 활성화");
_horizontalModel.AddSeparator();
// Radio
_horizontalModel.AddRadioButton("tool", "선택", true, UTKMaterialIcons.NearMe, tooltip: "선택 도구");
_horizontalModel.AddRadioButton("tool", "이동", false, UTKMaterialIcons.OpenWith, tooltip: "이동 도구");
_horizontalModel.AddRadioButton("tool", "회전", false, UTKMaterialIcons.Refresh, tooltip: "회전 도구");
_horizontalModel.AddSeparator();
// Expandable
var shapeBtn = _horizontalModel.AddExpandableButton("도형", UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "삼각형", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true });
_horizontalToolBar = new UTKToolBar();
_horizontalToolBar.Orientation = UTKToolBarOrientation.Horizontal;
_horizontalToolBar.OnAction += OnToolBarAction;
_horizontalToolBar.BuildToolBar(_horizontalModel);
parent.Add(_horizontalToolBar);
}
/// <summary>
/// 세로 툴바를 생성합니다.
/// </summary>
private void CreateVerticalToolBar(VisualElement parent)
{
var label = new Label("Vertical Toolbar");
label.style.fontSize = 14;
label.style.marginTop = 12;
label.style.marginBottom = 4;
parent.Add(label);
_verticalModel = new UTKToolBarModel();
_verticalModel.AddStandardButton("", UTKMaterialIcons.Save, new DebugLogCommand("세로: 저장"), "저장");
_verticalModel.AddStandardButton("", UTKMaterialIcons.Undo, new DebugLogCommand("세로: 실행 취소"), "실행 취소");
_verticalModel.AddSeparator();
_verticalModel.AddToggleButton("", false, UTKMaterialIcons.Visibility, UTKMaterialIcons.VisibilityOff, tooltip: "표시/숨김");
_verticalModel.AddToggleButton("", false, UTKMaterialIcons.Lock, UTKMaterialIcons.LockOpen, tooltip: "잠금/해제");
_verticalModel.AddSeparator();
_verticalModel.AddRadioButton("vtool", "", true, UTKMaterialIcons.NearMe, tooltip: "선택 도구");
_verticalModel.AddRadioButton("vtool", "", false, UTKMaterialIcons.OpenWith, tooltip: "이동 도구");
_verticalModel.AddRadioButton("vtool", "", false, UTKMaterialIcons.Refresh, tooltip: "회전 도구");
_verticalModel.AddSeparator();
// Expandable
var shapeBtn = _verticalModel.AddExpandableButton("", UTKMaterialIcons.Category, tooltip: "도형 추가", updateIconOnSelection: true);
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
shapeBtn.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "삼각형", IconPath = UTKMaterialIcons.ChangeHistory, UseMaterialIcon = true });
_verticalToolBar = new UTKToolBar();
_verticalToolBar.Orientation = UTKToolBarOrientation.Vertical;
_verticalToolBar.OnAction += OnToolBarAction;
_verticalToolBar.BuildToolBar(_verticalModel);
parent.Add(_verticalToolBar);
}
/// <summary>
/// 툴바 액션 이벤트 핸들러
/// </summary>
private void OnToolBarAction(UTKToolBarActionEventArgs args)
{
string valueStr = args.Value != null ? args.Value.ToString() ?? "" : "null";
string logEntry = $"[{args.ActionType}] {args.Text}: {valueStr}";
ULog.Debug($"[UTKToolBar Sample] {logEntry}");
AppendLog(logEntry);
}
/// <summary>
/// 이벤트 로그에 항목을 추가합니다.
/// </summary>
private void AppendLog(string message)
{
_eventLogs.Add(message);
while (_eventLogs.Count > MaxLogLines)
{
_eventLogs.RemoveAt(0);
}
if (_eventLabel != null)
{
_eventLabel.text = string.Join("\n", _eventLogs);
}
}
private void OnDestroy()
{
if (_horizontalToolBar != null)
{
_horizontalToolBar.OnAction -= OnToolBarAction;
_horizontalToolBar.Dispose();
_horizontalToolBar = null;
}
if (_verticalToolBar != null)
{
_verticalToolBar.OnAction -= OnToolBarAction;
_verticalToolBar.Dispose();
_verticalToolBar = null;
}
_horizontalModel?.Dispose();
_horizontalModel = null;
_verticalModel?.Dispose();
_verticalModel = null;
_eventLogs.Clear();
ULog.Debug("UTKToolBarSample 정리 완료");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39265a781c40bdb4a90aa56b0fbf44a6

View File

@@ -0,0 +1,6 @@
<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;" />
</VisualElement>
</UXML>

View File

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

View File

@@ -58,7 +58,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var boundsField = new UTKBoundsField("경계");
/// boundsField.ErrorMessage = "크기는 양수여야 합니다.";
/// boundsField.Validation = () => boundsField.Value.size.x > 0 &amp;&amp; boundsField.Value.size.y > 0 &amp;&amp; boundsField.Value.size.z > 0;

View File

@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var precisionField = new UTKDoubleField("정밀 값", 0);
/// precisionField.ErrorMessage = "값은 0보다 커야 합니다.";
/// precisionField.Validation = () => precisionField.Value > 0;

View File

@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var speedField = new UTKFloatField("속도", 0f);
/// speedField.ErrorMessage = "속도는 0보다 커야 합니다.";
/// speedField.Validation = () => speedField.Value > 0f;
@@ -58,7 +58,7 @@ namespace UVC.UIToolkit
/// // 범위 검증
/// var temperatureField = new UTKFloatField("온도", 20f);
/// temperatureField.ErrorMessage = "온도는 -40 ~ 60 사이여야 합니다.";
/// temperatureField.Validation = () => temperatureField.Value >= -40f &amp;&amp; temperatureField.Value &lt;= 60f;
/// temperatureField.Validation = () => temperatureField.Value >= -40f &amp;&amp; temperatureField.Value <= 60f;
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = speedField.Validate();

View File

@@ -66,7 +66,7 @@ namespace UVC.UIToolkit
/// <code>
/// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.5f, 0.1f);
/// volumeStepper.ErrorMessage = "볼륨은 0~1 사이여야 합니다.";
/// volumeStepper.Validation = () => volumeStepper.Value >= 0f &amp;&amp; volumeStepper.Value &lt;= 1f;
/// volumeStepper.Validation = () => volumeStepper.Value >= 0f &amp;&amp; volumeStepper.Value <= 1f;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[

View File

@@ -37,7 +37,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var emailInput = new UTKInputField("이메일", "example@email.com");
/// emailInput.ErrorMessage = "올바른 이메일 형식이 아닙니다.";
/// emailInput.Validation = () => emailInput.Value.Contains("@");

View File

@@ -85,7 +85,7 @@ namespace UVC.UIToolkit
/// <code>
/// var monthStepper = new UTKIntStepper(1, 12, 1, 1);
/// monthStepper.ErrorMessage = "유효하지 않은 월입니다.";
/// monthStepper.Validation = () => monthStepper.Value >= 1 &amp;&amp; monthStepper.Value &lt;= 12;
/// monthStepper.Validation = () => monthStepper.Value >= 1 &amp;&amp; monthStepper.Value <= 12;
///
/// bool isValid = monthStepper.Validate();
/// monthStepper.ClearError();

View File

@@ -46,10 +46,10 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var ageField = new UTKIntegerField("나이", 0);
/// ageField.ErrorMessage = "나이는 1~150 사이여야 합니다.";
/// ageField.Validation = () => ageField.Value >= 1 &amp;&amp; ageField.Value &lt;= 150;
/// ageField.Validation = () => ageField.Value >= 1 &amp;&amp; ageField.Value <= 150;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///

View File

@@ -43,7 +43,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var fileSizeField = new UTKLongField("파일 크기", 0);
/// fileSizeField.ErrorMessage = "파일 크기는 0보다 커야 합니다.";
/// fileSizeField.Validation = () => fileSizeField.Value > 0;

View File

@@ -51,7 +51,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var areaField = new UTKRectField("영역");
/// areaField.ErrorMessage = "너비와 높이는 0보다 커야 합니다.";
/// areaField.Validation = () => areaField.Value.width > 0 &amp;&amp; areaField.Value.height > 0;
@@ -91,7 +91,7 @@ namespace UVC.UIToolkit
/// var uvField = new UTKRectField("UV 영역");
/// uvField.Value = sprite.rect;
/// uvField.ErrorMessage = "UV 영역은 텍스처 범위를 초과할 수 없습니다.";
/// uvField.Validation = () => uvField.Value.xMax &lt;= texture.width &amp;&amp; uvField.Value.yMax &lt;= texture.height;
/// uvField.Validation = () => uvField.Value.xMax <= texture.width &amp;&amp; uvField.Value.yMax <= texture.height;
/// uvField.OnValueChanged += (newRect) => {
/// // 스프라이트 영역 업데이트
/// UpdateSpriteRect(sprite, newRect);

View File

@@ -53,7 +53,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var sizeField = new UTKVector2Field("크기");
/// sizeField.ErrorMessage = "크기는 양수여야 합니다.";
/// sizeField.Validation = () => sizeField.Value.x > 0 && sizeField.Value.y > 0;

View File

@@ -54,7 +54,7 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var posField = new UTKVector3Field("위치");
/// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
/// posField.Validation = () => posField.Value.y >= 0;

View File

@@ -54,10 +54,10 @@ namespace UVC.UIToolkit
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// // 검증 함수 설정 (Func<bool>)
/// var colorField = new UTKVector4Field("색상");
/// colorField.ErrorMessage = "알파 값은 0~1 사이여야 합니다.";
/// colorField.Validation = () => colorField.Value.w >= 0 &amp;&amp; colorField.Value.w &lt;= 1;
/// colorField.Validation = () => colorField.Value.w >= 0 &amp;&amp; colorField.Value.w <= 1;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///

View File

@@ -62,7 +62,7 @@ Assets/Scripts/UVC/UIToolkit/Menu/
├── UTKMenuItemBase.cs # 메뉴 아이템 베이스 클래스
├── UTKTopMenuItem.cs # 텍스트 메뉴 아이템 컴포넌트
├── UTKTopMenuImageItem.cs # 이미지 메뉴 아이템 컴포넌트
└── README.md
└── CLAUDE.md
Assets/Resources/UIToolkit/Menu/
├── UTKTopMenu.uxml # 메인 메뉴 구조
@@ -693,6 +693,21 @@ subMenuContainer.style.display = DisplayStyle.Flex; // 표시
---
## 유지보수 가이드
> **소스 코드를 수정할 때 반드시 CLAUDE.md와 코드 주석(XML 문서)도 함께 업데이트해야 합니다.**
| 변경 사항 | 업데이트 대상 |
|-----------|--------------|
| 클래스/메서드 추가·삭제·이름 변경 | CLAUDE API 문서 + 파일 구조 + 해당 클래스 XML 주석 |
| 생성자 파라미터 변경 | CLAUDE API 문서 + 사용 예제 + XML `<param>` 태그 |
| 공개 속성(Property) 추가·삭제 | CLAUDE API 테이블 + XML `<summary>` 태그 |
| 이벤트 추가·삭제 | CLAUDE 이벤트 테이블 + 메모리 관리 섹션 |
| UXML/USS 파일 추가·삭제 | CLAUDE 파일 구조 섹션 |
| 새로운 기능 추가 | CLAUDE 주요 기능 + 사용 예제 섹션 |
---
## 라이선스
이 프로젝트는 UVC 프레임워크의 일부입니다.

View File

@@ -0,0 +1,713 @@
# UIToolkit Property System
UIToolkit 기반의 Property 편집 시스템입니다. 데이터(Item)와 뷰(View)를 분리하는 MVVM 아키텍처를 적용하여, 20가지 속성 타입을 지원합니다.
## 📋 목차
- [주요 기능](#주요-기능)
- [아키텍처](#아키텍처)
- [파일 구조](#파일-구조)
- [빠른 시작](#빠른-시작)
- [사용 예제](#사용-예제)
- [지원 타입 목록](#지원-타입-목록)
- [API 문서](#api-문서)
- [메모리 관리](#메모리-관리)
- [성능 최적화](#성능-최적화)
---
## 주요 기능
**데이터/뷰 완전 분리 (MVVM)**
- Item: 순수 데이터 레이어 (UI 코드 없음)
- View: 순수 UI 레이어 (비즈니스 로직 없음)
- Bind/Unbind 패턴으로 양방향 데이터 동기화
**20가지 속성 타입 지원**
- 기본: String, Int, Float, Bool, Vector2, Vector3, Color
- 날짜: Date, DateTime, DateRange, DateTimeRange
- 선택: Dropdown, Enum, Radio, MultiSelectDropdown
- 범위: IntRange, FloatRange
- 특수: ColorState, FloatDropdown, Button
**ReadOnly 모드**
- 모든 View가 ReadOnly 상태 전환 지원
- ReadOnly 시 편집 컨트롤이 읽기 전용 InputField로 전환
- 그룹 단위 일괄 ReadOnly 설정 가능
**그룹(Group) 관리**
- 속성 아이템을 그룹으로 묶어 관리
- 그룹 접기/펼치기 지원
- 아이템 추가/제거 이벤트
**Factory 패턴**
- `UTKPropertyItemViewFactory`로 데이터 타입에 맞는 View 자동 생성
- 커스텀 View 등록/교체 가능
**완벽한 메모리 관리**
- 모든 클래스 IDisposable 구현
- Bind/Unbind 대칭 이벤트 관리
- 정적 UXML/USS 캐싱으로 반복 로드 방지
---
## 아키텍처
```
Data Layer (Items) View Layer (Views)
───────────────── ──────────────────
IUTKPropertyEntry IUTKPropertyItemView
├─ IUTKPropertyGroup IUTKPropertyItemView<T>
└─ IUTKPropertyItem<T> UTKPropertyItemViewBase (abstract)
└─ UTKPropertyItemBase<T> └─ UTK{Type}PropertyItemView
└─ UTK{Type}PropertyItem
Factory Bridge
──────────────
UTKPropertyItemViewFactory
CreateView(IUTKPropertyItem)
→ new View
→ View.Bind(item)
→ return VisualElement
```
### 데이터 흐름
```
1. Item 생성 (데이터)
var item = new UTKFloatPropertyItem("speed", "이동 속도", 1.0f);
2. Factory가 View 생성 + 바인딩
var view = UTKPropertyItemViewFactory.CreateView(item);
3. 양방향 동기화
Item.Value 변경 → OnTypedValueChanged → View.RefreshUI()
View 사용자 입력 → OnValueChanged → Item.Value 업데이트
```
---
## 파일 구조
```
Assets/Scripts/UVC/UIToolkit/Property/
├── Core/ # 인터페이스 & 핵심 타입
│ ├── IUTKPropertyEntry.cs # 트리 엔트리 베이스 인터페이스
│ ├── IUTKPropertyGroup.cs # 그룹 인터페이스
│ ├── IUTKPropertyItem.cs # 속성 아이템 인터페이스 (비제네릭/제네릭)
│ ├── UTKPropertyGroup.cs # 그룹 구현체
│ ├── UTKPropertyType.cs # 속성 타입 열거형 (20종)
│ └── UTKPropertyValueChangedEventArgs.cs # 값 변경 이벤트 인자
├── Data/ # 복합 데이터 구조체
│ ├── UTKColorState.cs # 상태-색상 쌍
│ ├── UTKRangeValue.cs # 범위 값 (Int/Float/Date/DateTime)
│ └── UTKFloatDropdownValue.cs # Float + Dropdown 복합 값
├── Items/ # 데이터 레이어 (Item 클래스)
│ ├── Base/
│ │ └── UTKPropertyItemBase.cs # Item 추상 베이스 클래스
│ ├── UTKStringPropertyItem.cs
│ ├── UTKIntPropertyItem.cs
│ ├── UTKFloatPropertyItem.cs
│ ├── UTKBoolPropertyItem.cs
│ ├── UTKVector2PropertyItem.cs
│ ├── UTKVector3PropertyItem.cs
│ ├── UTKColorPropertyItem.cs
│ ├── UTKColorStatePropertyItem.cs
│ ├── UTKDatePropertyItem.cs
│ ├── UTKDateRangePropertyItem.cs
│ ├── UTKDateTimePropertyItem.cs
│ ├── UTKDateTimeRangePropertyItem.cs
│ ├── UTKDropdownPropertyItem.cs
│ ├── UTKEnumPropertyItem.cs
│ ├── UTKRadioPropertyItem.cs
│ ├── UTKIntRangePropertyItem.cs
│ ├── UTKFloatRangePropertyItem.cs
│ ├── UTKMultiSelectDropdownPropertyItem.cs
│ ├── UTKFloatDropdownPropertyItem.cs
│ └── UTKButtonItem.cs
└── Views/ # 뷰 레이어 (View 클래스)
├── IUTKPropertyItemView.cs # View 인터페이스 (비제네릭/제네릭)
├── UTKPropertyItemViewBase.cs # View 추상 베이스 클래스
├── UTKPropertyItemViewFactory.cs # View 팩토리 (타입별 자동 생성)
├── UTKStringPropertyItemView.cs
├── UTKIntPropertyItemView.cs
├── UTKFloatPropertyItemView.cs
├── UTKBoolPropertyItemView.cs
├── UTKVector2PropertyItemView.cs
├── UTKVector3PropertyItemView.cs
├── UTKColorPropertyItemView.cs
├── UTKColorStatePropertyItemView.cs
├── UTKDatePropertyItemView.cs
├── UTKDateRangePropertyItemView.cs
├── UTKDateTimePropertyItemView.cs
├── UTKDateTimeRangePropertyItemView.cs
├── UTKDropdownPropertyItemView.cs
├── UTKEnumPropertyItemView.cs
├── UTKRadioPropertyItemView.cs
├── UTKIntRangePropertyItemView.cs
├── UTKFloatRangePropertyItemView.cs
├── UTKMultiSelectDropdownPropertyItemView.cs
├── UTKFloatDropdownPropertyItemView.cs
├── UTKButtonItemView.cs
└── CLAUDE.md
Assets/Resources/UIToolkit/Property/Views/
├── UTKPropertyItemViewCommonUss.uss # 공통 스타일 (모든 View 공유)
├── UTKStringPropertyItemView.uxml
├── UTKStringPropertyItemViewUss.uss
├── UTKIntPropertyItemView.uxml
├── UTKIntPropertyItemViewUss.uss
├── UTKFloatPropertyItemView.uxml
├── UTKFloatPropertyItemViewUss.uss
├── UTKBoolPropertyItemView.uxml
├── UTKBoolPropertyItemViewUss.uss
├── UTKVector2PropertyItemView.uxml
├── UTKVector2PropertyItemViewUss.uss
├── UTKVector3PropertyItemView.uxml
├── UTKVector3PropertyItemViewUss.uss
├── UTKColorPropertyItemView.uxml
├── UTKColorPropertyItemViewUss.uss
├── UTKColorStatePropertyItemView.uxml
├── UTKColorStatePropertyItemViewUss.uss
├── UTKDatePropertyItemView.uxml
├── UTKDatePropertyItemViewUss.uss
├── UTKDateRangePropertyItemView.uxml
├── UTKDateRangePropertyItemViewUss.uss
├── UTKDateTimePropertyItemView.uxml
├── UTKDateTimePropertyItemViewUss.uss
├── UTKDateTimeRangePropertyItemView.uxml
├── UTKDateTimeRangePropertyItemViewUss.uss
├── UTKDropdownPropertyItemView.uxml
├── UTKDropdownPropertyItemViewUss.uss
├── UTKEnumPropertyItemView.uxml
├── UTKEnumPropertyItemViewUss.uss
├── UTKRadioPropertyItemView.uxml
├── UTKRadioPropertyItemViewUss.uss
├── UTKIntRangePropertyItemView.uxml
├── UTKIntRangePropertyItemViewUss.uss
├── UTKFloatRangePropertyItemView.uxml
├── UTKFloatRangePropertyItemViewUss.uss
├── UTKMultiSelectDropdownPropertyItemView.uxml
├── UTKMultiSelectDropdownPropertyItemViewUss.uss
├── UTKFloatDropdownPropertyItemView.uxml
├── UTKFloatDropdownPropertyItemViewUss.uss
├── UTKButtonItemView.uxml
└── UTKButtonItemViewUss.uss
```
---
## 빠른 시작
### 1. 단일 속성 아이템 생성
```csharp
// 1. 데이터(Item) 생성
var nameItem = new UTKStringPropertyItem("name", "이름", "홍길동");
// 2. Factory로 View 생성 + 바인딩
var nameView = UTKPropertyItemViewFactory.CreateView(nameItem);
// 3. UI에 추가
rootVisualElement.Add(nameView);
// 4. 값 변경 감지
nameItem.OnTypedValueChanged += (item, oldVal, newVal) =>
{
Debug.Log($"이름 변경: {oldVal} → {newVal}");
};
```
### 2. 그룹으로 묶어서 관리
```csharp
// 그룹 생성
var transformGroup = new UTKPropertyGroup("transform", "Transform");
// 아이템 추가
transformGroup.AddItem(new UTKVector3PropertyItem("position", "Position", Vector3.zero));
transformGroup.AddItem(new UTKVector3PropertyItem("rotation", "Rotation", Vector3.zero));
transformGroup.AddItem(new UTKVector3PropertyItem("scale", "Scale", Vector3.one));
// 그룹 전체 ReadOnly 설정
transformGroup.SetAllItemsReadOnly(true);
```
---
## 사용 예제
### 다양한 속성 타입 생성
```csharp
// 기본 타입
var stringItem = new UTKStringPropertyItem("name", "이름", "홍길동");
var intItem = new UTKIntPropertyItem("count", "개수", 10);
var floatItem = new UTKFloatPropertyItem("speed", "속도", 1.5f);
var boolItem = new UTKBoolPropertyItem("active", "활성화", true);
// 벡터 타입
var posItem = new UTKVector2PropertyItem("pos", "위치", Vector2.zero);
var rotItem = new UTKVector3PropertyItem("rot", "회전", Vector3.zero);
// 색상 타입
var colorItem = new UTKColorPropertyItem("color", "색상", Color.white);
colorItem.UseAlpha = true; // 알파 채널 사용
// 날짜 타입
var dateItem = new UTKDatePropertyItem("start", "시작일", DateTime.Today);
dateItem.DateFormat = "yyyy-MM-dd";
// 드롭다운 타입
var dropdownItem = new UTKDropdownPropertyItem("type", "유형", "TypeA");
dropdownItem.Choices = new List<string> { "TypeA", "TypeB", "TypeC" };
// Enum 타입
var enumItem = new UTKEnumPropertyItem("align", "정렬", TextAlignment.Center);
```
### 슬라이더/스테퍼 활용
```csharp
// Int 슬라이더
var intItem = new UTKIntPropertyItem("volume", "볼륨", 50);
intItem.UseSlider = true;
intItem.MinValue = 0;
intItem.MaxValue = 100;
// Float 스테퍼
var floatItem = new UTKFloatPropertyItem("opacity", "투명도", 1.0f);
floatItem.UseStepper = true;
floatItem.Step = 0.1f;
floatItem.MinValue = 0f;
floatItem.MaxValue = 1f;
```
### 범위(Range) 타입 사용
```csharp
// Int 범위
var intRange = new UTKIntRangePropertyItem("level", "레벨 범위",
new UTKIntRange { Min = 1, Max = 10 });
intRange.UseStepper = true;
intRange.StepperStep = 1;
// Float 범위
var floatRange = new UTKFloatRangePropertyItem("temp", "온도 범위",
new UTKFloatRange { Min = -10f, Max = 40f });
// 날짜 범위
var dateRange = new UTKDateRangePropertyItem("period", "기간",
new UTKDateRange { Start = DateTime.Today, End = DateTime.Today.AddDays(30) });
```
### 멀티셀렉트 드롭다운
```csharp
var multiSelect = new UTKMultiSelectDropdownPropertyItem(
"tags", "태그", new List<string> { "UI", "3D" });
multiSelect.Choices = new List<string> { "UI", "3D", "Network", "Audio", "Physics" };
// 프로그래밍 방식 선택
multiSelect.SelectAll();
multiSelect.ClearSelection();
multiSelect.SetSelectedValues(new List<string> { "UI", "Audio" });
```
### ColorState 사용
```csharp
var colorState = new UTKColorStatePropertyItem("status", "상태",
new UTKColorState { State = "Normal", Color = Color.green });
// 상태/색상 개별 변경
colorState.SetState("Warning");
colorState.SetColor(Color.yellow);
```
### FloatDropdown 복합 타입
```csharp
var floatDropdown = new UTKFloatDropdownPropertyItem("size", "크기",
new UTKFloatDropdownValue { FloatValue = 100f, DropdownValue = "px" });
floatDropdown.Choices = new List<string> { "px", "%", "em", "rem" };
floatDropdown.UseStepper = true;
```
### Button 아이템
```csharp
var buttonItem = new UTKButtonItem("save", "저장");
buttonItem.Text = "저장하기";
buttonItem.Icon = UTKMaterialIcons.Save;
buttonItem.Variant = UTKButton.ButtonVariant.Primary;
buttonItem.ActionName = "save_action";
```
### String + ActionButton 조합
```csharp
var pathItem = new UTKStringPropertyItem("path", "파일 경로", "");
// ActionButton 설정 (찾아보기 버튼)
var browseButton = new UTKButtonItem("browse", "찾아보기");
browseButton.Icon = UTKMaterialIcons.FolderOpen;
browseButton.IconOnly = true;
pathItem.ActionButton = browseButton;
```
### 커스텀 View 등록
```csharp
// 기본 View를 커스텀 View로 교체
UTKPropertyItemViewFactory.RegisterCustomView(
UTKPropertyType.String,
() => new MyCustomStringView()
);
// 해제
UTKPropertyItemViewFactory.UnregisterCustomView(UTKPropertyType.String);
```
### 값 변경 이벤트 처리
```csharp
// 제네릭 이벤트 (타입 안전)
floatItem.OnTypedValueChanged += (item, oldVal, newVal) =>
{
Debug.Log($"{item.Name}: {oldVal} → {newVal}");
};
// 비제네릭 이벤트 (범용)
floatItem.OnValueChanged += (item, oldVal, newVal, notify) =>
{
Debug.Log($"{item.Id} changed");
};
// 상태 변경 이벤트 (ReadOnly, ShowLabel 등)
floatItem.OnStateChanged += (item) =>
{
Debug.Log($"{item.Id} state changed, ReadOnly={item.IsReadOnly}");
};
```
---
## 지원 타입 목록
| UTKPropertyType | Item 클래스 | View 클래스 | 데이터 타입 | 주요 컨트롤 |
|-----------------|------------|------------|------------|------------|
| `String` | UTKStringPropertyItem | UTKStringPropertyItemView | string | UTKInputField + ActionButton |
| `Int` | UTKIntPropertyItem | UTKIntPropertyItemView | int | UTKIntegerField + Slider/Stepper |
| `Float` | UTKFloatPropertyItem | UTKFloatPropertyItemView | float | UTKFloatField + Slider/Stepper |
| `Bool` | UTKBoolPropertyItem | UTKBoolPropertyItemView | bool | UTKToggle |
| `Vector2` | UTKVector2PropertyItem | UTKVector2PropertyItemView | Vector2 | UTKVector2Field |
| `Vector3` | UTKVector3PropertyItem | UTKVector3PropertyItemView | Vector3 | UTKVector3Field |
| `Color` | UTKColorPropertyItem | UTKColorPropertyItemView | Color | ColorPreview + UTKColorPicker |
| `ColorState` | UTKColorStatePropertyItem | UTKColorStatePropertyItemView | UTKColorState | Label + ColorPreview + Picker |
| `Date` | UTKDatePropertyItem | UTKDatePropertyItemView | DateTime | UTKInputField + UTKDatePicker |
| `DateTime` | UTKDateTimePropertyItem | UTKDateTimePropertyItemView | DateTime | UTKInputField + UTKDatePicker |
| `DateRange` | UTKDateRangePropertyItem | UTKDateRangePropertyItemView | UTKDateRange | InputField×2 + DatePicker×2 |
| `DateTimeRange` | UTKDateTimeRangePropertyItem | UTKDateTimeRangePropertyItemView | UTKDateTimeRange | InputField×2 + DatePicker×2 |
| `Enum` | UTKEnumPropertyItem | UTKEnumPropertyItemView | Enum | UTKEnumDropDown |
| `DropdownList` | UTKDropdownPropertyItem | UTKDropdownPropertyItemView | string | UTKDropdown |
| `MultiSelectDropdownList` | UTKMultiSelectDropdownPropertyItem | UTKMultiSelectDropdownPropertyItemView | List\<string\> | UTKMultiSelectDropdown |
| `RadioGroup` | UTKRadioPropertyItem | UTKRadioPropertyItemView | int (index) | UTKRadioButton × N |
| `IntRange` | UTKIntRangePropertyItem | UTKIntRangePropertyItemView | UTKIntRange | IntegerField×2 + Stepper×2 |
| `FloatRange` | UTKFloatRangePropertyItem | UTKFloatRangePropertyItemView | UTKFloatRange | FloatField×2 + Stepper×2 |
| `FloatDropdown` | UTKFloatDropdownPropertyItem | UTKFloatDropdownPropertyItemView | UTKFloatDropdownValue | FloatField/Stepper + Dropdown |
| `Button` | UTKButtonItem | UTKButtonItemView | string (actionName) | UTKButton |
---
## API 문서
### IUTKPropertyItem (데이터 인터페이스)
#### Properties
| 속성 | 타입 | 설명 |
|------|------|------|
| `Id` | string | 고유 식별자 |
| `Name` | string | 표시 이름 |
| `DisplayName` | string | 표시 이름 (Name과 동일) |
| `Description` | string | 설명 |
| `Tooltip` | string | 툴팁 텍스트 |
| `IsReadOnly` | bool | 읽기 전용 여부 |
| `IsVisible` | bool | 표시 여부 |
| `ShowLabel` | bool | 라벨 표시 여부 |
| `PropertyType` | UTKPropertyType | 속성 타입 열거형 |
| `GroupId` | string | 소속 그룹 ID |
#### Methods
| 메서드 | 설명 | 반환 |
|--------|------|------|
| `GetValue()` | 현재 값 가져오기 (object?) | object? |
| `SetValue(object?, bool)` | 값 설정 (notify로 이벤트 발생 제어) | void |
#### Events
| 이벤트 | 타입 | 설명 |
|--------|------|------|
| `OnValueChanged` | Action\<IUTKPropertyItem, object?, object?, bool\> | 값 변경 (비제네릭) |
| `OnTypedValueChanged` | Action\<IUTKPropertyItem\<T\>, T, T\> | 값 변경 (제네릭, 타입 안전) |
| `OnStateChanged` | Action\<IUTKPropertyItem\> | 상태 변경 (ReadOnly, ShowLabel 등) |
### IUTKPropertyGroup (그룹 인터페이스)
#### Properties
| 속성 | 타입 | 설명 |
|------|------|------|
| `GroupId` | string | 그룹 고유 ID |
| `GroupName` | string | 그룹 표시 이름 |
| `IsExpanded` | bool | 접기/펼치기 상태 |
| `Items` | IReadOnlyList\<IUTKPropertyItem\> | 하위 아이템 목록 |
| `ItemCount` | int | 아이템 수 |
#### Methods
| 메서드 | 설명 | 파라미터 |
|--------|------|----------|
| `AddItem(item)` | 아이템 추가 | IUTKPropertyItem |
| `RemoveItem(itemId)` | 아이템 제거 | string → bool |
| `GetItem(itemId)` | 아이템 검색 | string → IUTKPropertyItem? |
| `Clear()` | 전체 아이템 제거 | - |
| `SetAllItemsReadOnly(isReadOnly)` | 일괄 ReadOnly 설정 | bool |
#### Events
| 이벤트 | 타입 | 설명 |
|--------|------|------|
| `OnItemAdded` | Action\<IUTKPropertyGroup, IUTKPropertyItem\> | 아이템 추가 |
| `OnItemRemoved` | Action\<IUTKPropertyGroup, IUTKPropertyItem\> | 아이템 제거 |
| `OnExpandedChanged` | Action\<IUTKPropertyGroup, bool\> | 접기/펼치기 변경 |
### UTKPropertyItemViewFactory (팩토리)
#### Public Methods
| 메서드 | 설명 | 반환 |
|--------|------|------|
| `CreateView(data)` | View 생성 + 바인딩 후 VisualElement 반환 | VisualElement? |
| `CreateViewInstance(data)` | View 인스턴스만 생성 (데이터 기반 초기화) | IUTKPropertyItemView? |
| `CreateViewInstance(type)` | View 인스턴스만 생성 (타입 기반) | IUTKPropertyItemView? |
| `GetViewType(data)` | 데이터에 맞는 View Type 조회 | Type? |
| `RegisterCustomView(type, factory)` | 커스텀 View 등록 | void |
| `UnregisterCustomView(type)` | 커스텀 View 해제 | void |
| `ClearCustomViews()` | 모든 커스텀 View 해제 | void |
### UTKPropertyItemViewBase (View 베이스)
#### UxmlAttributes
| 속성 | UXML 어트리뷰트 | 타입 | 설명 |
|------|----------------|------|------|
| `Label` | `label` | string | 라벨 텍스트 |
| `IsReadOnly` | `is-read-only` | bool | 읽기 전용 |
| `IsVisible` | `is-visible` | bool | 표시 여부 |
| `TooltipText` | `tooltip-text` | string | 툴팁 |
| `ShowLabel` | `show-label` | bool | 라벨 표시 여부 |
#### Public Methods
| 메서드 | 설명 |
|--------|------|
| `Bind(data)` | 데이터 바인딩 (양방향 동기화 시작) |
| `Unbind()` | 바인딩 해제 (이벤트 구독 해제) |
| `RefreshUI()` | UI 갱신 |
| `Dispose()` | 리소스 정리 |
| `ClearAllCache()` | 정적 UXML/USS 캐시 전체 정리 (static) |
---
## 메모리 관리
### Bind/Unbind 패턴
모든 View는 `Bind()`에서 이벤트를 구독하고 `Unbind()`에서 해제합니다:
```csharp
// View 내부 구현 패턴
public void Bind(IUTKPropertyItem<float> data)
{
_boundData = data;
data.OnTypedValueChanged += OnDataValueChanged;
data.OnStateChanged += OnDataStateChanged;
RefreshUI();
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
_boundData.OnStateChanged -= OnDataStateChanged;
_boundData = null;
}
}
```
### IDisposable 구현
모든 Item과 View 클래스가 IDisposable을 구현합니다:
```csharp
// Item Dispose
var item = new UTKFloatPropertyItem("speed", "속도", 1.0f);
// ... 사용 ...
item.Dispose(); // 모든 이벤트 구독 해제
// Group Dispose
var group = new UTKPropertyGroup("transform", "Transform");
group.AddItem(item);
// ... 사용 ...
group.Dispose(); // 하위 아이템 전부 Dispose + 이벤트 해제
// View Dispose
var view = UTKPropertyItemViewFactory.CreateView(item);
// ... 사용 ...
if (view is IDisposable disposable)
disposable.Dispose(); // Unbind + 테마 구독 해제 + 콜백 해제
```
### 테마 이벤트 관리
View 베이스 클래스에서 `AttachToPanelEvent`/`DetachFromPanelEvent`로 테마 구독을 관리합니다:
```csharp
// UTKPropertyItemViewBase 내부
private void OnAttachToPanel(AttachToPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
```
---
## 성능 최적화
### 정적 UXML/USS 캐싱
View 베이스 클래스에서 UXML/USS를 정적 Dictionary로 캐싱하여 `Resources.Load` 호출을 최소화합니다:
```csharp
// UTKPropertyItemViewBase 내부
private static readonly Dictionary<string, VisualTreeAsset> _uxmlCache = new();
private static readonly Dictionary<string, StyleSheet> _ussCache = new();
private static StyleSheet? _commonUssCache;
// 최초 1회만 Resources.Load 호출, 이후 캐시에서 반환
// 결과: 20개 View 타입 × 다수 인스턴스 → 각 타입당 Resources.Load 1회
```
### Fallback UI 패턴
UXML 로드 실패 시 코드로 UI를 생성합니다:
```csharp
// UXML 존재 → UXML 기반 UI 생성
if (CreateUIFromUxml())
{
// _labelElement, _valueContainer 자동 쿼리
}
else
{
// UXML 없음 → 코드로 Fallback UI 생성
CreateUIFallback();
}
```
### ReadOnly 전환 최적화
컨트롤을 생성/파괴하지 않고 DisplayStyle 토글로 전환합니다:
```csharp
// ❌ 나쁜 예: 매번 생성/파괴
if (isReadOnly)
{
Remove(_editControl);
Add(new UTKInputField(...));
}
// ✅ 좋은 예: 미리 생성 후 DisplayStyle 전환
_editControl.style.display = isReadOnly ? DisplayStyle.None : DisplayStyle.Flex;
_readOnlyField.style.display = isReadOnly ? DisplayStyle.Flex : DisplayStyle.None;
```
### 성능 요약
| 항목 | 설명 |
|------|------|
| **UXML/USS 로드** | 정적 Dictionary 캐싱, 타입당 1회만 로드 |
| **공통 USS** | `UTKPropertyItemViewCommonUss.uss` 1회 로드 후 전체 View 공유 |
| **쿼리 캐싱** | `Q<T>()` 결과를 필드에 캐싱 (초기화 시 1회) |
| **ReadOnly 전환** | DisplayStyle 토글 (생성/파괴 없음) |
| **이벤트 관리** | Bind/Unbind로 정확한 구독/해제 대칭 |
| **GC Alloc** | LINQ 미사용, 클로저 캡처 최소화 |
---
## 문제 해결
### View가 표시되지 않는 경우
1. **Factory 사용 확인**
```csharp
var view = UTKPropertyItemViewFactory.CreateView(item);
if (view == null)
{
Debug.LogError($"지원하지 않는 PropertyType: {item.PropertyType}");
}
```
2. **Bind 호출 확인**
- `CreateView()`는 내부적으로 Bind까지 수행
- `CreateViewInstance()`는 Bind를 별도로 호출해야 함
3. **UXML/USS 경로 확인**
- 리소스 경로: `UIToolkit/Property/Views/{ViewTypeName}`
- USS 접미사: `{ViewTypeName}Uss`
### 값이 동기화되지 않는 경우
1. **Bind 상태 확인**: `Unbind()` 후 다시 `Bind()` 필요
2. **이벤트 확인**: `OnTypedValueChanged` 구독이 정상적으로 등록되었는지 확인
3. **SetValue notify 파라미터**: `SetValue(value, false)`는 이벤트를 발생시키지 않음
---
## 유지보수 가이드
> **소스 코드를 수정할 때 반드시 CLAUDE.md와 코드 주석(XML 문서)도 함께 업데이트해야 합니다.**
| 변경 사항 | 업데이트 대상 |
|-----------|--------------|
| 클래스/메서드 추가·삭제·이름 변경 | CLAUDE API 문서 + 파일 구조 + 해당 클래스 XML 주석 |
| 생성자 파라미터 변경 | CLAUDE API 문서 + 사용 예제 + XML `<param>` 태그 |
| 공개 속성(Property) 추가·삭제 | CLAUDE API 테이블 + 지원 타입 목록 + XML `<summary>` 태그 |
| 이벤트 추가·삭제 | CLAUDE 이벤트 테이블 + 메모리 관리 섹션 |
| 새로운 PropertyType 추가 | CLAUDE 지원 타입 목록 + Factory 문서 + 파일 구조 |
| UXML/USS 파일 추가·삭제 | CLAUDE 파일 구조 섹션 |
| 새로운 기능 추가 | CLAUDE 주요 기능 + 사용 예제 섹션 |
---
## 라이선스
이 프로젝트는 UVC 프레임워크의 일부입니다.
---
## 작성자
- **작성일**: 2026-02-19
- **작성자**: Claude Code Assistant
- **버전**: 1.0.0

View File

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

View File

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

View File

@@ -0,0 +1,122 @@
# UTKToolBar
UIToolkit 기반 툴바 컴포넌트입니다. 가로/세로 배치 전환을 지원하며, 4가지 버튼 타입과 구분선을 제공합니다.
## 구조
```
ToolBar/
├── Data/ # 데이터 레이어
│ ├── IUTKToolBarItem.cs # 아이템 인터페이스
│ ├── UTKToolBarButtonData.cs # 버튼 데이터 추상 클래스
│ ├── UTKToolBarStandardButtonData.cs
│ ├── UTKToolBarToggleButtonData.cs
│ ├── UTKToolBarRadioButtonData.cs
│ ├── UTKToolBarRadioButtonGroup.cs
│ ├── UTKToolBarExpandableButtonData.cs
│ └── UTKToolBarSeparatorData.cs
├── Items/ # View 아이템
│ ├── UTKToolBarButtonBase.cs # 버튼 View 추상 클래스
│ ├── UTKToolBarStandardButton.cs
│ ├── UTKToolBarToggleButton.cs
│ ├── UTKToolBarRadioButton.cs
│ ├── UTKToolBarExpandableButton.cs
│ └── UTKToolBarSeparator.cs
├── UTKToolBar.cs # 메인 View
├── UTKToolBarModel.cs # 데이터 모델 (팩토리)
└── UTKToolBarEnums.cs # 열거형, 이벤트 인자
```
## 버튼 타입
| 타입 | 설명 |
|------|------|
| **Standard** | 단순 클릭 버튼 |
| **Toggle** | On/Off 상태 전환 |
| **Radio** | 그룹 내 상호 배타적 선택 |
| **Expandable** | 서브 메뉴 확장 (Lazy Loading) |
| **Separator** | 시각적 구분선 |
## 사용법
### 기본 (View 직접 사용)
```csharp
// 1. 모델 생성
var model = new UTKToolBarModel();
model.AddStandardButton("저장", UTKMaterialIcons.Save);
model.AddSeparator();
model.AddToggleButton("그리드", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff);
model.AddRadioButton("tool", "선택", true, UTKMaterialIcons.NearMe);
model.AddRadioButton("tool", "이동", false, UTKMaterialIcons.OpenWith);
// 2. View 생성 및 빌드
var toolbar = new UTKToolBar();
toolbar.Orientation = UTKToolBarOrientation.Horizontal;
parent.Add(toolbar);
toolbar.BuildToolBar(model);
// 3. 이벤트 구독
toolbar.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
// 4. 배치 방향 전환
toolbar.SetOrientation(UTKToolBarOrientation.Vertical);
```
### Controller 사용 (MonoBehaviour)
```csharp
var controller = gameObject.AddComponent<UTKToolBarController>();
var model = new UTKToolBarModel();
// ... 모델 설정 ...
controller.SetData(model);
controller.Initialize();
controller.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
```
### 확장 버튼 (서브 메뉴)
```csharp
var shapes = model.AddExpandableButton("도형", UTKMaterialIcons.Category, updateIconOnSelection: true);
shapes.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
shapes.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
```
## API
### UTKToolBarModel
| 메서드 | 설명 |
|--------|------|
| `AddStandardButton()` | 일반 버튼 추가 |
| `AddToggleButton()` | 토글 버튼 추가 |
| `AddRadioButton()` | 라디오 버튼 추가 (자동 그룹 등록) |
| `AddExpandableButton()` | 확장 버튼 추가 |
| `AddSeparator()` | 구분선 추가 |
| `SetRadioButtonSelectionByText()` | 텍스트로 라디오 선택 |
| `SetToggleButtonStateByText()` | 텍스트로 토글 상태 변경 |
| `GetToggleButtonState()` | 토글 상태 조회 |
### UTKToolBar (View)
| 멤버 | 설명 |
|------|------|
| `Orientation` | 배치 방향 (Horizontal/Vertical) |
| `BuildToolBar(model)` | 모델로 툴바 빌드 |
| `ClearToolBar()` | 모든 아이템 제거 |
| `SetOrientation()` | 배치 방향 변경 |
| `OnAction` | 버튼 액션 이벤트 |
### 리소스 경로
```
Resources/UIToolkit/ToolBar/
├── UTKToolBar.uxml / UTKToolBarUss.uss
├── UTKToolBarButton.uxml / UTKToolBarButtonUss.uss
├── UTKToolBarToggleButton.uxml / UTKToolBarToggleButtonUss.uss
├── UTKToolBarExpandableButton.uxml / UTKToolBarExpandableButtonUss.uss
├── UTKToolBarSeparator.uxml / UTKToolBarSeparatorUss.uss
└── UTKToolBarSubMenu.uxml / UTKToolBarSubMenuUss.uss
```

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
#nullable enable
namespace UVC.UIToolkit
{
/// <summary>
/// 툴바에 추가될 수 있는 모든 항목(버튼, 구분선 등)의 기본 인터페이스입니다.
/// </summary>
public interface IUTKToolBarItem
{
/// <summary>아이템 고유 식별자</summary>
string ItemId { get; }
}
}

View File

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

View File

@@ -0,0 +1,198 @@
#nullable enable
using System;
using UVC.UI.Commands;
namespace UVC.UIToolkit
{
/// <summary>
/// 모든 툴바 버튼의 공통 데이터를 정의하는 추상 클래스입니다.
/// Text, Icon, Enabled, Tooltip, Command 등의 공통 속성과 상태 변경 이벤트를 제공합니다.
/// </summary>
public abstract class UTKToolBarButtonData : IUTKToolBarItem, IDisposable
{
#region Fields
private string _text = "";
private string? _iconPath;
private bool _isEnabled = true;
private bool _disposed;
#endregion
#region Properties
/// <summary>아이템 고유 식별자</summary>
public string ItemId { get; private set; }
/// <summary>
/// 버튼 텍스트 (다국어 키). 변경 시 OnStateChanged 이벤트 발생.
/// </summary>
public string Text
{
get => _text;
set
{
if (_text != value)
{
_text = value;
OnStateChanged?.Invoke();
}
}
}
/// <summary>
/// 아이콘 경로. Material Icon 유니코드 또는 Resources 경로.
/// 변경 시 OnStateChanged 이벤트 발생.
/// </summary>
public string? IconPath
{
get => _iconPath;
set
{
if (_iconPath != value)
{
_iconPath = value;
OnStateChanged?.Invoke();
}
}
}
/// <summary>Material Icon 사용 여부 (true: 폰트 아이콘, false: 이미지)</summary>
public bool UseMaterialIcon { get; set; } = true;
/// <summary>
/// 활성화 상태. 변경 시 OnStateChanged 이벤트 발생.
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnStateChanged?.Invoke();
}
}
}
/// <summary>툴팁 텍스트 (다국어 키)</summary>
public string? Tooltip { get; set; }
/// <summary>실행할 명령</summary>
public ICommand? ClickCommand { get; set; }
#endregion
#region Events
/// <summary>Text, Icon, Enabled 등 시각적 상태 변경 시 발생</summary>
public event Action? OnStateChanged;
/// <summary>버튼 클릭 시 발생</summary>
public event Action? OnClicked;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarButtonData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
protected UTKToolBarButtonData(string? itemId = null)
{
ItemId = itemId ?? Guid.NewGuid().ToString();
}
#endregion
#region Methods
/// <summary>
/// 클릭 실행. Command 실행 + OnClicked 이벤트 발생.
/// IUndoableCommand인 경우 UndoRedoManager를 통해 실행합니다.
/// </summary>
/// <param name="parameter">ClickCommand에 전달할 파라미터</param>
public virtual void ExecuteClick(object? parameter = null)
{
if (!IsEnabled) return;
if (ClickCommand != null)
{
if (ClickCommand is IUndoableCommand undoableCommand)
{
var undoRedoManager = UnityEngine.Object.FindAnyObjectByType<UVC.Studio.Manager.UndoRedoManager>();
if (undoRedoManager != null)
{
undoRedoManager.ExecuteCommand(undoableCommand, parameter);
}
else
{
ClickCommand.Execute(parameter);
}
}
else
{
ClickCommand.Execute(parameter);
}
}
OnClicked?.Invoke();
}
/// <summary>
/// OnStateChanged 이벤트를 수동으로 발생시킵니다.
/// 여러 속성을 변경 후 한 번에 UI 업데이트를 트리거할 때 사용합니다.
/// </summary>
public void NotifyStateChanged()
{
OnStateChanged?.Invoke();
}
/// <summary>
/// 모든 이벤트 핸들러를 해제합니다.
/// </summary>
public virtual void ClearEventHandlers()
{
OnStateChanged = null;
OnClicked = null;
}
#endregion
#region IDisposable
/// <summary>
/// 리소스를 정리합니다. Command가 IDisposable이면 함께 정리합니다.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 리소스 정리 구현.
/// </summary>
/// <param name="disposing">관리 리소스 정리 여부</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (disposing)
{
ClearEventHandlers();
if (ClickCommand is IDisposable disposableCommand)
{
disposableCommand.Dispose();
}
ClickCommand = null;
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,144 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace UVC.UIToolkit
{
/// <summary>
/// 서브 버튼 목록을 가진 확장 가능한 버튼 데이터입니다.
/// 클릭 시 서브 메뉴를 표시하고, 서브 버튼 선택 시 메인 아이콘을 업데이트할 수 있습니다.
/// </summary>
public class UTKToolBarExpandableButtonData : UTKToolBarButtonData
{
#region Fields
private UTKToolBarButtonData? _selectedSubButton;
private string _originalText = "";
#endregion
#region Properties
/// <summary>서브 버튼 목록</summary>
public List<UTKToolBarButtonData> SubButtons { get; private set; } = new();
/// <summary>서브 버튼 선택 시 메인 아이콘 업데이트 여부</summary>
public bool UpdateIconOnSelection { get; set; }
/// <summary>현재 선택된 서브 버튼</summary>
public UTKToolBarButtonData? SelectedSubButton => _selectedSubButton;
/// <summary>원본 텍스트 (서브 버튼 선택 시 변경 전 저장용)</summary>
public string OriginalText => _originalText;
/// <summary>서브 버튼 선택 콜백</summary>
public Action<UTKToolBarButtonData>? OnSubButtonSelected { get; set; }
#endregion
#region Events
/// <summary>서브 버튼 선택 변경 이벤트 (mainText, selectedSubText)</summary>
public event Action<string, string>? OnSubButtonSelectionChanged;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarExpandableButtonData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
public UTKToolBarExpandableButtonData(string? itemId = null) : base(itemId)
{
}
#endregion
#region Methods
/// <summary>
/// 원본 텍스트를 설정합니다. AddExpandableButton에서 호출됩니다.
/// </summary>
/// <param name="text">원본 텍스트</param>
public void SetOriginalText(string text)
{
_originalText = text;
}
/// <summary>
/// 서브 버튼을 선택합니다. UpdateIconOnSelection이 true이면 메인 아이콘도 업데이트합니다.
/// </summary>
/// <param name="selectedSubButton">선택할 서브 버튼</param>
public void SelectSubButton(UTKToolBarButtonData selectedSubButton)
{
if (selectedSubButton == null || !selectedSubButton.IsEnabled) return;
// 동일 버튼 재선택 시 무시
if (_selectedSubButton == selectedSubButton) return;
_selectedSubButton = selectedSubButton;
if (UpdateIconOnSelection)
{
if (Text != selectedSubButton.Text)
{
Text = selectedSubButton.Text;
}
if (IconPath != selectedSubButton.IconPath)
{
IconPath = selectedSubButton.IconPath;
}
}
OnSubButtonSelected?.Invoke(selectedSubButton);
OnSubButtonSelectionChanged?.Invoke(_originalText, selectedSubButton.Text);
}
/// <summary>
/// 클릭 실행. 기본 Command를 실행합니다.
/// 서브 메뉴 표시/숨김은 View에서 처리합니다.
/// </summary>
/// <param name="parameter">ClickCommand에 전달할 파라미터</param>
public override void ExecuteClick(object? parameter = null)
{
if (!IsEnabled) return;
base.ExecuteClick(parameter);
}
/// <summary>
/// 모든 이벤트 핸들러를 해제합니다. 서브 버튼의 핸들러도 정리합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers();
OnSubButtonSelected = null;
OnSubButtonSelectionChanged = null;
foreach (var subButton in SubButtons)
{
subButton.ClearEventHandlers();
}
}
/// <summary>
/// 리소스 정리. 서브 버튼도 재귀적으로 정리합니다.
/// </summary>
/// <param name="disposing">관리 리소스 정리 여부</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
foreach (var subButton in SubButtons)
{
subButton.Dispose();
}
SubButtons.Clear();
}
base.Dispose(disposing);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,79 @@
#nullable enable
using System;
namespace UVC.UIToolkit
{
/// <summary>
/// 그룹 내 상호 배타적 선택을 지원하는 라디오 버튼 데이터입니다.
/// 동일한 GroupName을 가진 라디오 버튼들 중 하나만 선택됩니다.
/// </summary>
public class UTKToolBarRadioButtonData : UTKToolBarToggleButtonData
{
#region Properties
/// <summary>소속 라디오 그룹 이름</summary>
public string GroupName { get; private set; }
/// <summary>라디오 그룹 참조 (모델에서 설정)</summary>
internal UTKToolBarRadioButtonGroup? RadioGroup { get; set; }
#endregion
#region Constructor
/// <summary>
/// UTKToolBarRadioButtonData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="groupName">소속 라디오 그룹 이름. null이거나 비어있을 수 없습니다.</param>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
/// <exception cref="ArgumentNullException">groupName이 null이거나 빈 문자열일 경우</exception>
public UTKToolBarRadioButtonData(string groupName, string? itemId = null) : base(itemId)
{
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentNullException(nameof(groupName), "라디오 버튼은 반드시 GroupName을 가져야 합니다.");
}
GroupName = groupName;
}
#endregion
#region Methods
/// <summary>
/// 클릭 시 그룹 내 다른 버튼은 해제하고 이 버튼만 선택합니다.
/// 선택된 상태에서만 ClickCommand가 실행됩니다.
/// </summary>
/// <param name="parameter">ClickCommand에 전달할 파라미터</param>
public override void ExecuteClick(object? parameter = null)
{
if (!IsEnabled) return;
if (RadioGroup != null)
{
RadioGroup.SetSelected(this);
}
else
{
UnityEngine.Debug.LogWarning($"UTKToolBarRadioButtonData '{Text}' (그룹: {GroupName})에 RadioGroup이 할당되지 않았습니다.");
}
// 선택된 상태에서만 Command 실행
if (IsSelected && ClickCommand != null)
{
ClickCommand.Execute(parameter ?? this);
}
}
/// <summary>
/// 모든 이벤트 핸들러를 해제합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 18507fdc9b7c65649b9f5604b45532d8

View File

@@ -0,0 +1,165 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace UVC.UIToolkit
{
/// <summary>
/// 라디오 버튼 그룹을 관리합니다. 하나의 버튼만 선택 상태를 유지합니다.
/// </summary>
public class UTKToolBarRadioButtonGroup : IDisposable
{
#region Fields
private readonly List<UTKToolBarRadioButtonData> _buttons = new();
private bool _disposed;
#endregion
#region Properties
/// <summary>그룹 이름</summary>
public string GroupName { get; private set; }
/// <summary>현재 선택된 버튼</summary>
public UTKToolBarRadioButtonData? SelectedButton { get; private set; }
#endregion
#region Constructor
/// <summary>
/// UTKToolBarRadioButtonGroup의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="groupName">그룹 이름</param>
public UTKToolBarRadioButtonGroup(string groupName)
{
GroupName = groupName;
}
#endregion
#region Methods
/// <summary>
/// 버튼을 그룹에 등록합니다.
/// </summary>
/// <param name="button">등록할 라디오 버튼</param>
public void RegisterButton(UTKToolBarRadioButtonData button)
{
if (!_buttons.Contains(button))
{
_buttons.Add(button);
}
}
/// <summary>
/// 특정 버튼을 선택합니다. 나머지 버튼은 해제됩니다.
/// </summary>
/// <param name="buttonToSelect">선택할 버튼</param>
/// <param name="raiseEvent">이벤트 발생 여부</param>
public void SetSelected(UTKToolBarRadioButtonData? buttonToSelect, bool raiseEvent = true)
{
if (buttonToSelect != null && !_buttons.Contains(buttonToSelect))
{
UnityEngine.Debug.LogWarning($"SetSelected: 버튼 '{buttonToSelect.Text}'은 그룹 '{GroupName}'에 등록되어 있지 않습니다.");
return;
}
// 이미 선택된 버튼을 다시 클릭한 경우 무시
if (SelectedButton == buttonToSelect && buttonToSelect != null && buttonToSelect.IsSelected)
{
return;
}
SelectedButton = buttonToSelect;
foreach (var button in _buttons)
{
bool shouldBeSelected = (button == buttonToSelect);
button.SetSelected(shouldBeSelected, raiseEvent);
}
}
/// <summary>
/// 모든 선택을 해제합니다.
/// </summary>
/// <param name="raiseEvent">이벤트 발생 여부</param>
public void ClearSelection(bool raiseEvent = true)
{
SelectedButton = null;
foreach (var button in _buttons)
{
button.SetSelected(false, raiseEvent);
}
}
/// <summary>
/// 그룹 내 버튼 목록을 반환합니다.
/// </summary>
/// <returns>버튼 목록 (읽기 전용)</returns>
public IReadOnlyList<UTKToolBarRadioButtonData> GetButtons()
{
return _buttons.AsReadOnly();
}
/// <summary>
/// 텍스트로 버튼을 검색합니다.
/// </summary>
/// <param name="text">검색할 텍스트</param>
/// <returns>일치하는 버튼 또는 null</returns>
public UTKToolBarRadioButtonData? FindButtonByText(string text)
{
foreach (var button in _buttons)
{
if (string.Equals(button.Text, text, StringComparison.Ordinal))
{
return button;
}
}
return null;
}
/// <summary>
/// 초기 선택 상태를 적용합니다.
/// IsSelected가 true인 버튼이 있으면 해당 버튼을 선택합니다.
/// </summary>
internal void InitializeSelection()
{
if (_buttons.Count == 0) return;
UTKToolBarRadioButtonData? initialButton = null;
foreach (var button in _buttons)
{
if (button.IsSelected)
{
initialButton = button;
break;
}
}
if (initialButton != null)
{
SetSelected(initialButton);
}
}
#endregion
#region IDisposable
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_buttons.Clear();
SelectedButton = null;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,24 @@
#nullable enable
using System;
namespace UVC.UIToolkit
{
/// <summary>
/// 툴바 내 시각적 구분선 데이터입니다.
/// </summary>
public class UTKToolBarSeparatorData : IUTKToolBarItem
{
/// <summary>아이템 고유 식별자</summary>
public string ItemId { get; private set; }
/// <summary>
/// UTKToolBarSeparatorData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
public UTKToolBarSeparatorData(string? itemId = null)
{
ItemId = itemId ?? Guid.NewGuid().ToString();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96caf0e0ca9659b4eb7f8c1b56139813

View File

@@ -0,0 +1,18 @@
#nullable enable
namespace UVC.UIToolkit
{
/// <summary>
/// 단순 클릭 동작의 일반 버튼 데이터입니다.
/// </summary>
public class UTKToolBarStandardButtonData : UTKToolBarButtonData
{
/// <summary>
/// UTKToolBarStandardButtonData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
public UTKToolBarStandardButtonData(string? itemId = null) : base(itemId)
{
}
}
}

View File

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

View File

@@ -0,0 +1,135 @@
#nullable enable
using System;
namespace UVC.UIToolkit
{
/// <summary>
/// On/Off 상태를 가지는 토글 버튼 데이터입니다.
/// 클릭 시 IsSelected 상태가 반전되고, OnToggleStateChanged 이벤트가 발생합니다.
/// </summary>
public class UTKToolBarToggleButtonData : UTKToolBarButtonData
{
#region Fields
private bool _isSelected;
private string? _offIconPath;
#endregion
#region Properties
/// <summary>
/// 현재 선택(On) 상태. 변경 시 OnToggleStateChanged, OnStateChanged 이벤트 발생.
/// </summary>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnToggle?.Invoke(_isSelected);
OnToggleStateChanged?.Invoke(_isSelected);
NotifyStateChanged();
}
}
}
/// <summary>Off 상태 아이콘 경로</summary>
public string? OffIconPath
{
get => _offIconPath;
set
{
if (_offIconPath != value)
{
_offIconPath = value;
NotifyStateChanged();
}
}
}
/// <summary>토글 상태 변경 시 콜백</summary>
public Action<bool>? OnToggle { get; set; }
#endregion
#region Events
/// <summary>IsSelected 상태 변경 시 발생</summary>
public event Action<bool>? OnToggleStateChanged;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarToggleButtonData의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="itemId">아이템 고유 식별자. null이면 GUID 자동 생성.</param>
public UTKToolBarToggleButtonData(string? itemId = null) : base(itemId)
{
}
#endregion
#region Methods
/// <summary>
/// 이벤트 발생 여부를 선택하여 선택 상태를 설정합니다.
/// </summary>
/// <param name="isSelected">새로운 선택 상태</param>
/// <param name="raiseEvent">true이면 OnToggle 콜백을 호출, false이면 UI 이벤트만 발생</param>
public void SetSelected(bool isSelected, bool raiseEvent = true)
{
if (_isSelected != isSelected)
{
_isSelected = isSelected;
if (raiseEvent)
{
OnToggle?.Invoke(_isSelected);
}
OnToggleStateChanged?.Invoke(_isSelected);
NotifyStateChanged();
}
}
/// <summary>
/// 클릭 시 상태를 반전시키고 Command를 실행합니다.
/// </summary>
/// <param name="parameter">전달된 파라미터. bool이면 직접 상태 설정, 아니면 토글.</param>
public override void ExecuteClick(object? parameter = null)
{
if (!IsEnabled) return;
if (parameter is bool newState)
{
IsSelected = newState;
}
else
{
IsSelected = !IsSelected;
}
// Command 실행 (현재 IsSelected 상태를 파라미터로 전달)
if (ClickCommand != null)
{
ClickCommand.Execute(IsSelected);
}
}
/// <summary>
/// 모든 이벤트 핸들러를 해제합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers();
OnToggleStateChanged = null;
OnToggle = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 368ce741cc208274989e0f6e37be2d87

View File

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

View File

@@ -0,0 +1,378 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 툴바 버튼 VisualElement의 추상 베이스 클래스입니다.
/// 아이콘(Material Icon / Image), 텍스트, 활성화 상태의 공통 UI를 제공합니다.
/// </summary>
public abstract partial class UTKToolBarButtonBase : VisualElement, IDisposable
{
#region Fields
/// <summary>아이콘 요소 (Material Icon Label 또는 Image)</summary>
protected Label? _iconLabel;
/// <summary>텍스트 라벨</summary>
protected Label? _textLabel;
/// <summary>루트 버튼 요소</summary>
protected VisualElement? _rootButton;
/// <summary>바인딩된 데이터</summary>
protected UTKToolBarButtonData? _data;
/// <summary>클릭 콜백</summary>
protected EventCallback<ClickEvent>? _onClickCallback;
/// <summary>정리 여부</summary>
protected bool _disposed;
/// <summary>UXML 리소스 경로</summary>
protected string _uxmlPath = "";
/// <summary>USS 리소스 경로</summary>
protected string _ussPath = "";
#endregion
#region Events
/// <summary>버튼 클릭 이벤트 (데이터 전달)</summary>
public event Action<UTKToolBarButtonData>? OnButtonClicked;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarButtonBase의 새 인스턴스를 초기화합니다.
/// </summary>
protected UTKToolBarButtonBase()
{
// 1. 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 2. 테마 구독
SubscribeToThemeChanges();
}
#endregion
#region Setup
/// <summary>
/// UI를 생성합니다. UXML 로드 또는 코드 Fallback.
/// </summary>
protected virtual void CreateUI()
{
// USS 로드
if (!string.IsNullOrEmpty(_ussPath))
{
var uss = Resources.Load<StyleSheet>(_ussPath);
if (uss != null)
{
styleSheets.Add(uss);
}
}
// UXML 로드
if (!string.IsNullOrEmpty(_uxmlPath))
{
var asset = Resources.Load<VisualTreeAsset>(_uxmlPath);
if (asset != null)
{
CreateUIFromUxml(asset);
return;
}
}
// Fallback
CreateUIFallback();
}
/// <summary>
/// UXML에서 UI를 생성합니다.
/// </summary>
/// <param name="asset">UXML 에셋</param>
protected virtual void CreateUIFromUxml(VisualTreeAsset asset)
{
var root = asset.Instantiate();
_rootButton = root.Q<VisualElement>("button-root");
_iconLabel = root.Q<Label>("icon");
_textLabel = root.Q<Label>("label");
// TemplateContainer가 아이콘 정렬을 방해하지 않도록 설정
root.style.flexGrow = 1;
root.style.alignItems = Align.Center;
root.style.justifyContent = Justify.Center;
Add(root);
// 클릭 이벤트 등록
if (_rootButton != null)
{
_onClickCallback = OnClick;
_rootButton.RegisterCallback(_onClickCallback);
}
}
/// <summary>
/// 코드 Fallback으로 UI를 생성합니다.
/// </summary>
protected virtual void CreateUIFallback()
{
_rootButton = new VisualElement();
_rootButton.AddToClassList("utk-toolbar-btn");
_iconLabel = new Label();
_iconLabel.AddToClassList("utk-toolbar-btn__icon");
_rootButton.Add(_iconLabel);
_textLabel = new Label();
_textLabel.AddToClassList("utk-toolbar-btn__label");
_rootButton.Add(_textLabel);
Add(_rootButton);
// 클릭 이벤트 등록
_onClickCallback = OnClick;
_rootButton.RegisterCallback(_onClickCallback);
}
#endregion
#region Data Binding
/// <summary>
/// 데이터를 바인딩합니다. OnStateChanged 이벤트를 구독합니다.
/// </summary>
/// <param name="data">바인딩할 데이터</param>
public virtual void BindData(UTKToolBarButtonData data)
{
// 기존 바인딩 해제
UnbindData();
_data = data;
_data.OnStateChanged += OnDataStateChanged;
// 초기 UI 업데이트
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
}
/// <summary>
/// 데이터 바인딩을 해제합니다.
/// </summary>
public virtual void UnbindData()
{
if (_data != null)
{
_data.OnStateChanged -= OnDataStateChanged;
_data = null;
}
}
/// <summary>바인딩된 데이터 (읽기 전용)</summary>
public UTKToolBarButtonData? BoundData => _data;
/// <summary>
/// 바인딩된 데이터의 활성화 상태를 변경합니다.
/// </summary>
/// <param name="isEnabled">활성화 여부</param>
public void SetDataEnabled(bool isEnabled)
{
if (_data != null)
{
_data.IsEnabled = isEnabled;
}
}
#endregion
#region UI Update
/// <summary>
/// 아이콘을 업데이트합니다.
/// </summary>
/// <param name="iconPath">아이콘 경로 (Material Icon 유니코드 또는 Resources 경로)</param>
/// <param name="useMaterialIcon">Material Icon 사용 여부</param>
protected void UpdateIcon(string? iconPath, bool useMaterialIcon)
{
if (_iconLabel == null) return;
if (string.IsNullOrEmpty(iconPath))
{
_iconLabel.style.display = DisplayStyle.None;
return;
}
_iconLabel.style.display = DisplayStyle.Flex;
if (useMaterialIcon)
{
// Material Icon (폰트 기반)
_iconLabel.RemoveFromClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = iconPath;
_iconLabel.style.backgroundImage = StyleKeyword.None;
}
else
{
// 이미지 아이콘
_iconLabel.AddToClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = "";
var sprite = Resources.Load<Sprite>(iconPath);
if (sprite != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(sprite);
}
else
{
var texture = Resources.Load<Texture2D>(iconPath);
if (texture != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(texture);
}
}
}
}
/// <summary>
/// 텍스트를 업데이트합니다.
/// </summary>
/// <param name="text">버튼 텍스트</param>
protected void UpdateText(string text)
{
if (_textLabel != null)
{
_textLabel.text = text;
}
}
/// <summary>
/// 활성화 상태를 업데이트합니다.
/// </summary>
/// <param name="isEnabled">활성화 여부</param>
protected void UpdateEnabled(bool isEnabled)
{
if (_rootButton != null)
{
if (isEnabled)
{
_rootButton.RemoveFromClassList("utk-toolbar-btn--disabled");
}
else
{
_rootButton.AddToClassList("utk-toolbar-btn--disabled");
}
}
}
/// <summary>
/// 모델 상태 변경 핸들러. 바인딩된 데이터의 시각 상태를 UI에 반영합니다.
/// </summary>
protected virtual void OnDataStateChanged()
{
if (_data == null) return;
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
}
#endregion
#region Click Handler
/// <summary>
/// 클릭 이벤트 핸들러.
/// </summary>
/// <param name="evt">클릭 이벤트</param>
protected virtual void OnClick(ClickEvent evt)
{
if (_data == null || !_data.IsEnabled) return;
_data.ExecuteClick();
RaiseOnButtonClicked(_data);
}
/// <summary>
/// OnButtonClicked 이벤트를 발생시킵니다. 파생 클래스에서 사용합니다.
/// </summary>
/// <param name="data">버튼 데이터</param>
protected void RaiseOnButtonClicked(UTKToolBarButtonData data)
{
OnButtonClicked?.Invoke(data);
}
#endregion
#region Theme
/// <summary>
/// 테마 변경 이벤트를 구독합니다.
/// </summary>
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 IDisposable
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public virtual void Dispose()
{
if (_disposed) return;
_disposed = true;
// 데이터 바인딩 해제
UnbindData();
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
// 클릭 이벤트 해제
if (_rootButton != null && _onClickCallback != null)
{
_rootButton.UnregisterCallback(_onClickCallback);
}
// 참조 정리
OnButtonClicked = null;
_rootButton = null;
_iconLabel = null;
_textLabel = null;
_onClickCallback = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9b48321f7efc72c43a616200f604416b

View File

@@ -0,0 +1,360 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 클릭 시 서브 메뉴를 펼치는 확장 버튼 컴포넌트입니다.
/// 서브 버튼 목록을 지연 생성(Lazy Loading)합니다.
/// </summary>
[UxmlElement]
public partial class UTKToolBarExpandableButton : UTKToolBarButtonBase
{
#region Fields
private VisualElement? _arrowIcon;
private VisualElement? _subMenuContainer;
private List<UTKToolBarButtonBase>? _subMenuItems;
private VisualTreeAsset? _cachedSubMenuAsset;
private StyleSheet? _cachedSubMenuUss;
private bool _subMenuCreated;
#endregion
#region Properties
/// <summary>서브 메뉴 열림 상태</summary>
public bool IsSubMenuOpen { get; private set; }
/// <summary>현재 툴바 배치 방향 (서브 메뉴 위치 계산용)</summary>
public UTKToolBarOrientation CurrentOrientation { get; set; } = UTKToolBarOrientation.Horizontal;
#endregion
#region Events
/// <summary>서브 메뉴 열림/닫힘 이벤트</summary>
public event Action<bool>? OnSubMenuToggled;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarExpandableButton의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKToolBarExpandableButton() : base()
{
_uxmlPath = "UIToolkit/ToolBar/UTKToolBarExpandableButton";
_ussPath = "UIToolkit/ToolBar/UTKToolBarExpandableButtonUss";
// 버튼 기본 USS도 로드
var buttonUss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarButtonUss");
if (buttonUss != null)
{
styleSheets.Add(buttonUss);
}
CreateUI();
}
#endregion
#region Setup
/// <summary>
/// UXML에서 UI 생성 후 화살표 아이콘 참조를 가져옵니다.
/// </summary>
/// <param name="asset">UXML 에셋</param>
protected override void CreateUIFromUxml(VisualTreeAsset asset)
{
base.CreateUIFromUxml(asset);
_arrowIcon = this.Q<VisualElement>("arrow");
}
/// <summary>
/// 코드 Fallback으로 UI 생성 시 화살표 아이콘을 추가합니다.
/// </summary>
protected override void CreateUIFallback()
{
base.CreateUIFallback();
_arrowIcon = new VisualElement();
_arrowIcon.AddToClassList("utk-toolbar-expandable__arrow");
_rootButton?.Add(_arrowIcon);
_rootButton?.AddToClassList("utk-toolbar-expandable");
}
#endregion
#region Click Handler
/// <summary>
/// 클릭 시 서브 메뉴를 토글합니다.
/// 서브 메뉴 영역 내 클릭은 무시합니다 (버블링 방지).
/// </summary>
/// <param name="evt">클릭 이벤트</param>
protected override void OnClick(ClickEvent evt)
{
if (_data == null || !_data.IsEnabled) return;
// 서브 메뉴 내부 클릭이면 무시 (서브 버튼이 자체 처리)
if (_subMenuContainer != null && evt.target is VisualElement target)
{
var ancestor = target;
while (ancestor != null)
{
if (ancestor == _subMenuContainer) return;
ancestor = ancestor.parent;
}
}
ToggleSubMenu();
RaiseOnButtonClicked(_data);
}
#endregion
#region Sub Menu
/// <summary>
/// 서브 메뉴를 토글합니다.
/// </summary>
public void ToggleSubMenu()
{
if (IsSubMenuOpen)
{
CloseSubMenu();
}
else
{
OpenSubMenu();
}
}
/// <summary>
/// 서브 메뉴를 엽니다. 처음 열 때 지연 생성합니다.
/// panel.visualTree에 서브 메뉴를 추가하여 overflow 제약 없이 표시합니다.
/// </summary>
private void OpenSubMenu()
{
if (_data is not UTKToolBarExpandableButtonData expandableData) return;
if (panel == null) return;
if (!_subMenuCreated)
{
CreateSubMenu(expandableData);
}
if (_subMenuContainer != null)
{
// panel.visualTree로 이동 (UTKDropdown 패턴)
if (_subMenuContainer.parent != panel.visualTree)
{
_subMenuContainer.RemoveFromHierarchy();
panel.visualTree.Add(_subMenuContainer);
UTKThemeManager.Instance.ApplyThemeToElement(_subMenuContainer);
if (_cachedSubMenuUss != null)
{
_subMenuContainer.styleSheets.Add(_cachedSubMenuUss);
}
}
_subMenuContainer.style.display = DisplayStyle.Flex;
PositionSubMenu();
}
IsSubMenuOpen = true;
OnSubMenuToggled?.Invoke(true);
}
/// <summary>
/// 서브 메뉴를 닫습니다.
/// 서브 메뉴를 원래 위치(this)로 되돌립니다.
/// </summary>
public void CloseSubMenu()
{
if (_subMenuContainer != null)
{
_subMenuContainer.style.display = DisplayStyle.None;
// panel.visualTree에서 제거하여 원래 위치로 되돌림
if (_subMenuContainer.parent != this)
{
_subMenuContainer.RemoveFromHierarchy();
Add(_subMenuContainer);
}
}
IsSubMenuOpen = false;
OnSubMenuToggled?.Invoke(false);
}
/// <summary>
/// 서브 메뉴를 생성합니다 (지연 로드).
/// 서브 메뉴는 초기에는 this에 추가되며, Open 시 panel.visualTree로 이동합니다.
/// </summary>
/// <param name="expandableData">확장 버튼 데이터</param>
private void CreateSubMenu(UTKToolBarExpandableButtonData expandableData)
{
_subMenuCreated = true;
// 서브 메뉴 리소스 캐싱
if (_cachedSubMenuAsset == null)
{
_cachedSubMenuAsset = Resources.Load<VisualTreeAsset>("UIToolkit/ToolBar/UTKToolBarSubMenu");
_cachedSubMenuUss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarSubMenuUss");
}
VisualElement? container;
if (_cachedSubMenuAsset != null)
{
var subMenuRoot = _cachedSubMenuAsset.Instantiate();
_subMenuContainer = subMenuRoot.Q<VisualElement>("submenu-root");
container = subMenuRoot.Q<VisualElement>("submenu-container");
if (_subMenuContainer != null)
{
// TemplateContainer에서 분리하여 직접 관리
_subMenuContainer.RemoveFromHierarchy();
}
else
{
_subMenuContainer = subMenuRoot;
}
}
else
{
// Fallback: 코드로 서브 메뉴 컨테이너 생성
_subMenuContainer = new VisualElement();
_subMenuContainer.AddToClassList("utk-toolbar-submenu");
container = new VisualElement();
container.AddToClassList("utk-toolbar-submenu__container");
_subMenuContainer.Add(container);
}
// 서브 메뉴를 닫힌 상태로 this에 추가 (Open 시 panel.visualTree로 이동)
_subMenuContainer.style.display = DisplayStyle.None;
Add(_subMenuContainer);
// 서브 버튼 생성
container ??= _subMenuContainer;
_subMenuItems = new List<UTKToolBarButtonBase>();
foreach (var subButtonData in expandableData.SubButtons)
{
var subButton = new UTKToolBarStandardButton();
subButton.BindData(subButtonData);
subButton.OnButtonClicked += OnSubButtonClicked;
container.Add(subButton);
_subMenuItems.Add(subButton);
}
}
/// <summary>
/// 서브 버튼 클릭 핸들러. 서브 메뉴를 닫고 선택을 반영합니다.
/// </summary>
/// <param name="clickedData">클릭된 서브 버튼 데이터</param>
private void OnSubButtonClicked(UTKToolBarButtonData clickedData)
{
if (_data is UTKToolBarExpandableButtonData expandableData)
{
expandableData.SelectSubButton(clickedData);
}
CloseSubMenu();
}
/// <summary>
/// 지정된 요소가 서브 메뉴 내부에 있는지 확인합니다.
/// panel.visualTree에 추가된 서브 메뉴의 외부 클릭 감지에 사용됩니다.
/// </summary>
/// <param name="target">확인할 요소</param>
/// <returns>서브 메뉴 내부이면 true</returns>
public bool IsInsideSubMenu(VisualElement target)
{
if (_subMenuContainer == null) return false;
var ancestor = target;
while (ancestor != null)
{
if (ancestor == _subMenuContainer) return true;
ancestor = ancestor.parent;
}
return false;
}
/// <summary>
/// 서브 메뉴 위치를 계산합니다 (가로/세로 배치 대응).
/// panel.visualTree에 추가된 상태이므로 worldBound 기준으로 절대 위치를 설정합니다.
/// </summary>
private void PositionSubMenu()
{
if (_subMenuContainer == null || _rootButton == null) return;
// schedule로 다음 프레임에 위치 계산 (레이아웃 완료 후)
schedule.Execute(() =>
{
if (_rootButton == null || _subMenuContainer == null) return;
var buttonBounds = _rootButton.worldBound;
_subMenuContainer.style.position = Position.Absolute;
if (CurrentOrientation == UTKToolBarOrientation.Horizontal)
{
// 가로 배치: 버튼 아래로 펼침
_subMenuContainer.style.left = buttonBounds.x;
_subMenuContainer.style.top = buttonBounds.yMax + 2;
}
else
{
// 세로 배치: 버튼 오른쪽으로 펼침
_subMenuContainer.style.left = buttonBounds.xMax + 2;
_subMenuContainer.style.top = buttonBounds.y;
}
});
}
#endregion
#region IDisposable
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public override void Dispose()
{
if (_disposed) return;
// 서브 메뉴 아이템 정리
if (_subMenuItems != null)
{
foreach (var item in _subMenuItems)
{
item.OnButtonClicked -= OnSubButtonClicked;
item.Dispose();
}
_subMenuItems.Clear();
_subMenuItems = null;
}
// panel.visualTree에 남아 있는 서브 메뉴 제거
_subMenuContainer?.RemoveFromHierarchy();
OnSubMenuToggled = null;
_subMenuContainer = null;
_arrowIcon = null;
base.Dispose();
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,22 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 라디오 그룹 내에서 상호 배타적으로 선택되는 버튼 컴포넌트입니다.
/// UTKToolBarToggleButton을 상속하여 토글 시각 효과를 재사용합니다.
/// </summary>
[UxmlElement]
public partial class UTKToolBarRadioButton : UTKToolBarToggleButton
{
/// <summary>
/// UTKToolBarRadioButton의 새 인스턴스를 초기화합니다.
/// Toggle과 동일한 UXML/USS를 사용합니다.
/// </summary>
public UTKToolBarRadioButton() : base()
{
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c7739980bc18db40b0d276d4cb9875b

View File

@@ -0,0 +1,117 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 툴바 내 시각적 구분선 컴포넌트입니다.
/// 가로 배치 시 세로선, 세로 배치 시 가로선으로 표시됩니다.
/// </summary>
[UxmlElement]
public partial class UTKToolBarSeparator : VisualElement, IDisposable
{
#region Fields
private bool _disposed;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarSeparator의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKToolBarSeparator()
{
// 1. 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 2. USS 로드
var uss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarSeparatorUss");
if (uss != null)
{
styleSheets.Add(uss);
}
// 3. UI 생성
CreateUI();
// 4. 테마 구독
SubscribeToThemeChanges();
}
#endregion
#region Setup
/// <summary>
/// UI를 생성합니다.
/// </summary>
private void CreateUI()
{
var asset = Resources.Load<VisualTreeAsset>("UIToolkit/ToolBar/UTKToolBarSeparator");
if (asset != null)
{
var root = asset.Instantiate();
Add(root);
}
else
{
// Fallback
var separator = new VisualElement();
separator.AddToClassList("utk-toolbar-separator");
Add(separator);
}
}
#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 IDisposable
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7348b7369e146ca43aed516ed25f874e

View File

@@ -0,0 +1,23 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 단순 클릭 동작의 툴바 버튼 컴포넌트입니다.
/// </summary>
[UxmlElement]
public partial class UTKToolBarStandardButton : UTKToolBarButtonBase
{
/// <summary>
/// UTKToolBarStandardButton의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKToolBarStandardButton() : base()
{
_uxmlPath = "UIToolkit/ToolBar/UTKToolBarButton";
_ussPath = "UIToolkit/ToolBar/UTKToolBarButtonUss";
CreateUI();
}
}
}

Some files were not shown because too many files have changed in this diff Show More