toolbar 개발 완료. 주석 다는 중

This commit is contained in:
logonkhi
2025-06-17 20:19:06 +09:00
parent 63b71216cb
commit 078fc6df4c
162 changed files with 129223 additions and 2365645 deletions

View File

@@ -1,7 +1,40 @@
namespace UVC.UI.Toolbar
{
/// <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

@@ -1,18 +1,74 @@
using System;
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.UI.Toolbar
{
/// <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 속성 사용
/// TooltipKey = tooltip; // 부모 클래스의 TooltipKey 속성 사용
/// 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
{
public event Action OnStateChanged; // 상태 변경 알림 이벤트
/// <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;
@@ -27,6 +83,16 @@ namespace UVC.UI.Toolbar
}
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;
@@ -41,6 +107,16 @@ namespace UVC.UI.Toolbar
}
protected bool _isEnabled = true;
/// <summary>
/// 버튼의 활성화 상태를 나타냅니다. true이면 사용자와 상호작용할 수 있고, false이면 비활성화되어 상호작용할 수 없습니다.
/// 값이 변경되면 OnStateChanged 이벤트가 발생합니다.
/// </summary>
/// <example>
/// <code>
/// button.IsEnabled = false; // 버튼 비활성화
/// if (button.IsEnabled) { /* 버튼 사용 가능 로직 */ }
/// </code>
/// </example>
public bool IsEnabled
{
get => _isEnabled;
@@ -54,7 +130,18 @@ namespace UVC.UI.Toolbar
}
}
protected string _tooltipKey; // 툴팁 다국어 키
protected string _tooltipKey;
/// <summary>
/// 버튼에 마우스를 올렸을 때 표시될 툴팁의 텍스트 또는 다국어 키입니다.
/// TooltipKey 변경 시에는 기본적으로 OnStateChanged 이벤트가 발생하지 않지만,
/// 필요에 따라 View에서 이 값을 직접 참조하여 툴팁을 업데이트할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// button.TooltipKey = "tooltip_save_button"; // 다국어 키 사용
/// button.TooltipKey = "클릭하여 문서를 저장합니다."; // 직접 텍스트 사용
/// </code>
/// </example>
public string TooltipKey
{
get => _tooltipKey;
@@ -66,17 +153,42 @@ namespace UVC.UI.Toolbar
// TooltipKey 변경 시 OnStateChanged를 호출할 필요는 일반적으로 없으나,
// 만약 UI가 TooltipKey 자체를 표시하는 등의 로직이 있다면 필요할 수 있습니다.
// 여기서는 툴팁 내용이 동적으로 변경되는 경우가 적다고 가정하고 생략합니다.
// 필요하다면 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 이벤트에 의해 호출됩니다.
/// 이 메서드는 일반적으로 UI 시스템(예: Unity UI의 Button.onClick 이벤트)에 의해 호출되도록 설계됩니다.
/// 버튼이 활성화(IsEnabled == true)되어 있고 ClickCommand가 할당되어 있다면, 해당 커맨드를 실행합니다.
/// 파생 클래스에서 이 메서드를 재정의하여 특정 버튼 타입에 맞는 추가적인 클릭 동작을 구현할 수 있습니다.
/// </summary>
/// <param name="parameter">커맨드에 전달할 파라미터입니다.</param>
/// <param name="parameter">ClickCommand에 전달할 선택적 파라미터입니다.</param>
public virtual void ExecuteClick(object parameter = null)
{
if (IsEnabled && ClickCommand != null)
@@ -85,10 +197,24 @@ namespace UVC.UI.Toolbar
}
}
// OnStateChanged 이벤트를 외부에서 강제로 발생시켜야 할 때 사용 (예: 복합적인 상태 변경 후)
/// <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();
}
}
}
}

View File

