Modal 개발 완료. Toolbar 개발 중

This commit is contained in:
logonkhi
2025-06-13 17:10:58 +09:00
parent e8d52b3e90
commit 2ffe7abac6
37 changed files with 3278 additions and 466 deletions

View File

@@ -0,0 +1,7 @@
namespace UVC.UI.ToolBar
{
/// <summary>
/// 툴바에 추가될 수 있는 모든 항목(버튼, 구분선 등)의 기본 인터페이스입니다.
/// </summary>
public interface IToolbarItem { }
}

View File

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

View File

@@ -0,0 +1,77 @@
namespace UVC.UI.ToolBar
{
/// <summary>
/// 툴바의 전체적인 컨테이너 및 관리 클래스입니다.
/// IToolbarItem 객체들을 동적으로 추가하고 관리합니다.
/// </summary>
public class Toolbar
{
public System.Collections.Generic.List<IToolbarItem> Items { get; private set; }
private System.Collections.Generic.Dictionary<string, ToolbarRadioButtonGroup> _radioGroups;
public Toolbar()
{
Items = new System.Collections.Generic.List<IToolbarItem>();
_radioGroups = new System.Collections.Generic.Dictionary<string, ToolbarRadioButtonGroup>();
}
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);
}
// UI 갱신 로직 호출
}
public ToolbarStandardButton AddStandardButton(string text, UnityEngine.Sprite icon = null, System.Action onClick = null, string tooltipKey = null)
{
var button = new ToolbarStandardButton { Text = text, Icon = icon, OnClick = onClick, TooltipKey = tooltipKey };
AddItem(button);
return button;
}
public ToolbarToggleButton AddToggleButton(string text, bool initialState = false, UnityEngine.Sprite icon = null, System.Action<bool> onToggle = null, string tooltipKey = null)
{
var button = new ToolbarToggleButton { Text = text, IsSelected = initialState, Icon = icon, OnToggle = onToggle, TooltipKey = tooltipKey };
AddItem(button);
return button;
}
public ToolbarRadioButton AddRadioButton(string groupName, string text, bool initialState = false, UnityEngine.Sprite icon = null, System.Action<bool> onToggle = null, string tooltipKey = null)
{
var button = new ToolbarRadioButton(groupName) { Text = text, IsSelected = initialState, Icon = icon, OnToggle = onToggle, TooltipKey = tooltipKey };
// AddItem 내에서 그룹 처리가 되므로, 여기서는 IsSelected 초기값만 주의 (그룹 내 하나만 true여야 함)
AddItem(button);
// 그룹의 초기 선택 상태를 설정하는 로직이 추가로 필요할 수 있습니다.
// 예를 들어, 첫 번째로 추가된 라디오 버튼을 기본 선택으로 하거나, 명시적으로 설정.
if (initialState && _radioGroups.TryGetValue(groupName, out var group))
{
group.SetSelected(button);
}
return button;
}
public ToolbarExpandableButton AddExpandableButton(string text, UnityEngine.Sprite icon = null, System.Action onClick = null, string tooltipKey = null)
{
var button = new ToolbarExpandableButton { Text = text, Icon = icon, OnClick = onClick, TooltipKey = tooltipKey };
AddItem(button);
return button;
}
public void AddSeparator()
{
AddItem(new ToolbarSeparator());
}
// 실제 UI 렌더링 및 상호작용 로직은 이 클래스 또는 별도의 UI View 클래스에서 처리됩니다.
// (예: Unity UI GameObject 생성, 이벤트 연결 등)
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using System;
using UnityEngine;
namespace UVC.UI.ToolBar
{
/// <summary>
/// 모든 버튼의 기본 추상 클래스입니다.
/// 공통적인 속성 (예: 텍스트, 아이콘, 활성화 상태) 및 동작을 정의합니다.
/// </summary>
public abstract class ToolbarButtonBase : IToolbarItem
{
public event Action OnStateChanged; // 상태 변경 알림 이벤트
private string _text;
public string Text
{
get => _text;
set
{
if (_text != value)
{
_text = value;
OnStateChanged?.Invoke();
}
}
}
private Sprite _icon;
public Sprite Icon
{
get => _icon;
set
{
if (_icon != value)
{
_icon = value;
OnStateChanged?.Invoke();
}
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnStateChanged?.Invoke();
}
}
}
private string _tooltipKey; // 툴팁 다국어 키
public string TooltipKey
{
get => _tooltipKey;
set
{
if (_tooltipKey != value)
{
_tooltipKey = value;
// TooltipKey 변경 시 OnStateChanged를 호출할 필요는 일반적으로 없으나,
// 만약 UI가 TooltipKey 자체를 표시하는 등의 로직이 있다면 필요할 수 있습니다.
// 여기서는 툴팁 내용이 동적으로 변경되는 경우가 적다고 가정하고 생략합니다.
}
}
}
public Action OnClick { get; set; }
public abstract void ExecuteClick();
// OnStateChanged 이벤트를 외부에서 강제로 발생시켜야 할 때 사용 (예: 복합적인 상태 변경 후)
public void NotifyStateChanged()
{
OnStateChanged?.Invoke();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 49e156554491e3c4fb49243701695feb

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
namespace UVC.UI.ToolBar
{
/// <summary>
/// 클릭 시 하위 버튼 그룹을 확장하여 보여주는 버튼입니다.
/// 하위 버튼 선택 시, 주 버튼의 내용이 업데이트될 수 있습니다.
/// </summary>
public class ToolbarExpandableButton : ToolbarButtonBase
{
public enum ExpansionDirection { Horizontal, Vertical }
public List<ToolbarButtonBase> SubButtons { get; private set; }
public ExpansionDirection Direction { get; set; } = ExpansionDirection.Vertical;
public Action<ToolbarButtonBase> OnSubButtonSelected { get; set; }
public ToolbarExpandableButton()
{
SubButtons = new List<ToolbarButtonBase>();
}
public override void ExecuteClick()
{
if (IsEnabled)
{
OnClick?.Invoke();
}
}
public void SelectSubButton(ToolbarButtonBase selectedSubButton)
{
if (selectedSubButton != null && selectedSubButton.IsEnabled)
{
bool changed = false;
if (this.Text != selectedSubButton.Text)
{
this.Text = selectedSubButton.Text; // Setter가 OnStateChanged 호출 (단, Text가 실제로 변경되어야 함)
changed = true;
}
if (this.Icon != selectedSubButton.Icon)
{
this.Icon = selectedSubButton.Icon; // Setter가 OnStateChanged 호출
changed = true;
}
OnSubButtonSelected?.Invoke(selectedSubButton);
// selectedSubButton.ExecuteClick(); // 하위 버튼의 클릭 로직 실행은 선택 사항
if (changed) // Text나 Icon이 실제로 변경된 경우에만 명시적으로 호출하거나, 각 setter에 맡김
{
// NotifyStateChanged(); // Text, Icon setter가 이미 호출하므로 중복될 수 있음.
// 만약 Text, Icon 외 다른 상태도 변경된다면 필요.
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,100 @@
using UnityEngine;
using UVC.Locale;
namespace UVC.UI.ToolBar
{
public class ToolbarManager : MonoBehaviour
{
public Toolbar mainToolbar;
public ToolbarView mainToolbarView; // Unity 에디터에서 할당
public Transform toolbarContainer; // Unity 에디터에서 할당 (툴바 UI가 생성될 부모)
// 여기에 HorizontalLayoutGroup 또는 VerticalLayoutGroup을 추가하는 것을 권장합니다.
// 버튼 프리팹들은 ToolbarView로 옮겨서 관리하는 것이 더 깔끔할 수 있습니다.
// 현재는 ToolbarManager에서 할당하여 ToolbarView로 전달하는 방식입니다.
public GameObject standardButtonPrefab;
public GameObject toggleButtonPrefab;
public GameObject radioButtonPrefab;
public GameObject expandableButtonPrefab;
public GameObject separatorPrefab;
public GameObject subMenuPanelPrefab;
void Start()
{
mainToolbar = new Toolbar();
// ToolbarView에 프리팹 설정
if (mainToolbarView != null)
{
mainToolbarView.standardButtonPrefab = standardButtonPrefab;
mainToolbarView.toggleButtonPrefab = toggleButtonPrefab;
mainToolbarView.radioButtonPrefab = radioButtonPrefab;
mainToolbarView.expandableButtonPrefab = expandableButtonPrefab;
mainToolbarView.separatorPrefab = separatorPrefab;
mainToolbarView.subMenuPanelPrefab = subMenuPanelPrefab;
}
else
{
Debug.LogError("ToolbarView가 할당되지 않았습니다.");
return;
}
// --- 툴바 모델 구성 ---
// "저장" 대신 다국어 키 "button_save" 사용
mainToolbar.AddStandardButton("button_save", null, () => Debug.Log("저장 버튼 클릭됨"), "tooltip_save_button");
// "음소거" 대신 다국어 키 "button_mute" 사용
mainToolbar.AddToggleButton("button_mute", false, null, (isSelected) => Debug.Log($"음소거: {isSelected}"), "tooltip_mute_button");
mainToolbar.AddSeparator();
// "펜" 대신 다국어 키 "tool_pen" 사용
mainToolbar.AddRadioButton("ToolGroup", "tool_pen", true, null, (isSelected) => { if (isSelected) Debug.Log("펜 도구 선택됨"); }, "tooltip_pen_tool");
// "지우개" 대신 다국어 키 "tool_eraser" 사용
mainToolbar.AddRadioButton("ToolGroup", "tool_eraser", false, null, (isSelected) => { if (isSelected) Debug.Log("지우개 도구 선택됨"); }, "tooltip_eraser_tool");
mainToolbar.AddSeparator();
// "브러시 크기" 대신 다국어 키 "button_brush_size" 사용
var expandableBtnModel = mainToolbar.AddExpandableButton("button_brush_size", null, null, "tooltip_brush_size");
// 하위 버튼도 다국어 키 사용
var smallBrush = new ToolbarStandardButton { Text = "brush_size_small", TooltipKey = "tooltip_brush_small" };
expandableBtnModel.SubButtons.Add(smallBrush);
expandableBtnModel.SubButtons.Add(new ToolbarStandardButton { Text = "brush_size_medium", TooltipKey = "tooltip_brush_medium" });
expandableBtnModel.OnSubButtonSelected = (selectedSubButton) => {
// selectedSubButton.Text 에는 이제 다국어 키가 들어있습니다.
// 실제 표시된 텍스트를 로그로 남기려면 LocalizationManager를 사용해야 합니다.
string localizedSubButtonText = LocalizationManager.Instance != null ? LocalizationManager.Instance.GetString(selectedSubButton.Text) : selectedSubButton.Text;
Debug.Log($"브러시 크기 '{localizedSubButtonText}' 선택됨 (주 버튼 업데이트)");
};
// --- 툴바 모델 구성 끝 ---
// ToolbarView 초기화 및 렌더링
if (toolbarContainer != null)
{
mainToolbarView.Initialize(mainToolbar, toolbarContainer);
}
else
{
Debug.LogError("ToolbarContainer가 할당되지 않았습니다.");
}
// 예시: 모델 상태를 코드로 변경하고 UI가 업데이트되는지 테스트
// StartCoroutine(TestModelChange(saveBtnModel, muteToggleModel));
}
// System.Collections.IEnumerator TestModelChange(ToolbarStandardButton standard, ToolbarToggleButton toggle)
// {
// yield return new WaitForSeconds(2f);
// Debug.Log("모델 변경 테스트: 저장 버튼 비활성화 및 텍스트 변경");
// standard.Text = "저장됨";
// standard.IsEnabled = false;
//
// yield return new WaitForSeconds(2f);
// Debug.Log("모델 변경 테스트: 음소거 토글 상태 변경");
// toggle.IsSelected = true;
// }
}
}

View File

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

View File

@@ -0,0 +1,32 @@
namespace UVC.UI.ToolBar
{
/// <summary>
/// 라디오 버튼 그룹 내에서 사용되는 버튼입니다.
/// 그룹 내 하나의 버튼만 선택될 수 있습니다.
/// </summary>
public class ToolbarRadioButton : ToolbarToggleButton
{
public string GroupName { get; private set; }
internal ToolbarRadioButtonGroup RadioGroup { get; set; }
public ToolbarRadioButton(string groupName)
{
GroupName = groupName;
}
public override void ExecuteClick()
{
if (IsEnabled)
{
// 라디오 버튼은 직접 IsSelected를 토글하지 않고, 그룹에 의해 상태가 결정됩니다.
// 그룹이 SetSelected를 호출하면, 해당 버튼의 IsSelected가 true로 설정되고,
// 다른 버튼들은 false로 설정되면서 각자의 OnStateChanged 이벤트가 발생합니다.
RadioGroup?.SetSelected(this);
// OnClick은 그룹에 의해 선택이 확정되었을 때 호출되도록 RadioGroup.SetSelected 내부에서 처리하거나,
// 여기서 IsSelected 상태를 확인 후 호출할 수 있습니다.
// 현재 구조에서는 RadioGroup.SetSelected가 IsSelected를 변경하고, IsSelected의 setter가 OnStateChanged를 호출합니다.
// OnClick은 ToolbarToggleButton의 ExecuteClick에서 이미 호출될 수 있으므로 중복 호출을 피하거나 의도에 맞게 조정합니다.
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 451c776768fed71479e8c7a4a73818ea

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace UVC.UI.ToolBar
{
/// <summary>
/// 라디오 버튼들을 그룹으로 관리하여 하나만 선택되도록 합니다.
/// ToolbarRadioButtonGroup, ToolbarExpandableButton 클래스는 이전 제안과 거의 동일하게 유지하되,
/// 상태 변경 시 NotifyStateChanged() 호출을 고려할 수 있습니다.
/// 예를 들어 ToolbarExpandableButton에서 SelectSubButton 후 주 버튼의 Text, Icon이 변경되면 NotifyStateChanged() 호출
/// </summary>
public class ToolbarRadioButtonGroup
{
private List<ToolbarRadioButton> _buttons = new List<ToolbarRadioButton>();
public ToolbarRadioButton SelectedButton { get; private set; }
public void RegisterButton(ToolbarRadioButton button)
{
if (!_buttons.Contains(button))
{
_buttons.Add(button);
button.RadioGroup = this;
}
}
public void SetSelected(ToolbarRadioButton buttonToSelect)
{
if (!_buttons.Contains(buttonToSelect) || !buttonToSelect.IsEnabled) return;
SelectedButton = buttonToSelect;
foreach (var btn in _buttons)
{
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로 대체 가능
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace UVC.UI.ToolBar
{
/// <summary>
/// 툴바 구분선을 나타냅니다.
/// </summary>
public class ToolbarSeparator : IToolbarItem { }
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 41943a25123704b4f82ec6417863d158

View File

@@ -0,0 +1,16 @@
namespace UVC.UI.ToolBar
{
/// <summary>
/// 일반적인 클릭 버튼입니다.
/// </summary>
public class ToolbarStandardButton : ToolbarButtonBase
{
public override void ExecuteClick()
{
if (IsEnabled && OnClick != null)
{
OnClick.Invoke();
}
}
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using System;
namespace UVC.UI.ToolBar
{
/// <summary>
/// 클릭할 때마다 선택/해제 상태가 변경되는 토글 버튼입니다.
/// </summary>
public class ToolbarToggleButton : ToolbarButtonBase
{
public event Action<bool> OnToggleStateChanged; // IsSelected 변경 시 IsSelected 값을 전달하는 이벤트
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnToggleStateChanged?.Invoke(_isSelected); // IsSelected 값과 함께 이벤트 발생
NotifyStateChanged(); // 일반 상태 변경 이벤트도 발생
}
}
}
public Action<bool> OnToggle { get; set; }
public override void ExecuteClick()
{
if (IsEnabled)
{
IsSelected = !IsSelected; // IsSelected의 setter가 OnStateChanged를 호출
OnClick?.Invoke(); // OnClick은 상태 변경과 별개로 클릭 시 항상 호출되도록 할 수 있음
OnToggle?.Invoke(IsSelected); // 기존 OnToggle 로직 유지
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6407c881188c7c04c9cb4efb1dd7b4ce

View File

@@ -0,0 +1,453 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Locale;
namespace UVC.UI.ToolBar
{
public class ToolbarView : MonoBehaviour
{
public Toolbar ToolbarModel { get; private set; }
public GameObject standardButtonPrefab;
public GameObject toggleButtonPrefab;
public GameObject radioButtonPrefab;
public GameObject expandableButtonPrefab;
public GameObject separatorPrefab;
public GameObject subMenuPanelPrefab;
public GameObject tooltipPrefab; // 툴팁 UI 프리팹
private Transform toolbarContainer;
// UI 요소와 모델을 매핑하여 상태 업데이트 시 사용
private Dictionary<ToolbarButtonBase, GameObject> _modelToGameObjectMap = new Dictionary<ToolbarButtonBase, GameObject>();
private GameObject _activeTooltipInstance;
private TextMeshProUGUI _tooltipTextElement;
private RectTransform _tooltipRectTransform;
public void Initialize(Toolbar toolbarModel, Transform container)
{
ToolbarModel = toolbarModel;
toolbarContainer = container;
// UI 레이아웃: toolbarContainer에 VerticalLayoutGroup 또는 HorizontalLayoutGroup 컴포넌트를 추가하고
// 자식 크기 제어 (Child Force Expand 등) 옵션을 조정하면 UI 요소들이 자동으로 정렬됩니다.
// 예: var layoutGroup = toolbarContainer.GetComponent<HorizontalLayoutGroup>();
// if (layoutGroup == null) layoutGroup = toolbarContainer.gameObject.AddComponent<HorizontalLayoutGroup>();
// layoutGroup.childControlHeight = true; layoutGroup.childControlWidth = false; // 예시 설정
RenderToolbar();
if (tooltipPrefab != null)
{
_activeTooltipInstance = Instantiate(tooltipPrefab, transform); // ToolbarView의 자식으로 생성 (Canvas 내 다른 곳이어도 됨)
_tooltipTextElement = _activeTooltipInstance.GetComponentInChildren<TextMeshProUGUI>();
_tooltipRectTransform = _activeTooltipInstance.GetComponent<RectTransform>();
_activeTooltipInstance.SetActive(false); // 처음에는 숨김
}
}
private void ClearToolbar()
{
foreach (var pair in _modelToGameObjectMap)
{
if (pair.Key != null)
{
pair.Key.OnStateChanged -= () => UpdateItemVisuals(pair.Key); // 이벤트 구독 해제
if (pair.Key is ToolbarToggleButton toggleButton)
{
toggleButton.OnToggleStateChanged -= (isSelected) => UpdateToggleVisuals(toggleButton, isSelected);
}
}
if (pair.Value != null)
{
// TooltipHandler 이벤트 구독 해제 (필요 시)
TooltipHandler handler = pair.Value.GetComponent<TooltipHandler>();
if (handler != null)
{
handler.OnPointerEnterAction = null;
handler.OnPointerExitAction = null;
}
Destroy(pair.Value);
}
}
_modelToGameObjectMap.Clear();
_toggleGroups.Clear(); // 토글 그룹도 정리
if (currentSubMenu != null) Destroy(currentSubMenu);
HideTooltip(); // 툴바가 클리어될 때 툴팁도 숨김
}
private void RenderToolbar()
{
ClearToolbar(); // 기존 UI 및 이벤트 구독 정리
if (ToolbarModel == null || ToolbarModel.Items == null) return;
foreach (var item in ToolbarModel.Items)
{
GameObject itemObj = null;
if (item is ToolbarSeparator)
{
itemObj = Instantiate(separatorPrefab, toolbarContainer);
}
else if (item is ToolbarButtonBase buttonModel) // 모든 버튼 타입의 기본 처리
{
// 적절한 프리팹 선택
if (buttonModel is ToolbarRadioButton) itemObj = Instantiate(radioButtonPrefab, toolbarContainer);
else if (buttonModel is ToolbarToggleButton) itemObj = Instantiate(toggleButtonPrefab, toolbarContainer);
else if (buttonModel is ToolbarExpandableButton) itemObj = Instantiate(expandableButtonPrefab, toolbarContainer);
else if (buttonModel is ToolbarStandardButton) itemObj = Instantiate(standardButtonPrefab, toolbarContainer);
// else // 다른 커스텀 버튼 타입이 있다면 추가
if (itemObj != null)
{
_modelToGameObjectMap[buttonModel] = itemObj;
buttonModel.OnStateChanged += () => UpdateItemVisuals(buttonModel); // 모델 상태 변경 시 UI 업데이트 구독
// 초기 UI 설정 및 이벤트 바인딩
SetupButtonVisualsAndInteractions(buttonModel, itemObj);
// 툴팁 핸들러 추가 및 설정
if (!string.IsNullOrEmpty(buttonModel.TooltipKey))
{
TooltipHandler tooltipHandler = itemObj.GetComponent<TooltipHandler>();
if (tooltipHandler == null) tooltipHandler = itemObj.AddComponent<TooltipHandler>();
tooltipHandler.TooltipKey = buttonModel.TooltipKey;
tooltipHandler.OnPointerEnterAction = HandlePointerEnter;
tooltipHandler.OnPointerExitAction = HandlePointerExit;
}
}
}
}
}
private void HandlePointerEnter(string tooltipKey, Vector3 mousePosition)
{
if (LocalizationManager.Instance != null && _tooltipTextElement != null)
{
string tooltipText = LocalizationManager.Instance.GetString(tooltipKey);
if (string.IsNullOrEmpty(tooltipText) || tooltipText == $"[{tooltipKey}]") // 번역 실패 또는 키 그대로 반환 시
{
// 번역이 없거나 실패한 경우 툴팁을 표시하지 않거나, 기본 메시지를 표시할 수 있습니다.
// 여기서는 표시하지 않도록 합니다.
HideTooltip();
return;
}
ShowTooltip(tooltipText, mousePosition);
}
}
private void HandlePointerExit()
{
HideTooltip();
}
private void ShowTooltip(string text, Vector3 mousePosition)
{
if (_activeTooltipInstance == null || _tooltipTextElement == null) return;
_tooltipTextElement.text = text;
_activeTooltipInstance.SetActive(true);
// 툴팁 위치 설정 (마우스 커서 기준, 화면 가장자리 넘어가지 않도록 조정 필요)
// Canvas Render Mode에 따라 위치 계산 방식이 달라질 수 있습니다.
// Screen Space - Overlay 예시:
if (_tooltipRectTransform != null)
{
// TextMeshPro의 preferredWidth/Height를 사용하여 크기 조절
_tooltipTextElement.ForceMeshUpdate(); // 텍스트 변경 후 메시 업데이트 강제
Vector2 textSize = _tooltipTextElement.GetRenderedValues(false);
Vector2 padding = new Vector2(10, 5); // 툴팁 내부 여백
_tooltipRectTransform.sizeDelta = textSize + padding * 2;
// 화면 가장자리 처리 (간단한 예시)
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
transform.root as RectTransform, // Canvas의 최상위 RectTransform
mousePosition,
transform.root.GetComponent<Canvas>().worldCamera, // Screen Space - Camera 경우 필요
out localPoint
);
// 툴팁을 마우스 오른쪽 아래에 표시 (오프셋 조정 가능)
_tooltipRectTransform.localPosition = localPoint + new Vector2(_tooltipRectTransform.sizeDelta.x * 0.5f + 10f, -_tooltipRectTransform.sizeDelta.y * 0.5f - 5f);
// 화면 경계 체크 및 위치 조정 (더 정교한 로직 필요)
Vector3[] corners = new Vector3[4];
_tooltipRectTransform.GetWorldCorners(corners);
float screenWidth = Screen.width;
float screenHeight = Screen.height;
// 오른쪽 경계 넘어감
if (corners[2].x > screenWidth)
{
Vector3 currentPos = _tooltipRectTransform.position;
currentPos.x -= (corners[2].x - screenWidth);
_tooltipRectTransform.position = currentPos;
}
// 왼쪽 경계 넘어감
if (corners[0].x < 0)
{
Vector3 currentPos = _tooltipRectTransform.position;
currentPos.x -= corners[0].x;
_tooltipRectTransform.position = currentPos;
}
// 아래쪽 경계 넘어감 (툴팁을 위로 표시하도록 변경 가능)
if (corners[0].y < 0)
{
Vector3 currentPos = _tooltipRectTransform.position;
currentPos.y -= corners[0].y; // 위로 올림
// 또는 마우스 위쪽으로 위치 변경
// _tooltipRectTransform.localPosition = localPoint + new Vector2(_tooltipRectTransform.sizeDelta.x * 0.5f + 10f, _tooltipRectTransform.sizeDelta.y * 0.5f + 5f);
_tooltipRectTransform.position = currentPos;
}
// 위쪽 경계 넘어감
if (corners[1].y > screenHeight)
{
Vector3 currentPos = _tooltipRectTransform.position;
currentPos.y -= (corners[1].y - screenHeight);
_tooltipRectTransform.position = currentPos;
}
}
}
private void HideTooltip()
{
if (_activeTooltipInstance != null)
{
_activeTooltipInstance.SetActive(false);
}
}
// 버튼 모델과 게임 오브젝트를 받아 초기 시각적 요소 설정 및 UI 상호작용을 연결합니다.
private void SetupButtonVisualsAndInteractions(ToolbarButtonBase model, GameObject itemObj)
{
// 공통 UI 요소 업데이트 (Text, Icon, Enabled)
UpdateCommonButtonVisuals(model, itemObj);
// 타입별 UI 요소 및 이벤트 설정
if (model is ToolbarRadioButton radioModel)
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null)
{
ToggleGroup toggleGroup = GetOrCreateToggleGroup(radioModel.GroupName);
toggle.group = toggleGroup;
toggle.SetIsOnWithoutNotify(radioModel.IsSelected); // 초기 상태 설정 (이벤트 발생 방지)
toggle.onValueChanged.AddListener((isSelected) =>
{
// UI에서 사용자가 직접 토글한 경우 모델 업데이트
// 중요: 라디오 버튼은 그룹에 의해 선택이 관리되므로, isSelected가 true일 때만 모델 업데이트 요청
if (isSelected) radioModel.ExecuteClick(); // 모델의 ExecuteClick -> RadioGroup.SetSelected 호출
});
// IsSelected 변경은 OnStateChanged를 통해 UpdateItemVisuals에서 처리되거나,
// 좀 더 명시적인 OnToggleStateChanged 이벤트를 사용할 수 있습니다.
radioModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(radioModel, isSelected);
}
}
else if (model is ToolbarToggleButton toggleModel)
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null)
{
toggle.SetIsOnWithoutNotify(toggleModel.IsSelected);
toggle.onValueChanged.AddListener((isSelected) =>
{
toggleModel.ExecuteClick(); // 모델의 ExecuteClick이 IsSelected를 변경하고 OnStateChanged 호출
});
toggleModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(toggleModel, isSelected);
}
}
else if (model is ToolbarExpandableButton expandableModel)
{
Button uiButton = itemObj.GetComponent<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
expandableModel.ExecuteClick();
ToggleSubMenu(expandableModel, itemObj); // itemObj 전달하여 위치 기준으로 삼기
});
}
}
else if (model is ToolbarStandardButton standardModel)
{
Button uiButton = itemObj.GetComponent<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
standardModel.ExecuteClick();
});
}
}
}
// 모델의 상태가 변경되었을 때 호출되어 모든 관련 UI를 업데이트합니다.
private void UpdateItemVisuals(ToolbarButtonBase model)
{
if (_modelToGameObjectMap.TryGetValue(model, out GameObject itemObj))
{
UpdateCommonButtonVisuals(model, itemObj); // 공통 부분 업데이트
// 타입별 특화된 부분 업데이트 (예: Toggle의 isOn 상태)
if (model is ToolbarToggleButton tb) // ToolbarRadioButton도 여기에 해당
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null && toggle.isOn != tb.IsSelected) // UI와 모델 상태가 다를 때만 업데이트
{
toggle.SetIsOnWithoutNotify(tb.IsSelected);
}
}
// 다른 버튼 타입에 대한 추가적인 시각적 업데이트 로직
}
}
// 특정 토글 버튼/라디오 버튼의 IsSelected 상태가 모델에서 변경되었을 때 호출됩니다.
private void UpdateToggleVisuals(ToolbarToggleButton model, bool isSelected)
{
if (_modelToGameObjectMap.TryGetValue(model, out GameObject itemObj))
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null && toggle.isOn != isSelected)
{
toggle.SetIsOnWithoutNotify(isSelected);
}
}
}
// 공통 버튼 시각적 요소(텍스트, 아이콘, 활성화 상태)를 업데이트합니다.
private void UpdateCommonButtonVisuals(ToolbarButtonBase model, GameObject itemObj)
{
// 프리팹 구조에 대한 가정:
// - 텍스트는 TextMeshProUGUI 컴포넌트를 가진 자식 오브젝트에 표시됩니다.
// - 아이콘은 "Icon"이라는 이름의 자식 오브젝트에 Image 컴포넌트로 표시됩니다.
// 이러한 구조는 프로젝트의 프리팹 표준에 맞게 조정해야 합니다.
TextMeshProUGUI buttonTextComponent = itemObj.GetComponentInChildren<TextMeshProUGUI>(true);
if (buttonTextComponent != null)
{
if (LocalizationManager.Instance != null && !string.IsNullOrEmpty(model.Text))
{
// model.Text에는 이제 다국어 키가 저장되어 있습니다.
buttonTextComponent.text = LocalizationManager.Instance.GetString(model.Text);
}
else
{
// LocalizationManager가 없거나 Text(키)가 비어있는 경우, 키를 그대로 표시하거나 기본값 처리
buttonTextComponent.text = model.Text;
}
}
Transform iconTransform = itemObj.transform.Find("Icon"); // 프리팹에 "Icon" 자식 오브젝트가 있다고 가정
if (iconTransform != null)
{
Image buttonIcon = iconTransform.GetComponent<Image>();
if (buttonIcon != null)
{
buttonIcon.sprite = model.Icon;
buttonIcon.gameObject.SetActive(model.Icon != null);
}
}
// 상호작용 가능 상태 업데이트
Selectable selectable = itemObj.GetComponent<Selectable>(); // Button, Toggle 등
if (selectable != null)
{
selectable.interactable = model.IsEnabled;
}
}
private Dictionary<string, ToggleGroup> _toggleGroups = new Dictionary<string, ToggleGroup>();
private ToggleGroup GetOrCreateToggleGroup(string groupName)
{
if (!_toggleGroups.TryGetValue(groupName, out ToggleGroup group))
{
GameObject groupObj = new GameObject($"ToggleGroup_{groupName}");
groupObj.transform.SetParent(toolbarContainer);
group = groupObj.AddComponent<ToggleGroup>();
group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 하나는 선택되어 있도록 함
_toggleGroups.Add(groupName, group);
}
return group;
}
private GameObject currentSubMenu = null;
// expandableButtonObj는 확장 메뉴의 위치를 잡기 위해 사용될 수 있습니다.
private void ToggleSubMenu(ToolbarExpandableButton expandableButton, GameObject expandableButtonObj)
{
if (currentSubMenu != null)
{
Destroy(currentSubMenu);
currentSubMenu = null;
return;
}
if (subMenuPanelPrefab == null || expandableButton.SubButtons.Count == 0) return;
currentSubMenu = Instantiate(subMenuPanelPrefab, transform); // ToolbarView의 자식으로 생성 후 위치 조정
// 위치 조정 로직: expandableButtonObj의 위치를 기준으로 currentSubMenu의 RectTransform을 조정합니다.
// 예: currentSubMenu.transform.position = expandableButtonObj.transform.position + offset;
RectTransform panelRect = currentSubMenu.GetComponent<RectTransform>();
// 하위 메뉴 패널에 LayoutGroup이 있다면 자식 버튼들이 자동으로 정렬됩니다.
foreach (var subItemBase in expandableButton.SubButtons)
{
if (subItemBase is ToolbarButtonBase subItem) // 모든 하위 아이템은 ToolbarButtonBase라고 가정
{
// 하위 버튼도 적절한 프리팹을 사용해야 합니다. 여기서는 standardButtonPrefab을 예시로 사용합니다.
// 실제로는 subItem의 타입에 따라 다른 프리팹을 선택할 수 있습니다.
GameObject subButtonObj = Instantiate(standardButtonPrefab, panelRect); // 패널의 자식으로 생성
// 하위 버튼의 시각적 요소 설정 및 상호작용 연결
UpdateCommonButtonVisuals(subItem, subButtonObj); // 공통 시각 요소 업데이트
Button subUiButton = subButtonObj.GetComponent<Button>();
if (subUiButton != null)
{
subUiButton.interactable = subItem.IsEnabled; // 상호작용 상태 설정
subUiButton.onClick.AddListener(() =>
{
expandableButton.SelectSubButton(subItem); // 모델 업데이트 및 주 버튼 외형 변경 요청
// 주 버튼 UI는 expandableButton의 OnStateChanged 이벤트에 의해 자동으로 업데이트됩니다.
Destroy(currentSubMenu);
currentSubMenu = null;
});
}
// 하위 버튼 툴팁 처리
if (!string.IsNullOrEmpty(subItem.TooltipKey))
{
TooltipHandler tooltipHandler = subButtonObj.GetComponent<TooltipHandler>();
if (tooltipHandler == null) tooltipHandler = subButtonObj.AddComponent<TooltipHandler>();
tooltipHandler.TooltipKey = subItem.TooltipKey;
tooltipHandler.OnPointerEnterAction = HandlePointerEnter;
tooltipHandler.OnPointerExitAction = HandlePointerExit;
}
// 하위 버튼 모델의 OnStateChanged도 구독하여 하위 버튼 자체의 상태 변경(예: 텍스트)도 반영할 수 있습니다.
// subItem.OnStateChanged += () => UpdateCommonButtonVisuals(subItem, subButtonObj);
// _modelToGameObjectMap에 하위 버튼도 추가하여 ClearToolbar에서 정리되도록 해야 합니다. (선택적 확장)
}
}
}
void OnDestroy()
{
// 씬 전환 등으로 ToolbarView가 파괴될 때 모든 이벤트 구독 해제
ClearToolbar();
if (_activeTooltipInstance != null)
{
Destroy(_activeTooltipInstance); // 툴팁 인스턴스도 파괴
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 60240c8d04420604681084344d3a0253

View File

@@ -0,0 +1,26 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UVC.UI.ToolBar
{
public class TooltipHandler : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
public System.Action<string, Vector3> OnPointerEnterAction; // 툴팁 내용(키), 마우스 위치
public System.Action OnPointerExitAction;
public string TooltipKey { get; set; }
public void OnPointerEnter(PointerEventData eventData)
{
if (!string.IsNullOrEmpty(TooltipKey) && gameObject.GetComponent<Selectable>()?.interactable == true) // 버튼이 활성화 상태일 때만
{
OnPointerEnterAction?.Invoke(TooltipKey, Input.mousePosition);
}
}
public void OnPointerExit(PointerEventData eventData)
{
OnPointerExitAction?.Invoke();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 266dd70132eff3d4eb32c995c009634a