UTKToolBar 개발 완료
This commit is contained in:
@@ -58,7 +58,7 @@ namespace UVC.UIToolkit
|
||||
/// </code>
|
||||
/// <para><b>Validation (입력 검증):</b></para>
|
||||
/// <code>
|
||||
/// // 검증 함수 설정 (Func<bool>)
|
||||
/// // 검증 함수 설정 (Func<bool>)
|
||||
/// var boundsField = new UTKBoundsField("경계");
|
||||
/// boundsField.ErrorMessage = "크기는 양수여야 합니다.";
|
||||
/// boundsField.Validation = () => boundsField.Value.size.x > 0 && boundsField.Value.size.y > 0 && boundsField.Value.size.z > 0;
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
|
||||
/// </code>
|
||||
/// <para><b>Validation (입력 검증):</b></para>
|
||||
/// <code>
|
||||
/// // 검증 함수 설정 (Func<bool>)
|
||||
/// // 검증 함수 설정 (Func<bool>)
|
||||
/// var precisionField = new UTKDoubleField("정밀 값", 0);
|
||||
/// precisionField.ErrorMessage = "값은 0보다 커야 합니다.";
|
||||
/// precisionField.Validation = () => precisionField.Value > 0;
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
|
||||
/// </code>
|
||||
/// <para><b>Validation (입력 검증):</b></para>
|
||||
/// <code>
|
||||
/// // 검증 함수 설정 (Func<bool>)
|
||||
/// // 검증 함수 설정 (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 && temperatureField.Value <= 60f;
|
||||
/// temperatureField.Validation = () => temperatureField.Value >= -40f && temperatureField.Value <= 60f;
|
||||
///
|
||||
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
|
||||
/// bool isValid = speedField.Validate();
|
||||
|
||||
@@ -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 && volumeStepper.Value <= 1f;
|
||||
/// volumeStepper.Validation = () => volumeStepper.Value >= 0f && volumeStepper.Value <= 1f;
|
||||
/// </code>
|
||||
/// <para><b>UXML에서 사용:</b></para>
|
||||
/// <code><
|
||||
- [아키텍처](#아키텍처)
|
||||
- [파일 구조](#파일-구조)
|
||||
- [빠른 시작](#빠른-시작)
|
||||
- [사용 예제](#사용-예제)
|
||||
- [지원 타입 목록](#지원-타입-목록)
|
||||
- [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
|
||||
7
Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md.meta
Normal file
7
Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec07f57efe3a4eb44a2f9af0e4ba1c43
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/UVC/UIToolkit/ToolBar.meta
Normal file
8
Assets/Scripts/UVC/UIToolkit/ToolBar.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5eff1e00a9c969746bc445a6e3d2c3f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
122
Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md
Normal file
122
Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md
Normal 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
|
||||
```
|
||||
7
Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md.meta
Normal file
7
Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3ef3a53fad49ce44b17e6bc0364dee7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/UVC/UIToolkit/ToolBar/Data.meta
Normal file
8
Assets/Scripts/UVC/UIToolkit/ToolBar/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c31476da5ce38e74b98dc0915ac84dca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
13
Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs
Normal file
13
Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 툴바에 추가될 수 있는 모든 항목(버튼, 구분선 등)의 기본 인터페이스입니다.
|
||||
/// </summary>
|
||||
public interface IUTKToolBarItem
|
||||
{
|
||||
/// <summary>아이템 고유 식별자</summary>
|
||||
string ItemId { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aba55b5f2a940314f9c7057358cbbaab
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cacd491a50884cc44a93efc10241adc0
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa64cd1b0827c724b965c2fba2d37ce1
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18507fdc9b7c65649b9f5604b45532d8
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d601a9d6935c50a40b474d4587c15deb
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96caf0e0ca9659b4eb7f8c1b56139813
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baa2093a77f0b6c42bda1eab25c03ce2
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 368ce741cc208274989e0f6e37be2d87
|
||||
8
Assets/Scripts/UVC/UIToolkit/ToolBar/Items.meta
Normal file
8
Assets/Scripts/UVC/UIToolkit/ToolBar/Items.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49f69438f86bb8040ab9173e5233be8b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b48321f7efc72c43a616200f604416b
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b9f4074756caae49a1b07fe73fac86d
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c7739980bc18db40b0d276d4cb9875b
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7348b7369e146ca43aed516ed25f874e
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9379d9d45829a8b49a7e4b9d8be18305
|
||||
@@ -0,0 +1,129 @@
|
||||
#nullable enable
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// On/Off 상태 전환이 가능한 토글 버튼 컴포넌트입니다.
|
||||
/// 선택 상태에 따라 아이콘과 스타일이 변경됩니다.
|
||||
/// 라디오 버튼의 베이스로도 사용됩니다.
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKToolBarToggleButton : UTKToolBarButtonBase
|
||||
{
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// UTKToolBarToggleButton의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
public UTKToolBarToggleButton() : base()
|
||||
{
|
||||
_uxmlPath = "UIToolkit/ToolBar/UTKToolBarToggleButton";
|
||||
_ussPath = "UIToolkit/ToolBar/UTKToolBarToggleButtonUss";
|
||||
|
||||
// 버튼 기본 USS도 로드
|
||||
var buttonUss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarButtonUss");
|
||||
if (buttonUss != null)
|
||||
{
|
||||
styleSheets.Add(buttonUss);
|
||||
}
|
||||
|
||||
CreateUI();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Binding
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 바인딩. Toggle 전용 이벤트를 추가로 구독합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">바인딩할 데이터</param>
|
||||
public override void BindData(UTKToolBarButtonData data)
|
||||
{
|
||||
base.BindData(data);
|
||||
|
||||
if (data is UTKToolBarToggleButtonData toggleData)
|
||||
{
|
||||
toggleData.OnToggleStateChanged += OnToggleStateChanged;
|
||||
UpdateToggleVisuals(toggleData.IsSelected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 바인딩 해제. Toggle 전용 이벤트도 해제합니다.
|
||||
/// </summary>
|
||||
public override void UnbindData()
|
||||
{
|
||||
if (_data is UTKToolBarToggleButtonData toggleData)
|
||||
{
|
||||
toggleData.OnToggleStateChanged -= OnToggleStateChanged;
|
||||
}
|
||||
base.UnbindData();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Toggle Visuals
|
||||
|
||||
/// <summary>
|
||||
/// 토글 상태에 따른 시각적 업데이트.
|
||||
/// 선택 시 선택 스타일 추가, 아이콘 전환.
|
||||
/// </summary>
|
||||
/// <param name="isSelected">선택 상태</param>
|
||||
protected virtual void UpdateToggleVisuals(bool isSelected)
|
||||
{
|
||||
if (_rootButton == null) return;
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
_rootButton.AddToClassList("utk-toolbar-toggle--selected");
|
||||
}
|
||||
else
|
||||
{
|
||||
_rootButton.RemoveFromClassList("utk-toolbar-toggle--selected");
|
||||
}
|
||||
|
||||
// 아이콘 전환 (On/Off)
|
||||
if (_data is UTKToolBarToggleButtonData toggleData)
|
||||
{
|
||||
string? iconPath = isSelected ? toggleData.IconPath : toggleData.OffIconPath;
|
||||
// OffIconPath가 없으면 기본 아이콘 사용
|
||||
if (string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
iconPath = toggleData.IconPath;
|
||||
}
|
||||
UpdateIcon(iconPath, toggleData.UseMaterialIcon);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토글 상태 변경 핸들러.
|
||||
/// </summary>
|
||||
/// <param name="isSelected">새로운 선택 상태</param>
|
||||
private void OnToggleStateChanged(bool isSelected)
|
||||
{
|
||||
UpdateToggleVisuals(isSelected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Override
|
||||
|
||||
/// <summary>
|
||||
/// 모델 상태 변경 시 토글 시각 효과도 업데이트합니다.
|
||||
/// </summary>
|
||||
protected override void OnDataStateChanged()
|
||||
{
|
||||
base.OnDataStateChanged();
|
||||
if (_data is UTKToolBarToggleButtonData toggleData)
|
||||
{
|
||||
UpdateToggleVisuals(toggleData.IsSelected);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d04bf339742d7a4d92b2afebca10e49
|
||||
570
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs
Normal file
570
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs
Normal file
@@ -0,0 +1,570 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// UIToolkit 기반 툴바 메인 컴포넌트입니다.
|
||||
/// 가로/세로 배치를 지원하며, 4가지 버튼 타입과 구분선을 렌더링합니다.
|
||||
///
|
||||
/// <para><strong>주요 기능:</strong></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>가로/세로 배치 전환 (Orientation)</item>
|
||||
/// <item>Standard, Toggle, Radio, Expandable 버튼 지원</item>
|
||||
/// <item>구분선 지원</item>
|
||||
/// <item>서브 메뉴 외부 클릭 감지 자동 닫기</item>
|
||||
/// <item>테마 변경 지원 (UTKThemeManager 연동)</item>
|
||||
/// <item>성능 최적화 (리소스 캐싱, Dictionary 추적)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 1. 모델 생성
|
||||
/// var model = new UTKToolBarModel();
|
||||
/// model.AddStandardButton("Save", UTKMaterialIcons.Save);
|
||||
/// model.AddSeparator();
|
||||
/// model.AddToggleButton("Grid", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff);
|
||||
///
|
||||
/// // 2. View 생성 및 추가
|
||||
/// var toolbar = new UTKToolBar();
|
||||
/// toolbar.Orientation = UTKToolBarOrientation.Horizontal;
|
||||
/// uiDocument.rootVisualElement.Add(toolbar);
|
||||
///
|
||||
/// // 3. 툴바 빌드
|
||||
/// toolbar.BuildToolBar(model);
|
||||
///
|
||||
/// // 4. 이벤트 구독
|
||||
/// toolbar.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
|
||||
///
|
||||
/// // 5. 배치 방향 변경
|
||||
/// toolbar.SetOrientation(UTKToolBarOrientation.Vertical);
|
||||
/// </code>
|
||||
/// </example>
|
||||
[UxmlElement]
|
||||
public partial class UTKToolBar : VisualElement, IDisposable
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBar";
|
||||
private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarUss";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private UTKToolBarOrientation _orientation = UTKToolBarOrientation.Horizontal;
|
||||
private VisualElement? _itemContainer;
|
||||
private readonly List<VisualElement> _itemElements = new();
|
||||
private readonly Dictionary<string, UTKToolBarButtonBase> _buttonMap = new();
|
||||
private readonly List<UTKToolBarExpandableButton> _expandableButtons = new();
|
||||
private bool _disposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UXML Attributes
|
||||
|
||||
/// <summary>
|
||||
/// 툴바 배치 방향. 변경 시 즉시 레이아웃이 전환됩니다.
|
||||
/// </summary>
|
||||
[UxmlAttribute("orientation")]
|
||||
public UTKToolBarOrientation Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
set
|
||||
{
|
||||
if (_orientation != value)
|
||||
{
|
||||
_orientation = value;
|
||||
ApplyOrientation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>아이템 간 간격 (px)</summary>
|
||||
[UxmlAttribute("item-spacing")]
|
||||
public float ItemSpacing { get; set; } = 2f;
|
||||
|
||||
/// <summary>툴바 크기 (가로 시 높이, 세로 시 너비)</summary>
|
||||
[UxmlAttribute("toolbar-size")]
|
||||
public float ToolBarSize { get; set; } = 40f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>버튼 액션 이벤트</summary>
|
||||
public event Action<UTKToolBarActionEventArgs>? OnAction;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// UTKToolBar의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
public UTKToolBar()
|
||||
{
|
||||
// 1. 테마 적용
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
|
||||
// 2. USS 로드
|
||||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||||
if (uss != null)
|
||||
{
|
||||
styleSheets.Add(uss);
|
||||
}
|
||||
|
||||
// 3. UI 생성
|
||||
CreateUI();
|
||||
|
||||
// 4. 배치 방향 적용
|
||||
ApplyOrientation();
|
||||
|
||||
// 5. 테마 구독
|
||||
SubscribeToThemeChanges();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup
|
||||
|
||||
/// <summary>
|
||||
/// UXML/USS 로드 및 UI 구성.
|
||||
/// </summary>
|
||||
private void CreateUI()
|
||||
{
|
||||
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||||
if (asset != null)
|
||||
{
|
||||
var root = asset.Instantiate();
|
||||
root.style.flexGrow = 1;
|
||||
_itemContainer = root.Q<VisualElement>("toolbar-container");
|
||||
Add(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
AddToClassList("utk-toolbar");
|
||||
_itemContainer = new VisualElement();
|
||||
_itemContainer.AddToClassList("utk-toolbar__container");
|
||||
Add(_itemContainer);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// 모델 데이터로 툴바를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="model">툴바 데이터 모델</param>
|
||||
public void BuildToolBar(UTKToolBarModel model)
|
||||
{
|
||||
ClearToolBar();
|
||||
|
||||
if (_itemContainer == null || model == null) return;
|
||||
|
||||
foreach (var item in model.Items)
|
||||
{
|
||||
var element = CreateItemElement(item);
|
||||
if (element != null)
|
||||
{
|
||||
_itemContainer.Add(element);
|
||||
_itemElements.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
// 아이템 간격 적용
|
||||
ApplyItemSpacing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 툴바의 모든 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
public void ClearToolBar()
|
||||
{
|
||||
// 서브 메뉴 닫기
|
||||
CloseAllSubMenus();
|
||||
|
||||
// 버튼 정리
|
||||
foreach (var kvp in _buttonMap)
|
||||
{
|
||||
kvp.Value.OnButtonClicked -= OnItemClicked;
|
||||
kvp.Value.Dispose();
|
||||
}
|
||||
_buttonMap.Clear();
|
||||
|
||||
// 확장 버튼 리스트 정리
|
||||
_expandableButtons.Clear();
|
||||
|
||||
// 구분선 정리
|
||||
foreach (var element in _itemElements)
|
||||
{
|
||||
if (element is UTKToolBarSeparator separator)
|
||||
{
|
||||
separator.Dispose();
|
||||
}
|
||||
}
|
||||
_itemElements.Clear();
|
||||
|
||||
_itemContainer?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 버튼의 활성화 상태를 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">아이템 ID</param>
|
||||
/// <param name="isEnabled">활성화 여부</param>
|
||||
public void SetButtonEnabled(string itemId, bool isEnabled)
|
||||
{
|
||||
if (_buttonMap.TryGetValue(itemId, out var button))
|
||||
{
|
||||
button.SetDataEnabled(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배치 방향을 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="orientation">새 배치 방향</param>
|
||||
public void SetOrientation(UTKToolBarOrientation orientation)
|
||||
{
|
||||
Orientation = orientation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 버튼 요소를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">아이템 ID</param>
|
||||
/// <param name="button">찾은 버튼 (out)</param>
|
||||
/// <returns>존재 여부</returns>
|
||||
public bool TryGetButtonElement(string itemId, out UTKToolBarButtonBase? button)
|
||||
{
|
||||
return _buttonMap.TryGetValue(itemId, out button);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// 배치 방향을 적용합니다. CSS 클래스를 전환합니다.
|
||||
/// </summary>
|
||||
private void ApplyOrientation()
|
||||
{
|
||||
RemoveFromClassList("utk-toolbar--horizontal");
|
||||
RemoveFromClassList("utk-toolbar--vertical");
|
||||
|
||||
// 루트 요소에도 클래스 적용 (자손 선택자 동작을 위해)
|
||||
var root = this.Q<VisualElement>("toolbar-root");
|
||||
root?.RemoveFromClassList("utk-toolbar--horizontal");
|
||||
root?.RemoveFromClassList("utk-toolbar--vertical");
|
||||
|
||||
if (_orientation == UTKToolBarOrientation.Horizontal)
|
||||
{
|
||||
AddToClassList("utk-toolbar--horizontal");
|
||||
root?.AddToClassList("utk-toolbar--horizontal");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddToClassList("utk-toolbar--vertical");
|
||||
root?.AddToClassList("utk-toolbar--vertical");
|
||||
}
|
||||
|
||||
// 확장 버튼의 방향 업데이트
|
||||
foreach (var expandable in _expandableButtons)
|
||||
{
|
||||
expandable.CurrentOrientation = _orientation;
|
||||
}
|
||||
|
||||
// 아이템 간격 재적용 (방향에 따라 margin 축이 변경됨)
|
||||
ApplyItemSpacing();
|
||||
|
||||
// 열린 서브 메뉴 닫기 (위치 재계산 필요)
|
||||
CloseAllSubMenus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 간격을 적용합니다. 첫 번째 아이템을 제외하고 방향에 맞는 margin을 설정합니다.
|
||||
/// </summary>
|
||||
private void ApplyItemSpacing()
|
||||
{
|
||||
bool isHorizontal = _orientation == UTKToolBarOrientation.Horizontal;
|
||||
|
||||
for (int i = 0; i < _itemElements.Count; i++)
|
||||
{
|
||||
var element = _itemElements[i];
|
||||
if (i == 0)
|
||||
{
|
||||
element.style.marginLeft = 0;
|
||||
element.style.marginTop = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isHorizontal)
|
||||
{
|
||||
element.style.marginLeft = ItemSpacing;
|
||||
element.style.marginTop = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
element.style.marginTop = ItemSpacing;
|
||||
element.style.marginLeft = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모델 아이템으로 개별 버튼/구분선을 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">아이템 데이터</param>
|
||||
/// <returns>생성된 VisualElement</returns>
|
||||
private VisualElement? CreateItemElement(IUTKToolBarItem item)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case UTKToolBarRadioButtonData radioData:
|
||||
{
|
||||
var button = new UTKToolBarRadioButton();
|
||||
button.BindData(radioData);
|
||||
button.OnButtonClicked += OnItemClicked;
|
||||
_buttonMap[radioData.ItemId] = button;
|
||||
|
||||
// 라디오 버튼 선택 변경 → OnAction 이벤트
|
||||
radioData.OnToggleStateChanged += (isSelected) =>
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
RaiseOnAction(radioData.GroupName, UTKToolBarActionType.Radio, radioData.Text);
|
||||
}
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
case UTKToolBarToggleButtonData toggleData:
|
||||
{
|
||||
var button = new UTKToolBarToggleButton();
|
||||
button.BindData(toggleData);
|
||||
button.OnButtonClicked += OnItemClicked;
|
||||
_buttonMap[toggleData.ItemId] = button;
|
||||
|
||||
// 토글 상태 변경 → OnAction 이벤트
|
||||
toggleData.OnToggleStateChanged += (isSelected) =>
|
||||
{
|
||||
RaiseOnAction(toggleData.Text, UTKToolBarActionType.Toggle, isSelected);
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
case UTKToolBarExpandableButtonData expandableData:
|
||||
{
|
||||
var button = new UTKToolBarExpandableButton();
|
||||
button.CurrentOrientation = _orientation;
|
||||
button.BindData(expandableData);
|
||||
button.OnButtonClicked += OnItemClicked;
|
||||
_buttonMap[expandableData.ItemId] = button;
|
||||
_expandableButtons.Add(button);
|
||||
|
||||
// 서브 버튼 선택 변경 → OnAction 이벤트
|
||||
expandableData.OnSubButtonSelectionChanged += (mainText, subText) =>
|
||||
{
|
||||
RaiseOnAction(mainText, UTKToolBarActionType.Expandable, subText);
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
case UTKToolBarStandardButtonData standardData:
|
||||
{
|
||||
var button = new UTKToolBarStandardButton();
|
||||
button.BindData(standardData);
|
||||
button.OnButtonClicked += OnItemClicked;
|
||||
_buttonMap[standardData.ItemId] = button;
|
||||
|
||||
// 클릭 → OnAction 이벤트
|
||||
standardData.OnClicked += () =>
|
||||
{
|
||||
RaiseOnAction(standardData.Text, UTKToolBarActionType.Standard, null);
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
case UTKToolBarSeparatorData:
|
||||
{
|
||||
var separator = new UTKToolBarSeparator();
|
||||
return separator;
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 클릭 핸들러 (공통).
|
||||
/// </summary>
|
||||
/// <param name="data">클릭된 버튼 데이터</param>
|
||||
private void OnItemClicked(UTKToolBarButtonData data)
|
||||
{
|
||||
if (data is UTKToolBarExpandableButtonData)
|
||||
{
|
||||
// 다른 Expandable 버튼의 서브 메뉴 닫기 (클릭한 것만 남김)
|
||||
foreach (var expandable in _expandableButtons)
|
||||
{
|
||||
if (expandable.IsSubMenuOpen && expandable.BoundData != data)
|
||||
{
|
||||
expandable.CloseSubMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 일반 버튼 클릭 시, 열린 서브 메뉴 모두 닫기
|
||||
CloseAllSubMenus();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 열린 서브 메뉴를 닫습니다.
|
||||
/// </summary>
|
||||
private void CloseAllSubMenus()
|
||||
{
|
||||
foreach (var expandable in _expandableButtons)
|
||||
{
|
||||
if (expandable.IsSubMenuOpen)
|
||||
{
|
||||
expandable.CloseSubMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OnAction 이벤트를 발생시킵니다.
|
||||
/// </summary>
|
||||
/// <param name="text">버튼 텍스트</param>
|
||||
/// <param name="actionType">액션 타입</param>
|
||||
/// <param name="value">액션 값</param>
|
||||
private void RaiseOnAction(string text, UTKToolBarActionType actionType, object? value = null)
|
||||
{
|
||||
OnAction?.Invoke(new UTKToolBarActionEventArgs
|
||||
{
|
||||
Text = text,
|
||||
ActionType = actionType,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부 클릭 감지 (서브 메뉴 닫기용).
|
||||
/// Panel의 PointerDownEvent를 캡처 단계에서 감지합니다.
|
||||
/// </summary>
|
||||
/// <param name="evt">포인터 다운 이벤트</param>
|
||||
private void OnPanelPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
// 열린 서브 메뉴가 없으면 무시
|
||||
bool hasOpenSubMenu = false;
|
||||
foreach (var expandable in _expandableButtons)
|
||||
{
|
||||
if (expandable.IsSubMenuOpen)
|
||||
{
|
||||
hasOpenSubMenu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasOpenSubMenu) return;
|
||||
|
||||
var target = evt.target as VisualElement;
|
||||
if (target != null)
|
||||
{
|
||||
// 이 UTKToolBar 내부 클릭이면 무시 (개별 버튼이 처리)
|
||||
var ancestor = target;
|
||||
while (ancestor != null)
|
||||
{
|
||||
if (ancestor == this) return;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
// panel.visualTree에 추가된 서브 메뉴 내부 클릭이면 무시
|
||||
foreach (var expandable in _expandableButtons)
|
||||
{
|
||||
if (expandable.IsSubMenuOpen && expandable.IsInsideSubMenu(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 외부 클릭 → 모든 서브 메뉴 닫기
|
||||
CloseAllSubMenus();
|
||||
}
|
||||
|
||||
#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);
|
||||
|
||||
// 패널 레벨 외부 클릭 감지 등록
|
||||
if (panel != null)
|
||||
{
|
||||
panel.visualTree.RegisterCallback<PointerDownEvent>(OnPanelPointerDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
|
||||
{
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
|
||||
// 패널 레벨 외부 클릭 감지 해제
|
||||
if (evt.originPanel?.visualTree != null)
|
||||
{
|
||||
evt.originPanel.visualTree.UnregisterCallback<PointerDownEvent>(OnPanelPointerDown, TrickleDown.TrickleDown);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThemeChanged(UTKTheme theme)
|
||||
{
|
||||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// 리소스를 정리합니다.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
ClearToolBar();
|
||||
|
||||
// 테마 구독 해제
|
||||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||||
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||||
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||||
|
||||
OnAction = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs.meta
Normal file
2
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92ab9bd5842453e4eb2a6679da362627
|
||||
48
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs
Normal file
48
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
#nullable enable
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// 툴바 배치 방향을 정의합니다.
|
||||
/// </summary>
|
||||
public enum UTKToolBarOrientation
|
||||
{
|
||||
/// <summary>가로 배치 (좌→우)</summary>
|
||||
Horizontal,
|
||||
/// <summary>세로 배치 (위→아래)</summary>
|
||||
Vertical
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 툴바 버튼 액션 타입을 정의합니다.
|
||||
/// </summary>
|
||||
public enum UTKToolBarActionType
|
||||
{
|
||||
/// <summary>일반 클릭</summary>
|
||||
Standard,
|
||||
/// <summary>라디오 그룹 선택</summary>
|
||||
Radio,
|
||||
/// <summary>토글 상태 변경</summary>
|
||||
Toggle,
|
||||
/// <summary>확장 버튼 서브 선택</summary>
|
||||
Expandable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 툴바 버튼 액션 이벤트 인자입니다.
|
||||
/// </summary>
|
||||
public class UTKToolBarActionEventArgs
|
||||
{
|
||||
/// <summary>버튼 텍스트 (또는 라디오 그룹 이름)</summary>
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 컨텍스트별 값.
|
||||
/// Standard: null, Toggle: bool, Radio: 선택 텍스트, Expandable: 서브 텍스트
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>액션 타입</summary>
|
||||
public UTKToolBarActionType ActionType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 056960f2aef4c834898d8fe6abe6dc5a
|
||||
382
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs
Normal file
382
Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs
Normal file
@@ -0,0 +1,382 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UVC.UI.Commands;
|
||||
|
||||
namespace UVC.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// UTKToolBar의 데이터 모델입니다.
|
||||
/// 아이템 컬렉션을 관리하고, 라디오 그룹 등록을 자동화합니다.
|
||||
/// 기존 ToolbarModel과 동일한 팩토리 API를 제공합니다.
|
||||
/// </summary>
|
||||
public class UTKToolBarModel : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly Dictionary<string, UTKToolBarRadioButtonGroup> _radioGroups = new();
|
||||
private bool _disposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>전체 아이템 목록 (순서 유지)</summary>
|
||||
public List<IUTKToolBarItem> Items { get; private set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Add Methods
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 모델에 추가합니다.
|
||||
/// ToolbarRadioButton이면 자동으로 그룹에 등록합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">추가할 아이템</param>
|
||||
public void AddItem(IUTKToolBarItem item)
|
||||
{
|
||||
Items.Add(item);
|
||||
|
||||
if (item is UTKToolBarRadioButtonData radioButton)
|
||||
{
|
||||
if (!_radioGroups.TryGetValue(radioButton.GroupName, out var group))
|
||||
{
|
||||
group = new UTKToolBarRadioButtonGroup(radioButton.GroupName);
|
||||
_radioGroups.Add(radioButton.GroupName, group);
|
||||
}
|
||||
group.RegisterButton(radioButton);
|
||||
radioButton.RadioGroup = group;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 일반 버튼을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="text">버튼 텍스트 (다국어 키)</param>
|
||||
/// <param name="iconPath">아이콘 경로</param>
|
||||
/// <param name="command">실행할 명령</param>
|
||||
/// <param name="tooltip">툴팁 (다국어 키)</param>
|
||||
/// <param name="useMaterialIcon">Material Icon 사용 여부 (기본값: true)</param>
|
||||
/// <returns>생성된 버튼 데이터</returns>
|
||||
public UTKToolBarStandardButtonData AddStandardButton(
|
||||
string text,
|
||||
string? iconPath = null,
|
||||
ICommand? command = null,
|
||||
string? tooltip = null,
|
||||
bool useMaterialIcon = true)
|
||||
{
|
||||
var button = new UTKToolBarStandardButtonData
|
||||
{
|
||||
Text = text,
|
||||
IconPath = iconPath,
|
||||
ClickCommand = command,
|
||||
Tooltip = tooltip,
|
||||
UseMaterialIcon = useMaterialIcon
|
||||
};
|
||||
AddItem(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토글 버튼을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="text">버튼 텍스트</param>
|
||||
/// <param name="initialState">초기 On/Off 상태</param>
|
||||
/// <param name="onIconPath">On 상태 아이콘</param>
|
||||
/// <param name="offIconPath">Off 상태 아이콘</param>
|
||||
/// <param name="onToggle">토글 콜백</param>
|
||||
/// <param name="command">실행할 명령</param>
|
||||
/// <param name="tooltip">툴팁</param>
|
||||
/// <param name="useMaterialIcon">Material Icon 사용 여부</param>
|
||||
/// <returns>생성된 토글 버튼 데이터</returns>
|
||||
public UTKToolBarToggleButtonData AddToggleButton(
|
||||
string text,
|
||||
bool initialState = false,
|
||||
string? onIconPath = null,
|
||||
string? offIconPath = null,
|
||||
Action<bool>? onToggle = null,
|
||||
ICommand? command = null,
|
||||
string? tooltip = null,
|
||||
bool useMaterialIcon = true)
|
||||
{
|
||||
var button = new UTKToolBarToggleButtonData
|
||||
{
|
||||
Text = text,
|
||||
IconPath = onIconPath,
|
||||
OffIconPath = offIconPath,
|
||||
OnToggle = onToggle,
|
||||
ClickCommand = command,
|
||||
Tooltip = tooltip,
|
||||
UseMaterialIcon = useMaterialIcon
|
||||
};
|
||||
// IsSelected를 직접 필드에 설정 (이벤트 발생 방지)
|
||||
button.SetSelected(initialState, false);
|
||||
AddItem(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라디오 버튼을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">라디오 그룹 이름</param>
|
||||
/// <param name="text">버튼 텍스트</param>
|
||||
/// <param name="initialState">초기 선택 상태</param>
|
||||
/// <param name="onIconPath">선택 상태 아이콘</param>
|
||||
/// <param name="offIconPath">비선택 상태 아이콘</param>
|
||||
/// <param name="onToggle">토글 콜백</param>
|
||||
/// <param name="command">실행할 명령</param>
|
||||
/// <param name="tooltip">툴팁</param>
|
||||
/// <param name="useMaterialIcon">Material Icon 사용 여부</param>
|
||||
/// <returns>생성된 라디오 버튼 데이터</returns>
|
||||
public UTKToolBarRadioButtonData AddRadioButton(
|
||||
string groupName,
|
||||
string text,
|
||||
bool initialState = false,
|
||||
string? onIconPath = null,
|
||||
string? offIconPath = null,
|
||||
Action<bool>? onToggle = null,
|
||||
ICommand? command = null,
|
||||
string? tooltip = null,
|
||||
bool useMaterialIcon = true)
|
||||
{
|
||||
var button = new UTKToolBarRadioButtonData(groupName)
|
||||
{
|
||||
Text = text,
|
||||
IconPath = onIconPath,
|
||||
OffIconPath = offIconPath,
|
||||
OnToggle = onToggle,
|
||||
ClickCommand = command,
|
||||
Tooltip = tooltip,
|
||||
UseMaterialIcon = useMaterialIcon
|
||||
};
|
||||
button.SetSelected(initialState, false);
|
||||
AddItem(button);
|
||||
|
||||
// initialState가 true이면 그룹에서 명시적으로 선택
|
||||
if (initialState && _radioGroups.TryGetValue(groupName, out var group))
|
||||
{
|
||||
group.SetSelected(button, false);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 확장 버튼을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="text">버튼 텍스트</param>
|
||||
/// <param name="iconPath">아이콘 경로</param>
|
||||
/// <param name="command">실행할 명령</param>
|
||||
/// <param name="tooltip">툴팁</param>
|
||||
/// <param name="updateIconOnSelection">서브 선택 시 아이콘 업데이트 여부</param>
|
||||
/// <param name="useMaterialIcon">Material Icon 사용 여부</param>
|
||||
/// <returns>생성된 확장 버튼 데이터</returns>
|
||||
public UTKToolBarExpandableButtonData AddExpandableButton(
|
||||
string text,
|
||||
string? iconPath = null,
|
||||
ICommand? command = null,
|
||||
string? tooltip = null,
|
||||
bool updateIconOnSelection = false,
|
||||
bool useMaterialIcon = true)
|
||||
{
|
||||
var button = new UTKToolBarExpandableButtonData
|
||||
{
|
||||
Text = text,
|
||||
IconPath = iconPath,
|
||||
ClickCommand = command,
|
||||
Tooltip = tooltip,
|
||||
UpdateIconOnSelection = updateIconOnSelection,
|
||||
UseMaterialIcon = useMaterialIcon
|
||||
};
|
||||
button.SetOriginalText(text);
|
||||
AddItem(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 구분선을 추가합니다.
|
||||
/// </summary>
|
||||
/// <returns>생성된 구분선 데이터</returns>
|
||||
public UTKToolBarSeparatorData AddSeparator()
|
||||
{
|
||||
var separator = new UTKToolBarSeparatorData();
|
||||
AddItem(separator);
|
||||
return separator;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management
|
||||
|
||||
/// <summary>
|
||||
/// 라디오 그룹의 특정 버튼을 선택합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">그룹 이름</param>
|
||||
/// <param name="buttonToSelect">선택할 버튼. null이면 모두 해제.</param>
|
||||
/// <param name="raiseEvent">이벤트 발생 여부</param>
|
||||
/// <returns>성공 여부</returns>
|
||||
public bool SetRadioButtonSelection(string groupName, UTKToolBarRadioButtonData? buttonToSelect, bool raiseEvent = true)
|
||||
{
|
||||
if (!_radioGroups.TryGetValue(groupName, out var group))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"SetRadioButtonSelection: 그룹 '{groupName}'을 찾을 수 없습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttonToSelect == null)
|
||||
{
|
||||
group.ClearSelection(raiseEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
group.SetSelected(buttonToSelect, raiseEvent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트로 라디오 버튼을 찾아 선택합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">그룹 이름</param>
|
||||
/// <param name="buttonText">버튼 텍스트. null이면 모두 해제.</param>
|
||||
/// <param name="raiseEvent">이벤트 발생 여부</param>
|
||||
/// <returns>선택된 버튼 또는 null</returns>
|
||||
public UTKToolBarRadioButtonData? SetRadioButtonSelectionByText(string groupName, string? buttonText, bool raiseEvent = true)
|
||||
{
|
||||
if (!_radioGroups.TryGetValue(groupName, out var group))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"SetRadioButtonSelectionByText: 그룹 '{groupName}'을 찾을 수 없습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(buttonText))
|
||||
{
|
||||
group.ClearSelection(raiseEvent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var button = group.FindButtonByText(buttonText);
|
||||
if (button == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"SetRadioButtonSelectionByText: 그룹 '{groupName}'에서 '{buttonText}' 버튼을 찾을 수 없습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
group.SetSelected(button, raiseEvent);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라디오 그룹 선택 해제.
|
||||
/// </summary>
|
||||
/// <param name="groupName">그룹 이름</param>
|
||||
/// <param name="raiseEvent">이벤트 발생 여부</param>
|
||||
/// <returns>성공 여부</returns>
|
||||
public bool ClearRadioButtonSelection(string groupName, bool raiseEvent = true)
|
||||
{
|
||||
return SetRadioButtonSelection(groupName, null, raiseEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토글 버튼 상태 변경.
|
||||
/// </summary>
|
||||
/// <param name="toggleButton">토글 버튼</param>
|
||||
/// <param name="isSelected">설정할 상태</param>
|
||||
/// <param name="raiseEvent">이벤트 발생 여부</param>
|
||||
public void SetToggleButtonState(UTKToolBarToggleButtonData toggleButton, bool isSelected, bool raiseEvent = true)
|
||||
{
|
||||
toggleButton.SetSelected(isSelected, raiseEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트로 토글 버튼을 찾아 상태를 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="buttonText">버튼 텍스트</param>
|
||||
/// <param name="isSelected">설정할 상태</param>
|
||||
/// <param name="raiseEvent">이벤트 발생 여부</param>
|
||||
/// <returns>찾은 토글 버튼 또는 null</returns>
|
||||
public UTKToolBarToggleButtonData? SetToggleButtonStateByText(string buttonText, bool isSelected, bool raiseEvent = true)
|
||||
{
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item is UTKToolBarToggleButtonData toggleButton && string.Equals(toggleButton.Text, buttonText, StringComparison.Ordinal))
|
||||
{
|
||||
toggleButton.SetSelected(isSelected, raiseEvent);
|
||||
return toggleButton;
|
||||
}
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"SetToggleButtonStateByText: '{buttonText}' 토글 버튼을 찾을 수 없습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트로 토글 버튼 상태를 조회합니다.
|
||||
/// </summary>
|
||||
/// <param name="buttonText">버튼 텍스트</param>
|
||||
/// <returns>선택 상태. 버튼을 찾지 못하면 false.</returns>
|
||||
public bool GetToggleButtonState(string buttonText)
|
||||
{
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item is UTKToolBarToggleButtonData toggleButton && string.Equals(toggleButton.Text, buttonText, StringComparison.Ordinal))
|
||||
{
|
||||
return toggleButton.IsSelected;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라디오 그룹을 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">그룹 이름</param>
|
||||
/// <returns>라디오 그룹 또는 null</returns>
|
||||
public UTKToolBarRadioButtonGroup? GetRadioButtonGroup(string groupName)
|
||||
{
|
||||
return _radioGroups.TryGetValue(groupName, out var group) ? group : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// 모든 아이템과 라디오 그룹을 정리합니다.
|
||||
/// </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)
|
||||
{
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
Items.Clear();
|
||||
|
||||
foreach (var group in _radioGroups.Values)
|
||||
{
|
||||
group.Dispose();
|
||||
}
|
||||
_radioGroups.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c621f3a44b612e47bd1b04140477c88
|
||||
@@ -70,10 +70,10 @@ namespace UVC.UIToolkit
|
||||
///
|
||||
/// <para><b>사용 예 (UXML):</b></para>
|
||||
/// <code>
|
||||
/// <?xml version="1.0" encoding="utf-8"?>
|
||||
/// <UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
|
||||
/// <utk:UTKPropertyListWindow name="property-window" />
|
||||
/// </UXML>
|
||||
/// <?xml version="1.0" encoding="utf-8"?>
|
||||
/// <UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
|
||||
/// <utk:UTKPropertyListWindow name="property-window" />
|
||||
/// </UXML>
|
||||
/// </code>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
|
||||
Reference in New Issue
Block a user