toolbar 코드 개선

This commit is contained in:
김형인
2025-06-18 00:16:49 +09:00
parent 078fc6df4c
commit e897c911bb
60 changed files with 816 additions and 547 deletions

View File

@@ -0,0 +1,40 @@
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 툴바에 추가될 수 있는 모든 항목(예: 버튼, 구분선, 드롭다운 메뉴 등)의
/// 기본 인터페이스입니다.
/// 이 인터페이스를 구현하는 클래스는 툴바 시스템의 일부로 인식되어
/// ToolbarModel에 추가되고 ToolbarView에 의해 렌더링될 수 있습니다.
///
/// 역할:
/// - 툴바 항목의 통일성: 다양한 UI 요소들을 툴바 시스템 내에서 동일한 타입(IToolbarItem)으로 취급할 수 있게 합니다.
/// - 확장성: 새로운 종류의 툴바 항목을 추가할 때, 이 인터페이스를 구현함으로써
/// 기존 툴바 시스템과 쉽게 통합될 수 있도록 합니다.
///
/// 사용 예시:
/// - ToolbarButtonBase: 모든 버튼 타입의 기본 클래스로, IToolbarItem을 구현합니다.
/// - ToolbarSeparator: 툴바 내 항목들을 시각적으로 구분하는 구분선 클래스로, IToolbarItem을 구현합니다.
///
/// 개발자가 새로운 툴바 항목(예: 색상 선택기, 슬라이더 등)을 만들고 싶다면,
/// 해당 클래스가 IToolbarItem 인터페이스를 구현하도록 해야 합니다.
/// </summary>
/// <example>
/// <code>
/// // 새로운 툴바 항목으로 'MyCustomToolbarItem'을 정의하는 경우
/// public class MyCustomToolbarItem : IToolbarItem
/// {
/// // 이 항목에 필요한 속성 및 메서드 정의
/// public string CustomData { get; set; }
/// public void PerformAction() { /* ... */ }
/// }
///
/// // ToolbarModel에 추가하는 방법
/// ToolbarModel toolbarModel = new ToolbarModel();
/// MyCustomToolbarItem customItem = new MyCustomToolbarItem { CustomData = "Example" };
/// toolbarModel.AddItem(customItem);
/// // ToolbarView는 이 customItem을 렌더링하기 위한 로직이 추가적으로 필요할 수 있습니다.
/// // (예: MyCustomToolbarItem 타입에 맞는 프리팹과 렌더링 로직)
/// </code>
/// </example>
public interface IToolbarItem { }
}

View File

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

View File

@@ -0,0 +1,230 @@
using System;
using UVC.UI.Commands;
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 툴바에 사용되는 모든 버튼(표준 버튼, 토글 버튼, 확장 버튼 등)의 기본 추상 클래스입니다.
/// IToolbarItem 인터페이스를 구현하여 툴바 시스템의 일부가 됩니다.
/// 버튼이 가져야 할 공통적인 속성(예: 텍스트, 아이콘 경로, 활성화 상태, 툴팁 키)과
/// 기본적인 동작(예: 클릭 시 커맨드 실행, 상태 변경 알림)을 정의합니다.
///
/// 이 클래스를 상속받아 특정 유형의 버튼(예: ToolbarStandardButton, ToolbarToggleButton)을 구현할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// // ToolbarButtonBase를 상속받는 사용자 정의 버튼 예시
/// public class MyCustomButton : ToolbarButtonBase
/// {
/// public string CustomProperty { get; set; }
///
/// public MyCustomButton(string text, string iconPath, ICommand command, string tooltip = null)
/// {
/// Text = text; // 부모 클래스의 Text 속성 사용
/// IconSpritePath = iconPath; // 부모 클래스의 IconSpritePath 속성 사용
/// ClickCommand = command; // 부모 클래스의 ClickCommand 속성 사용
/// Tooltip = tooltip; // 부모 클래스의 Tooltip 속성 사용
/// IsEnabled = true; // 기본적으로 활성화 상태로 설정
/// }
///
/// public override void ExecuteClick(object parameter = null)
/// {
/// if (!IsEnabled) return; // 비활성화 상태면 아무것도 하지 않음
///
/// Debug.Log($"MyCustomButton '{Text}' clicked!");
/// // 기본 클릭 로직 실행 (예: ClickCommand 실행)
/// base.ExecuteClick(parameter);
///
/// // 이 버튼만의 추가적인 로직 수행
/// // PerformCustomAction();
/// }
/// }
///
/// // 사용 예
/// // ICommand saveCommand = new ActionCommand(() => { Debug.Log("Save action triggered!"); });
/// // MyCustomButton saveButton = new MyCustomButton("Save", "icons/save_icon", saveCommand, "Save the current file");
/// // toolbarModel.AddItem(saveButton);
/// </code>
/// </example>
public abstract class ToolbarButtonBase : IToolbarItem
{
/// <summary>
/// 버튼의 상태(예: Text, IconSpritePath, IsEnabled)가 변경되었을 때 발생하는 이벤트입니다.
/// View 레이어(예: ToolbarView)는 이 이벤트를 구독하여 버튼의 시각적 표현을 업데이트합니다.
/// </summary>
/// <remarks>
/// 속성(Text, IconSpritePath, IsEnabled)의 setter 내부에서 자동으로 호출됩니다.
/// 여러 상태를 한 번에 변경한 후 수동으로 알리고 싶다면 NotifyStateChanged() 메서드를 사용할 수 있습니다.
/// </remarks>
public event Action OnStateChanged;
protected string _text;
/// <summary>
/// 버튼에 표시될 텍스트 또는 텍스트의 다국어 키입니다.
/// 값이 변경되면 OnStateChanged 이벤트가 발생합니다.
/// </summary>
/// <example>
/// <code>
/// button.Text = "새 작업"; // 직접 텍스트 설정
/// button.Text = "toolbar_button_new_task_key"; // 다국어 키 설정 (LocalizationManager가 이 키를 실제 텍스트로 변환)
/// </code>
/// </example>
public string Text
{
get => _text;
set
{
if (_text != value)
{
_text = value;
OnStateChanged?.Invoke();
}
}
}
protected string _iconSpritePath;
/// <summary>
/// 버튼에 표시될 아이콘의 Resources 폴더 내 경로입니다 (확장자 제외).
/// 값이 변경되면 OnStateChanged 이벤트가 발생합니다.
/// 아이콘이 없는 경우 null 또는 빈 문자열로 설정할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// button.IconSpritePath = "ToolbarIcons/OpenIcon"; // Resources/ToolbarIcons/OpenIcon.png (또는 다른 지원 형식)
/// </code>
/// </example>
public string IconSpritePath
{
get => _iconSpritePath;
set
{
if (_iconSpritePath != value)
{
_iconSpritePath = value;
OnStateChanged?.Invoke();
}
}
}
protected bool _isEnabled = true;
/// <summary>
/// 버튼의 활성화 상태를 나타냅니다. true이면 사용자와 상호작용할 수 있고, false이면 비활성화되어 상호작용할 수 없습니다.
/// 값이 변경되면 OnStateChanged 이벤트가 발생합니다.
/// </summary>
/// <example>
/// <code>
/// button.IsEnabled = false; // 버튼 비활성화
/// if (button.IsEnabled) { /* 버튼 사용 가능 로직 */ }
/// </code>
/// </example>
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnStateChanged?.Invoke();
}
}
}
protected string _tooltip;
/// <summary>
/// 버튼에 마우스를 올렸을 때 표시될 툴팁의 텍스트 또는 다국어 키입니다.
/// Tooltip 변경 시에는 기본적으로 OnStateChanged 이벤트가 발생하지 않지만,
/// 필요에 따라 View에서 이 값을 직접 참조하여 툴팁을 업데이트할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// button.Tooltip = "tooltip_save_button"; // 다국어 키 사용
/// button.Tooltip = "클릭하여 문서를 저장합니다."; // 직접 텍스트 사용
/// </code>
/// </example>
public string Tooltip
{
get => _tooltip;
set
{
if (_tooltip != value)
{
_tooltip = value;
// Tooltip 변경 시 OnStateChanged를 호출할 필요는 일반적으로 없으나,
// 만약 UI가 Tooltip 자체를 표시하는 등의 로직이 있다면 필요할 수 있습니다.
// 여기서는 툴팁 내용이 동적으로 변경되는 경우가 적다고 가정하고 생략합니다.
// 필요하다면 OnStateChanged?.Invoke(); 추가
}
}
}
/// <summary>
/// 버튼이 클릭되었을 때 실행될 동작을 정의하는 커맨드 객체입니다.
/// ICommand 인터페이스를 구현한 객체를 할당합니다 (예: ActionCommand).
/// </summary>
/// <example>
/// <code>
/// // ActionCommand를 사용하여 간단한 동작 정의
/// button.ClickCommand = new ActionCommand(() =>
/// {
/// Debug.Log($"'{button.Text}' 버튼이 클릭되었습니다.");
/// // 특정 기능 수행
/// });
///
/// // 파라미터가 있는 커맨드
/// button.ClickCommand = new ActionCommand<string>((fileName) =>
/// {
/// Debug.Log($"파일 열기: {fileName}");
/// });
/// // ExecuteClick 메서드 호출 시 파라미터 전달 필요
/// // button.ExecuteClick("MyDocument.txt");
/// </code>
/// </example>
public ICommand ClickCommand { get; set; }
/// <summary>
/// 버튼 클릭 로직을 실행합니다.
/// 이 메서드는 일반적으로 UI 시스템(예: Unity UI의 Button.onClick 이벤트)에 의해 호출되도록 설계됩니다.
/// 버튼이 활성화(IsEnabled == true)되어 있고 ClickCommand가 할당되어 있다면, 해당 커맨드를 실행합니다.
/// 파생 클래스에서 이 메서드를 재정의하여 특정 버튼 타입에 맞는 추가적인 클릭 동작을 구현할 수 있습니다.
/// </summary>
/// <param name="parameter">ClickCommand에 전달할 선택적 파라미터입니다.</param>
public virtual void ExecuteClick(object parameter = null)
{
if (IsEnabled && ClickCommand != null)
{
ClickCommand.Execute(parameter); // 커맨드에 파라미터 전달
}
}
/// <summary>
/// OnStateChanged 이벤트를 외부에서 강제로 발생시킵니다.
/// 여러 속성을 변경한 후 한 번에 UI 업데이트를 트리거하거나,
/// 내부 상태 변경이 속성 변경을 통하지 않고 발생했을 때 유용합니다.
/// </summary>
/// <example>
/// <code>
/// public void UpdateButtonAppearance(string newText, string newIcon)
/// {
/// _text = newText; // 직접 필드 값 변경 (setter의 OnStateChanged 호출 안됨)
/// _iconSpritePath = newIcon; // 직접 필드 값 변경
/// NotifyStateChanged(); // 수동으로 상태 변경 알림
/// }
/// </code>
/// </example>
public void NotifyStateChanged()
{
OnStateChanged?.Invoke();
}
/// <summary>
/// 이 버튼 모델에 연결된 모든 이벤트 핸들러를 정리(구독 해제)합니다.
/// 주로 View가 파괴되거나 UI가 재생성될 때 호출되어 메모리 누수를 방지합니다.
/// 파생 클래스에서는 이 메서드를 재정의하여 해당 클래스에 특화된 이벤트를 추가로 정리해야 합니다.
/// </summary>
public virtual void ClearEventHandlers()
{
OnStateChanged = null;
}
}
}