@@ -1,70 +1,145 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine; // GameObject 사용을 위해 필요
// using UnityEngine.UI; // Image 사용을 위해 필요할 수 있으나, 모델 클래스에서는 직접적인 UI 참조를 최소화하는 것이 좋음
namespace UVC.UI.Toolbar
{
/// <summary>
/// 클릭 시 하위 버튼 그룹을 확장하여 보여주는 버튼입니다.
/// 하위 버튼 선택 시, 주 버튼의 내용이 업데이트될 수 있습니다.
/// 클릭 시 하위 버튼 목록(서브 메뉴)을 표시하거나 숨길 수 있는 확장형 버튼입니다.
/// ToolbarButtonBase를 상속받아 기본적인 버튼 속성(텍스트, 아이콘, 커맨드 등)을 가집니다.
/// 주 버튼을 클릭하면 연결된 서브 메뉴가 표시되며, 서브 메뉴의 버튼을 선택하면
/// 주 버튼의 텍스트나 아이콘이 선택된 하위 버튼의 것으로 변경될 수 있습니다.
/// </summary>
/// <remarks>
/// 이 모델 클래스는 데이터와 상태를 관리합니다. 실제 서브 메뉴의 표시/숨김 및 UI 렌더링은
/// ToolbarView와 같은 View 클래스에서 처리됩니다.
/// </remarks>
/// <example>
/// <code>
/// // ToolbarController 등에서 확장 버튼 생성 및 설정 예시
/// 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",
/// TooltipKey = "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",
/// TooltipKey = "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
{
public enum ExpansionDirection { Horizontal, Vertical }
/// <summary>
/// 이 확장 버튼에 속한 하위 버튼들의 목록입니다.
/// 이 목록에 있는 버튼들은 주 버튼 클릭 시 View에 의해 표시될 수 있습니다.
/// 각 하위 버튼은 ToolbarButtonBase 또는 그 파생 클래스의 인스턴스여야 합니다.
/// </summary>
public List<ToolbarButtonBase> SubButtons { get; private set; }
public ExpansionDirection Direction { get; set; } = ExpansionDirection.Vertical;
/// <summary>
/// 하위 버튼 중 하나가 선택되었을 때 호출되는 액션(콜백)입니다.
/// 선택된 하위 버튼 객체가 파라미터로 전달됩니다.
/// 주 버튼의 모양이 변경된 후, 추가적인 로직을 수행하고자 할 때 사용합니다.
/// </summary>
/// <remarks>
/// 이 콜백은 SelectSubButton 메서드 내에서, 주 버튼의 Text/IconSpritePath가 업데이트된 후 호출됩니다.
/// </remarks>
public Action<ToolbarButtonBase> OnSubButtonSelected { get; set; }
/// <summary>
/// ToolbarExpandableButton의 새 인스턴스를 초기화합니다.
/// SubButtons 리스트를 빈 리스트로 생성합니다.
/// </summary>
public ToolbarExpandableButton()
{
SubButtons = new List<ToolbarButtonBase>();
}
// 주 버튼 클릭 시 하위 메뉴를 토글하는 동작은 View에서 처리될 수 있고,
// ClickCommand는 주 버튼 자체의 액션(있다면)을 정의합니다.
// 또는 ClickCommand가 하위 메뉴 토글 로직을 포함할 수도 있습니다.
// 여기서는 ClickCommand는 주 버튼의 고유 액션, 하위 메뉴 토글은 View의 역할로 가정합니다.
/// <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는 주 버튼 자체의 액션 (예: 상태 변경, 특정 기능 수행)
ClickCommand?.Execute(parameter);
// 주 버튼 자체에 할당된 ClickCommand 실행 (예: 특정 모드 진입, 상태 변경 등)
base.ExecuteClick(parameter);
// 하위 메뉴를 여는 동작은 보통 View에서 이 버튼 클릭 시 별도로 처리합니다.
// OnClick (이제 ClickCommand)이 그 역할을 할 수지만,
// View에서 직접 ToggleSubMenu를 호출하는 것이 더 명확할 수 있습니다.
// ToolbarView의 SetupButtonVisualsAndInteractions에서 expandableModel.ExecuteClick() 후
// ToggleSubMenu()를 호출하는 현재 구조를 유지할 수 있습니다.
// 하위 메뉴를 여는 시각적 동작은 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)
{
bool changed = false;
// 주 버튼의 텍스트를 선택된 하위 버튼의 텍스트로 변경
// Text 속성의 setter는 내부적으로 OnStateChanged를 호출하여 View 업데이트를 트리거합니다.
if (this.Text != selectedSubButton.Text)
{
this.Text = selectedSubButton.Text; // Setter가 OnStateChanged 호출 (단, Text가 실제로 변경되어야 함)
changed = true;
this.Text = selectedSubButton.Text;
}
// 주 버튼의 아이콘 경로를 선택된 하위 버튼의 아이콘 경로로 변경
// IconSpritePath 속성의 setter는 내부적으로 OnStateChanged를 호출합니다.
if (this.IconSpritePath != selectedSubButton.IconSpritePath)
{
this.IconSpritePath = selectedSubButton.IconSpritePath; // Setter가 OnStateChanged 호출
changed = true;
this.IconSpritePath = selectedSubButton.IconSpritePath;
}
// 하위 버튼 선택 콜백 호출
OnSubButtonSelected?.Invoke(selectedSubButton);
// selectedSubButton.ExecuteClick(); // 하위 버튼의 클릭 로직 실행은 선택 사항
if (changed) // Text나 Icon이 실제로 변경된 경우에만 명시적으로 호출하거나, 각 setter에 맡김
{
// NotifyStateChanged(); // Text, Icon setter가 이미 호출하므로 중복될 수 있음.
// 만약 Text, Icon 외 다른 상태도 변경된다면 필요.
}
// 선택된 하위 버튼 자체의 ClickCommand 실행은 여기서 하지 않습니다.
// View에서 하위 버튼 UI 클릭 시 해당 하위 버튼의 ExecuteClick()이 직접 호출되는 것이 일반적입니다.
// 만약 여기서 실행해야 한다면: selectedSubButton.ExecuteClick();
}
}
}
}
}

