탐색기, 라이브러리, 화면 객체 배치

This commit is contained in:
logonkhi
2025-12-18 20:38:38 +09:00
parent beca5f0da5
commit deeaa9a7ad
102 changed files with 4956 additions and 387 deletions

View File

@@ -58,6 +58,12 @@ namespace UVC.UI.Toolbar.Model
/// </remarks>
public event Action OnStateChanged;
/// <summary>
/// 버튼이 클릭되었을 때 발생하는 이벤트입니다.
/// ExecuteClick 메서드가 호출될 때 발생합니다.
/// </summary>
public event Action OnClicked;
protected string _text;
/// <summary>
/// 버튼에 표시될 텍스트 또는 텍스트의 다국어 키입니다.
@@ -191,10 +197,10 @@ namespace UVC.UI.Toolbar.Model
/// <param name="parameter">ClickCommand에 전달할 선택적 파라미터입니다.</param>
public virtual void ExecuteClick(object parameter = null)
{
if (IsEnabled && ClickCommand != null)
{
ClickCommand.Execute(parameter); // 커맨드에 파라미터 전달
}
if (!IsEnabled) return;
ClickCommand?.Execute(parameter); // 커맨드에 파라미터 전달
OnClicked?.Invoke(); // 클릭 이벤트 발생
}
/// <summary>
@@ -225,6 +231,7 @@ namespace UVC.UI.Toolbar.Model
public virtual void ClearEventHandlers()
{
OnStateChanged = null;
OnClicked = null;
}
}
}

View File