View File

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

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
// using UnityEngine.UI; // Image 사용을 위해 필요할 수 있으나, 모델 클래스에서는 직접적인 UI 참조를 최소화하는 것이 좋음
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 클릭 시 하위 버튼 목록(서브 메뉴)을 표시하거나 숨길 수 있는 확장형 버튼입니다.
/// ToolbarButtonBase를 상속받아 기본적인 버튼 속성(텍스트, 아이콘, 커맨드 등)을 가집니다.
/// 주 버튼을 클릭하면 연결된 서브 메뉴가 표시되며, 서브 메뉴의 버튼을 선택하면
/// 주 버튼의 텍스트나 아이콘이 선택된 하위 버튼의 것으로 변경될 수 있습니다.
/// </summary>
/// <remarks>
/// 이 모델 클래스는 데이터와 상태를 관리합니다. 실제 서브 메뉴의 표시/숨김 및 UI 렌더링은
/// ToolbarView와 같은 View 클래스에서 처리됩니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 등에서 확장 버튼 생성 및 설정 예시
/// var brushToolButton = mainToolbar.AddExpandableButton(
/// "brush_tool_main", // 주 버튼 텍스트 (또는 다국어 키)
/// "icons/brush_default", // 주 버튼 기본 아이콘 경로
/// new ActionCommand(() => Debug.Log("브러시 주 버튼 클릭됨 (하위 메뉴 토글은 View에서 처리)")),
/// "tooltip_brush_tool" // 주 버튼 툴팁 키
/// );
///
/// // 하위 버튼1: 작은 브러시
/// var smallBrush = new ToolbarStandardButton
/// {
/// Text = "brush_small",
/// IconSpritePath = "icons/brush_small_icon",
/// Tooltip = "tooltip_brush_small",
/// ClickCommand = new ActionCommand(() => Debug.Log("작은 브러시 선택됨"))
/// };
/// brushToolButton.SubButtons.Add(smallBrush);
///
/// // 하위 버튼2: 중간 브러시
/// var mediumBrush = new ToolbarStandardButton
/// {
/// Text = "brush_medium",
/// IconSpritePath = "icons/brush_medium_icon",
/// Tooltip = "tooltip_brush_medium",
/// ClickCommand = new ActionCommand(() => Debug.Log("중간 브러시 선택됨"))
/// };
/// brushToolButton.SubButtons.Add(mediumBrush);
///
/// // 하위 버튼 선택 시 콜백 설정 (선택 사항)
/// brushToolButton.OnSubButtonSelected = (selectedSub) =>
/// {
/// Debug.Log($"하위 버튼 '{selectedSub.Text}' 선택됨. 주 버튼 아이콘/텍스트 업데이트됨.");
/// // 여기서 추가적인 로직 수행 가능 (예: 실제 브러시 크기 변경)
/// };
/// </code>
/// </example>
public class ToolbarExpandableButton : ToolbarButtonBase
{
/// <summary>
/// 이 확장 버튼에 속한 하위 버튼들의 목록입니다.
/// 이 목록에 있는 버튼들은 주 버튼 클릭 시 View에 의해 표시될 수 있습니다.
/// 각 하위 버튼은 ToolbarButtonBase 또는 그 파생 클래스의 인스턴스여야 합니다.
/// </summary>
public List<ToolbarButtonBase> SubButtons { get; private set; }
/// <summary>
/// 하위 버튼 중 하나가 선택되었을 때 호출되는 액션(콜백)입니다.
/// 선택된 하위 버튼 객체가 파라미터로 전달됩니다.
/// 주 버튼의 모양이 변경된 후, 추가적인 로직을 수행하고자 할 때 사용합니다.
/// </summary>
/// <remarks>
/// 이 콜백은 SelectSubButton 메서드 내에서, 주 버튼의 Text/IconSpritePath가 업데이트된 후 호출됩니다.
/// </remarks>
public Action<ToolbarButtonBase> OnSubButtonSelected { get; set; }
/// <summary>
/// ToolbarExpandableButton의 새 인스턴스를 초기화합니다.
/// SubButtons 리스트를 빈 리스트로 생성합니다.
/// </summary>
public ToolbarExpandableButton()
{
SubButtons = new List<ToolbarButtonBase>();
}
/// <summary>
/// 주 확장 버튼이 클릭되었을 때의 로직을 실행합니다.
/// 기본적으로 부모 클래스(ToolbarButtonBase)의 ExecuteClick을 호출하여
/// 할당된 ClickCommand를 실행합니다.
/// </summary>
/// <remarks>
/// 하위 메뉴를 실제로 열고 닫는 동작은 View 레이어(예: ToolbarView)에서 이 버튼의 클릭 이벤트를 감지하여
/// 별도로 처리하는 것이 일반적입니다. 이 메서드는 주 버튼 자체의 커맨드 실행에 중점을 둡니다.
/// </remarks>
/// <param name="parameter">ClickCommand에 전달할 파라미터입니다.</param>
public override void ExecuteClick(object parameter = null)
{
if (!IsEnabled) return;
// 주 버튼 자체에 할당된 ClickCommand 실행 (예: 특정 모드 진입, 상태 변경 등)
base.ExecuteClick(parameter);
// 하위 메뉴를 여는 시각적 동작은 View(예: ToolbarView)에서 이 버튼의 UI 클릭 이벤트를 받아 처리합니다.
// 예를 들어, ToolbarView에서는 이 ExecuteClick() 호출 후, ToggleSubMenu(this)와 같은 메서드를 호출할 수 있습니다.
}
/// <summary>
/// 지정된 하위 버튼을 "선택된" 상태로 처리하고, 주 버튼의 모양(텍스트, 아이콘)을
/// 선택된 하위 버튼의 것으로 업데이트합니다.
/// OnSubButtonSelected 콜백이 설정되어 있다면 호출합니다.
/// </summary>
/// <param name="selectedSubButton">선택된 하위 버튼 객체입니다. ToolbarButtonBase 또는 그 파생 타입이어야 합니다.</param>
/// <param name="buttonObj">
/// [주의] 이 파라미터는 View 레이어의 GameObject를 참조하며, 모델 클래스 설계 원칙에 따르면
/// 모델이 View 객체를 직접 참조하는 것은 바람직하지 않을 수 있습니다.
/// 현재 코드(ToolbarExpandableButton.cs의 SelectSubButton)에서는 아이콘 즉시 로드를 위해 사용되고 있으나,
/// 이상적으로는 아이콘 업데이트도 OnStateChanged 이벤트를 통해 View에서 처리하는 것이 좋습니다.
/// 이 파라미터는 향후 리팩토링 대상이 될 수 있습니다. 현재는 기존 코드 구조를 유지합니다.
/// </param>
public void SelectSubButton(ToolbarButtonBase selectedSubButton)
{
if (selectedSubButton != null && selectedSubButton.IsEnabled)
{
// 주 버튼의 텍스트를 선택된 하위 버튼의 텍스트로 변경
// Text 속성의 setter는 내부적으로 OnStateChanged를 호출하여 View 업데이트를 트리거합니다.
if (this.Text != selectedSubButton.Text)
{
this.Text = selectedSubButton.Text;
}
// 주 버튼의 아이콘 경로를 선택된 하위 버튼의 아이콘 경로로 변경
// IconSpritePath 속성의 setter는 내부적으로 OnStateChanged를 호출합니다.
if (this.IconSpritePath != selectedSubButton.IconSpritePath)
{
this.IconSpritePath = selectedSubButton.IconSpritePath;
}
// 하위 버튼 선택 콜백 호출
OnSubButtonSelected?.Invoke(selectedSubButton);
// 선택된 하위 버튼 자체의 ClickCommand 실행은 여기서 하지 않습니다.
// View에서 하위 버튼 UI 클릭 시 해당 하위 버튼의 ExecuteClick()이 직접 호출되는 것이 일반적입니다.
// 만약 여기서 실행해야 한다면: selectedSubButton.ExecuteClick();
}
}
/// <summary>
/// 이 버튼 모델 및 모든 하위 버튼 모델에 연결된 이벤트 핸들러를 정리합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers(); // 부모 클래스의 이벤트 정리 (OnStateChanged)
OnSubButtonSelected = null;
if (SubButtons != null)
{
foreach (var subButton in SubButtons)
{
subButton?.ClearEventHandlers(); // 각 하위 버튼의 이벤트 핸들러도 정리
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,235 @@
using UVC.UI.Commands;
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 툴바의 전체적인 데이터 컨테이너 및 관리 클래스입니다.
/// 툴바에 표시될 모든 항목(IToolbarItem)들의 목록을 관리하며,
/// 각 항목의 생성 및 초기 설정을 위한 헬퍼 메서드들을 제공합니다.
/// 이 모델은 UI 표현(View)과 로직(Controller)으로부터 분리되어 툴바의 순수한 데이터 구조를 나타냅니다.
/// </summary>
/// <remarks>
/// 역할:
/// - 툴바 항목 저장: 툴바에 포함될 버튼, 구분선 등의 IToolbarItem 객체들을 리스트 형태로 보관합니다.
/// - 툴바 항목 생성: AddStandardButton, AddToggleButton 등의 팩토리 메서드를 통해 특정 타입의 툴바 항목을 쉽게 생성하고 모델에 추가할 수 있도록 합니다.
/// - 라디오 버튼 그룹 관리: 동일한 그룹 이름을 가진 ToolbarRadioButton들을 내부적으로 ToolbarRadioButtonGroup으로 묶어 관리합니다.
///
/// 사용 흐름:
/// 1. Toolbar (또는 유사한 관리 클래스)에서 ToolbarModel의 인스턴스를 생성합니다.
/// 2. 다양한 Add[ButtonType] 메서드 또는 AddItem 메서드를 호출하여 툴바 항목 모델들을 생성하고 ToolbarModel에 추가합니다.
/// 3. 설정이 완료된 ToolbarModel 객체를 ToolbarView의 Initialize 메서드에 전달하여 UI를 렌더링하도록 합니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 등에서 ToolbarModel 사용 예시
///
/// // 1. ToolbarModel 인스턴스 생성
/// ToolbarModel myToolbar = new ToolbarModel();
///
/// // 2. 표준 버튼 추가
/// ICommand openCommand = new ActionCommand(() => Debug.Log("파일 열기 실행"));
/// myToolbar.AddStandardButton("button_open_file", "icons/open", openCommand, "tooltip_open_file_description");
///
/// // 3. 구분선 추가
/// myToolbar.AddSeparator();
///
/// // 4. 토글 버튼 추가
/// myToolbar.AddToggleButton(
/// "button_toggle_grid",
/// false, // 초기 상태 (꺼짐)
/// "icons/grid_on", // 켜짐 상태 아이콘
/// "icons/grid_off", // 꺼짐 상태 아이콘
/// (isSelected) => Debug.Log($"그리드 표시: {isSelected}"), // OnToggle 콜백
/// null, // ClickCommand (필요시 설정)
/// "tooltip_toggle_grid_visibility"
/// );
///
/// // 5. 라디오 버튼 그룹 추가
/// string viewModeGroup = "ViewModeSelection";
/// myToolbar.AddRadioButton(viewModeGroup, "button_2d_view", true, "icons/2d_view", null,
/// (isSelected) => { if(isSelected) Debug.Log("2D 뷰 선택됨"); },
/// null, "tooltip_2d_view");
/// myToolbar.AddRadioButton(viewModeGroup, "button_3d_view", false, "icons/3d_view", null,
/// (isSelected) => { if(isSelected) Debug.Log("3D 뷰 선택됨"); },
/// null, "tooltip_3d_view");
///
/// // 6. 설정된 모델을 ToolbarView에 전달
/// // toolbarView.Initialize(myToolbar);
/// </code>
/// </example>
public class ToolbarModel
{
/// <summary>
/// 툴바에 추가된 모든 항목(IToolbarItem)들의 리스트입니다.
/// 이 리스트의 순서대로 툴바에 항목들이 표시됩니다.
/// 외부에서는 읽기만 가능하도록 private set으로 설정되어 있으며, 항목 추가는 AddItem 또는 Add[ButtonType] 메서드를 통해 이루어집니다.
/// </summary>
public System.Collections.Generic.List<IToolbarItem> Items { get; private set; }
/// <summary>
/// 라디오 버튼 그룹들을 관리하는 딕셔너리입니다.
/// 키는 라디오 버튼의 GroupName이며, 값은 해당 그룹을 관리하는 ToolbarRadioButtonGroup 객체입니다.
/// ToolbarRadioButton이 AddItem을 통해 추가될 때 내부적으로 사용됩니다.
/// </summary>
private System.Collections.Generic.Dictionary<string, ToolbarRadioButtonGroup> _radioGroups;
/// <summary>
/// ToolbarModel의 새 인스턴스를 초기화합니다.
/// Items 리스트와 _radioGroups 딕셔너리를 빈 상태로 생성합니다.
/// </summary>
public ToolbarModel()
{
Items = new System.Collections.Generic.List<IToolbarItem>();
_radioGroups = new System.Collections.Generic.Dictionary<string, ToolbarRadioButtonGroup>();
}
/// <summary>
/// 지정된 툴바 항목(IToolbarItem)을 모델에 추가합니다.
/// 추가된 항목은 Items 리스트의 끝에 추가됩니다.
/// 만약 추가되는 항목이 ToolbarRadioButton이라면, 해당 버튼을 적절한 ToolbarRadioButtonGroup에 등록하거나 새로 생성하여 관리합니다.
/// </summary>
/// <param name="item">툴바에 추가할 IToolbarItem 객체입니다 (예: ToolbarStandardButton, ToolbarSeparator 등).</param>
public void AddItem(IToolbarItem item)
{
Items.Add(item);
if (item is ToolbarRadioButton radioButton)
{
if (!_radioGroups.TryGetValue(radioButton.GroupName, out var group))
{
group = new ToolbarRadioButtonGroup();
_radioGroups.Add(radioButton.GroupName, group);
}
group.RegisterButton(radioButton);
radioButton.RadioGroup = group; // 버튼 모델에 그룹 참조 설정
// 그룹 내 초기 선택 상태 결정 (만약 이 버튼이 initialState=true이고, 그룹에 아직 선택된 버튼이 없다면)
// 또는 모든 버튼이 추가된 후 그룹별로 InitializeSelection()을 호출할 수도 있습니다.
// 현재는 AddRadioButton 메서드에서 initialState에 따른 SetSelected 호출로 처리하고 있습니다.
}
// UI 갱신 로직은 ToolbarModel 자체에서는 직접 호출하지 않습니다.
// View가 Model의 변경 사항을 구독하거나, Controller가 View의 업데이트를 트리거합니다.
// 혹은, Model에 아이템이 추가/제거될 때 이벤트를 발생시켜 View가 감지하도록 할 수 있습니다.
}
/// <summary>
/// 새로운 ToolbarStandardButton을 생성하여 툴바 모델에 추가합니다.
/// </summary>
/// <param name="text">버튼에 표시될 텍스트 또는 다국어 키입니다.</param>
/// <param name="iconSpritePath">버튼 아이콘의 Resources 경로입니다 (선택 사항).</param>
/// <param name="command">버튼 클릭 시 실행될 ICommand 객체입니다 (선택 사항).</param>
/// <param name="tooltip">버튼 툴팁의 텍스트 또는 다국어 키입니다 (선택 사항).</param>
/// <returns>생성되고 추가된 ToolbarStandardButton 객체입니다.</returns>
public ToolbarStandardButton AddStandardButton(string text, string iconSpritePath = null, ICommand command = null, string tooltip = null)
{
var button = new ToolbarStandardButton { Text = text, IconSpritePath = iconSpritePath, ClickCommand = command, Tooltip = tooltip };
AddItem(button);
return button;
}
/// <summary>
/// 새로운 ToolbarToggleButton을 생성하여 툴바 모델에 추가합니다.
/// </summary>
/// <param name="text">버튼 텍스트 또는 다국어 키입니다.</param>
/// <param name="initialState">버튼의 초기 선택 상태입니다 (기본값: false, 해제됨).</param>
/// <param name="onIconSpritePath">버튼이 선택(On) 상태일 때의 아이콘 경로입니다 (선택 사항).</param>
/// <param name="offIconSpritePath">버튼이 해제(Off) 상태일 때의 아이콘 경로입니다 (선택 사항).</param>
/// <param name="onToggle">버튼의 선택 상태가 변경될 때 호출될 콜백 함수입니다 (선택 사항).</param>
/// <param name="command">버튼 클릭 시 실행될 ICommand 객체입니다 (선택 사항).</param>
/// <param name="tooltip">버튼 툴팁의 텍스트 또는 다국어 키입니다 (선택 사항).</param>
/// <returns>생성되고 추가된 ToolbarToggleButton 객체입니다.</returns>
public ToolbarToggleButton AddToggleButton(string text, bool initialState = false, string onIconSpritePath = null, string offIconSpritePath = null, System.Action<bool> onToggle = null, ICommand command = null, string tooltip = null)
{
// ToolbarToggleButton의 IconSpritePath는 '선택된 상태'의 아이콘을 의미합니다.
var button = new ToolbarToggleButton
{
Text = text,
IsSelected = initialState,
IconSpritePath = onIconSpritePath, // 선택 시 아이콘
OffIconSpritePath = offIconSpritePath, // 해제 시 아이콘
OnToggle = onToggle,
ClickCommand = command,
Tooltip = tooltip
};
AddItem(button);
return button;
}
/// <summary>
/// 새로운 ToolbarRadioButton을 생성하여 툴바 모델에 추가합니다.
/// 동일한 groupName을 가진 라디오 버튼들은 하나의 그룹으로 동작합니다.
/// </summary>
/// <param name="groupName">이 라디오 버튼이 속할 그룹의 이름입니다.</param>
/// <param name="text">버튼 텍스트 또는 다국어 키입니다.</param>
/// <param name="initialState">버튼의 초기 선택 상태입니다. 같은 그룹 내에서 하나의 버튼만 true로 설정해야 합니다.</param>
/// <param name="iconSpritePath">버튼이 선택(On) 상태일 때의 아이콘 경로입니다 (선택 사항).</param>
/// <param name="offIconSpritePath">버튼이 해제(Off) 상태일 때의 아이콘 경로입니다 (선택 사항).</param>
/// <param name="onToggle">버튼의 선택 상태가 변경될 때 호출될 콜백 함수입니다 (선택 사항).</param>
/// <param name="command">버튼 클릭(선택) 시 실행될 ICommand 객체입니다 (선택 사항).</param>
/// <param name="tooltip">버튼 툴팁의 텍스트 또는 다국어 키입니다 (선택 사항).</param>
/// <returns>생성되고 추가된 ToolbarRadioButton 객체입니다.</returns>
public ToolbarRadioButton AddRadioButton(string groupName, string text, bool initialState = false, string iconSpritePath = null, string offIconSpritePath = null, System.Action<bool> onToggle = null, ICommand command = null, string tooltip = null)
{
var button = new ToolbarRadioButton(groupName)
{
Text = text,
IsSelected = initialState,// 초기 IsSelected 상태 설정
IconSpritePath = iconSpritePath,// 선택 시 아이콘
OffIconSpritePath = offIconSpritePath,// 해제 시 아이콘
OnToggle = onToggle,
ClickCommand = command,
Tooltip = tooltip
};
// AddItem 내에서 그룹 등록 및 버튼에 그룹 참조 설정이 이루어집니다.
AddItem(button);
// initialState가 true인 경우, 해당 그룹 내에서 이 버튼을 명시적으로 선택된 상태로 만듭니다.
// 이렇게 하면 여러 라디오 버튼 중 initialState가 true인 마지막 버튼이 최종 선택됩니다.
// 또는, 그룹별로 모든 버튼이 추가된 후 한 번만 선택 로직을 실행하는 것도 방법입니다.
if (initialState && _radioGroups.TryGetValue(groupName, out var group))
{
group.SetSelected(button);
}
return button;
}
/// <summary>
/// 새로운 ToolbarExpandableButton(확장 가능한 버튼)을 생성하여 툴바 모델에 추가합니다.
/// </summary>
/// <param name="text">주 버튼에 표시될 텍스트 또는 다국어 키입니다.</param>
/// <param name="iconSpritePath">주 버튼의 기본 아이콘 경로입니다 (선택 사항).</param>
/// <param name="command">주 버튼 클릭 시 실행될 ICommand 객체입니다 (선택 사항, 하위 메뉴 토글과는 별개일 수 있음).</param>
/// <param name="tooltip">주 버튼 툴팁의 텍스트 또는 다국어 키입니다 (선택 사항).</param>
/// <returns>생성되고 추가된 ToolbarExpandableButton 객체입니다.</returns>
public ToolbarExpandableButton AddExpandableButton(string text, string iconSpritePath = null, ICommand command = null, string tooltip = null)
{
var button = new ToolbarExpandableButton { Text = text, IconSpritePath = iconSpritePath, ClickCommand = command, Tooltip = tooltip };
AddItem(button);
return button;
}
// <summary>
/// 새로운 ToolbarSeparator(구분선)를 생성하여 툴바 모델에 추가합니다.
/// </summary>
public void AddSeparator()
{
AddItem(new ToolbarSeparator());
}
// 만약 모든 라디오 버튼 그룹의 초기 선택 상태를 한 번에 설정하고 싶다면 다음과 같은 메서드를 추가할 수 있습니다.
// /// <summary>
// /// 모든 라디오 버튼 그룹에 대해 초기 선택 상태를 설정합니다.
// /// 각 그룹에서 IsSelected가 true로 설정된 버튼이 없다면 첫 번째 버튼을 선택합니다.
// /// </summary>
// public void FinalizeRadioGroupSelections()
// {
// foreach (var group in _radioGroups.Values)
// {
// group.InitializeSelection();
// }
// }
// 실제 UI 렌더링 및 상호작용 로직은 이 클래스 또는 별도의 UI View 클래스에서 처리됩니다.
// (예: Unity UI GameObject 생성, 이벤트 연결 등)
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 27e53214281a62642adbdbf761ffd630

View File

@@ -0,0 +1,164 @@
using System;
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 라디오 버튼 그룹 내에서 사용되는 버튼입니다. 동일한 GroupName을 가진 라디오 버튼들 중에서
/// 단 하나만 선택될 수 있도록 동작합니다. ToolbarToggleButton을 상속받습니다.
/// </summary>
/// <remarks>
/// 각 ToolbarRadioButton은 GroupName을 가지며, 이 GroupName을 기준으로 ToolbarModel에서
/// ToolbarRadioButtonGroup에 의해 관리됩니다. 사용자가 라디오 버튼을 클릭하면,
/// 이 버튼은 자신이 속한 RadioGroup에 선택 상태 변경을 요청하고, RadioGroup은
/// 그룹 내 다른 버튼들의 선택을 해제하고 이 버튼만 선택된 상태로 만듭니다.
/// IsSelected 상태 변경 시 OnToggle, OnStateChanged, OnToggleStateChanged 이벤트가 발생합니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 또는 ToolbarModel 등에서 ToolbarRadioButton 생성 및 사용 예시
///
/// string cameraViewGroupName = "CameraViewOptions";
///
/// // 1. 라디오 버튼1: 탑뷰
/// ToolbarRadioButton topViewRadio = new ToolbarRadioButton(cameraViewGroupName)
/// {
/// Text = "view_top",
/// IconSpritePath = "icons/toolbar/view_top_on",
/// OffIconSpritePath = "icons/toolbar/view_top_off",
/// Tooltip = "tooltip_top_view",
/// IsSelected = true, // 초기 선택 상태
/// OnToggle = (isSelected) =>
/// {
/// if (isSelected) UnityEngine.Debug.Log("탑뷰 선택됨 (OnToggle)");
/// },
/// ClickCommand = new ActionCommand(() =>
/// {
/// UnityEngine.Debug.Log("탑뷰 커맨드 실행");
/// // 실제 탑뷰로 변경하는 로직
/// })
/// };
///
/// // 2. 라디오 버튼2: 프론트뷰
/// ToolbarRadioButton frontViewRadio = new ToolbarRadioButton(cameraViewGroupName)
/// {
/// Text = "view_front",
/// IconSpritePath = "icons/toolbar/view_front_on",
/// OffIconSpritePath = "icons/toolbar/view_front_off",
/// Tooltip = "tooltip_front_view",
/// IsSelected = false, // 초기 선택 안됨
/// OnToggle = (isSelected) =>
/// {
/// if (isSelected) UnityEngine.Debug.Log("프론트뷰 선택됨 (OnToggle)");
/// },
/// ClickCommand = new ActionCommand(() =>
/// {
/// UnityEngine.Debug.Log("프론트뷰 커맨드 실행");
/// // 실제 프론트뷰로 변경하는 로직
/// })
/// };
///
/// // 3. 생성된 라디오 버튼들을 ToolbarModel에 추가
/// // toolbarModel.AddItem(topViewRadio);
/// // toolbarModel.AddItem(frontViewRadio);
/// // 또는 ToolbarModel의 AddRadioButton 헬퍼 메서드 사용
/// // toolbarModel.AddRadioButton(cameraViewGroupName, "view_top", true, "icons/toolbar/view_top_on", ...);
/// // toolbarModel.AddRadioButton(cameraViewGroupName, "view_front", false, "icons/toolbar/view_front_on", ...);
///
/// // ToolbarModel은 AddItem 시 GroupName을 보고 내부적으로 ToolbarRadioButtonGroup을 생성/관리하며
/// // 각 라디오 버튼을 해당 그룹에 등록합니다.
/// </code>
/// </example>
public class ToolbarRadioButton : ToolbarToggleButton
{
/// <summary>
/// 이 라디오 버튼이 속한 그룹의 이름입니다.
/// 동일한 GroupName을 가진 라디오 버튼들은 하나의 그룹으로 묶여 동작합니다.
/// </summary>
public string GroupName { get; private set; }
/// <summary>
/// 이 라디오 버튼을 관리하는 ToolbarRadioButtonGroup의 내부 참조입니다.
/// ToolbarModel에 의해 설정되며, 버튼이 그룹에 등록될 때 할당됩니다.
/// </summary>
internal ToolbarRadioButtonGroup RadioGroup { get; set; }
/// <summary>
/// 지정된 그룹 이름을 사용하여 ToolbarRadioButton의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="groupName">이 라디오 버튼이 속할 그룹의 이름입니다. null이거나 비어있을 수 없습니다.</param>
/// <exception cref="ArgumentNullException">groupName이 null이거나 빈 문자열일 경우 발생합니다.</exception>
public ToolbarRadioButton(string groupName)
{
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentNullException(nameof(groupName), "라디오 버튼은 반드시 GroupName을 가져야 합니다.");
}
GroupName = groupName;
}
/// <summary>
/// 라디오 버튼 클릭 로직을 실행합니다.
/// 버튼이 활성화되어 있다면, 자신이 속한 RadioGroup에 자신을 선택하도록 요청합니다.
/// RadioGroup은 이 요청을 받아 그룹 내 다른 버튼들의 선택을 해제하고 이 버튼만 선택 상태로 만듭니다.
/// 그 후, 이 버튼의 ClickCommand가 실행됩니다 (선택된 상태에서만).
/// </summary>
/// <param name="parameter">
/// 이 파라미터는 ClickCommand에 전달될 수 있습니다.
/// ToolbarView에서 UI 이벤트 연결 시, 라디오 버튼의 경우 보통 선택 상태(true)를 전달하거나,
/// 버튼 모델 자체를 전달하여 Command가 필요한 정보를 추출하도록 할 수 있습니다.
/// 기본적으로는 현재 IsSelected 상태가 ClickCommand에 전달되도록 고려할 수 있습니다.
/// </param>
public override void ExecuteClick(object parameter = null)
{
if (!IsEnabled) return;
// bool previousSelectionState = IsSelected; // 이전 선택 상태 (필요하다면)
if (RadioGroup != null)
{
// RadioGroup의 SetSelected 메서드를 호출하여 그룹 내 선택 상태를 관리합니다.
// SetSelected 내부에서 이 버튼의 IsSelected가 true로 설정되고,
// 다른 버튼들은 false로 설정됩니다.
// IsSelected setter는 OnToggle, OnStateChanged, OnToggleStateChanged 이벤트를 발생시킵니다.
RadioGroup.SetSelected(this);
}
else
{
// RadioGroup이 할당되지 않은 경우 (예: ToolbarModel에 추가되기 전 또는 독립적으로 사용 시도)
// 이 경우 일반 토글 버튼처럼 동작하거나, 경고를 로깅할 수 있습니다.
// 현재 구현은 그룹이 없으면 단독으로 선택되는 것을 방지하거나 특별 처리를 하도록 되어 있습니다.
UnityEngine.Debug.LogWarning($"ToolbarRadioButton '{Text}' (그룹: {GroupName})에 RadioGroup이 할당되지 않았습니다. 단독으로 상태가 변경될 수 없습니다. ToolbarModel에 먼저 추가되어야 합니다.");
// 만약 그룹 없이도 토글 가능하게 하려면 아래 주석 해제 및 로직 수정 필요
// IsSelected = !IsSelected;
}
// ClickCommand 실행:
// 라디오 버튼의 ClickCommand는 일반적으로 해당 버튼이 "선택되었을 때"의 액션을 정의합니다.
// RadioGroup.SetSelected에 의해 IsSelected 상태가 true로 변경된 후에 실행되어야 합니다.
if (IsSelected && ClickCommand != null)
{
// 파라미터 처리: parameter가 null이 아니면 그것을 우선 사용하고,
// 아니면 현재 IsSelected 상태(true) 또는 버튼 자체(this)를 전달할 수 있습니다.
// ToolbarView의 SetupButtonVisualsAndInteractions에서 radioModel.ExecuteClick(true)로 호출하는 경우,
// parameter는 true가 됩니다.
object commandParameterToUse = parameter ?? this; // 예: 명시적 파라미터가 없으면 버튼 인스턴스 전달
ClickCommand.Execute(commandParameterToUse);
}
// 만약 선택 해제 시(다른 라디오 버튼이 선택되어 이 버튼이 해제될 때)에도 Command를 실행해야 한다면,
// 위 if(IsSelected) 조건을 제거하거나, 별도의 Command(예: DeselectCommand)를 고려해야 합니다.
// 하지만 일반적인 라디오 버튼의 사용 패턴은 선택 시의 액션에 중점을 둡니다.
}
/// <summary>
/// 이 버튼 모델에 연결된 모든 이벤트 핸들러를 정리합니다.
/// ToolbarRadioButton은 ToolbarToggleButton에서 상속받은 이벤트 외에 추가적인 이벤트가 없으므로,
/// 부모 클래스의 ClearEventHandlers를 호출합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers(); // 부모 클래스(ToolbarToggleButton)의 이벤트 정리
// ToolbarRadioButton에 특화된 이벤트가 있다면 여기서 정리합니다.
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 357d7605383a64d4cb43bd295b261c1a

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Linq;
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 여러 개의 ToolbarRadioButton 객체들을 하나의 그룹으로 관리합니다.
/// 이 그룹 내에서는 오직 하나의 라디오 버튼만이 선택된(IsSelected = true) 상태를 가질 수 있도록 보장합니다.
/// </summary>
/// <remarks>
/// ToolbarModel은 ToolbarRadioButton이 추가될 때 GroupName을 기준으로 이 클래스의 인스턴스를 생성하거나
/// 기존 인스턴스를 찾아 라디오 버튼을 등록(RegisterButton)합니다.
/// 사용자가 라디오 버튼을 클릭하면, 해당 버튼은 이 그룹의 SetSelected 메서드를 호출하여
/// 자신을 선택 상태로 만들고 그룹 내 다른 버튼들은 선택 해제 상태로 변경합니다.
/// </remarks>
/// <example>
/// <code>
/// // 이 클래스는 주로 ToolbarModel 내부에서 사용됩니다. 개발자가 직접 생성하기보다는
/// // ToolbarModel에 ToolbarRadioButton을 추가함으로써 간접적으로 사용됩니다.
///
/// // ToolbarModel 내부에서의 사용 흐름 (간략화된 예시)
/// // public class ToolbarModel
/// // {
/// // private Dictionary<string, ToolbarRadioButtonGroup> _radioGroups = new Dictionary<string, ToolbarRadioButtonGroup>();
/// //
/// // public void AddItem(IToolbarItem item)
/// // {
/// // if (item is ToolbarRadioButton radioButton)
/// // {
/// // if (!_radioGroups.TryGetValue(radioButton.GroupName, out var group))
/// // {
/// // group = new ToolbarRadioButtonGroup();
/// // _radioGroups.Add(radioButton.GroupName, group);
/// // }
/// // group.RegisterButton(radioButton); // 라디오 버튼을 그룹에 등록
/// // radioButton.RadioGroup = group; // 버튼에 그룹 참조 설정
/// // }
/// // // ...
/// // }
/// // }
///
/// // 그룹 내 버튼 선택 로직 (ToolbarRadioButton의 ExecuteClick 내부에서 호출됨)
/// // ToolbarRadioButtonGroup cameraGroup = new ToolbarRadioButtonGroup();
/// // ToolbarRadioButton radio1 = new ToolbarRadioButton("camera") { Text = "Cam1" };
/// // ToolbarRadioButton radio2 = new ToolbarRadioButton("camera") { Text = "Cam2" };
/// //
/// // cameraGroup.RegisterButton(radio1);
/// // cameraGroup.RegisterButton(radio2);
/// // radio1.RadioGroup = cameraGroup;
/// // radio2.RadioGroup = cameraGroup;
/// //
/// // // radio1을 선택 (radio1.IsSelected = true, radio2.IsSelected = false가 됨)
/// // cameraGroup.SetSelected(radio1);
/// // UnityEngine.Debug.Log($"Radio1 Selected: {radio1.IsSelected}, Radio2 Selected: {radio2.IsSelected}");
/// //
/// // // radio2를 선택 (radio1.IsSelected = false, radio2.IsSelected = true가 됨)
/// // cameraGroup.SetSelected(radio2);
/// // UnityEngine.Debug.Log($"Radio1 Selected: {radio1.IsSelected}, Radio2 Selected: {radio2.IsSelected}");
/// </code>
/// </example>
public class ToolbarRadioButtonGroup
{
private List<ToolbarRadioButton> _buttons = new List<ToolbarRadioButton>();
/// <summary>
/// 현재 그룹 내에서 선택된 라디오 버튼입니다.
/// 선택된 버튼이 없으면 null을 반환할 수 있습니다 (일반적으로 그룹은 항상 하나가 선택되도록 설계되지만, 초기 상태 등 예외 가능).
/// </summary>
public ToolbarRadioButton SelectedButton { get; private set; }
/// <summary>
/// 지정된 라디오 버튼을 이 그룹에 등록합니다.
/// 등록된 버튼은 그룹의 선택 관리 대상이 됩니다.
/// 버튼의 RadioGroup 속성에도 이 그룹의 참조가 설정되어야 합니다 (보통 ToolbarModel에서 처리).
/// </summary>
/// <param name="button">그룹에 등록할 ToolbarRadioButton입니다.</param>
public void RegisterButton(ToolbarRadioButton button)
{
if (!_buttons.Contains(button))
{
_buttons.Add(button);
}
}
/// <summary>
/// 지정된 라디오 버튼을 그룹 내에서 선택된 상태로 설정합니다.
/// 이전에 선택되었던 다른 버튼은 선택 해제 상태(IsSelected = false)로 변경됩니다.
/// </summary>
/// <param name="buttonToSelect">선택할 ToolbarRadioButton입니다. 이 버튼은 반드시 그룹에 미리 등록되어 있어야 합니다.</param>
public void SetSelected(ToolbarRadioButton buttonToSelect)
{
if (buttonToSelect == null || !_buttons.Contains(buttonToSelect))
{
// 그룹에 등록되지 않은 버튼을 선택하려고 하면 무시하거나 경고를 로깅할 수 있습니다.
UnityEngine.Debug.LogWarning($"SetSelected: 버튼 '{buttonToSelect?.Text}' (그룹: {buttonToSelect?.GroupName})은 현재 라디오 그룹에 등록되어 있지 않습니다.");
return;
}
// 이미 선택된 버튼을 다시 클릭한 경우, 상태를 변경하지 않고 유지합니다.
// (라디오 버튼은 일반적으로 한 번 선택되면 사용자가 직접 해제할 수 없고, 다른 버튼을 선택해야 해제됨)
if (SelectedButton == buttonToSelect && buttonToSelect.IsSelected)
{
return;
}
SelectedButton = buttonToSelect; // 새 버튼을 선택된 버튼으로 설정
foreach (var buttonInGroup in _buttons)
{
// 현재 순회 중인 버튼이 선택하려는 버튼(buttonToSelect)과 동일한지 비교하여
// IsSelected 상태를 설정합니다.
// 이렇게 하면 buttonToSelect만 true가 되고 나머지는 false가 됩니다.
// IsSelected 속성의 setter는 필요한 이벤트(OnToggle, OnStateChanged 등)를 발생시킵니다.
buttonInGroup.IsSelected = (buttonInGroup == buttonToSelect);
}
}
/// <summary>
/// 그룹에 등록된 모든 라디오 버튼의 목록을 가져옵니다.
/// </summary>
/// <returns>그룹 내 모든 ToolbarRadioButton의 읽기 전용 컬렉션입니다.</returns>
public IReadOnlyList<ToolbarRadioButton> GetButtons()
{
return _buttons.AsReadOnly();
}
/// <summary>
/// 그룹 내 버튼들의 초기 선택 상태를 설정합니다.
/// 주로 ToolbarModel에서 라디오 버튼들을 추가한 후 호출될 수 있습니다.
/// initialState가 true인 버튼이 있다면 해당 버튼을, 없다면 첫 번째 버튼을 선택합니다.
/// </summary>
internal void InitializeSelection()
{
if (!_buttons.Any()) return;
ToolbarRadioButton buttonToSelectInitially = _buttons.FirstOrDefault(b => b.IsSelected);
if (buttonToSelectInitially != null)
{
// initialState가 true로 설정된 버튼이 있다면, 해당 버튼으로 최종 선택 상태를 확정합니다.
// (다른 initialState=true 버튼이 실수로 여러 개 있었을 경우를 대비하여 명확히 하나만 선택되도록 함)
SetSelected(buttonToSelectInitially);
}
else
{
// IsSelected가 true인 버튼이 하나도 없다면, 그룹의 첫 번째 버튼을 기본으로 선택합니다.
// (라디오 그룹은 일반적으로 항상 하나가 선택되어 있는 상태를 유지하려 함)
SetSelected(_buttons[0]);
}
}
}
}

View File

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

View File

@@ -0,0 +1,44 @@
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 툴바 내에서 항목들(예: 버튼 그룹)을 시각적으로 구분하는 구분선을 나타냅니다.
/// IToolbarItem 인터페이스를 구현하여 툴바 시스템(ToolbarModel)에 추가될 수 있습니다.
/// </summary>
/// <remarks>
/// ToolbarSeparator는 일반적으로 특별한 동작이나 속성을 가지지 않으며,
/// View 레이어(예: ToolbarView)에서 이 항목을 만나면 시각적인 구분선 UI (예: 선, 공백 등)를 렌더링합니다.
/// 프리팹 기반으로 UI가 생성될 경우, 구분선에 해당하는 프리팹이 사용됩니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 또는 ToolbarModel 등에서 ToolbarSeparator 사용 예시
///
/// // ToolbarModel 인스턴스가 있다고 가정
/// // ToolbarModel toolbarModel = new ToolbarModel();
///
/// // 1. 표준 버튼 추가
/// // toolbarModel.AddStandardButton("버튼1", null, new ActionCommand(() => {}));
///
/// // 2. 구분선 추가
/// ToolbarSeparator separator = new ToolbarSeparator();
/// toolbarModel.AddItem(separator);
/// // 또는 ToolbarModel에 AddSeparator() 헬퍼 메서드가 있다면 그것을 사용
/// // toolbarModel.AddSeparator();
///
/// // 3. 또 다른 버튼 추가
/// // toolbarModel.AddStandardButton("버튼2", null, new ActionCommand(() => {}));
///
/// // 위와 같이 추가하면, ToolbarView는 "버튼1"과 "버튼2" 사이에 구분선을 렌더링하게 됩니다.
/// </code>
/// </example>
public class ToolbarSeparator : IToolbarItem
{
// ToolbarSeparator는 일반적으로 데이터를 가지지 않으므로 내용이 비어있습니다.
// 만약 구분선의 특정 스타일(예: 두께, 색상, 여백 등)을 모델 수준에서 제어해야 한다면,
// 여기에 관련 속성을 추가할 수 있습니다. 하지만 보통 이러한 시각적 요소는 View에서 처리합니다.
//
// 예시: 구분선 유형을 나타내는 속성 (필요한 경우)
// public enum SeparatorType { Line, Spacer }
// public SeparatorType Type { get; set; } = SeparatorType.Line;
}
}

View File

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

View File

@@ -0,0 +1,96 @@
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 한 번 클릭으로 특정 동작을 수행하는 표준적인 버튼입니다.
/// ToolbarButtonBase를 상속받아 텍스트, 아이콘, 툴팁, 활성화 상태 및 클릭 커맨드 기능을 가집니다.
/// </summary>
/// <remarks>
/// 이 클래스는 ToolbarButtonBase의 기능을 그대로 사용하며, 특별히 추가된 로직은 없습니다.
/// 복잡한 상태 관리 없이 단순 클릭 동작만을 필요로 하는 경우에 사용됩니다.
/// View 레이어(예: ToolbarView)에서는 이 모델에 해당하는 UI 요소(예: UnityEngine.UI.Button)를 생성하고,
/// 사용자가 UI 버튼을 클릭하면 이 모델의 ExecuteClick 메서드가 호출되도록 연결합니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 또는 ToolbarModel 등에서 ToolbarStandardButton 생성 및 사용 예시
///
/// // 1. 버튼 클릭 시 실행될 커맨드 정의 (예: 간단한 로그 출력)
/// ICommand saveCommand = new ActionCommand(() =>
/// {
/// UnityEngine.Debug.Log("저장 버튼 클릭됨. 저장 로직 실행...");
/// // 여기에 실제 저장 로직 구현
/// });
///
/// // 2. ToolbarStandardButton 인스턴스 생성 및 속성 설정
/// ToolbarStandardButton saveButton = new ToolbarStandardButton
/// {
/// Text = "button_save", // 버튼 텍스트 (다국어 키 또는 실제 텍스트)
/// IconSpritePath = "icons/toolbar/save_icon", // Resources 폴더 내 아이콘 경로
/// Tooltip = "tooltip_save_button_description", // 툴팁 (다국어 키 또는 실제 텍스트)
/// ClickCommand = saveCommand, // 위에서 정의한 커맨드 할당
/// IsEnabled = true // 버튼 활성화 상태
/// };
///
/// // 3. 생성된 버튼 모델을 ToolbarModel에 추가 (ToolbarModel의 AddItem 또는 AddStandardButton 메서드 사용)
/// // toolbarModel.AddItem(saveButton);
/// // 또는
/// // toolbarModel.AddStandardButton("button_save", "icons/toolbar/save_icon", saveCommand, "tooltip_save_button_description");
///
/// // 4. 버튼 상태 변경 예시
/// // saveButton.IsEnabled = false; // 버튼 비활성화 (UI에 반영됨)
/// // saveButton.Text = "button_saved"; // 버튼 텍스트 변경 (UI에 반영됨)
/// </code>
/// </example>
public class ToolbarStandardButton : ToolbarButtonBase
{
// ToolbarStandardButton은 ToolbarButtonBase의 모든 기능을 상속받습니다.
// 특별히 ToolbarStandardButton만을 위한 추가적인 속성이나 메서드가 필요하다면 여기에 정의할 수 있습니다.
// 예시: 생성자를 통해 주요 속성을 설정하도록 할 수도 있습니다.
// /// <summary>
// /// 지정된 속성으로 ToolbarStandardButton의 새 인스턴스를 초기화합니다.
// /// </summary>
// /// <param name="text">버튼 텍스트 또는 다국어 키입니다.</param>
// /// <param name="iconSpritePath">아이콘의 Resources 경로입니다.</param>
// /// <param name="command">클릭 시 실행될 커맨드입니다.</param>
// /// <param name="tooltip">툴팁 텍스트 또는 다국어 키입니다.</param>
// public ToolbarStandardButton(string text, string iconSpritePath = null, ICommand command = null, string tooltip = null)
// {
// this.Text = text;
// this.IconSpritePath = iconSpritePath;
// this.ClickCommand = command;
// this.TooltipKey = tooltip;
// }
// ToolbarButtonBase의 ExecuteClick 메서드를 그대로 사용합니다.
// 만약 StandardButton만의 특별한 클릭 전/후 처리가 필요하다면 ExecuteClick 메서드를 override 할 수 있습니다.
// /// <summary>
// /// 버튼 클릭 로직을 실행합니다.
// /// </summary>
// /// <param name="parameter">커맨드에 전달할 파라미터입니다.</param>
// public override void ExecuteClick(object parameter = null)
// {
// if (!IsEnabled) return;
//
// // UnityEngine.Debug.Log($"ToolbarStandardButton '{Text}' ExecuteClick 호출됨.");
//
// // 부모 클래스의 ExecuteClick (ClickCommand 실행 등)
// base.ExecuteClick(parameter);
//
// // 여기에 ToolbarStandardButton만의 추가적인 로직이 있다면 작성
// // 예를 들어, 클릭 후 특정 사운드 재생 등
// }
/// <summary>
/// 이 버튼 모델에 연결된 모든 이벤트 핸들러를 정리합니다.
/// ToolbarStandardButton은 ToolbarButtonBase에서 상속받은 이벤트 외에 추가적인 이벤트가 없으므로,
/// 기본 구현을 호출합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers();
// ToolbarStandardButton에 특화된 이벤트가 있다면 여기서 정리합니다.
// 예: CustomEvent = null;
}
}
}

