331 lines
16 KiB
C#
331 lines
16 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UVC.Extension;
|
|
using UVC.UI.Toolbar.Model;
|
|
using UVC.UI.Tooltip;
|
|
|
|
namespace UVC.UI.Toolbar.View
|
|
{
|
|
/// <summary>
|
|
/// 확장 가능한 툴바 버튼(ToolbarExpandableButton)의 하위 메뉴 UI를 생성, 관리 및 상호작용을 처리하는 헬퍼 클래스입니다.
|
|
/// ToolbarView로부터 하위 메뉴 관련 로직을 위임받아 처리하여 ToolbarView의 복잡도를 낮춥니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 주요 역할:
|
|
/// - 하위 메뉴 토글: 특정 확장 버튼에 대한 하위 메뉴를 열거나 닫습니다.
|
|
/// - 하위 메뉴 UI 생성: `subMenuPanelPrefab`을 사용하여 하위 메뉴의 패널과 그 안의 버튼들을 동적으로 생성합니다.
|
|
/// - 위치 계산: 하위 메뉴가 나타날 위치를 주 확장 버튼의 위치를 기준으로 계산하여 설정합니다.
|
|
/// - 이벤트 처리: 생성된 하위 메뉴의 버튼들에 대한 클릭 이벤트를 설정하고, 클릭 시 메뉴를 닫는 등의 동작을 처리합니다.
|
|
/// - 외부 클릭 감지: 하위 메뉴가 열려 있을 때 메뉴 영역 바깥을 클릭하면 메뉴가 닫히도록 처리합니다.
|
|
/// - 리소스 정리: 하위 메뉴가 닫힐 때 생성되었던 모든 UI 요소와 이벤트 리스너를 깨끗하게 제거합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 이 클래스는 ToolbarView 내부에서 다음과 같이 사용됩니다.
|
|
///
|
|
/// // 1. ToolbarView의 Awake 메서드에서 SubMenuHandler 인스턴스 생성
|
|
/// // public class ToolbarView : MonoBehaviour
|
|
/// // {
|
|
/// // private SubMenuHandler _subMenuHandler;
|
|
/// //
|
|
/// // void Awake()
|
|
/// // {
|
|
/// // _subMenuHandler = new SubMenuHandler(this);
|
|
/// // }
|
|
/// //
|
|
/// // // ...
|
|
/// // }
|
|
///
|
|
/// // 2. 확장 버튼(ExpandableButton)의 UI 프로세서에서 `ToggleSubMenu` 호출
|
|
/// // public class ToolbarExpandableButtonViewProcessor : IButtonViewProcessor
|
|
/// // {
|
|
/// // public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
|
|
/// // {
|
|
/// // ToolbarExpandableButton expandableModel = buttonModel as ToolbarExpandableButton;
|
|
/// // Button uiButton = buttonUIObject.GetComponent<Button>();
|
|
/// // if (uiButton != null)
|
|
/// // {
|
|
/// // uiButton.onClick.AddListener(() =>
|
|
/// // {
|
|
/// // // viewContext의 ToggleSubMenu를 호출하면 내부적으로 SubMenuHandler의 ToggleSubMenu가 실행됩니다.
|
|
/// // viewContext.ToggleSubMenu(expandableModel, buttonUIObject);
|
|
/// // });
|
|
/// // }
|
|
/// // }
|
|
/// // }
|
|
///
|
|
/// // 3. ToolbarView의 Update 메서드에서 매 프레임 외부 클릭 감지 로직 실행
|
|
/// // public class ToolbarView : MonoBehaviour
|
|
/// // {
|
|
/// // void Update()
|
|
/// // {
|
|
/// // _subMenuHandler.Update(); // 매 프레임 호출하여 외부 클릭 감지
|
|
/// // }
|
|
/// // }
|
|
/// </code>
|
|
/// </example>
|
|
public class SubMenuHandler
|
|
{
|
|
private readonly ToolbarView _view;
|
|
private GameObject _currentSubMenu;
|
|
private ToolbarExpandableButton _ownerOfCurrentSubMenu;
|
|
|
|
/// <summary>
|
|
/// SubMenuHandler의 새 인스턴스를 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="view">이 핸들러를 소유하고 관리하는 ToolbarView의 인스턴스입니다. 프리팹, 캔버스 등의 참조에 사용됩니다.</param>
|
|
public SubMenuHandler(ToolbarView view)
|
|
{
|
|
_view = view;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 확장 버튼에 대한 하위 메뉴 UI를 토글합니다.
|
|
/// 이미 해당 버튼의 하위 메뉴가 열려있으면 닫고, 닫혀있으면 엽니다.
|
|
/// 다른 버튼의 하위 메뉴가 열려있었다면, 그 메뉴는 닫고 새로운 메뉴를 엽니다.
|
|
/// </summary>
|
|
/// <param name="expandableButtonModel">하위 버튼 목록을 가진 확장 버튼의 데이터 모델입니다.</param>
|
|
/// <param name="expandableButtonObj">하위 메뉴의 위치를 결정하는 데 사용될 주 확장 버튼의 UI GameObject입니다.</param>
|
|
public void ToggleSubMenu(ToolbarExpandableButton expandableButtonModel, GameObject expandableButtonObj)
|
|
{
|
|
bool closeOnly = false;
|
|
// 이미 하위 메뉴가 열려있는 경우
|
|
if (_currentSubMenu != null)
|
|
{
|
|
// 현재 열린 메뉴가 지금 클릭한 버튼에 의해 열린 것인지 확인
|
|
if (_ownerOfCurrentSubMenu == expandableButtonModel)
|
|
{
|
|
// 같은 버튼을 다시 클릭했으므로, 메뉴를 닫기만 하고 다시 열지 않습니다.
|
|
closeOnly = true;
|
|
}
|
|
// 기존에 열려있던 하위 메뉴를 정리합니다.
|
|
DestroyCurrentSubMenuAndClearListeners();
|
|
}
|
|
|
|
// 닫기만 해야 하는 경우, 여기서 함수를 종료합니다.
|
|
if (closeOnly)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// --- 새 하위 메뉴를 여는 로직 ---
|
|
|
|
// 필요한 프리팹이 할당되었는지 확인합니다.
|
|
if (_view.subMenuPanelPrefab == null)
|
|
{
|
|
Debug.LogError("ToolbarView: subMenuPanelPrefab이 할당되지 않아 하위 메뉴를 열 수 없습니다.", _view);
|
|
return;
|
|
}
|
|
// 하위 버튼이 하나라도 있는지 확인합니다.
|
|
if (expandableButtonModel.SubButtons == null || expandableButtonModel.SubButtons.Count == 0)
|
|
{
|
|
return; // 하위 버튼이 없으면 메뉴를 열지 않습니다.
|
|
}
|
|
|
|
// 하위 메뉴 패널 UI를 생성합니다. 생성 위치는 ToolbarView의 자식으로 합니다.
|
|
_currentSubMenu = Object.Instantiate(_view.subMenuPanelPrefab, _view.transform);
|
|
_currentSubMenu.name = $"SubMenu_{expandableButtonModel.Text}";
|
|
_ownerOfCurrentSubMenu = expandableButtonModel; // 이 메뉴를 연 버튼 모델을 기록합니다.
|
|
|
|
RectTransform panelRect = _currentSubMenu.GetComponent<RectTransform>();
|
|
if (panelRect == null)
|
|
{
|
|
Debug.LogError("ToolbarView: subMenuPanelPrefab에 RectTransform이 없습니다.", _currentSubMenu);
|
|
Object.Destroy(_currentSubMenu);
|
|
_currentSubMenu = null;
|
|
return;
|
|
}
|
|
|
|
// 하위 메뉴 패널의 위치와 피벗을 설정합니다.
|
|
RectTransform expandableButtonRect = expandableButtonObj.GetComponent<RectTransform>();
|
|
panelRect.anchorMin = new Vector2(0, 1);
|
|
panelRect.anchorMax = new Vector2(0, 1);
|
|
panelRect.pivot = new Vector2(0, 1); // 좌상단 기준
|
|
_currentSubMenu.SetActive(true);
|
|
|
|
// UI 계층에서 가장 마지막 자식으로 만들어 다른 UI 요소들 위에 표시되도록 합니다.
|
|
if (panelRect.parent != null) panelRect.SetAsLastSibling();
|
|
|
|
// 하위 메뉴 패널에 LayoutGroup이 있는지 확인하고, 없다면 기본값으로 추가합니다.
|
|
LayoutGroup subMenuLayoutGroup = _currentSubMenu.GetComponent<LayoutGroup>();
|
|
if (subMenuLayoutGroup == null)
|
|
{
|
|
subMenuLayoutGroup = _currentSubMenu.AddComponent<VerticalLayoutGroup>();
|
|
}
|
|
|
|
// 툴바의 레이아웃 방향(수직/수평)에 따라 하위 메뉴의 위치를 조정합니다.
|
|
Vector2 offset = new Vector2(expandableButtonRect.anchoredPosition.x, expandableButtonRect.anchoredPosition.y);
|
|
if (_view.layoutGroup is VerticalLayoutGroup)
|
|
{
|
|
// 툴바가 수직이면, 버튼 오른쪽에 메뉴 표시
|
|
offset.x += expandableButtonRect.rect.width;
|
|
}
|
|
else
|
|
{
|
|
// 툴바가 수평이면, 버튼 아래쪽에 메뉴 표시
|
|
if (subMenuLayoutGroup != null) offset.x -= subMenuLayoutGroup.padding.left;
|
|
offset.y -= expandableButtonRect.rect.height;
|
|
}
|
|
panelRect.anchoredPosition = offset;
|
|
|
|
// 확장 버튼 모델에 포함된 모든 하위 버튼들을 UI로 생성합니다.
|
|
foreach (var subItemBase in expandableButtonModel.SubButtons)
|
|
{
|
|
if (subItemBase is ToolbarButtonBase subItemModel)
|
|
{
|
|
// 하위 버튼 타입에 맞는 UI 처리기를 가져옵니다.
|
|
IButtonViewProcessor subProcessor = _view.GetButtonViewProcessor(subItemModel.GetType());
|
|
if (subProcessor == null)
|
|
{
|
|
Debug.LogWarning($"하위 버튼 '{subItemModel.Text}'에 대한 ViewProcessor를 찾을 수 없습니다.", _view);
|
|
continue;
|
|
}
|
|
|
|
// 하위 버튼 UI를 생성합니다.
|
|
GameObject subButtonObj = subProcessor.CreateButtonUI(subItemModel, panelRect, _view);
|
|
if (subButtonObj == null)
|
|
{
|
|
Debug.LogError($"하위 버튼 '{subItemModel.Text}'의 UI를 생성하지 못했습니다.", _view);
|
|
continue;
|
|
}
|
|
|
|
subButtonObj.name = $"SubItem_{subItemModel.Text}";
|
|
|
|
// 하위 버튼의 상호작용을 설정합니다. (클릭 이벤트 등)
|
|
subProcessor.SetupButtonInteractions(subItemModel, subButtonObj, _view);
|
|
|
|
// 하위 버튼의 공통 시각적 요소(텍스트, 아이콘 등)를 업데이트합니다.
|
|
_view.InternalUpdateCommonButtonVisuals(subItemModel, subButtonObj);
|
|
|
|
// 하위 버튼의 클릭 이벤트에 추가적인 로직을 연결합니다.
|
|
// (하위 메뉴를 닫고, 주 확장 버튼의 상태를 업데이트하는 등)
|
|
var buttonComponent = subButtonObj.GetComponent<Button>();
|
|
if (buttonComponent != null)
|
|
{
|
|
// 기존 리스너를 모두 지우고 새로 설정하여 중복을 방지합니다.
|
|
buttonComponent.onClick.RemoveAllListeners();
|
|
buttonComponent.onClick.AddListener(() =>
|
|
{
|
|
// 1. 주 확장 버튼 모델에 하위 버튼이 선택되었음을 알립니다.
|
|
// 이를 통해 주 버튼의 아이콘이나 텍스트가 선택된 하위 버튼의 것으로 변경될 수 있습니다.
|
|
expandableButtonModel.SelectSubButton(subItemModel);
|
|
|
|
// 2. 하위 버튼 자체의 커맨드를 실행합니다.
|
|
subItemModel.ExecuteClick();
|
|
|
|
// 3. 하위 메뉴를 닫습니다.
|
|
CloseSubMenu();
|
|
|
|
// 4. 툴팁이 남아있을 경우를 대비해 숨깁니다.
|
|
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
|
{
|
|
TooltipManager.Instance.HideTooltip();
|
|
}
|
|
});
|
|
}
|
|
|
|
var toggleComponent = subButtonObj.GetComponent<Toggle>();
|
|
if (toggleComponent != null)
|
|
{
|
|
toggleComponent.onValueChanged.RemoveAllListeners();
|
|
toggleComponent.onValueChanged.AddListener((isSelected) =>
|
|
{
|
|
if (isSelected) // 토글은 선택될 때만 동작하도록 처리
|
|
{
|
|
expandableButtonModel.SelectSubButton(subItemModel);
|
|
subItemModel.ExecuteClick();
|
|
CloseSubMenu();
|
|
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
|
{
|
|
TooltipManager.Instance.HideTooltip();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 모든 하위 버튼이 추가된 후, 레이아웃 그룹의 크기를 자식 요소에 맞게 조절합니다.
|
|
if (subMenuLayoutGroup != null)
|
|
{
|
|
subMenuLayoutGroup.FitToChildren();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 열려있는 하위 메뉴를 닫습니다.
|
|
/// </summary>
|
|
public void CloseSubMenu()
|
|
{
|
|
DestroyCurrentSubMenuAndClearListeners();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 열려있는 하위 메뉴의 모든 UI 요소와 이벤트 리스너를 정리하고 GameObject를 파괴합니다.
|
|
/// </summary>
|
|
private void DestroyCurrentSubMenuAndClearListeners()
|
|
{
|
|
if (_currentSubMenu == null) return;
|
|
|
|
// 하위 메뉴에 있는 모든 버튼의 클릭 리스너를 제거합니다.
|
|
Button[] subButtons = _currentSubMenu.GetComponentsInChildren<Button>(true);
|
|
foreach (Button btn in subButtons)
|
|
{
|
|
btn.onClick.RemoveAllListeners();
|
|
}
|
|
|
|
// 하위 메뉴에 있는 모든 토글의 값 변경 리스너를 제거합니다.
|
|
Toggle[] subToggles = _currentSubMenu.GetComponentsInChildren<Toggle>(true);
|
|
foreach (Toggle tgl in subToggles)
|
|
{
|
|
tgl.onValueChanged.RemoveAllListeners();
|
|
}
|
|
|
|
// 하위 메뉴에 있는 모든 툴팁 핸들러의 액션을 정리합니다.
|
|
TooltipHandler[] subTooltipHandlers = _currentSubMenu.GetComponentsInChildren<TooltipHandler>(true);
|
|
foreach (TooltipHandler handler in subTooltipHandlers)
|
|
{
|
|
handler.OnPointerEnterAction = null;
|
|
handler.OnPointerExitAction = null;
|
|
}
|
|
|
|
// 하위 메뉴 GameObject를 파괴합니다.
|
|
Object.Destroy(_currentSubMenu);
|
|
_currentSubMenu = null;
|
|
_ownerOfCurrentSubMenu = null; // 메뉴 소유자 참조도 초기화합니다.
|
|
}
|
|
|
|
/// <summary>
|
|
/// 매 프레임 호출되어야 하는 로직을 처리합니다.
|
|
/// 현재는 하위 메뉴 외부를 클릭했을 때 메뉴를 닫는 기능을 수행합니다.
|
|
/// </summary>
|
|
public void Update()
|
|
{
|
|
// 마우스 왼쪽 버튼이 클릭되었고, 하위 메뉴가 열려있는 상태일 때
|
|
if (Input.GetMouseButtonDown(0) && _currentSubMenu != null && _currentSubMenu.activeSelf && _view.Canvas != null)
|
|
{
|
|
RectTransform subMenuRect = _currentSubMenu.GetComponent<RectTransform>();
|
|
if (subMenuRect == null) return;
|
|
|
|
// 캔버스의 렌더 모드에 따라 이벤트 카메라를 가져옵니다.
|
|
Camera eventCamera = null;
|
|
if (_view.Canvas.renderMode == RenderMode.ScreenSpaceCamera || _view.Canvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
eventCamera = _view.Canvas.worldCamera;
|
|
}
|
|
|
|
// 마우스 포인터가 하위 메뉴 영역 바깥에 있는지 확인합니다.
|
|
if (!RectTransformUtility.RectangleContainsScreenPoint(subMenuRect, Input.mousePosition, eventCamera))
|
|
{
|
|
// 바깥을 클릭했다면 하위 메뉴를 닫습니다.
|
|
CloseSubMenu();
|
|
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
|
{
|
|
TooltipManager.Instance.HideTooltip();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|