@@ -71,6 +71,42 @@ namespace UVC.UI.Toolbar.Model
/// </remarks>
public Action<ToolbarButtonBase> OnSubButtonSelected { get; set; }
/// <summary>
/// 하위 버튼 선택 변경 시 발생하는 이벤트입니다.
/// 확장 버튼의 텍스트와 선택된 하위 버튼의 텍스트가 파라미터로 전달됩니다.
/// Toolbar/Toolbox에서 OnAction 이벤트를 발생시키기 위해 구독합니다.
/// </summary>
public event Action<string, string> OnSubButtonSelectionChanged;
/// <summary>
/// 현재 선택된 하위 버튼을 저장합니다.
/// 동일한 버튼이 다시 선택될 때 이벤트 중복 발생을 방지합니다.
/// </summary>
private ToolbarButtonBase _selectedSubButton;
/// <summary>
/// 현재 선택된 하위 버튼을 가져옵니다.
/// </summary>
public ToolbarButtonBase SelectedSubButton => _selectedSubButton;
/// <summary>
/// AddExpandableButton에서 설정한 원본 텍스트를 저장합니다.
/// OnSubButtonSelectionChanged 이벤트 발생 시 이 값이 Text로 전달됩니다.
/// </summary>
private string _originalText;
/// <summary>
/// AddExpandableButton에서 설정한 원본 텍스트를 가져옵니다.
/// </summary>
public string OriginalText => _originalText;
/// <summary>
/// 서브 버튼 선택 시 주 버튼의 아이콘을 선택된 서브 버튼의 아이콘으로 변경할지 여부입니다.
/// true이면 아이콘이 변경되고, false이면 원래 아이콘을 유지합니다.
/// 기본값은 true입니다.
/// </summary>
public bool UpdateIconOnSelection { get; set; } = false;
/// <summary>
/// ToolbarExpandableButton의 새 인스턴스를 초기화합니다.
/// SubButtons 리스트를 빈 리스트로 생성합니다.
@@ -80,6 +116,15 @@ namespace UVC.UI.Toolbar.Model
SubButtons = new List<ToolbarButtonBase>();
}
/// <summary>
/// 원본 텍스트를 설정합니다. AddExpandableButton에서 호출됩니다.
/// </summary>
/// <param name="text">원본 텍스트</param>
public void SetOriginalText(string text)
{
_originalText = text;
}
/// <summary>
/// 주 확장 버튼이 클릭되었을 때의 로직을 실행합니다.
/// 기본적으로 부모 클래스(ToolbarButtonBase)의 ExecuteClick을 호출하여
@@ -118,23 +163,40 @@ namespace UVC.UI.Toolbar.Model
{
if (selectedSubButton != null && selectedSubButton.IsEnabled)
{
// 주 버튼의 텍스트를 선택된 하위 버튼의 텍스트로 변경
// Text 속성의 setter는 내부적으로 OnStateChanged를 호출하여 View 업데이트를 트리거합니다.
if (this.Text != selectedSubButton.Text)
// 동일한 버튼이 다시 선택된 경우 이벤트를 발생시키지 않음
if (_selectedSubButton == selectedSubButton)
{
this.Text = selectedSubButton.Text;
return;
}
// 주 버튼의 아이콘 경로를 선택된 하위 버튼의 아이콘 경로로 변경
// IconSpritePath 속성의 setter는 내부적으로 OnStateChanged를 호출합니다.
if (this.IconSpritePath != selectedSubButton.IconSpritePath)
// 현재 선택된 하위 버튼 업데이트
_selectedSubButton = selectedSubButton;
// UpdateIconOnSelection이 true일 때만 주 버튼의 텍스트와 아이콘을 변경
if (UpdateIconOnSelection)
{
this.IconSpritePath = selectedSubButton.IconSpritePath;
// 주 버튼의 텍스트를 선택된 하위 버튼의 텍스트로 변경
// Text 속성의 setter는 내부적으로 OnStateChanged를 호출하여 View 업데이트를 트리거합니다.
if (Text != selectedSubButton.Text)
{
Text = selectedSubButton.Text;
}
// 주 버튼의 아이콘 경로를 선택된 하위 버튼의 아이콘 경로로 변경
// IconSpritePath 속성의 setter는 내부적으로 OnStateChanged를 호출합니다.
if (IconSpritePath != selectedSubButton.IconSpritePath)
{
IconSpritePath = selectedSubButton.IconSpritePath;
}
}
// 하위 버튼 선택 콜백 호출
OnSubButtonSelected?.Invoke(selectedSubButton);
// 하위 버튼 선택 변경 이벤트 발생 (Toolbar/Toolbox에서 OnAction 이벤트 발생용)
// _originalText는 AddExpandableButton에서 설정한 원본 텍스트
OnSubButtonSelectionChanged?.Invoke(_originalText, selectedSubButton.Text);
// 선택된 하위 버튼 자체의 ClickCommand 실행은 여기서 하지 않습니다.
// View에서 하위 버튼 UI 클릭 시 해당 하위 버튼의 ExecuteClick()이 직접 호출되는 것이 일반적입니다.
// 만약 여기서 실행해야 한다면: selectedSubButton.ExecuteClick();
@@ -148,6 +210,7 @@ namespace UVC.UI.Toolbar.Model
{
base.ClearEventHandlers(); // 부모 클래스의 이벤트 정리 (OnStateChanged)
OnSubButtonSelected = null;
OnSubButtonSelectionChanged = null;
if (SubButtons != null)
{

View File

@@ -200,10 +200,12 @@ namespace UVC.UI.Toolbar.Model
/// <param name="iconSpritePath">주 버튼의 기본 아이콘 경로입니다 (선택 사항).</param>
/// <param name="command">주 버튼 클릭 시 실행될 ICommand 객체입니다 (선택 사항, 하위 메뉴 토글과는 별개일 수 있음).</param>
/// <param name="tooltip">주 버튼 툴팁의 텍스트 또는 다국어 키입니다 (선택 사항).</param>
/// <param name="updateIconOnSelection">서브 버튼 선택 시 주 버튼의 아이콘을 변경할지 여부입니다 (기본값: true).</param>
/// <returns>생성되고 추가된 ToolbarExpandableButton 객체입니다.</returns>
public ToolbarExpandableButton AddExpandableButton(string text, string iconSpritePath = null, ICommand command = null, string tooltip = null)
public ToolbarExpandableButton AddExpandableButton(string text, string iconSpritePath = null, ICommand command = null, string tooltip = null, bool updateIconOnSelection = true)
{
var button = new ToolbarExpandableButton { Text = text, IconSpritePath = iconSpritePath, ClickCommand = command, Tooltip = tooltip };
var button = new ToolbarExpandableButton { Text = text, IconSpritePath = iconSpritePath, ClickCommand = command, Tooltip = tooltip, UpdateIconOnSelection = updateIconOnSelection };
button.SetOriginalText(text); // 원본 텍스트 저장 (OnSubButtonSelectionChanged 이벤트에서 사용)
AddItem(button);
return button;
}
@@ -216,6 +218,127 @@ namespace UVC.UI.Toolbar.Model
AddItem(new ToolbarSeparator());
}
/// <summary>
/// 지정된 이름의 라디오 버튼 그룹을 가져옵니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <returns>해당 이름의 ToolbarRadioButtonGroup 객체입니다. 존재하지 않으면 null을 반환합니다.</returns>
public ToolbarRadioButtonGroup GetRadioButtonGroup(string groupName)
{
if (_radioGroups.TryGetValue(groupName, out var group))
{
return group;
}
return null;
}
/// <summary>
/// 지정된 그룹 내에서 특정 라디오 버튼을 선택하거나, 모든 선택을 해제합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonToSelect">선택할 라디오 버튼입니다. null을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 상태 변경 이벤트(OnToggle, OnStateChanged 등)를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 그룹이 존재하지 않으면 false를 반환합니다.</returns>
public bool SetRadioButtonSelection(string groupName, ToolbarRadioButton 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="raiseEvent">true이면 상태 변경 이벤트(OnToggle, OnStateChanged 등)를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 그룹이 존재하지 않으면 false를 반환합니다.</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">true이면 상태 변경 이벤트(OnToggle, OnStateChanged 등)를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
public void SetToggleButtonState(ToolbarToggleButton toggleButton, bool isSelected, bool raiseEvent = true)
{
if (toggleButton == null)
{
UnityEngine.Debug.LogWarning("SetToggleButtonState: toggleButton이 null입니다.");
return;
}
toggleButton.SetSelected(isSelected, raiseEvent);
}
/// <summary>
/// 텍스트로 라디오 버튼을 찾아 선택 상태를 설정합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonText">선택할 라디오 버튼의 텍스트입니다. null 또는 빈 문자열을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 상태 변경 이벤트를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
/// <returns>찾아서 선택한 라디오 버튼입니다. 그룹이 없거나 버튼을 찾지 못하면 null을 반환합니다.</returns>
public ToolbarRadioButton 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="buttonText">상태를 변경할 토글 버튼의 텍스트입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 상태 변경 이벤트를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
/// <returns>찾아서 상태를 변경한 토글 버튼입니다. 찾지 못하면 null을 반환합니다.</returns>
public ToolbarToggleButton SetToggleButtonStateByText(string buttonText, bool isSelected, bool raiseEvent = true)
{
foreach (var item in Items)
{
if (item is ToolbarToggleButton toggleButton && toggleButton.Text == buttonText)
{
toggleButton.SetSelected(isSelected, raiseEvent);
return toggleButton;
}
}
UnityEngine.Debug.LogWarning($"SetToggleButtonStateByText: 텍스트 '{buttonText}'인 토글 버튼을 찾을 수 없습니다.");
return null;
}
// 만약 모든 라디오 버튼 그룹의 초기 선택 상태를 한 번에 설정하고 싶다면 다음과 같은 메서드를 추가할 수 있습니다.
// /// <summary>
// /// 모든 라디오 버튼 그룹에 대해 초기 선택 상태를 설정합니다.

View File

@@ -87,7 +87,8 @@ namespace UVC.UI.Toolbar.Model
/// 이전에 선택되었던 다른 버튼은 선택 해제 상태(IsSelected = false)로 변경됩니다.
/// </summary>
/// <param name="buttonToSelect">선택할 ToolbarRadioButton입니다. 이 버튼은 반드시 그룹에 미리 등록되어 있어야 합니다.</param>
public void SetSelected(ToolbarRadioButton buttonToSelect)
/// <param name="raiseEvent">true이면 상태 변경 이벤트(OnToggle, OnStateChanged 등)를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
public void SetSelected(ToolbarRadioButton buttonToSelect, bool raiseEvent = true)
{
if (buttonToSelect == null || !_buttons.Contains(buttonToSelect))
{
@@ -109,8 +110,22 @@ namespace UVC.UI.Toolbar.Model
// 현재 순회 중인 버튼이 선택하려는 버튼(buttonToSelect)과 동일한지 비교하여
// IsSelected 상태를 설정합니다.
// 이렇게 하면 buttonToSelect만 true가 되고 나머지는 false가 됩니다.
// IsSelected 속성의 setter는 필요한 이벤트(OnToggle, OnStateChanged 등)를 발생시킵니다.
buttonInGroup.IsSelected = (buttonInGroup == buttonToSelect);
bool shouldBeSelected = (buttonInGroup == buttonToSelect);
buttonInGroup.SetSelected(shouldBeSelected, raiseEvent);
}
}
/// <summary>
/// 그룹 내 모든 라디오 버튼의 선택을 해제합니다.
/// 이 메서드를 호출하면 그룹 내 어떤 버튼도 선택되지 않은 상태가 됩니다.
/// </summary>
/// <param name="raiseEvent">true이면 상태 변경 이벤트(OnToggle, OnStateChanged 등)를 발생시키고, false이면 이벤트 없이 상태만 변경합니다. 기본값은 true입니다.</param>
public void ClearSelection(bool raiseEvent = true)
{
SelectedButton = null;
foreach (var buttonInGroup in _buttons)
{
buttonInGroup.SetSelected(false, raiseEvent);
}
}
@@ -123,6 +138,16 @@ namespace UVC.UI.Toolbar.Model
return _buttons.AsReadOnly();
}
/// <summary>
/// 텍스트로 그룹 내 라디오 버튼을 찾습니다.
/// </summary>
/// <param name="buttonText">찾을 버튼의 텍스트입니다.</param>
/// <returns>해당 텍스트를 가진 라디오 버튼입니다. 찾지 못하면 null을 반환합니다.</returns>
public ToolbarRadioButton FindButtonByText(string buttonText)
{
return _buttons.FirstOrDefault(b => b.Text == buttonText);
}
/// <summary>
/// 그룹 내 버튼들의 초기 선택 상태를 설정합니다.
/// 주로 ToolbarModel에서 라디오 버튼들을 추가한 후 호출될 수 있습니다.
@@ -142,9 +167,8 @@ namespace UVC.UI.Toolbar.Model
}
else
{
// IsSelected가 true인 버튼이 하나도 없다면, 그룹의 첫 번째 버튼을 기본으로 선택합니다.
// (라디오 그룹은 일반적으로 항상 하나가 선택되어 있는 상태를 유지하려 함)
SetSelected(_buttons[0]);
// IsSelected가 true인 버튼이 하나도 없다면
}
}
}