View File

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

View File

@@ -0,0 +1,169 @@
using System;
namespace UVC.UI.Toolbar.Model
{
/// <summary>
/// 클릭할 때마다 선택(On) 또는 해제(Off) 상태가 전환되는 토글 버튼입니다.
/// ToolbarButtonBase를 상속받으며, 추가적으로 선택 상태(IsSelected)와
/// 해제 상태일 때의 아이콘 경로(OffIconSpritePath), 상태 변경 시 호출될 콜백(OnToggle)을 관리합니다.
/// </summary>
/// <remarks>
/// View 레이어(예: ToolbarView)에서는 이 모델에 해당하는 UI 요소(예: UnityEngine.UI.Toggle)를 생성하고,
/// 사용자가 UI 토글을 조작하면 이 모델의 ExecuteClick 메서드를 호출하거나 IsSelected 속성을 직접 변경하여
/// 모델과 UI의 상태를 동기화합니다. IsSelected 속성이 변경되면 OnStateChanged 및 OnToggleStateChanged 이벤트가 발생하여
/// View가 아이콘(선택/해제 상태에 따라 IconSpritePath 또는 OffIconSpritePath 사용) 및 기타 시각적 요소를 업데이트합니다.
/// </remarks>
/// <example>
/// <code>
/// // Toolbar 또는 ToolbarModel 등에서 ToolbarToggleButton 생성 및 사용 예시
///
/// // 1. 토글 상태 변경 시 실행될 액션 정의 (OnToggle 콜백용)
/// Action<bool> handleMuteToggle = (isMuted) =>
/// {
/// UnityEngine.Debug.Log(isMuted ? "음소거됨" : "음소거 해제됨");
/// // 여기에 실제 음소거/해제 로직 구현
/// };
///
/// // 2. 토글 버튼 클릭 시 실행될 커맨드 정의 (선택 사항, 주로 상태 변경 후 추가 작업)
/// // 이 커맨드는 IsSelected 상태가 변경된 *후에* ExecuteClick 내부에서 호출됩니다.
/// // 커맨드 파라미터로 현재 IsSelected 상태를 받고 싶다면 ActionCommand<bool> 사용 가능.
/// ICommand muteCommand = new ActionCommand<bool>((isSelected) =>
/// {
/// UnityEngine.Debug.Log($"음소거 버튼 커맨드 실행됨. 현재 선택 상태: {isSelected}");
/// });
///
/// // 3. ToolbarToggleButton 인스턴스 생성 및 속성 설정
/// ToolbarToggleButton muteToggleButton = new ToolbarToggleButton
/// {
/// Text = "button_mute", // 버튼 텍스트 (다국어 키)
/// IconSpritePath = "icons/toolbar/sound_on_icon", // 선택(On) 상태 아이콘
/// OffIconSpritePath = "icons/toolbar/sound_off_icon", // 해제(Off) 상태 아이콘
/// Tooltip = "tooltip_mute_button", // 툴팁 (다국어 키)
/// IsSelected = false, // 초기 상태는 해제(Off)
/// OnToggle = handleMuteToggle, // 상태 변경 시 콜백 할당
/// ClickCommand = muteCommand, // 클릭 커맨드 할당
/// IsEnabled = true // 버튼 활성화 상태
/// };
///
/// // 4. 생성된 버튼 모델을 ToolbarModel에 추가
/// // toolbarModel.AddItem(muteToggleButton);
/// // 또는
/// // toolbarModel.AddToggleButton("button_mute", false, "icons/toolbar/sound_on_icon", "icons/toolbar/sound_off_icon", handleMuteToggle, muteCommand, "tooltip_mute_button");
///
/// // 5. 버튼 상태 프로그래밍 방식으로 변경 예시
/// // muteToggleButton.IsSelected = true; // 버튼을 선택 상태로 변경 (OnToggle 콜백 및 이벤트 발생)
/// </code>
/// </example>
public class ToolbarToggleButton : ToolbarButtonBase
{
/// <summary>
/// 버튼의 선택 상태(IsSelected)가 변경될 때 발생하는 이벤트입니다.
/// 변경된 IsSelected 값을 파라미터로 전달합니다.
/// View에서 이 이벤트를 구독하여 UI의 시각적 상태(예: Toggle 컴포넌트의 isOn 상태)를 업데이트할 수 있습니다.
/// OnStateChanged 이벤트와 별개로, IsSelected 상태 변경에 좀 더 특화된 알림입니다.
/// </summary>
public event Action<bool> OnToggleStateChanged; // IsSelected 변경 시 IsSelected 값을 전달하는 이벤트
protected string _offIconSpritePath;
/// <summary>
/// 버튼이 선택되지 않은(해제된, Off) 상태일 때 표시될 아이콘의 Resources 폴더 내 경로입니다.
/// 이 값이 변경되면 OnStateChanged 이벤트가 발생하여 View가 아이콘을 업데이트할 수 있습니다.
/// 선택(On) 상태일 때는 ToolbarButtonBase의 IconSpritePath가 사용됩니다.
/// </summary>
public string OffIconSpritePath
{
get => _offIconSpritePath;
set
{
if (_offIconSpritePath != value)
{
_offIconSpritePath = value;
NotifyStateChanged();// 아이콘 경로 변경 시 전체 상태 변경으로 간주
}
}
}
private bool _isSelected;
/// <summary>
/// 버튼의 현재 선택 상태를 나타냅니다. true이면 선택(On)된 상태, false이면 해제(Off)된 상태입니다.
/// 이 속성 값이 변경되면 OnStateChanged 이벤트와 OnToggleStateChanged 이벤트가 발생하며,
/// OnToggle 콜백이 호출됩니다.
/// </summary>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnToggle?.Invoke(_isSelected); // OnToggle 콜백 호출
OnToggleStateChanged?.Invoke(_isSelected); // IsSelected 값과 함께 이벤트 발생
NotifyStateChanged(); // 일반 상태 변경 이벤트도 발생
}
}
}
/// <summary>
/// 버튼의 선택 상태(IsSelected)가 변경될 때 호출될 사용자 정의 콜백 액션입니다.
/// IsSelected 속성의 setter 내부에서 호출됩니다.
/// </summary>
/// <remarks>
/// ClickCommand와는 별개로, 순수하게 토글 상태 변경에 대한 응답 로직을 정의할 때 유용합니다.
/// 예를 들어, UI 상태 변경에 따른 즉각적인 모델 데이터 변경 등에 사용할 수 있습니다.
/// </remarks>
public Action<bool> OnToggle { get; set; }
/// <summary>
/// 버튼 클릭 로직을 실행합니다.
/// 토글 버튼의 경우, 클릭 시 IsSelected 상태를 반전시키고,
/// ClickCommand가 있다면 현재 IsSelected 상태를 파라미터로 전달하여 실행합니다.
/// </summary>
/// <param name="parameter">
/// 이 파라미터는 일반적으로 사용되지 않거나 무시될 수 있습니다.
/// ClickCommand에는 현재 IsSelected 상태가 전달됩니다.
/// 만약 외부에서 특정 값을 전달해야 한다면, ClickCommand가 ActionCommand<object> 등으로 정의되어야 합니다.
/// </param>
public override void ExecuteClick(object parameter = null)
{
if (!IsEnabled) return;
if (parameter is bool newSelectedStateFromUI)
{
// UI로부터 직접 상태가 전달된 경우 (View의 onValueChanged 리스너)
// IsSelected 프로퍼티 setter가 OnToggle 및 NotifyStateChanged를 호출
IsSelected = newSelectedStateFromUI;
}
else
{
// 일반적인 ExecuteClick (파라미터 없거나 bool이 아님) - 기존 토글 로직
IsSelected = !IsSelected;
}
// ClickCommand 실행. IsSelected 상태가 변경된 *후에* 실행됩니다.
// ClickCommand에 현재 선택 상태(IsSelected)를 파라미터로 전달합니다.
// 만약 ClickCommand가 파라미터를 받지 않는 ActionCommand라면, 파라미터 없이 실행됩니다.
// ClickCommand가 ActionCommand<bool>이라면 IsSelected 값이 전달됩니다.
// ClickCommand가 ActionCommand<object>라면 IsSelected 값이 object로 박싱되어 전달됩니다.
if (ClickCommand != null)
{
// ClickCommand의 구체적인 타입(제네릭 여부 등)에 따라 파라미터 전달 방식이 달라질 수 있습니다.
// 가장 일반적인 경우는 ClickCommand가 현재 상태를 알 수 있도록 하는 것입니다.
// ToolbarView에서 연결 시 new ActionCommand<bool>((isSelectedParam) => { /* 로직 */ }) 형태로 커맨드를 생성했다면,
// isSelectedParam으로 현재 IsSelected 값이 전달됩니다.
ClickCommand.Execute(IsSelected);
}
}
/// <summary>
/// 이 버튼 모델에 연결된 모든 이벤트 핸들러를 정리합니다.
/// ToolbarButtonBase의 이벤트 외에 ToolbarToggleButton에 특화된 이벤트(OnToggleStateChanged, OnToggle)를 추가로 정리합니다.
/// </summary>
public override void ClearEventHandlers()
{
base.ClearEventHandlers(); // 부모 클래스의 이벤트 정리 (OnStateChanged)
OnToggleStateChanged = null;
OnToggle = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9530db2a5b423bf4eb8c5d8e4f9dd5ed