View File

@@ -1,59 +1,153 @@
namespace UVC.UI.Toolbar
using System;
namespace UVC.UI.Toolbar
{
/// <summary>
/// 라디오 버튼 그룹 내에서 사용되는 버튼입니다.
/// 그룹 내 하나의 버튼만 선택될 수 있습니다.
/// 라디오 버튼 그룹 내에서 사용되는 버튼입니다. 동일한 GroupName을 가진 라디오 버튼들 중에서
/// 단 하나만 선택될 수 있도록 동작합니다. ToolbarToggleButton을 상속받습니다.
/// </summary>
/// <remarks>
/// 각 ToolbarRadioButton은 GroupName을 가지며, 이 GroupName을 기준으로 ToolbarModel에서
/// ToolbarRadioButtonGroup에 의해 관리됩니다. 사용자가 라디오 버튼을 클릭하면,
/// 이 버튼은 자신이 속한 RadioGroup에 선택 상태 변경을 요청하고, RadioGroup은
/// 그룹 내 다른 버튼들의 선택을 해제하고 이 버튼만 선택된 상태로 만듭니다.
/// IsSelected 상태 변경 시 OnToggle, OnStateChanged, OnToggleStateChanged 이벤트가 발생합니다.
/// </remarks>
/// <example>
/// <code>
/// // ToolbarController 또는 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",
/// TooltipKey = "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",
/// TooltipKey = "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 wasSelected = IsSelected;
// bool previousSelectionState = IsSelected; // 이전 선택 상태 (필요하다면)
if (RadioGroup != null)
{
// SetSelected는 IsSelected 상태를 변경하고,
// IsSelected setter는 OnToggleStateChanged 및 NotifyStateChanged를 호출합니다.
// OnToggle 콜백도 IsSelected setter 내부 또는 SetSelected 메서드 내에서 호출될 수 있습니다.
// RadioGroup의 SetSelected 메서드를 호출하여 그룹 내 선택 상태를 관리합니다.
// SetSelected 내부에서 이 버튼의 IsSelected가 true로 설정되고,
// 다른 버튼들은 false로 설정됩니다.
// IsSelected setter는 OnToggle, OnStateChanged, OnToggleStateChanged 이벤트를 발생시킵니다.
RadioGroup.SetSelected(this);
}
else
{
// 그룹이 없는 라디오 버튼은 의미가 모호하므로, 단독으로 선택되는 것을 방지하거나 특별 처리.
// 여기서는 그룹이 없으면 아무것도 하지 않거나, 경고를 로깅할 수 있습니다.
UnityEngine.Debug.LogWarning($"ToolbarRadioButton '{Text}' (그룹: {GroupName})에 RadioGroup이 할당되지 않았습니다.");
// 또는 강제로 선택 상태로 만들고 콜백 호출 (기존 주석 참고)
// if (!IsSelected) // 현재 선택되지 않았다면 선택
// {
// IsSelected = true;
// OnToggle?.Invoke(IsSelected);
// }
// RadioGroup이 할당되지 않은 경우 (예: ToolbarModel에 추가되기 전 또는 독립적으로 사용 시도)
// 이 경우 일반 토글 버튼처럼 동작하거나, 경고를 로깅할 수 있습니다.
// 현재 구현은 그룹이 없으면 단독으로 선택되는 것을 방지하거나 특별 처리를 하도록 되어 있습니다.
UnityEngine.Debug.LogWarning($"ToolbarRadioButton '{Text}' (그룹: {GroupName})에 RadioGroup이 할당되지 않았습니다. 단독으로 상태가 변경될 수 없습니다. ToolbarModel에 먼저 추가되어야 합니다.");
// 만약 그룹 없이도 토글 가능하게 하려면 아래 주석 해제 및 로직 수정 필요
// IsSelected = !IsSelected;
}
// ClickCommand 실행
// RadioGroup.SetSelected에 의해 IsSelected 상태가 변경된 후 실행됩니다.
// parameter가 null이 아니면 그것을 우선 사용하고, 아니면 현재 IsSelected 상태를 전달할 수 있습니다.
// 또는 버튼 자체를 파라미터로 전달하여 Command가 필요한 정보를 추출하도록 할 수도 있습니다.
object commandParameterToUse = parameter ?? this; // 예: 파라미터가 없으면 버튼 인스턴스 전달
// 라디오 버튼의 경우, ClickCommand는 주로 해당 버튼이 "선택되었을 때"의 액션을 정의합니다.
// 따라서 IsSelected가 true일 때만 Command를 실행하는 것이 일반적일 수 있습니다.
if (IsSelected) // 현재 버튼이 선택된 상태일 때만 Command 실행
// ClickCommand 실행:
// 라디오 버튼의 ClickCommand는 일반적으로 해당 버튼이 "선택되었을 때"의 액션을 정의합니다.
// RadioGroup.SetSelected에 의해 IsSelected 상태가 true로 변경된 후에 실행되어야 합니다.
if (IsSelected && ClickCommand != null)
{
ClickCommand?.Execute(commandParameterToUse);
// 파라미터 처리: parameter가 null이 아니면 그것을 우선 사용하고,
// 아니면 현재 IsSelected 상태(true) 또는 버튼 자체(this)를 전달할 수 있습니다.
// ToolbarView의 SetupButtonVisualsAndInteractions에서 radioModel.ExecuteClick(true)로 호출하는 경우,
// parameter는 true가 됩니다.
object commandParameterToUse = parameter ?? this; // 예: 명시적 파라미터가 없으면 버튼 인스턴스 전달
ClickCommand.Execute(commandParameterToUse);
}
// 만약 선택 해제 시에도 Command를 실행해야 한다면 위 if 조건을 제거합니다.
// 혹은, 선택/해제 상태 모두에 대해 Command를 실행하되, Command 내부에서 IsSelected 값을 확인하도록 합니다.
// 만약 선택 해제 시(다른 라디오 버튼이 선택되어 이 버튼이 해제될 때)에도 Command를 실행해야 한다면,
// 위 if(IsSelected) 조건을 제거하거나, 별도의 Command(예: DeselectCommand)를 고려해야 합니다.
// 하지만 일반적인 라디오 버튼의 사용 패턴은 선택 시의 액션에 중점을 둡니다.
}
}
}

