Files
EnglewoodLAB/Assets/Scripts/UVC/UI/ToolBar/View/SubMenuHandler.cs

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();
}
}
}
}
}
}