#nullable enable using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; 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 { /// /// ToolbarModel에 정의된 데이터를 기반으로 실제 툴바 UI를 생성하고 관리하는 MonoBehaviour 클래스입니다. /// 사용자의 UI 상호작용을 감지하여 모델의 상태를 변경하거나 커맨드를 실행하고, /// 모델의 상태 변경에 따라 UI를 업데이트합니다. /// /// /// 주요 역할: /// - UI 렌더링: ToolbarModel의 Items 리스트를 순회하며 각 항목에 맞는 UI GameObject(버튼, 구분선 등)를 프리팹으로부터 생성하여 화면에 표시합니다. /// - 이벤트 바인딩: 생성된 UI 요소(예: Unity Button의 onClick)와 ToolbarButtonBase 모델의 ExecuteClick 메서드를 연결합니다. /// 또한, 모델의 OnStateChanged, OnToggleStateChanged 등의 이벤트를 구독하여 모델 상태 변경 시 UI를 업데이트합니다. /// - UI 업데이트: 모델의 상태(텍스트, 아이콘, 활성화 상태, 선택 상태 등)가 변경되면 해당 UI 요소의 시각적 표현을 업데이트합니다. /// - 리소스 관리: 생성된 UI GameObject들과 이벤트 구독을 관리하며, 툴바가 파괴되거나 다시 렌더링될 때 정리합니다(ClearToolbar). /// - 하위 로직 위임: 라디오 버튼 그룹 관리는 `ToggleGroupManager`에, 확장 버튼의 하위 메뉴 관리는 `SubMenuHandler`에 위임하여 클래스의 복잡도를 낮춥니다. /// /// 이 클래스의 인스턴스는 Unity 씬 내의 GameObject에 컴포넌트로 추가되어야 하며, /// Inspector를 통해 필요한 프리팹들(standardButtonPrefab, toggleButtonPrefab 등)과 /// UI 요소들이 배치될 부모 Transform(toolbarContainer), LayoutGroup 등을 할당받아야 합니다. /// /// /// /// // 1. Unity 에디터에서 빈 GameObject를 만들고 ToolbarView 스크립트를 추가합니다. /// // 2. Inspector에서 ToolbarView 컴포넌트의 다음 필드들을 설정합니다: /// // - Standard Button Prefab: 표준 버튼 UI 프리팹 /// // - Toggle Button Prefab: 토글 버튼 UI 프리팹 /// // - Radio Button Prefab: 라디오 버튼 UI 프리팹 /// // - Expandable Button Prefab: 확장 버튼 UI 프리팹 /// // - Separator Prefab: 구분선 UI 프리팹 /// // - Sub Menu Panel Prefab: 확장 버튼의 하위 메뉴 패널 UI 프리팹 /// // - Toolbar Container: 생성된 툴바 항목 UI들이 자식으로 추가될 Transform /// // - Layout Group: Toolbar Container에 연결된 LayoutGroup (예: VerticalLayoutGroup) /// /// // 3. 툴바를 제어하는 다른 스크립트(예: UIManager, ToolbarController)에서 아래와 같이 사용합니다. /// public class MyToolbarController : MonoBehaviour /// { /// public ToolbarView toolbarView; // Inspector에서 ToolbarView 할당 /// /// void Start() /// { /// // 3-1. 툴바에 표시할 데이터 모델(ToolbarModel)을 생성하고 설정합니다. /// ToolbarModel myModel = new ToolbarModel(); /// /// // 표준 버튼 추가 /// myModel.AddStandardButton("새 파일", "icons/new_file", null, "새 파일을 생성합니다."); /// /// // 구분선 추가 /// myModel.AddSeparator(); /// /// // 토글 버튼 추가 /// myModel.AddToggleButton("그리드 보기", false, "icons/grid_on", "icons/grid_off", /// (IsSelected) => Debug.Log($"그리드 표시: {IsSelected}")); /// /// // 라디오 버튼 그룹 추가 /// string viewModeGroup = "ViewMode"; /// myModel.AddRadioButton(viewModeGroup, "2D 보기", true, "icons/view_2d"); /// myModel.AddRadioButton(viewModeGroup, "3D 보기", false, "icons/view_3d"); /// /// // 3-2. 설정된 모델을 ToolbarView에 전달하여 UI를 생성하도록 합니다. /// toolbarView.Initialize(myModel); /// } /// } /// /// public class ToolbarView : MonoBehaviour { /// /// 현재 툴바 UI의 기반이 되는 데이터 모델입니다. /// Initialize 메서드를 통해 외부에서 주입받습니다. /// protected ToolbarModel ToolbarModel { get; private set; } // --- Inspector에서 할당할 프리팹 및 UI 요소들 --- [Header("UI Prefabs")] [Tooltip("표준 버튼 UI에 사용될 프리팹입니다.")] public GameObject standardButtonPrefab; [Tooltip("토글 버튼 UI에 사용될 프리팹입니다. IsOn=false로 설정해 놔야 합니다.")] public GameObject toggleButtonPrefab; [Tooltip("라디오 버튼 UI에 사용될 프리팹입니다. IsOn=false로 설정해 놔야 합니다.")] public GameObject radioButtonPrefab; [Tooltip("확장 가능한 버튼 UI에 사용될 프리팹입니다.")] public GameObject expandableButtonPrefab; [Tooltip("구분선 UI에 사용될 프리팹입니다.")] public GameObject separatorPrefab; [Tooltip("확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다.")] public GameObject subMenuPanelPrefab; [Header("UI Layout")] [Tooltip("생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다.")] public Transform toolbarContainer; [Tooltip("툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다 (예: VerticalLayoutGroup).")] public LayoutGroup layoutGroup; /// /// 툴바 버튼 모델(ToolbarButtonBase)과 해당 모델을 나타내는 실제 UI GameObject를 매핑하는 딕셔너리입니다. /// 모델 상태 변경 시 해당 GameObject를 찾아 UI를 업데이트하거나, UI 정리 시 사용됩니다. /// protected Dictionary _modelToGameObjectMap = new Dictionary(); /// /// 버튼 타입별 UI 처리기를 저장하는 딕셔너리입니다. /// 각 버튼 타입(표준, 토글 등)에 맞는 IButtonViewProcessor 구현체를 등록하여 사용합니다. /// private Dictionary _buttonViewProcessors = new Dictionary(); // --- 헬퍼 클래스 --- private ToggleGroupManager _toggleGroupManager; private SubMenuHandler _subMenuHandler; private Canvas _canvas; /// /// 렌더링 작업에 사용되는 정적 캔버스 인스턴스를 가져옵니다. /// public Canvas Canvas { get { if (_canvas == null) _canvas = CanvasUtil.GetOrCreate("StaticCanvas"); return _canvas; } } /// /// MonoBehaviour의 Awake 메서드입니다. /// 필수 참조를 확인 및 초기화하고, 버튼 뷰 프로세서와 헬퍼 클래스들을 준비합니다. /// protected virtual void Awake() { // 필수 프리팹들이 Inspector에서 할당되었는지 확인합니다. if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null || expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null) { Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this); } // UI 컨테이너와 레이아웃 그룹을 자동으로 찾거나 설정합니다. if (toolbarContainer == null) toolbarContainer = GetComponent(); if (toolbarContainer == null) toolbarContainer = GetComponentInChildren(true); if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다.", this); if (layoutGroup == null && toolbarContainer != null) { layoutGroup = toolbarContainer.gameObject.GetComponent(); if (layoutGroup == null) { layoutGroup = toolbarContainer.gameObject.AddComponent(); Debug.LogWarning("ToolbarView: LayoutGroup이 toolbarContainer에 없어 새로 추가합니다. (기본 VerticalLayoutGroup)", this); } } if (layoutGroup == null) Debug.LogError("ToolbarView: layoutGroup이 할당되지 않았습니다.", this); // 각 버튼 타입에 대한 뷰 프로세서를 등록합니다. _buttonViewProcessors[typeof(ToolbarStandardButton)] = new ToolbarStandardButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarToggleButton)] = new ToolbarToggleButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarRadioButton)] = new ToolbarRadioButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarExpandableButton)] = new ToolbarExpandableButtonViewProcessor(); // 헬퍼 클래스들을 초기화합니다. _toggleGroupManager = new ToggleGroupManager(toolbarContainer); _subMenuHandler = new SubMenuHandler(this); } /// /// 지정된 ToolbarModel을 사용하여 툴바 UI를 초기화하고 렌더링합니다. /// 기존 UI가 있다면 정리(ClearToolbar)한 후, 새 모델에 따라 UI를 다시 생성(RenderToolbar)합니다. /// /// 화면에 표시할 툴바의 데이터 모델입니다. public virtual void Initialize(ToolbarModel toolbarModel) { this.ToolbarModel = toolbarModel; // 필수 조건들을 다시 한 번 확인합니다. if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null || expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null) { Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다.", this); return; } if (toolbarContainer == null) { Debug.LogError("ToolbarView: Initialize 실패. toolbarContainer가 할당되지 않았습니다.", this); return; } if (layoutGroup == null) { Debug.LogError("ToolbarView: Initialize 실패. layoutGroup이 할당되지 않았습니다.", this); return; } if (this.ToolbarModel == null) { Debug.LogError("ToolbarView: Initialize 실패. 제공된 toolbarModel이 null입니다.", this); ClearToolbar(); // 모델이 없으면 기존 UI라도 정리합니다. return; } // 모든 조건이 충족되면 툴바 렌더링을 시작합니다. RenderToolbar(); } /// /// 현재 툴바에 표시된 모든 UI 요소들을 제거하고, 관련된 이벤트 구독을 해제합니다. /// 툴바를 새로 그리거나 뷰가 파괴될 때 호출됩니다. /// protected virtual void ClearToolbar() { if (_modelToGameObjectMap != null) { foreach (var pair in _modelToGameObjectMap) { if (pair.Key != null) { // 모델에 연결된 모든 이벤트 구독을 명시적으로 해제합니다. pair.Key.ClearEventHandlers(); } if (pair.Value != null) { // UI 컴포넌트의 이벤트 리스너를 명시적으로 해제합니다. Toggle toggleComponent = pair.Value.GetComponent(); if (toggleComponent != null) toggleComponent.onValueChanged.RemoveAllListeners(); Button buttonComponent = pair.Value.GetComponent