View File

@@ -114,6 +114,27 @@ namespace UVC.UI.Toolbar.Model
/// </remarks>
public Action<bool> OnToggle { get; set; }
/// <summary>
/// 이벤트 발생 여부를 선택하여 선택 상태를 설정합니다.
/// UI 업데이트 이벤트(OnToggleStateChanged, OnStateChanged)는 항상 발생하며,
/// raiseEvent는 OnToggle 콜백 호출 여부만 제어합니다.
/// </summary>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 OnToggle 콜백을 호출하고, false이면 호출하지 않습니다.</param>
public void SetSelected(bool isSelected, bool raiseEvent)
{
if (_isSelected != isSelected)
{
_isSelected = isSelected;
if (raiseEvent)
{
OnToggle?.Invoke(_isSelected);
}
OnToggleStateChanged?.Invoke(_isSelected);
NotifyStateChanged();
}
}
/// <summary>
/// 버튼 클릭 로직을 실행합니다.
/// 토글 버튼의 경우, 클릭 시 IsSelected 상태를 반전시키고,

View File

@@ -1,3 +1,4 @@
#nullable enable
using UnityEngine;
using UVC.UI.List.ComponentList;
using UVC.Factory.Playback;
@@ -8,14 +9,84 @@ using UVC.UI.Toolbar.View;
using UVC.UI.Window;
using UVC.Factory.Component;
using System.Collections.Generic;
using System;
namespace UVC.UI.Toolbar
{
/// <summary>
/// 툴바 액션 타입을 나타내는 열거형입니다.
/// </summary>
public enum ToolbarActionType
{
/// <summary>
/// 일반 버튼 클릭
/// </summary>
Standard,
/// <summary>
/// 라디오 버튼 선택 변경
/// </summary>
Radio,
/// <summary>
/// 토글 버튼 상태 변경
/// </summary>
Toggle,
/// <summary>
/// 확장 버튼의 하위 버튼 선택 변경
/// </summary>
Expandable
}
/// <summary>
/// 툴바 액션 정보를 담는 클래스입니다.
/// </summary>
public class ToolbarActionEventArgs
{
/// <summary>
/// 액션을 발생시킨 툴바 아이템의 텍스트입니다.
/// </summary>
public string Text { get; set; }
/// <summary>
/// 액션에 따른 값입니다.
/// Standard: null
/// Radio: 선택된 항목의 텍스트 (전부 해제 시 null)
/// Toggle: true/false
/// Expandable: 선택된 하위 버튼의 텍스트
/// </summary>
public object? Value { get; set; }
/// <summary>
/// 액션 타입입니다.
/// </summary>
public ToolbarActionType ActionType { get; set; }
}
public class Toolbar : MonoBehaviour
{
protected ToolbarModel model;
protected ToolbarView view;
/// <summary>
/// 툴바의 버튼이 클릭되거나 상태가 변경될 때 발생하는 이벤트입니다.
/// raiseEvent가 true일 때만 호출됩니다.
/// </summary>
public event Action<ToolbarActionEventArgs>? OnAction;
/// <summary>
/// OnAction 이벤트를 발생시킵니다.
/// </summary>
/// <param name="text">액션을 발생시킨 툴바 아이템의 텍스트</param>
/// <param name="actionType">액션 타입</param>
/// <param name="value">액션에 따른 값 (Standard: null, Radio: 선택된 항목 텍스트 또는 null, Toggle: bool, Expandable: 선택된 하위 버튼 텍스트)</param>
protected void RaiseOnAction(string text, ToolbarActionType actionType, object? value = null)
{
OnAction?.Invoke(new ToolbarActionEventArgs
{
Text = text,
ActionType = actionType,
Value = value
});
}
protected void Awake()
{
@@ -62,10 +133,61 @@ namespace UVC.UI.Toolbar
return;
}
// 모든 버튼 타입의 이벤트 구독
SubscribeButtonEvents();
// ToolbarView 초기화 및 렌더링
view.Initialize(model);
}
/// <summary>
/// 모델 내의 모든 버튼에 대해 이벤트를 구독합니다.
/// </summary>
private void SubscribeButtonEvents()
{
if (model?.Items == null) return;
foreach (var item in model.Items)
{
switch (item)
{
case ToolbarRadioButton radioButton:
// 라디오 버튼의 선택 상태 변경 이벤트 구독
radioButton.OnToggleStateChanged += (isSelected) =>
{
if (isSelected)
{
RaiseOnAction(radioButton.GroupName, ToolbarActionType.Radio, radioButton.Text);
}
};
break;
case ToolbarToggleButton toggleButton:
// 토글 버튼의 상태 변경 이벤트 구독
toggleButton.OnToggleStateChanged += (isSelected) =>
{
RaiseOnAction(toggleButton.Text, ToolbarActionType.Toggle, isSelected);
};
break;
case ToolbarExpandableButton expandableButton:
// 확장 버튼의 하위 버튼 선택 변경 이벤트 구독
expandableButton.OnSubButtonSelectionChanged += (expandableText, selectedSubButtonText) =>
{
RaiseOnAction(expandableText, ToolbarActionType.Expandable, selectedSubButtonText);
};
break;
case ToolbarStandardButton standardButton:
// 표준 버튼의 클릭 이벤트 구독
standardButton.OnClicked += () =>
{
RaiseOnAction(standardButton.Text, ToolbarActionType.Standard, null);
};
break;
}
}
}
private ToolbarModel generateModel()
{
@@ -175,6 +297,157 @@ namespace UVC.UI.Toolbar
return toolbarModel;
}
/// <summary>
/// 지정된 그룹 내에서 특정 라디오 버튼을 선택하거나, 모든 선택을 해제합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonToSelect">선택할 라디오 버튼입니다. null을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 그룹이 존재하지 않으면 false를 반환합니다.</returns>
public bool SetRadioButtonSelection(string groupName, ToolbarRadioButton buttonToSelect, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbar: ToolbarModel이 설정되지 않았습니다.");
return false;
}
bool result = model.SetRadioButtonSelection(groupName, buttonToSelect, raiseEvent);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, buttonToSelect);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, buttonToSelect?.Text);
}
}
return result;
}
/// <summary>
/// 그룹 이름으로 라디오 버튼 그룹 내 모든 선택을 해제합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 그룹이 존재하지 않으면 false를 반환합니다.</returns>
public bool ClearRadioButtonSelection(string groupName, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbar: ToolbarModel이 설정되지 않았습니다.");
return false;
}
bool result = model.ClearRadioButtonSelection(groupName, raiseEvent);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트 (모두 비선택)
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, null);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, null);
}
}
return result;
}
/// <summary>
/// 지정된 토글 버튼의 선택 상태를 설정합니다.
/// </summary>
/// <param name="toggleButton">상태를 변경할 토글 버튼입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
public void SetToggleButtonState(ToolbarToggleButton toggleButton, bool isSelected, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbar: ToolbarModel이 설정되지 않았습니다.");
return;
}
model.SetToggleButtonState(toggleButton, isSelected, raiseEvent);
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateIconColorState(toggleButton, isSelected);
}
if (raiseEvent)
{
RaiseOnAction(toggleButton.Text, ToolbarActionType.Toggle, isSelected);
}
}
/// <summary>
/// 텍스트로 라디오 버튼을 찾아 선택 상태를 설정합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonText">선택할 라디오 버튼의 텍스트입니다. null 또는 빈 문자열을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 버튼을 찾지 못하면 false를 반환합니다.</returns>
public bool SetRadioButtonSelection(string groupName, string buttonText, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbar: ToolbarModel이 설정되지 않았습니다.");
return false;
}
var button = model.SetRadioButtonSelectionByText(groupName, buttonText, raiseEvent);
bool result = button != null || string.IsNullOrEmpty(buttonText);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, button);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, button?.Text);
}
}
return result;
}
/// <summary>
/// 텍스트로 토글 버튼을 찾아 선택 상태를 설정합니다.
/// </summary>
/// <param name="buttonText">상태를 변경할 토글 버튼의 텍스트입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 버튼을 찾지 못하면 false를 반환합니다.</returns>
public bool SetToggleButtonState(string buttonText, bool isSelected, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbar: ToolbarModel이 설정되지 않았습니다.");
return false;
}
var button = model.SetToggleButtonStateByText(buttonText, isSelected, raiseEvent);
if (button != null)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateIconColorState(button, isSelected);
}
if (raiseEvent)
{
RaiseOnAction(buttonText, ToolbarActionType.Toggle, isSelected);
}
}
return button != null;
}
protected void OnDestroy()
{
model = null;

View File

@@ -1,3 +1,4 @@
#nullable enable
using UnityEngine;
using UVC.Factory.Playback;
using UVC.Locale;
@@ -48,6 +49,28 @@ namespace UVC.UI.ToolBar
/// </summary>
protected ToolbarView view;
/// <summary>
/// 툴박스의 버튼이 클릭되거나 상태가 변경될 때 발생하는 이벤트입니다.
/// raiseEvent가 true일 때만 호출됩니다.
/// </summary>
public event System.Action<ToolbarActionEventArgs> OnAction;
/// <summary>
/// OnAction 이벤트를 발생시킵니다.
/// </summary>
/// <param name="text">액션을 발생시킨 툴바 아이템의 텍스트</param>
/// <param name="actionType">액션 타입</param>
/// <param name="value">액션에 따른 값 (Standard: null, Radio: 선택된 항목 텍스트 또는 null, Toggle: bool, Expandable: 선택된 하위 버튼 텍스트)</param>
protected void RaiseOnAction(string text, ToolbarActionType actionType, object value = null)
{
OnAction?.Invoke(new ToolbarActionEventArgs
{
Text = text,
ActionType = actionType,
Value = value
});
}
/// <summary>
/// MonoBehaviour의 Awake 메서드입니다.
/// 주로 현재 GameObject 또는 자식 GameObject에서 ToolbarView 컴포넌트를 찾아 mainToolbarView 필드에 할당합니다.
@@ -101,10 +124,62 @@ namespace UVC.UI.ToolBar
return;
}
// 모든 버튼 타입의 이벤트 구독
SubscribeButtonEvents();
// ToolbarView 초기화 및 렌더링
view.Initialize(model);
}
/// <summary>
/// 모델 내의 모든 버튼에 대해 이벤트를 구독합니다.
/// </summary>
private void SubscribeButtonEvents()
{
if (model?.Items == null) return;
foreach (var item in model.Items)
{
switch (item)
{
case ToolbarRadioButton radioButton:
// 라디오 버튼의 선택 상태 변경 이벤트 구독
radioButton.OnToggleStateChanged += (isSelected) =>
{
if (isSelected)
{
RaiseOnAction(radioButton.GroupName, ToolbarActionType.Radio, radioButton.Text);
}
};
break;
case ToolbarToggleButton toggleButton:
// 토글 버튼의 상태 변경 이벤트 구독
toggleButton.OnToggleStateChanged += (isSelected) =>
{
RaiseOnAction(toggleButton.Text, ToolbarActionType.Toggle, isSelected);
};
break;
case ToolbarExpandableButton expandableButton:
// 확장 버튼의 하위 버튼 선택 변경 이벤트 구독
expandableButton.OnSubButtonSelectionChanged += (expandableText, selectedSubButtonText) =>
{
RaiseOnAction(expandableText, ToolbarActionType.Expandable, selectedSubButtonText);
};
break;
case ToolbarStandardButton standardButton:
// 표준 버튼의 클릭 이벤트 구독
standardButton.OnClicked += () =>
{
RaiseOnAction(standardButton.Text, ToolbarActionType.Standard, null);
};
break;
}
}
}
/// <summary>
/// MonoBehaviour의 Start 메서드입니다. 첫 번째 프레임 업데이트 전에 호출됩니다.
/// ToolbarModel을 생성하고, 다양한 툴바 항목들을 모델에 추가한 후,
@@ -220,6 +295,157 @@ namespace UVC.UI.ToolBar
return toolbarModel;
}
/// <summary>
/// 지정된 그룹 내에서 특정 라디오 버튼을 선택하거나, 모든 선택을 해제합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonToSelect">선택할 라디오 버튼입니다. null을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 그룹이 존재하지 않으면 false를 반환합니다.</returns>
public bool SetRadioButtonSelection(string groupName, ToolbarRadioButton buttonToSelect, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbox: ToolbarModel이 설정되지 않았습니다.");
return false;
}
bool result = model.SetRadioButtonSelection(groupName, buttonToSelect, raiseEvent);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, buttonToSelect);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, buttonToSelect?.Text);
}
}
return result;
}
/// <summary>
/// 그룹 이름으로 라디오 버튼 그룹 내 모든 선택을 해제합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 그룹이 존재하지 않으면 false를 반환합니다.</returns>
public bool ClearRadioButtonSelection(string groupName, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbox: ToolbarModel이 설정되지 않았습니다.");
return false;
}
bool result = model.ClearRadioButtonSelection(groupName, raiseEvent);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트 (모두 비선택)
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, null);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, null);
}
}
return result;
}
/// <summary>
/// 지정된 토글 버튼의 선택 상태를 설정합니다.
/// </summary>
/// <param name="toggleButton">상태를 변경할 토글 버튼입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
public void SetToggleButtonState(ToolbarToggleButton toggleButton, bool isSelected, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbox: ToolbarModel이 설정되지 않았습니다.");
return;
}
model.SetToggleButtonState(toggleButton, isSelected, raiseEvent);
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateIconColorState(toggleButton, isSelected);
}
if (raiseEvent)
{
RaiseOnAction(toggleButton.Text, ToolbarActionType.Toggle, isSelected);
}
}
/// <summary>
/// 텍스트로 라디오 버튼을 찾아 선택 상태를 설정합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹의 이름입니다.</param>
/// <param name="buttonText">선택할 라디오 버튼의 텍스트입니다. null 또는 빈 문자열을 전달하면 그룹 내 모든 버튼의 선택이 해제됩니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 버튼을 찾지 못하면 false를 반환합니다.</returns>
public bool SetRadioButtonSelection(string groupName, string buttonText, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbox: ToolbarModel이 설정되지 않았습니다.");
return false;
}
var button = model.SetRadioButtonSelectionByText(groupName, buttonText, raiseEvent);
bool result = button != null || string.IsNullOrEmpty(buttonText);
if (result)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateRadioGroupIconColors(groupName, button);
}
if (raiseEvent)
{
RaiseOnAction(groupName, ToolbarActionType.Radio, button?.Text);
}
}
return result;
}
/// <summary>
/// 텍스트로 토글 버튼을 찾아 선택 상태를 설정합니다.
/// </summary>
/// <param name="buttonText">상태를 변경할 토글 버튼의 텍스트입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
/// <param name="raiseEvent">true이면 OnAction 이벤트를 발생시키고, false이면 UI만 업데이트합니다. 기본값은 true입니다.</param>
/// <returns>작업 성공 여부입니다. 모델이 없거나 버튼을 찾지 못하면 false를 반환합니다.</returns>
public bool SetToggleButtonState(string buttonText, bool isSelected, bool raiseEvent = false)
{
if (model == null)
{
Debug.LogError("Toolbox: ToolbarModel이 설정되지 않았습니다.");
return false;
}
var button = model.SetToggleButtonStateByText(buttonText, isSelected, raiseEvent);
if (button != null)
{
// Icon의 ImageColorChangeBehaviour 색상 상태 업데이트
if (view != null)
{
view.UpdateIconColorState(button, isSelected);
}
if (raiseEvent)
{
RaiseOnAction(buttonText, ToolbarActionType.Toggle, isSelected);
}
}
return button != null;
}
protected void OnDestroy()
{
model = null;

View File

@@ -103,7 +103,7 @@ namespace UVC.UI.Toolbar.View
// ToggleGroup 컴포넌트를 추가합니다.
group = groupObj.AddComponent<ToggleGroup>();
group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 항상 하나가 선택된 상태를 유지해야 하므로, 선택 해제를 허용하지 않습니다.
group.allowSwitchOff = true; // 모든 버튼이 선택 해제된 상태를 허용합니다. false로 설정하면 Unity가 첫 번째 Toggle을 자동으로 선택합니다.
// LayoutElement를 추가하고 ignoreLayout을 true로 설정하여 부모의 LayoutGroup 계산에서 이 GameObject를 무시하도록 합니다.
LayoutElement element = groupObj.AddComponent<LayoutElement>();

View File

@@ -7,6 +7,7 @@ using UVC.Extension;
using UVC.Locale;
using UVC.UI.Toolbar.Model;
using UVC.UI.Tooltip;
using UVC.UI.Util;
using UVC.Util;
namespace UVC.UI.Toolbar.View
@@ -463,5 +464,44 @@ namespace UVC.UI.Toolbar.View
TooltipManager.Instance.HideTooltip();
}
}
/// <summary>
/// 버튼 모델에 해당하는 UI GameObject의 Icon 자식에 있는 ImageColorChangeBehaviour의 선택 상태를 업데이트합니다.
/// </summary>
/// <param name="buttonModel">상태를 업데이트할 버튼 모델입니다.</param>
/// <param name="isSelected">설정할 선택 상태입니다.</param>
public void UpdateIconColorState(ToolbarButtonBase buttonModel, bool isSelected)
{
if (buttonModel == null) return;
if (_modelToGameObjectMap.TryGetValue(buttonModel, out GameObject buttonObj) && buttonObj != null)
{
ImageColorChangeBehaviour colorBehaviour = buttonObj.GetComponentInChildren<ImageColorChangeBehaviour>();
if (colorBehaviour != null)
{
colorBehaviour.SetSelected(isSelected);
}
}
}
/// <summary>
/// 라디오 버튼 그룹 내 모든 버튼의 Icon 색상 상태를 업데이트합니다.
/// </summary>
/// <param name="groupName">라디오 버튼 그룹 이름입니다.</param>
/// <param name="selectedButton">선택된 버튼입니다. null이면 모든 버튼이 비선택 상태가 됩니다.</param>
public void UpdateRadioGroupIconColors(string groupName, ToolbarRadioButton? selectedButton)
{
if (ToolbarModel == null) return;
var group = ToolbarModel.GetRadioButtonGroup(groupName);
if (group == null) return;
foreach (var button in group.GetButtons())
{
bool isSelected = (selectedButton != null && button == selectedButton);
UpdateIconColorState(button, isSelected);
}
}
}
}