View File

@@ -1,42 +1,150 @@
using System.Collections.Generic;
using System.Linq;
namespace UVC.UI.Toolbar
{
/// <summary>
/// 라디오 버튼들을 그룹으로 관리하여 하나만 선택되도록 합니다.
/// ToolbarRadioButtonGroup, ToolbarExpandableButton 클래스는 이전 제안과 거의 동일하게 유지하되,
/// 상태 변경 시 NotifyStateChanged() 호출을 고려할 수 있습니다.
/// 예를 들어 ToolbarExpandableButton에서 SelectSubButton 후 주 버튼의 Text, Icon이 변경되면 NotifyStateChanged() 호출
/// 여러 개의 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);
button.RadioGroup = this;
}
}
/// <summary>
/// 지정된 라디오 버튼을 그룹 내에서 선택된 상태로 설정합니다.
/// 이전에 선택되었던 다른 버튼은 선택 해제 상태(IsSelected = false)로 변경됩니다.
/// </summary>
/// <param name="buttonToSelect">선택할 ToolbarRadioButton입니다. 이 버튼은 반드시 그룹에 미리 등록되어 있어야 합니다.</param>
public void SetSelected(ToolbarRadioButton buttonToSelect)
{
if (!_buttons.Contains(buttonToSelect) || !buttonToSelect.IsEnabled) return;
SelectedButton = buttonToSelect;
foreach (var btn in _buttons)
if (buttonToSelect == null || !_buttons.Contains(buttonToSelect))
{
bool isNowSelected = (btn == SelectedButton);
if (btn.IsSelected != isNowSelected) // 실제 상태 변경이 있을 때만
{
btn.IsSelected = isNowSelected; // 이 setter가 OnStateChanged를 호출
// btn.OnClick?.Invoke(); // OnClick은 버튼 자체의 ExecuteClick에서 관리하는 것이 더 적절할 수 있음
// 또는 선택 변경 시 항상 호출하고 싶다면 여기에 둠
// btn.OnToggle?.Invoke(isNowSelected); // OnToggle은 IsSelected setter에서 OnToggleStateChanged로 대체 가능
}
// 그룹에 등록되지 않은 버튼을 선택하려고 하면 무시하거나 경고를 로깅할 수 있습니다.
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

@@ -1,7 +1,44 @@
namespace UVC.UI.Toolbar
{
/// <summary>
/// 툴바 구분선을 나타냅니다.
/// 툴바 내에서 항목들(예: 버튼 그룹)을 시각적으로 구분하는 구분선을 나타냅니다.
/// IToolbarItem 인터페이스를 구현하여 툴바 시스템(ToolbarModel)에 추가될 수 있습니다.
/// </summary>
public class ToolbarSeparator : IToolbarItem { }
}
/// <remarks>
/// ToolbarSeparator는 일반적으로 특별한 동작이나 속성을 가지지 않으며,
/// View 레이어(예: ToolbarView)에서 이 항목을 만나면 시각적인 구분선 UI (예: 선, 공백 등)를 렌더링합니다.
/// 프리팹 기반으로 UI가 생성될 경우, 구분선에 해당하는 프리팹이 사용됩니다.
/// </remarks>
/// <example>
/// <code>
/// // ToolbarController 또는 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

@@ -1,19 +1,84 @@
namespace UVC.UI.Toolbar
{
/// <summary>
/// 일반적인 클릭 버튼입니다.
/// 한 번 클릭으로 특정 동작을 수행하는 표준적인 버튼입니다.
/// ToolbarButtonBase를 상속받아 텍스트, 아이콘, 툴팁, 활성화 상태 및 클릭 커맨드 기능을 가집니다.
/// </summary>
/// <remarks>
/// 이 클래스는 ToolbarButtonBase의 기능을 그대로 사용하며, 특별히 추가된 로직은 없습니다.
/// 복잡한 상태 관리 없이 단순 클릭 동작만을 필요로 하는 경우에 사용됩니다.
/// View 레이어(예: ToolbarView)에서는 이 모델에 해당하는 UI 요소(예: UnityEngine.UI.Button)를 생성하고,
/// 사용자가 UI 버튼을 클릭하면 이 모델의 ExecuteClick 메서드가 호출되도록 연결합니다.
/// </remarks>
/// <example>
/// <code>
/// // ToolbarController 또는 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 폴더 내 아이콘 경로
/// TooltipKey = "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
{
// 생성자 또는 초기화 메서드가 있다면 ICommand를 받도록 수정
// 예: public ToolbarStandardButton(ICommand command) { this.ClickCommand = command; }
// ToolbarStandardButton은 ToolbarButtonBase의 모든 기능을 상속받습니다.
// 특별히 ToolbarStandardButton만을 위한 추가적인 속성이나 메서드가 필요하다면 여기에 정의할 수 있습니다.
// ToolbarButtonBase의 ExecuteClick을 그대로 사용하거나,
// 필요시 override 할 수 있습니다.
// public override void ExecuteClick()
// 예시: 생성자를 통해 주요 속성을 설정하도록 할 수도 있습니다.
// /// <summary>
// /// 지정된 속성으로 ToolbarStandardButton의 새 인스턴스를 초기화합니다.
// /// </summary>
// /// <param name="text">버튼 텍스트 또는 다국어 키입니다.</param>
// /// <param name="iconSpritePath">아이콘의 Resources 경로입니다.</param>
// /// <param name="command">클릭 시 실행될 커맨드입니다.</param>
// /// <param name="tooltipKey">툴팁 텍스트 또는 다국어 키입니다.</param>
// public ToolbarStandardButton(string text, string iconSpritePath = null, ICommand command = null, string tooltipKey = null)
// {
// base.ExecuteClick(); // 기본 Command 실행
// // 추가적인 StandardButton만의 로직
// this.Text = text;
// this.IconSpritePath = iconSpritePath;
// this.ClickCommand = command;
// this.TooltipKey = tooltipKey;
// }
// 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만의 추가적인 로직이 있다면 작성
// // 예를 들어, 클릭 후 특정 사운드 재생 등
// }
}
}
}

View File

@@ -4,14 +4,73 @@ using UnityEngine;
namespace UVC.UI.Toolbar
{
/// <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>
/// // ToolbarController 또는 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) 상태 아이콘
/// TooltipKey = "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;
@@ -20,12 +79,17 @@ namespace UVC.UI.Toolbar
if (_offIconSpritePath != value)
{
_offIconSpritePath = value;
NotifyStateChanged();
NotifyStateChanged();// 아이콘 경로 변경 시 전체 상태 변경으로 간주
}
}
}
private bool _isSelected;
/// <summary>
/// 버튼의 현재 선택 상태를 나타냅니다. true이면 선택(On)된 상태, false이면 해제(Off)된 상태입니다.
/// 이 속성 값이 변경되면 OnStateChanged 이벤트와 OnToggleStateChanged 이벤트가 발생하며,
/// OnToggle 콜백이 호출됩니다.
/// </summary>
public bool IsSelected
{
get => _isSelected;
@@ -34,13 +98,33 @@ namespace UVC.UI.Toolbar
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;
@@ -57,12 +141,19 @@ namespace UVC.UI.Toolbar
IsSelected = !IsSelected;
}
OnToggle?.Invoke(IsSelected); // IsSelected는 이미 위에서 최종 상태로 설정됨
// ClickCommand 실행. 필요하다면 IsSelected나 다른 값을 파라미터로 전달.
// 여기서는 parameter 인자로 받은 값을 우선 사용하고, 없으면 IsSelected를 사용.
object commandParameter = parameter ?? IsSelected;
ClickCommand?.Execute(commandParameter);
// 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);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,219 @@
using UnityEngine;
using UVC.Locale;
using UVC.UI.Commands;
using UVC.UI.Toolbar;
namespace UVC.UI.ToolBar
{
/// <summary>
/// 툴바의 생성, 설정 및 관리를 담당하는 MonoBehaviour 컨트롤러 클래스입니다.
/// ToolbarModel(데이터)과 ToolbarView(UI 표현) 사이의 중재자 역할을 합니다.
/// </summary>
/// <remarks>
/// 주요 역할:
/// - 초기화(Awake, Start): 필요한 컴포넌트(특히 ToolbarView)를 찾거나 확인합니다.
/// - 모델 생성 및 설정(Start): ToolbarModel 인스턴스를 만들고,
/// AddStandardButton, AddRadioButton 등의 메서드를 사용해 툴바에 표시될 항목들을 정의하고 추가합니다.
/// - 뷰 초기화(Start): 설정된 ToolbarModel을 ToolbarView에 전달하여 UI를 렌더링하도록 합니다.
///
/// 이 클래스의 인스턴스는 Unity 씬 내의 GameObject에 컴포넌트로 추가되어야 합니다.
/// ToolbarView 컴포넌트도 동일한 GameObject 또는 그 자식에 존재해야 합니다.
/// </remarks>
/// <example>
/// <code>
/// // Unity 에디터에서 GameObject를 생성하고 이 ToolboxController 스크립트를 추가합니다.
/// // 또한, ToolbarView 스크립트도 해당 GameObject 또는 자식 GameObject에 추가하고,
/// // ToolbarView의 프리팹 필드들(standardButtonPrefab 등)을 Inspector에서 할당해야 합니다.
///
/// // 이 스크립트의 Start 메서드 내에서 툴바 항목들이 정의됩니다.
/// // 예:
/// // mainToolbar.AddStandardButton("파일 열기", "icons/open", new ActionCommand(OpenFile), "파일을 엽니다.");
/// // private void OpenFile() { Debug.Log("파일 열기 기능 실행"); }
/// </code>
/// </example>
public class ToolboxController : MonoBehaviour
{
/// <summary>
/// 툴바의 데이터 모델입니다. 툴바 항목들의 정보와 구조를 담고 있습니다.
/// protected로 선언되어 파생 클래스에서 접근 가능합니다.
/// </summary>
protected ToolbarModel mainToolbar;
/// <summary>
/// 툴바의 UI 표현을 담당하는 뷰 컴포넌트입니다.
/// protected로 선언되어 파생 클래스에서 접근 가능합니다.
/// </summary>
protected ToolbarView mainToolbarView;
/// <summary>
/// MonoBehaviour의 Awake 메서드입니다.
/// 주로 현재 GameObject 또는 자식 GameObject에서 ToolbarView 컴포넌트를 찾아 mainToolbarView 필드에 할당합니다.
/// </summary>
protected virtual void Awake()
{
// 1. 이 GameObject에 연결된 ToolbarView 컴포넌트를 찾습니다.
mainToolbarView = GetComponent<ToolbarView>();
// 2. 만약 현재 GameObject에 없다면, 자식 GameObject들 중에서 ToolbarView 컴포넌트를 찾습니다.
if (mainToolbarView == null)
{
mainToolbarView = GetComponentInChildren<ToolbarView>();
}
if (mainToolbarView == null)
{
Debug.LogError("ToolboxController: ToolbarView 컴포넌트를 찾을 수 없습니다. GameObject에 ToolbarView를 추가하고 연결해주세요.");
}
}
/// <summary>
/// MonoBehaviour의 Start 메서드입니다. 첫 번째 프레임 업데이트 전에 호출됩니다.
/// ToolbarModel을 생성하고, 다양한 툴바 항목들을 모델에 추가한 후,
/// 설정된 모델을 사용하여 ToolbarView를 초기화(UI 렌더링)합니다.
/// </summary>
protected virtual void Start()
{
// ToolbarModel 인스턴스 생성
mainToolbar = new ToolbarModel();
// ToolbarView가 제대로 할당되었는지 확인
if (mainToolbarView == null)
{
// Awake에서 이미 로그를 남겼을 수 있지만, 한 번 더 확인하여 Start 로직 중단
Debug.LogError("ToolboxController: ToolbarView가 할당되지 않아 툴바를 초기화할 수 없습니다.");
return;
}
// --- 툴바 모델 구성 시작 ---
// 여기에 다양한 툴바 항목(버튼, 구분선 등)을 mainToolbar 모델에 추가합니다.
// 예시 1: 카메라 조절 라디오 버튼 그룹
// "CameraControlGroup"이라는 이름으로 라디오 버튼 그룹을 만듭니다.
// AddRadioButton의 세 번째 파라미터(initialState)는 해당 버튼이 초기에 선택될지 여부입니다.
// 각 버튼은 아이콘 경로(선택 시/해제 시), OnToggle 콜백, ClickCommand, 툴팁 키를 가질 수 있습니다.
mainToolbar.AddRadioButton("CameraControlGroup", "Top View", true,
"Prefabs/UI/Toolbar/images/ic_camera_top_on",
"Prefabs/UI/Toolbar/images/ic_camera_top_off_white",
(isSelected) => { if (isSelected) Debug.Log("탑뷰 카메라 선택됨"); },
new ActionCommand(() => Debug.Log("탑뷰 카메라 Command 실행")),
"Top View 시점으로 변경합니다.");
mainToolbar.AddRadioButton("CameraControlGroup", "Quarter View", false,
"Prefabs/UI/Toolbar/images/ic_camera_quarter_on",
"Prefabs/UI/Toolbar/images/ic_camera_quarter_off_white",
(isSelected) => { if (isSelected) Debug.Log("쿼터뷰 카메라 선택됨"); },
new ActionCommand(() => Debug.Log("쿼터뷰 카메라 Command 실행")),
"Quarter View 시점으로 변경합니다.");
mainToolbar.AddRadioButton("CameraControlGroup", "Front View", false,
"Prefabs/UI/Toolbar/images/ic_camera_top_on",
"Prefabs/UI/Toolbar/images/ic_camera_top_off_white",
(isSelected) => { if (isSelected) Debug.Log("프런트뷰 카메라 선택됨"); },
new ActionCommand(() => Debug.Log("프런트뷰 카메라 Command 실행")),
"Front View 시점으로 변경합니다.");
// 예시 2: 구분선 추가
mainToolbar.AddSeparator();
// 예시 3: 표준 버튼 (객체 선택)
// AddStandardButton은 텍스트, 아이콘 경로, 클릭 커맨드, 툴팁 키를 파라미터로 받습니다.
mainToolbar.AddStandardButton("선택", // 버튼 텍스트 (또는 다국어 키)
"Prefabs/UI/Toolbar/images/ic_select_white", // 아이콘 경로
new ActionCommand(() => Debug.Log("객체 선택 버튼 클릭됨")), // 클릭 시 실행될 커맨드
"객체를 선택합니다."); // 툴팁
// 객체 이동
mainToolbar.AddStandardButton("이동",
"Prefabs/UI/Toolbar/images/ic_move_white",
new ActionCommand(() => Debug.Log("객체 이동 버튼 클릭됨")),
"객체를 이동 시킵니다.");
// 객체 회전
mainToolbar.AddStandardButton("회전",
"Prefabs/UI/Toolbar/images/ic_rotation_white",
new ActionCommand(() => Debug.Log("객체 회전 버튼 클릭됨")),
"객체의 각도를 조절합니다.");
// 객체 크기조절
mainToolbar.AddStandardButton("크기조절",
"Prefabs/UI/Toolbar/images/ic_scale_white",
new ActionCommand(() => Debug.Log("객체 크기조절 버튼 클릭됨")),
"객체 크기를 조절합니다.");
// 객체 복제
mainToolbar.AddStandardButton("복제",
"Prefabs/UI/Toolbar/images/ic_copy_white",
new ActionCommand(() => Debug.Log("객체 복제 버튼 클릭됨")),
"객체를 복제 합니다.");
// 객체 삭제
mainToolbar.AddStandardButton("삭제",
"Prefabs/UI/Toolbar/images/ic_delete_white",
new ActionCommand(() => Debug.Log("객체 삭제 버튼 클릭됨")),
"객체를 삭제 합니다.");
mainToolbar.AddSeparator();
// 예시 4: 화면 캡처 버튼 (텍스트가 다국어 키일 수 있음)
mainToolbar.AddStandardButton("button_capture_screen", // 다국어 키로 사용될 수 있는 텍스트
"Prefabs/UI/Toolbar/images/ic_chapture_white",
new ActionCommand(() => Debug.Log("화면 캡처 버튼 클릭됨")),
"tooltip_capture_screen"); // 툴팁도 다국어 키 사용 가능
// 예시 5: 화면 녹화 시작/중지 토글 버튼
// AddToggleButton은 초기 상태, 선택/해제 아이콘, OnToggle 콜백 등을 설정합니다.
// ClickCommand는 ActionCommand<bool>을 사용하여 현재 토글 상태를 파라미터로 받을 수 있습니다.
mainToolbar.AddToggleButton("button_record_screen", false, // 초기 상태: 꺼짐(false)
"Prefabs/UI/Toolbar/images/ic_record_on_white", // 켜짐(selected) 상태 아이콘
"Prefabs/UI/Toolbar/images/ic_record_off_white", // 꺼짐(deselected) 상태 아이콘
(isSelected) => Debug.Log($"화면 녹화 상태: {(isSelected ? " " : "")} (OnToggle 콜백)"),
new ActionCommand<bool>((isRecording) => Debug.Log($"화면 녹화 Command 실행: {(isRecording ? " " : " ")}")),
"tooltip_record_screen");
// 예시 6: 확장 버튼 (브러시 크기 선택)
// AddExpandableButton으로 주 버튼을 만들고, 반환된 객체의 SubButtons 리스트에 하위 버튼들을 추가합니다.
var expandableBtnModel = mainToolbar.AddExpandableButton("button_brush_size", // 주 버튼 텍스트/키
"Prefabs/UI/Toolbar/images/ic_brush_default_white", // 주 버튼 기본 아이콘
new ActionCommand(() => Debug.Log("브러시 크기 주 버튼 클릭됨 (Command)")), // 주 버튼 자체의 커맨드
"붓 사이즈 선택 합니다."); // 주 버튼 툴팁
// 하위 버튼1: 작은 브러시 (ToolbarStandardButton 사용)
var smallBrushCmd = new ActionCommand(() => Debug.Log($"작은 브러시 선택됨"));
var smallBrush = new ToolbarStandardButton
{
Text = "brush_size_small", // 하위 버튼 텍스트/키
IconSpritePath = "Prefabs/UI/Toolbar/images/ic_brush_small_white", // 하위 버튼 아이콘
TooltipKey = "tooltip_brush_small", // 하위 버튼 툴팁
ClickCommand = smallBrushCmd
};
expandableBtnModel.SubButtons.Add(smallBrush); // 확장 버튼 모델에 하위 버튼 추가
// 하위 버튼2: 중간 브러시
var mediumBrush = new ToolbarStandardButton
{
Text = "brush_size_medium",
IconSpritePath = "Prefabs/UI/Toolbar/images/ic_brush_medium_white",
TooltipKey = "tooltip_brush_medium",
ClickCommand = new ActionCommand(() => Debug.Log("중간 브러시 선택됨 (Sub-Command 실행)"))
};
expandableBtnModel.SubButtons.Add(mediumBrush);
// 확장 버튼의 하위 버튼이 선택되었을 때 호출될 콜백 설정
expandableBtnModel.OnSubButtonSelected = (selectedSubButtonModel) =>
{
// LocalizationManager를 사용하여 텍스트를 현재 언어에 맞게 가져올 수 있습니다.
string localizedSubButtonText = LocalizationManager.Instance != null ? LocalizationManager.Instance.GetString(selectedSubButtonModel.Text) : selectedSubButtonModel.Text;
Debug.Log($"브러시 크기 '{localizedSubButtonText}' 선택됨 (OnSubButtonSelected 콜백). 주 버튼 업데이트 로직 실행 가능.");
};
// --- 툴바 모델 구성 끝 ---
// 설정이 완료된 ToolbarModel을 ToolbarView에 전달하여 UI 렌더링 시작
mainToolbarView.Initialize(mainToolbar);
// 예시: 모델 상태를 코드로 변경하고 UI가 업데이트되는지 테스트 (주석 처리된 기존 코드)
// StartCoroutine(TestModelChange(saveBtnModel, muteToggleModel));
// 이 테스트는 특정 버튼 모델의 상태를 시간차를 두고 변경하여 UI가 반응하는지 확인하는 용도입니다.
}
}
}

View File

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